mirror of
https://github.com/kodjodevf/mangayomi.git
synced 2026-05-24 00:12:16 +00:00
Merge pull request #697 from NBA2K1/performance-improvements
fix page jumps in vertical/webtoon mode + performance improvements in library/reader
This commit is contained in:
commit
dacebb660a
23 changed files with 387 additions and 559 deletions
|
|
@ -76,7 +76,7 @@ class _SubtitlesWidgetSearchState extends ConsumerState<SubtitlesWidgetSearch> {
|
|||
bottomLeft: Radius.circular(20),
|
||||
bottomRight: Radius.circular(20),
|
||||
),
|
||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: _isLoading
|
||||
? SizedBox(
|
||||
height: context.height(0.3),
|
||||
|
|
@ -229,7 +229,7 @@ class _SubtitlesWidgetSearchState extends ConsumerState<SubtitlesWidgetSearch> {
|
|||
Material(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
color: Colors.transparent,
|
||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Ink.image(
|
||||
height: 120,
|
||||
width: 80,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
// ignore_for_file: use_build_context_synchronously
|
||||
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:mangayomi/main.dart';
|
||||
|
|
@ -54,6 +55,7 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen>
|
|||
final _textEditingController = TextEditingController();
|
||||
TabController? tabBarController;
|
||||
int _tabIndex = 0;
|
||||
Timer? _searchDebounce;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -68,6 +70,7 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen>
|
|||
void dispose() {
|
||||
_textEditingController.dispose();
|
||||
tabBarController?.dispose();
|
||||
_searchDebounce?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
@ -266,7 +269,15 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen>
|
|||
textEditingController: _textEditingController,
|
||||
onSearchToggle: () =>
|
||||
setState(() => _isSearch = !_isSearch),
|
||||
onSearchClear: () => setState(() {}),
|
||||
onSearchClear: () {
|
||||
_searchDebounce?.cancel();
|
||||
_searchDebounce = Timer(
|
||||
const Duration(milliseconds: 300),
|
||||
() {
|
||||
if (mounted) setState(() {});
|
||||
},
|
||||
);
|
||||
},
|
||||
onIgnoreFiltersChanged: (val) =>
|
||||
setState(() => _ignoreFiltersOnSearch = val),
|
||||
vsync: this,
|
||||
|
|
@ -346,7 +357,12 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen>
|
|||
ignoreFiltersOnSearch: _ignoreFiltersOnSearch,
|
||||
textEditingController: _textEditingController,
|
||||
onSearchToggle: () => setState(() => _isSearch = !_isSearch),
|
||||
onSearchClear: () => setState(() {}),
|
||||
onSearchClear: () {
|
||||
_searchDebounce?.cancel();
|
||||
_searchDebounce = Timer(const Duration(milliseconds: 300), () {
|
||||
if (mounted) setState(() {});
|
||||
});
|
||||
},
|
||||
onIgnoreFiltersChanged: (val) =>
|
||||
setState(() => _ignoreFiltersOnSearch = val),
|
||||
vsync: this,
|
||||
|
|
@ -436,8 +452,7 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen>
|
|||
BottomSelectButton(
|
||||
icon: Icon(Icons.label_outline_rounded, color: color),
|
||||
onPressed: () {
|
||||
final mangaIdsList = ref.watch(mangasListStateProvider);
|
||||
final List<Manga> bulkMangas = mangaIdsList
|
||||
final List<Manga> bulkMangas = mangaIds
|
||||
.map((id) => isar.mangas.getSync(id)!)
|
||||
.toList();
|
||||
showCategorySelectionDialog(
|
||||
|
|
|
|||
|
|
@ -902,51 +902,40 @@ class SortLibraryMangaState extends _$SortLibraryMangaState {
|
|||
@riverpod
|
||||
class MangasListState extends _$MangasListState {
|
||||
@override
|
||||
List<int> build() {
|
||||
return [];
|
||||
}
|
||||
Set<int> build() => {};
|
||||
|
||||
void update(Manga value) {
|
||||
var newList = state.reversed.toList();
|
||||
if (newList.contains(value.id)) {
|
||||
newList.remove(value.id);
|
||||
var newSet = Set<int>.from(state);
|
||||
if (newSet.contains(value.id)) {
|
||||
newSet.remove(value.id);
|
||||
} else {
|
||||
newList.add(value.id!);
|
||||
newSet.add(value.id!);
|
||||
}
|
||||
if (newList.isEmpty) {
|
||||
if (newSet.isEmpty) {
|
||||
ref.read(isLongPressedStateProvider.notifier).update(false);
|
||||
}
|
||||
state = newList;
|
||||
state = newSet;
|
||||
}
|
||||
|
||||
void selectAll(Manga value) {
|
||||
var newList = state.reversed.toList();
|
||||
if (!newList.contains(value.id)) {
|
||||
newList.add(value.id!);
|
||||
}
|
||||
|
||||
state = newList;
|
||||
}
|
||||
void selectAll(Manga value) => state = {...state, value.id!};
|
||||
|
||||
void selectSome(Manga value) {
|
||||
var newList = state.reversed.toList();
|
||||
if (newList.contains(value.id)) {
|
||||
newList.remove(value.id);
|
||||
final newSet = Set<int>.from(state);
|
||||
if (newSet.contains(value.id)) {
|
||||
newSet.remove(value.id);
|
||||
} else {
|
||||
newList.add(value.id!);
|
||||
newSet.add(value.id!);
|
||||
}
|
||||
state = newList;
|
||||
state = newSet;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
state = [];
|
||||
}
|
||||
void clear() => state = {};
|
||||
}
|
||||
|
||||
@riverpod
|
||||
class MangasSetIsReadState extends _$MangasSetIsReadState {
|
||||
@override
|
||||
void build({required List<int> mangaIds, required bool markAsRead}) {}
|
||||
void build({required Set<int> mangaIds, required bool markAsRead}) {}
|
||||
|
||||
void set() {
|
||||
final allChapters = <Chapter>[];
|
||||
|
|
|
|||
|
|
@ -1823,7 +1823,7 @@ abstract class _$SortLibraryMangaState extends $Notifier<SortLibraryManga> {
|
|||
final mangasListStateProvider = MangasListStateProvider._();
|
||||
|
||||
final class MangasListStateProvider
|
||||
extends $NotifierProvider<MangasListState, List<int>> {
|
||||
extends $NotifierProvider<MangasListState, Set<int>> {
|
||||
MangasListStateProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
|
|
@ -1843,27 +1843,27 @@ final class MangasListStateProvider
|
|||
MangasListState create() => MangasListState();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(List<int> value) {
|
||||
Override overrideWithValue(Set<int> value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<List<int>>(value),
|
||||
providerOverride: $SyncValueProvider<Set<int>>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$mangasListStateHash() => r'bbd2e3600ec22a774b1774ae3c221815e52bfef6';
|
||||
String _$mangasListStateHash() => r'61c6477ea43c6113caa89ef13984cd4370d303ee';
|
||||
|
||||
abstract class _$MangasListState extends $Notifier<List<int>> {
|
||||
List<int> build();
|
||||
abstract class _$MangasListState extends $Notifier<Set<int>> {
|
||||
Set<int> build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final ref = this.ref as $Ref<List<int>, List<int>>;
|
||||
final ref = this.ref as $Ref<Set<int>, Set<int>>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<List<int>, List<int>>,
|
||||
List<int>,
|
||||
AnyNotifier<Set<int>, Set<int>>,
|
||||
Set<int>,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
|
|
@ -1878,7 +1878,7 @@ final class MangasSetIsReadStateProvider
|
|||
extends $NotifierProvider<MangasSetIsReadState, void> {
|
||||
MangasSetIsReadStateProvider._({
|
||||
required MangasSetIsReadStateFamily super.from,
|
||||
required ({List<int> mangaIds, bool markAsRead}) super.argument,
|
||||
required ({Set<int> mangaIds, bool markAsRead}) super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'mangasSetIsReadStateProvider',
|
||||
|
|
@ -1921,7 +1921,7 @@ final class MangasSetIsReadStateProvider
|
|||
}
|
||||
|
||||
String _$mangasSetIsReadStateHash() =>
|
||||
r'2a1b1005e2ed5068d36188a3fb969d21b64bfef6';
|
||||
r'a2c64ecdf03b3d27282c63d8cadbc1cc44943e39';
|
||||
|
||||
final class MangasSetIsReadStateFamily extends $Family
|
||||
with
|
||||
|
|
@ -1930,7 +1930,7 @@ final class MangasSetIsReadStateFamily extends $Family
|
|||
void,
|
||||
void,
|
||||
void,
|
||||
({List<int> mangaIds, bool markAsRead})
|
||||
({Set<int> mangaIds, bool markAsRead})
|
||||
> {
|
||||
MangasSetIsReadStateFamily._()
|
||||
: super(
|
||||
|
|
@ -1942,7 +1942,7 @@ final class MangasSetIsReadStateFamily extends $Family
|
|||
);
|
||||
|
||||
MangasSetIsReadStateProvider call({
|
||||
required List<int> mangaIds,
|
||||
required Set<int> mangaIds,
|
||||
required bool markAsRead,
|
||||
}) => MangasSetIsReadStateProvider._(
|
||||
argument: (mangaIds: mangaIds, markAsRead: markAsRead),
|
||||
|
|
@ -1954,11 +1954,11 @@ final class MangasSetIsReadStateFamily extends $Family
|
|||
}
|
||||
|
||||
abstract class _$MangasSetIsReadState extends $Notifier<void> {
|
||||
late final _$args = ref.$arg as ({List<int> mangaIds, bool markAsRead});
|
||||
List<int> get mangaIds => _$args.mangaIds;
|
||||
late final _$args = ref.$arg as ({Set<int> mangaIds, bool markAsRead});
|
||||
Set<int> get mangaIds => _$args.mangaIds;
|
||||
bool get markAsRead => _$args.markAsRead;
|
||||
|
||||
void build({required List<int> mangaIds, required bool markAsRead});
|
||||
void build({required Set<int> mangaIds, required bool markAsRead});
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
|
|
|
|||
45
lib/modules/library/widgets/continue_reader_button.dart
Normal file
45
lib/modules/library/widgets/continue_reader_button.dart
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
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/history.dart';
|
||||
import 'package:mangayomi/models/manga.dart';
|
||||
import 'package:mangayomi/modules/more/providers/incognito_mode_state_provider.dart';
|
||||
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
|
||||
import 'package:mangayomi/utils/extensions/chapter.dart';
|
||||
|
||||
class ContinueReaderButton extends ConsumerWidget {
|
||||
final Manga entry;
|
||||
|
||||
const ContinueReaderButton({super.key, required this.entry});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return StreamBuilder(
|
||||
stream: isar.historys
|
||||
.filter()
|
||||
.mangaIdEqualTo(entry.id!)
|
||||
.watch(fireImmediately: true),
|
||||
builder: (context, snapshot) => GestureDetector(
|
||||
onTap: () {
|
||||
final incognitoMode = ref.read(incognitoModeStateProvider);
|
||||
if (snapshot.hasData && snapshot.data!.isNotEmpty && !incognitoMode) {
|
||||
snapshot.data!.first.chapter.value!.pushToReaderView(context);
|
||||
} else {
|
||||
entry.chapters.first.pushToReaderView(context);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
color: context.primaryColor.withValues(alpha: 0.9),
|
||||
),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(7),
|
||||
child: Icon(Icons.play_arrow, size: 19, color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -226,7 +226,7 @@ class LibraryAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
|||
/// AppBar shown when items are long-pressed for bulk selection.
|
||||
class _SelectionAppBar extends ConsumerWidget {
|
||||
final ItemType itemType;
|
||||
final List<int> mangaIdsList;
|
||||
final Set<int> mangaIdsList;
|
||||
final List<Manga> data;
|
||||
|
||||
const _SelectionAppBar({
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:isar_community/isar.dart';
|
||||
|
|
@ -27,8 +28,8 @@ void showDeleteMangaDialog({
|
|||
required WidgetRef ref,
|
||||
required ItemType itemType,
|
||||
}) {
|
||||
List<int> fromLibList = [];
|
||||
List<int> downloadedChapsList = [];
|
||||
Set<int> fromLibList = {};
|
||||
Set<int> downloadedChapsList = {};
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
|
|
@ -53,10 +54,10 @@ void showDeleteMangaDialog({
|
|||
label: l10n.from_library,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
if (fromLibList == mangaIdsList) {
|
||||
fromLibList = [];
|
||||
if (setEquals(fromLibList, mangaIdsList)) {
|
||||
fromLibList = {};
|
||||
} else {
|
||||
fromLibList = mangaIdsList;
|
||||
fromLibList = {...mangaIdsList};
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
@ -68,10 +69,10 @@ void showDeleteMangaDialog({
|
|||
: l10n.downloaded_episodes,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
if (downloadedChapsList == mangaIdsList) {
|
||||
downloadedChapsList = [];
|
||||
if (setEquals(downloadedChapsList, mangaIdsList)) {
|
||||
downloadedChapsList = {};
|
||||
} else {
|
||||
downloadedChapsList = mangaIdsList;
|
||||
downloadedChapsList = {...mangaIdsList};
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3,19 +3,16 @@ 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/history.dart';
|
||||
import 'package:mangayomi/modules/library/providers/isar_providers.dart';
|
||||
import 'package:mangayomi/modules/library/providers/library_state_provider.dart';
|
||||
import 'package:mangayomi/models/manga.dart';
|
||||
import 'package:mangayomi/modules/library/widgets/continue_reader_button.dart';
|
||||
import 'package:mangayomi/modules/manga/detail/providers/state_providers.dart';
|
||||
import 'package:mangayomi/modules/widgets/custom_extended_image_provider.dart';
|
||||
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
|
||||
import 'package:mangayomi/utils/constant.dart';
|
||||
import 'package:mangayomi/utils/extensions/chapter.dart';
|
||||
import 'package:mangayomi/utils/headers.dart';
|
||||
import 'package:mangayomi/modules/more/providers/incognito_mode_state_provider.dart';
|
||||
import 'package:mangayomi/modules/widgets/bottom_text_widget.dart';
|
||||
import 'package:mangayomi/modules/widgets/cover_view_widget.dart';
|
||||
import 'package:mangayomi/modules/widgets/gridview_widget.dart';
|
||||
|
|
@ -24,7 +21,7 @@ import 'package:mangayomi/modules/widgets/manga_image_card_widget.dart';
|
|||
class LibraryGridViewWidget extends StatefulWidget {
|
||||
final bool isCoverOnlyGrid;
|
||||
final bool isComfortableGrid;
|
||||
final List<int> mangaIdsList;
|
||||
final Set<int> mangaIdsList;
|
||||
final List<Manga> entriesManga;
|
||||
final bool language;
|
||||
final bool downloadedChapter;
|
||||
|
|
@ -178,29 +175,22 @@ class _LibraryGridViewWidgetState extends State<LibraryGridViewWidget> {
|
|||
builder: (context, ref, child) {
|
||||
List nbrDown = [];
|
||||
if (widget.downloadedChapter) {
|
||||
isar.txnSync(() {
|
||||
for (
|
||||
var i = 0;
|
||||
i < entry.chapters.length;
|
||||
i++
|
||||
) {
|
||||
final entries = isar.downloads
|
||||
.filter()
|
||||
.idEqualTo(
|
||||
entry.chapters
|
||||
.toList()[i]
|
||||
.id,
|
||||
)
|
||||
.findAllSync();
|
||||
|
||||
if (entries.isNotEmpty &&
|
||||
entries.first.isDownload!) {
|
||||
nbrDown.add(1);
|
||||
}
|
||||
}
|
||||
});
|
||||
final chapterIds = entry.chapters
|
||||
.toList()
|
||||
.map((c) => c.id)
|
||||
.whereType<int>()
|
||||
.toList();
|
||||
if (chapterIds.isNotEmpty) {
|
||||
nbrDown = isar.downloads
|
||||
.filter()
|
||||
.anyOf(
|
||||
chapterIds,
|
||||
(q, id) => q.idEqualTo(id),
|
||||
)
|
||||
.isDownloadEqualTo(true)
|
||||
.findAllSync();
|
||||
}
|
||||
}
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
if (nbrDown.isNotEmpty &&
|
||||
|
|
@ -303,116 +293,7 @@ class _LibraryGridViewWidgetState extends State<LibraryGridViewWidget> {
|
|||
right: 0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(9),
|
||||
child: Consumer(
|
||||
builder: (context, ref, child) {
|
||||
return StreamBuilder(
|
||||
stream: isar.historys
|
||||
.filter()
|
||||
.idIsNotNull()
|
||||
.and()
|
||||
.chapter(
|
||||
(q) => q.manga(
|
||||
(q) =>
|
||||
q.itemTypeEqualTo(entry.itemType),
|
||||
),
|
||||
)
|
||||
.watch(fireImmediately: true),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData &&
|
||||
snapshot.data!.isNotEmpty) {
|
||||
final incognitoMode = ref.watch(
|
||||
incognitoModeStateProvider,
|
||||
);
|
||||
final entries = snapshot.data!
|
||||
.where(
|
||||
(element) =>
|
||||
element.mangaId == entry.id,
|
||||
)
|
||||
.toList();
|
||||
if (entries.isNotEmpty &&
|
||||
!incognitoMode) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
entries.first.chapter.value!
|
||||
.pushToReaderView(context);
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
BorderRadius.circular(5),
|
||||
color: context.primaryColor
|
||||
.withValues(alpha: 0.9),
|
||||
),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(7),
|
||||
child: Icon(
|
||||
Icons.play_arrow,
|
||||
size: 19,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
entry.chapters
|
||||
.toList()
|
||||
.reversed
|
||||
.toList()
|
||||
.last
|
||||
.pushToReaderView(context);
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(
|
||||
5,
|
||||
),
|
||||
color: context.primaryColor
|
||||
.withValues(alpha: 0.9),
|
||||
),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(7),
|
||||
child: Icon(
|
||||
Icons.play_arrow,
|
||||
size: 19,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
entry.chapters
|
||||
.toList()
|
||||
.reversed
|
||||
.toList()
|
||||
.last
|
||||
.pushToReaderView(context);
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(
|
||||
5,
|
||||
),
|
||||
color: context.primaryColor
|
||||
.withValues(alpha: 0.9),
|
||||
),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(7),
|
||||
child: Icon(
|
||||
Icons.play_arrow,
|
||||
size: 19,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
child: ContinueReaderButton(entry: entry),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -3,19 +3,16 @@ 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/history.dart';
|
||||
import 'package:mangayomi/modules/library/providers/isar_providers.dart';
|
||||
import 'package:mangayomi/modules/library/providers/library_state_provider.dart';
|
||||
import 'package:mangayomi/models/manga.dart';
|
||||
import 'package:mangayomi/modules/library/widgets/continue_reader_button.dart';
|
||||
import 'package:mangayomi/modules/manga/detail/providers/state_providers.dart';
|
||||
import 'package:mangayomi/modules/widgets/custom_extended_image_provider.dart';
|
||||
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
|
||||
import 'package:mangayomi/utils/constant.dart';
|
||||
import 'package:mangayomi/utils/extensions/chapter.dart';
|
||||
import 'package:mangayomi/utils/headers.dart';
|
||||
import 'package:mangayomi/modules/more/providers/incognito_mode_state_provider.dart';
|
||||
import 'package:mangayomi/modules/widgets/listview_widget.dart';
|
||||
import 'package:mangayomi/modules/widgets/manga_image_card_widget.dart';
|
||||
|
||||
|
|
@ -23,7 +20,7 @@ class LibraryListViewWidget extends StatelessWidget {
|
|||
final List<Manga> entriesManga;
|
||||
final bool language;
|
||||
final bool downloadedChapter;
|
||||
final List<int> mangaIdsList;
|
||||
final Set<int> mangaIdsList;
|
||||
final bool continueReaderBtn;
|
||||
final bool localSource;
|
||||
const LibraryListViewWidget({
|
||||
|
|
@ -49,7 +46,7 @@ class LibraryListViewWidget extends StatelessWidget {
|
|||
return Material(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
color: Colors.transparent,
|
||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
if (isLongPressed) {
|
||||
|
|
@ -208,28 +205,22 @@ class LibraryListViewWidget extends StatelessWidget {
|
|||
),
|
||||
child: Consumer(
|
||||
builder: (context, ref, child) {
|
||||
List nbrDown = [];
|
||||
isar.txnSync(() {
|
||||
for (
|
||||
var i = 0;
|
||||
i < entry.chapters.length;
|
||||
i++
|
||||
) {
|
||||
final entries = isar.downloads
|
||||
.filter()
|
||||
.idEqualTo(
|
||||
entry.chapters
|
||||
.toList()[i]
|
||||
.id,
|
||||
)
|
||||
.findAllSync();
|
||||
|
||||
if (entries.isNotEmpty &&
|
||||
entries.first.isDownload!) {
|
||||
nbrDown.add(entries.first);
|
||||
}
|
||||
}
|
||||
});
|
||||
final chapterIds = entry.chapters
|
||||
.toList()
|
||||
.map((c) => c.id)
|
||||
.whereType<int>()
|
||||
.toList();
|
||||
List nbrDown = chapterIds.isNotEmpty
|
||||
? isar.downloads
|
||||
.filter()
|
||||
.anyOf(
|
||||
chapterIds,
|
||||
(q, id) =>
|
||||
q.idEqualTo(id),
|
||||
)
|
||||
.isDownloadEqualTo(true)
|
||||
.findAllSync()
|
||||
: [];
|
||||
if (nbrDown.isNotEmpty) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
|
|
@ -307,117 +298,7 @@ class LibraryListViewWidget extends StatelessWidget {
|
|||
),
|
||||
),
|
||||
if (continueReaderBtn)
|
||||
Consumer(
|
||||
builder: (context, ref, child) {
|
||||
return StreamBuilder(
|
||||
stream: isar.historys
|
||||
.filter()
|
||||
.idIsNotNull()
|
||||
.and()
|
||||
.chapter(
|
||||
(q) => q.manga(
|
||||
(q) =>
|
||||
q.itemTypeEqualTo(entry.itemType),
|
||||
),
|
||||
)
|
||||
.watch(fireImmediately: true),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData &&
|
||||
snapshot.data!.isNotEmpty) {
|
||||
final incognitoMode = ref.watch(
|
||||
incognitoModeStateProvider,
|
||||
);
|
||||
final entries = snapshot.data!
|
||||
.where(
|
||||
(element) =>
|
||||
element.mangaId == entry.id,
|
||||
)
|
||||
.toList();
|
||||
if (entries.isNotEmpty &&
|
||||
!incognitoMode) {
|
||||
final chap =
|
||||
entries.first.chapter.value!;
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
chap.pushToReaderView(context);
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
BorderRadius.circular(5),
|
||||
color: context.primaryColor
|
||||
.withValues(alpha: 0.9),
|
||||
),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(7),
|
||||
child: Icon(
|
||||
Icons.play_arrow,
|
||||
size: 19,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
entry.chapters
|
||||
.toList()
|
||||
.reversed
|
||||
.toList()
|
||||
.last
|
||||
.pushToReaderView(context);
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(
|
||||
5,
|
||||
),
|
||||
color: context.primaryColor
|
||||
.withValues(alpha: 0.9),
|
||||
),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(7),
|
||||
child: Icon(
|
||||
Icons.play_arrow,
|
||||
size: 19,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
entry.chapters
|
||||
.toList()
|
||||
.reversed
|
||||
.toList()
|
||||
.last
|
||||
.pushToReaderView(context);
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(
|
||||
5,
|
||||
),
|
||||
color: context.primaryColor
|
||||
.withValues(alpha: 0.9),
|
||||
),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(7),
|
||||
child: Icon(
|
||||
Icons.play_arrow,
|
||||
size: 19,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
ContinueReaderButton(entry: entry),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -2509,7 +2509,7 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
|
|||
child: Material(
|
||||
color: bgColor,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: SuperListView.separated(
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ class _TrackerWidgetSearchState extends ConsumerState<TrackerWidgetSearch> {
|
|||
bottomLeft: Radius.circular(20),
|
||||
bottomRight: Radius.circular(20),
|
||||
),
|
||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: _isLoading
|
||||
? SizedBox(
|
||||
height: context.height(0.3),
|
||||
|
|
@ -123,8 +123,7 @@ class _TrackerWidgetSearchState extends ConsumerState<TrackerWidgetSearch> {
|
|||
5,
|
||||
),
|
||||
color: Colors.transparent,
|
||||
clipBehavior:
|
||||
Clip.antiAliasWithSaveLayer,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Ink.image(
|
||||
height: 120,
|
||||
width: 80,
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ final class DownloadChapterProvider
|
|||
}
|
||||
}
|
||||
|
||||
String _$downloadChapterHash() => r'690619b8914877f3913ed1601818b6149752279b';
|
||||
String _$downloadChapterHash() => r'c0d7bc9cd975bb5f1abdf29f9aa6d9d8dc8ca441';
|
||||
|
||||
final class DownloadChapterFamily extends $Family
|
||||
with
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ class ImageViewVertical extends ConsumerWidget {
|
|||
final UChapDataPreload data;
|
||||
final Function(UChapDataPreload data) onLongPressData;
|
||||
final bool isHorizontal;
|
||||
final ValueNotifier<bool> isScrolling;
|
||||
|
||||
final Function(bool) failedToLoadImage;
|
||||
|
||||
|
|
@ -23,94 +24,100 @@ class ImageViewVertical extends ConsumerWidget {
|
|||
required this.onLongPressData,
|
||||
required this.failedToLoadImage,
|
||||
required this.isHorizontal,
|
||||
required this.isScrolling,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final (colorBlendMode, color) = chapterColorFIlterValues(context, ref);
|
||||
final imageWidget = ExtendedImage(
|
||||
colorBlendMode: colorBlendMode,
|
||||
color: color,
|
||||
image: data.getImageProvider(ref, true),
|
||||
filterQuality: FilterQuality.medium,
|
||||
handleLoadingProgress: true,
|
||||
fit: getBoxFit(ref.watch(scaleTypeStateProvider)),
|
||||
enableLoadState: true,
|
||||
loadStateChanged: (state) {
|
||||
if (state.extendedImageLoadState == LoadState.completed) {
|
||||
failedToLoadImage(false);
|
||||
final rawSize = state.extendedImageInfo?.image;
|
||||
if (rawSize != null && data.loadedHeight == null) {
|
||||
final screenWidth = isHorizontal
|
||||
? context.width(0.8)
|
||||
: MediaQuery.of(context).size.width;
|
||||
final aspect = rawSize.width / rawSize.height;
|
||||
data.loadedWidth = screenWidth;
|
||||
data.loadedHeight = screenWidth / aspect;
|
||||
final imageWidget = ValueListenableBuilder<bool>(
|
||||
valueListenable: isScrolling,
|
||||
builder: (context, scrolling, _) => ExtendedImage(
|
||||
colorBlendMode: colorBlendMode,
|
||||
color: color,
|
||||
image: data.getImageProvider(ref, true),
|
||||
filterQuality: scrolling ? FilterQuality.low : FilterQuality.medium,
|
||||
handleLoadingProgress: true,
|
||||
fit: getBoxFit(ref.watch(scaleTypeStateProvider)),
|
||||
enableLoadState: true,
|
||||
loadStateChanged: (state) {
|
||||
if (state.extendedImageLoadState == LoadState.completed) {
|
||||
failedToLoadImage(false);
|
||||
final rawSize = state.extendedImageInfo?.image;
|
||||
if (rawSize != null && data.loadedHeight == null) {
|
||||
final screenWidth = isHorizontal
|
||||
? context.width(0.8)
|
||||
: MediaQuery.of(context).size.width;
|
||||
final aspect = rawSize.width / rawSize.height;
|
||||
data.loadedWidth = screenWidth;
|
||||
data.loadedHeight = screenWidth / aspect;
|
||||
}
|
||||
}
|
||||
}
|
||||
final placeholderHeight = data.loadedHeight ?? context.height(0.8);
|
||||
final placeholderWidth = isHorizontal
|
||||
? (data.loadedWidth ?? context.width(0.8))
|
||||
: null;
|
||||
if (state.extendedImageLoadState == LoadState.loading) {
|
||||
final ImageChunkEvent? loadingProgress = state.loadingProgress;
|
||||
final double progress = loadingProgress?.expectedTotalBytes != null
|
||||
? loadingProgress!.cumulativeBytesLoaded /
|
||||
loadingProgress.expectedTotalBytes!
|
||||
: 0;
|
||||
return Container(
|
||||
color: Colors.black,
|
||||
height: placeholderHeight,
|
||||
width: placeholderWidth,
|
||||
child: CircularProgressIndicatorAnimateRotate(progress: progress),
|
||||
);
|
||||
}
|
||||
if (state.extendedImageLoadState == LoadState.failed) {
|
||||
failedToLoadImage(true);
|
||||
return Container(
|
||||
color: Colors.black,
|
||||
height: placeholderHeight,
|
||||
width: placeholderWidth,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.image_loading_error,
|
||||
style: TextStyle(color: Colors.white.withValues(alpha: 0.7)),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: GestureDetector(
|
||||
onLongPress: () {
|
||||
state.reLoadImage();
|
||||
failedToLoadImage(false);
|
||||
},
|
||||
onTap: () {
|
||||
state.reLoadImage();
|
||||
failedToLoadImage(false);
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: context.primaryColor,
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 16,
|
||||
final placeholderHeight = data.loadedHeight ?? context.height(0.8);
|
||||
final placeholderWidth = isHorizontal
|
||||
? (data.loadedWidth ?? context.width(0.8))
|
||||
: null;
|
||||
if (state.extendedImageLoadState == LoadState.loading) {
|
||||
final ImageChunkEvent? loadingProgress = state.loadingProgress;
|
||||
final double progress = loadingProgress?.expectedTotalBytes != null
|
||||
? loadingProgress!.cumulativeBytesLoaded /
|
||||
loadingProgress.expectedTotalBytes!
|
||||
: 0;
|
||||
return Container(
|
||||
color: Colors.black,
|
||||
height: placeholderHeight,
|
||||
width: placeholderWidth,
|
||||
child: CircularProgressIndicatorAnimateRotate(progress: progress),
|
||||
);
|
||||
}
|
||||
if (state.extendedImageLoadState == LoadState.failed) {
|
||||
failedToLoadImage(true);
|
||||
return Container(
|
||||
color: Colors.black,
|
||||
height: placeholderHeight,
|
||||
width: placeholderWidth,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.image_loading_error,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: GestureDetector(
|
||||
onLongPress: () {
|
||||
state.reLoadImage();
|
||||
failedToLoadImage(false);
|
||||
},
|
||||
onTap: () {
|
||||
state.reLoadImage();
|
||||
failedToLoadImage(false);
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: context.primaryColor,
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 16,
|
||||
),
|
||||
child: Text(context.l10n.retry),
|
||||
),
|
||||
child: Text(context.l10n.retry),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
);
|
||||
return applyReaderColorFilter(
|
||||
GestureDetector(
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ class ImageViewWebtoon extends StatelessWidget {
|
|||
final int webtoonSidePadding;
|
||||
final bool showPageGaps;
|
||||
final bool reverse;
|
||||
final ValueNotifier<bool> isScrolling;
|
||||
|
||||
const ImageViewWebtoon({
|
||||
super.key,
|
||||
|
|
@ -57,6 +58,7 @@ class ImageViewWebtoon extends StatelessWidget {
|
|||
required this.onScaleEnd,
|
||||
required this.onDoubleTapDown,
|
||||
required this.onDoubleTap,
|
||||
required this.isScrolling,
|
||||
this.webtoonSidePadding = 0,
|
||||
this.showPageGaps = true,
|
||||
this.reverse = false,
|
||||
|
|
@ -89,11 +91,17 @@ class ImageViewWebtoon extends StatelessWidget {
|
|||
}
|
||||
|
||||
Widget _buildItem(BuildContext context, int index) {
|
||||
if (isDoublePageMode && !isHorizontalContinuous) {
|
||||
return _buildDoublePageItem(context, index);
|
||||
} else {
|
||||
return _buildSinglePageItem(context, index);
|
||||
}
|
||||
final currentPage = pages[index];
|
||||
final uniqueKey = ValueKey(
|
||||
'${currentPage.chapter?.id ?? "trans"}-${currentPage.index ?? index}',
|
||||
);
|
||||
|
||||
return KeyedSubtree(
|
||||
key: uniqueKey,
|
||||
child: (isDoublePageMode && !isHorizontalContinuous)
|
||||
? _buildDoublePageItem(context, index)
|
||||
: _buildSinglePageItem(context, index),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSinglePageItem(BuildContext context, int index) {
|
||||
|
|
@ -124,6 +132,7 @@ class ImageViewWebtoon extends StatelessWidget {
|
|||
failedToLoadImage: onFailedToLoadImage,
|
||||
onLongPressData: onLongPressData,
|
||||
isHorizontal: isHorizontalContinuous,
|
||||
isScrolling: isScrolling,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -20,9 +20,6 @@ class ChapterPreloadManager {
|
|||
/// Queue of chapter IDs in order of loading (for LRU eviction)
|
||||
final Queue<String> _chapterLoadOrder = Queue();
|
||||
|
||||
/// Current reading index
|
||||
int _currentIndex = 0;
|
||||
|
||||
/// Separate flags to allow concurrent prev/next preloading
|
||||
bool _isPreloadingNext = false;
|
||||
bool _isPreloadingPrev = false;
|
||||
|
|
@ -36,9 +33,6 @@ class ChapterPreloadManager {
|
|||
/// Gets the current number of pages
|
||||
int get pageCount => _pages.length;
|
||||
|
||||
/// Gets the current index
|
||||
int get currentIndex => _currentIndex;
|
||||
|
||||
/// Gets the loaded chapter count
|
||||
int get loadedChapterCount => _loadedChapterIds.length;
|
||||
|
||||
|
|
@ -48,13 +42,6 @@ class ChapterPreloadManager {
|
|||
/// Whether a next chapter preload is in progress.
|
||||
bool get isPreloadingNext => _isPreloadingNext;
|
||||
|
||||
/// Sets the current reading index
|
||||
set currentIndex(int value) {
|
||||
if (value >= 0 && value < _pages.length) {
|
||||
_currentIndex = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if pages from [chapter] are already in memory.
|
||||
bool isChapterLoaded(Chapter? chapter) {
|
||||
final id = _getChapterIdentifier(chapter);
|
||||
|
|
@ -62,13 +49,12 @@ class ChapterPreloadManager {
|
|||
}
|
||||
|
||||
/// Initializes the manager with the first chapter's pages.
|
||||
void initialize(List<UChapDataPreload> initialPages, int startIndex) {
|
||||
void initialize(List<UChapDataPreload> initialPages) {
|
||||
_pages.clear();
|
||||
_loadedChapterIds.clear();
|
||||
_chapterLoadOrder.clear();
|
||||
|
||||
_pages.addAll(initialPages);
|
||||
_currentIndex = startIndex;
|
||||
|
||||
// Track the initial chapter
|
||||
if (initialPages.isNotEmpty) {
|
||||
|
|
@ -263,9 +249,6 @@ class ChapterPreloadManager {
|
|||
// Prepend to pages list
|
||||
_pages.insertAll(0, prependList);
|
||||
|
||||
// Update current index to account for prepended pages
|
||||
_currentIndex += prependCount;
|
||||
|
||||
// Track the new chapter
|
||||
if (chapterId != null) {
|
||||
_loadedChapterIds.add(chapterId);
|
||||
|
|
|
|||
|
|
@ -20,23 +20,13 @@ mixin ReaderMemoryManagement {
|
|||
/// Gets the total page count.
|
||||
int get pageCount => _preloadManager.pageCount;
|
||||
|
||||
/// Gets the current page index.
|
||||
int get currentPageIndex => _preloadManager.currentIndex;
|
||||
|
||||
/// Sets the current page index.
|
||||
set currentPageIndex(int value) {
|
||||
_preloadManager.currentIndex = value;
|
||||
}
|
||||
|
||||
/// Initializes the preload manager with initial chapter data.
|
||||
///
|
||||
/// [chapterData] - The initial chapter pages to load.
|
||||
/// [startIndex] - The initial page index (default: 0).
|
||||
/// [onPagesUpdated] - Callback when pages are added/removed.
|
||||
/// [onIndexAdjusted] - Callback when current index needs adjustment.
|
||||
void initializePreloadManager(
|
||||
GetChapterPagesModel chapterData, {
|
||||
int startIndex = 0,
|
||||
VoidCallback? onPagesUpdated,
|
||||
}) {
|
||||
if (_isPreloadManagerInitialized) {
|
||||
|
|
@ -48,7 +38,7 @@ mixin ReaderMemoryManagement {
|
|||
|
||||
_preloadManager.onPagesUpdated = onPagesUpdated;
|
||||
|
||||
_preloadManager.initialize(chapterData.uChapDataPreload, startIndex);
|
||||
_preloadManager.initialize(chapterData.uChapDataPreload);
|
||||
|
||||
_isPreloadManagerInitialized = true;
|
||||
|
||||
|
|
|
|||
|
|
@ -67,6 +67,9 @@ class ReaderController extends _$ReaderController {
|
|||
}
|
||||
|
||||
final incognitoMode = isar.settings.getSync(227)!.incognitoMode!;
|
||||
Settings? _cachedSettings;
|
||||
void _invalidateSettingsCache() => _cachedSettings = null;
|
||||
|
||||
ReaderMode getReaderMode() {
|
||||
final personalReaderModeList =
|
||||
getIsarSetting().personalReaderModeList ?? [];
|
||||
|
|
@ -113,6 +116,7 @@ class ReaderController extends _$ReaderController {
|
|||
..updatedAt = DateTime.now().millisecondsSinceEpoch,
|
||||
),
|
||||
);
|
||||
_invalidateSettingsCache();
|
||||
}
|
||||
|
||||
PageMode getPageMode() {
|
||||
|
|
@ -146,6 +150,7 @@ class ReaderController extends _$ReaderController {
|
|||
..updatedAt = DateTime.now().millisecondsSinceEpoch,
|
||||
),
|
||||
);
|
||||
_invalidateSettingsCache();
|
||||
}
|
||||
|
||||
void setPageMode(PageMode newPageMode) {
|
||||
|
|
@ -167,6 +172,7 @@ class ReaderController extends _$ReaderController {
|
|||
..updatedAt = DateTime.now().millisecondsSinceEpoch,
|
||||
),
|
||||
);
|
||||
_invalidateSettingsCache();
|
||||
}
|
||||
|
||||
void setShowPageNumber(bool value) {
|
||||
|
|
@ -178,17 +184,14 @@ class ReaderController extends _$ReaderController {
|
|||
..updatedAt = DateTime.now().millisecondsSinceEpoch,
|
||||
),
|
||||
);
|
||||
_invalidateSettingsCache();
|
||||
}
|
||||
}
|
||||
|
||||
Settings getIsarSetting() {
|
||||
return isar.settings.getSync(227)!;
|
||||
}
|
||||
Settings getIsarSetting() => _cachedSettings ??= isar.settings.getSync(227)!;
|
||||
|
||||
bool getShowPageNumber() {
|
||||
if (!incognitoMode) {
|
||||
return getIsarSetting().showPagesNumber!;
|
||||
}
|
||||
if (!incognitoMode) return getIsarSetting().showPagesNumber!;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -353,9 +356,11 @@ class ReaderController extends _$ReaderController {
|
|||
return urls.length;
|
||||
}
|
||||
|
||||
int? _lastSavedIndex;
|
||||
void setPageIndex(int newIndex, bool save) {
|
||||
if (chapter.isRead!) return;
|
||||
if (incognitoMode) return;
|
||||
if (chapter.isRead! || incognitoMode) return;
|
||||
if (!save && newIndex == _lastSavedIndex) return;
|
||||
_lastSavedIndex = newIndex;
|
||||
final isContinuousLike =
|
||||
getReaderMode() == ReaderMode.verticalContinuous ||
|
||||
getReaderMode() == ReaderMode.webtoon;
|
||||
|
|
@ -387,6 +392,7 @@ class ReaderController extends _$ReaderController {
|
|||
chap.updatedAt = DateTime.now().millisecondsSinceEpoch;
|
||||
isar.chapters.putSync(chap);
|
||||
});
|
||||
_invalidateSettingsCache();
|
||||
if (isRead) {
|
||||
chapter.updateTrackChapterRead(ref);
|
||||
if (ref.read(deleteDownloadAfterReadingStateProvider)) {
|
||||
|
|
@ -469,10 +475,9 @@ extension ChapterExtensions on Chapter {
|
|||
extension MangaExtensions on Manga {
|
||||
List<Chapter> getFilteredChapterList() {
|
||||
final data = this.chapters.toList().reversed.toList();
|
||||
final settings = isar.settings.getSync(227)!;
|
||||
final filterUnread =
|
||||
(isar.settings
|
||||
.getSync(227)!
|
||||
.chapterFilterUnreadList!
|
||||
(settings.chapterFilterUnreadList!
|
||||
.where((element) => element.mangaId == id)
|
||||
.toList()
|
||||
.firstOrNull ??
|
||||
|
|
@ -480,18 +485,14 @@ extension MangaExtensions on Manga {
|
|||
.type!;
|
||||
|
||||
final filterBookmarked =
|
||||
(isar.settings
|
||||
.getSync(227)!
|
||||
.chapterFilterBookmarkedList!
|
||||
(settings.chapterFilterBookmarkedList!
|
||||
.where((element) => element.mangaId == id)
|
||||
.toList()
|
||||
.firstOrNull ??
|
||||
ChapterFilterBookmarked(mangaId: id, type: 0))
|
||||
.type!;
|
||||
final filterDownloaded =
|
||||
(isar.settings
|
||||
.getSync(227)!
|
||||
.chapterFilterDownloadedList!
|
||||
(settings.chapterFilterDownloadedList!
|
||||
.where((element) => element.mangaId == id)
|
||||
.toList()
|
||||
.firstOrNull ??
|
||||
|
|
@ -499,15 +500,23 @@ extension MangaExtensions on Manga {
|
|||
.type!;
|
||||
|
||||
final sortChapter =
|
||||
(isar.settings
|
||||
.getSync(227)!
|
||||
.sortChapterList!
|
||||
(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(
|
||||
|
|
@ -525,21 +534,13 @@ extension MangaExtensions on Manga {
|
|||
: true,
|
||||
)
|
||||
.where((element) {
|
||||
final modelChapDownload = isar.downloads
|
||||
.filter()
|
||||
.idEqualTo(element.id)
|
||||
.findAllSync();
|
||||
return filterDownloaded == 1
|
||||
? modelChapDownload.isNotEmpty &&
|
||||
modelChapDownload.first.isDownload == true
|
||||
: filterDownloaded == 2
|
||||
? !(modelChapDownload.isNotEmpty &&
|
||||
modelChapDownload.first.isDownload == true)
|
||||
: true;
|
||||
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 == 1
|
||||
List<Chapter> chapters = sortChapter == 0
|
||||
? chapterList.reversed.toList()
|
||||
: chapterList;
|
||||
if (sortChapter == 0) {
|
||||
|
|
@ -565,7 +566,7 @@ extension MangaExtensions on Manga {
|
|||
: a.name!.compareTo(b.name!);
|
||||
});
|
||||
}
|
||||
return chapterList;
|
||||
return chapters;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ final class ReaderControllerProvider
|
|||
}
|
||||
}
|
||||
|
||||
String _$readerControllerHash() => r'fd5ce439786209d9e218fa4067f91f606bb8458a';
|
||||
String _$readerControllerHash() => r'adab728fa21939302d0f928b11be204e8e8a0527';
|
||||
|
||||
final class ReaderControllerFamily extends $Family
|
||||
with
|
||||
|
|
|
|||
|
|
@ -152,6 +152,8 @@ class _MangaChapterPageGalleryState
|
|||
);
|
||||
|
||||
bool isDesktop = Platform.isMacOS || Platform.isLinux || Platform.isWindows;
|
||||
final ValueNotifier<bool> _isScrolling = ValueNotifier(false);
|
||||
Timer? _scrollIdleTimer;
|
||||
|
||||
final Stopwatch _readingStopwatch = Stopwatch();
|
||||
|
||||
|
|
@ -174,6 +176,8 @@ class _MangaChapterPageGalleryState
|
|||
_autoScroll.value = false;
|
||||
_autoScroll.dispose();
|
||||
_autoScrollPage.dispose();
|
||||
_scrollIdleTimer?.cancel();
|
||||
_isScrolling.dispose();
|
||||
_itemPositionsListener.itemPositions.removeListener(_readProgressListener);
|
||||
_photoViewController.dispose();
|
||||
_photoViewScaleStateController.dispose();
|
||||
|
|
@ -344,14 +348,10 @@ class _MangaChapterPageGalleryState
|
|||
Widget build(BuildContext context) {
|
||||
final backgroundColor = ref.watch(backgroundColorStateProvider);
|
||||
final fullScreenReader = ref.watch(fullScreenReaderStateProvider);
|
||||
final cropBorders = ref.watch(cropBordersStateProvider);
|
||||
final readerMode = ref.watch(_currentReaderMode);
|
||||
final bool isHorizontalContinuaous =
|
||||
final bool isHorizontalContinuous =
|
||||
readerMode == ReaderMode.horizontalContinuous ||
|
||||
readerMode == ReaderMode.horizontalContinuousRTL;
|
||||
if (cropBorders) {
|
||||
_processCropBorders();
|
||||
}
|
||||
|
||||
final l10n = l10nLocalizations(context)!;
|
||||
return ReaderKeyboardHandler(
|
||||
|
|
@ -419,7 +419,7 @@ class _MangaChapterPageGalleryState
|
|||
itemScrollController: _itemScrollController,
|
||||
scrollOffsetController: _pageOffsetController,
|
||||
itemPositionsListener: _itemPositionsListener,
|
||||
scrollDirection: isHorizontalContinuaous
|
||||
scrollDirection: isHorizontalContinuous
|
||||
? Axis.horizontal
|
||||
: Axis.vertical,
|
||||
minCacheExtent:
|
||||
|
|
@ -434,7 +434,7 @@ class _MangaChapterPageGalleryState
|
|||
chapterName: widget.chapter.name!,
|
||||
),
|
||||
onFailedToLoadImage: (value) {
|
||||
// // Handle failed image loading
|
||||
// TODO: Handle failed image loading
|
||||
// if (_failedToLoadImage.value != value &&
|
||||
// context.mounted) {
|
||||
// _failedToLoadImage.value = value;
|
||||
|
|
@ -443,8 +443,8 @@ class _MangaChapterPageGalleryState
|
|||
backgroundColor: backgroundColor,
|
||||
isDoublePageMode:
|
||||
_pageMode == PageMode.doublePage &&
|
||||
!isHorizontalContinuaous,
|
||||
isHorizontalContinuous: isHorizontalContinuaous,
|
||||
!isHorizontalContinuous,
|
||||
isHorizontalContinuous: isHorizontalContinuous,
|
||||
readerMode: ref.watch(_currentReaderMode)!,
|
||||
photoViewController: _photoViewController,
|
||||
photoViewScaleStateController:
|
||||
|
|
@ -462,13 +462,14 @@ class _MangaChapterPageGalleryState
|
|||
),
|
||||
showPageGaps: ref.watch(showPageGapsStateProvider),
|
||||
reverse: _isReverseHorizontal,
|
||||
isScrolling: _isScrolling,
|
||||
)
|
||||
: Material(
|
||||
color: getBackgroundColor(backgroundColor),
|
||||
shadowColor: getBackgroundColor(backgroundColor),
|
||||
child:
|
||||
(_pageMode == PageMode.doublePage &&
|
||||
!isHorizontalContinuaous)
|
||||
!isHorizontalContinuous)
|
||||
? ExtendedImageGesturePageView.builder(
|
||||
controller: _extendedController,
|
||||
scrollDirection: _scrollDirection,
|
||||
|
|
@ -958,57 +959,60 @@ class _MangaChapterPageGalleryState
|
|||
void _readProgressListener() async {
|
||||
if (_isAdjustingScroll) return;
|
||||
final itemPositions = _itemPositionsListener.itemPositions.value;
|
||||
if (itemPositions.isNotEmpty) {
|
||||
_currentIndex = itemPositions.first.index;
|
||||
int pagesLength =
|
||||
(_pageMode == PageMode.doublePage &&
|
||||
!(ref.watch(_currentReaderMode) ==
|
||||
ReaderMode.horizontalContinuous ||
|
||||
ref.watch(_currentReaderMode) ==
|
||||
ReaderMode.horizontalContinuousRTL))
|
||||
? (pages.length / 2).ceil()
|
||||
: pages.length;
|
||||
if (_currentIndex! >= 0 && _currentIndex! < pagesLength) {
|
||||
if (_readerController.chapter.id != pages[_currentIndex!].chapter!.id) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_readerController = ref.read(
|
||||
readerControllerProvider(
|
||||
chapter: pages[_currentIndex!].chapter!,
|
||||
).notifier,
|
||||
);
|
||||
if (itemPositions.isEmpty) return;
|
||||
_currentIndex = itemPositions.first.index;
|
||||
if (!_isScrolling.value) _isScrolling.value = true;
|
||||
_scrollIdleTimer?.cancel();
|
||||
_scrollIdleTimer = Timer(const Duration(milliseconds: 150), () {
|
||||
if (mounted) _isScrolling.value = false;
|
||||
});
|
||||
final currentReaderMode = ref.read(_currentReaderMode);
|
||||
int pagesLength =
|
||||
(_pageMode == PageMode.doublePage &&
|
||||
currentReaderMode != ReaderMode.horizontalContinuous &&
|
||||
currentReaderMode != ReaderMode.horizontalContinuousRTL)
|
||||
? (pages.length / 2).ceil()
|
||||
: pages.length;
|
||||
if (_currentIndex! >= 0 && _currentIndex! < pagesLength) {
|
||||
if (_readerController.chapter.id != pages[_currentIndex!].chapter!.id) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_readerController = ref.read(
|
||||
readerControllerProvider(
|
||||
chapter: pages[_currentIndex!].chapter!,
|
||||
).notifier,
|
||||
);
|
||||
|
||||
chapter = pages[_currentIndex!].chapter!;
|
||||
final chapterUrlModel = pages[_currentIndex!].chapterUrlModel;
|
||||
chapter = pages[_currentIndex!].chapter!;
|
||||
final chapterUrlModel = pages[_currentIndex!].chapterUrlModel;
|
||||
|
||||
if (chapterUrlModel != null) {
|
||||
_chapterUrlModel = chapterUrlModel;
|
||||
}
|
||||
if (chapterUrlModel != null) {
|
||||
_chapterUrlModel = chapterUrlModel;
|
||||
}
|
||||
|
||||
_isBookmarked = _readerController.getChapterBookmarked();
|
||||
});
|
||||
}
|
||||
_isBookmarked = _readerController.getChapterBookmarked();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── Next-chapter preloading: trigger when near the end ──
|
||||
final distToEnd = pagesLength - 1 - itemPositions.last.index;
|
||||
if (distToEnd <= pagePreloadAmount && !_isLastPageTransition) {
|
||||
_triggerNextChapterPreload();
|
||||
}
|
||||
// ── Next-chapter preloading: trigger when near the end ──
|
||||
final distToEnd = pagesLength - 1 - itemPositions.last.index;
|
||||
if (distToEnd <= pagePreloadAmount && !_isLastPageTransition) {
|
||||
_triggerNextChapterPreload();
|
||||
}
|
||||
|
||||
// ── Previous-chapter preloading: trigger when near the start ──
|
||||
if (itemPositions.first.index <= pagePreloadAmount) {
|
||||
_triggerPrevChapterPreload();
|
||||
}
|
||||
// ── Previous-chapter preloading: trigger when near the start ──
|
||||
if (itemPositions.first.index <= pagePreloadAmount) {
|
||||
_triggerPrevChapterPreload();
|
||||
}
|
||||
|
||||
final idx = pages[_currentIndex!].index;
|
||||
if (idx != null) {
|
||||
_readerController.setPageIndex(
|
||||
_isDoublePageActive ? idx : _geCurrentIndex(idx),
|
||||
false,
|
||||
);
|
||||
ref.read(currentIndexProvider(chapter).notifier).setCurrentIndex(idx);
|
||||
}
|
||||
final idx = pages[_currentIndex!].index;
|
||||
if (idx != null) {
|
||||
_readerController.setPageIndex(
|
||||
_isDoublePageActive ? idx : _geCurrentIndex(idx),
|
||||
false,
|
||||
);
|
||||
ref.read(currentIndexProvider(chapter).notifier).setCurrentIndex(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1108,24 +1112,35 @@ class _MangaChapterPageGalleryState
|
|||
) {
|
||||
try {
|
||||
if (chapterData.uChapDataPreload.isEmpty || !mounted) return;
|
||||
|
||||
// Record the CURRENT visible top index BEFORE prepending
|
||||
final currentVisibleItems = _itemPositionsListener.itemPositions.value;
|
||||
final oldTopIndex = currentVisibleItems.isNotEmpty
|
||||
? currentVisibleItems.first.index
|
||||
: _currentIndex ?? 0;
|
||||
|
||||
preloadPreviousChapter(chapterData, chap).then((prependCount) {
|
||||
if (prependCount > 0 && mounted) {
|
||||
_isAdjustingScroll = true;
|
||||
|
||||
// New index = old visible index + how many items we just prepended
|
||||
final newIndex = oldTopIndex + prependCount;
|
||||
|
||||
// In double page mode, _currentIndex stores the page view index,
|
||||
// so convert the prepended page count to page view units.
|
||||
if (_isDoublePageActive) {
|
||||
// Recompute the page view index from the new actual index.
|
||||
final oldActual = _pageViewToActualIndex(_currentIndex!);
|
||||
final oldActual = _pageViewToActualIndex(oldTopIndex);
|
||||
final newActual = oldActual + prependCount;
|
||||
_currentIndex = _actualToPageViewIndex(newActual);
|
||||
} else {
|
||||
_currentIndex = _currentIndex! + prependCount;
|
||||
_currentIndex = newIndex;
|
||||
}
|
||||
setState(() {});
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
if (_isContinuousMode()) {
|
||||
_itemScrollController.jumpTo(index: _currentIndex!);
|
||||
_itemScrollController.jumpTo(index: newIndex);
|
||||
} else if (_extendedController.hasClients) {
|
||||
_extendedController.jumpToPage(_currentIndex!);
|
||||
}
|
||||
|
|
@ -1138,14 +1153,17 @@ class _MangaChapterPageGalleryState
|
|||
}
|
||||
|
||||
void _initCurrentIndex() async {
|
||||
if (ref.read(cropBordersStateProvider)) _processCropBorders();
|
||||
final readerMode = _readerController.getReaderMode();
|
||||
|
||||
// Initialize the preload manager with bounded memory (from ReaderMemoryManagement mixin)
|
||||
initializePreloadManager(
|
||||
_chapterUrlModel,
|
||||
startIndex: _currentIndex ?? 0,
|
||||
onPagesUpdated: () {
|
||||
if (mounted) setState(() {});
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
if (ref.read(cropBordersStateProvider)) _processCropBorders();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -1363,20 +1381,29 @@ class _MangaChapterPageGalleryState
|
|||
}
|
||||
}
|
||||
|
||||
bool _isCropBordersProcessing = false;
|
||||
void _processCropBorders() async {
|
||||
if (_cropBorderCheckList.length == pages.length) return;
|
||||
if (_isCropBordersProcessing ||
|
||||
_cropBorderCheckList.length == pages.length) {
|
||||
return;
|
||||
}
|
||||
_isCropBordersProcessing = true;
|
||||
|
||||
for (var i = 0; i < pages.length; i++) {
|
||||
if (!_cropBorderCheckList.contains(i)) {
|
||||
_cropBorderCheckList.add(i);
|
||||
if (!mounted) return;
|
||||
final value = await ref.read(
|
||||
cropBordersProvider(data: pages[i], cropBorder: true).future,
|
||||
);
|
||||
if (mounted) {
|
||||
updatePageCropImage(i, value);
|
||||
try {
|
||||
for (var i = 0; i < pages.length; i++) {
|
||||
if (!_cropBorderCheckList.contains(i)) {
|
||||
_cropBorderCheckList.add(i);
|
||||
if (!mounted) return;
|
||||
final value = await ref.read(
|
||||
cropBordersProvider(data: pages[i], cropBorder: true).future,
|
||||
);
|
||||
if (mounted) {
|
||||
updatePageCropImage(i, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
_isCropBordersProcessing = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1452,7 +1479,7 @@ class _MangaChapterPageGalleryState
|
|||
_isDoublePageActive ? (pages.length / 2).ceil() : pages.length;
|
||||
|
||||
bool _isContinuousMode() {
|
||||
final readerMode = ref.watch(_currentReaderMode);
|
||||
final readerMode = ref.read(_currentReaderMode);
|
||||
return readerMode == ReaderMode.verticalContinuous ||
|
||||
readerMode == ReaderMode.webtoon ||
|
||||
readerMode == ReaderMode.horizontalContinuous ||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ class UpdateChapterListTileWidget extends ConsumerWidget {
|
|||
return Material(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
color: Colors.transparent,
|
||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
chapter.pushToReaderView(context, ignoreIsRead: true);
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ class CoverViewWidget extends StatelessWidget {
|
|||
child: Material(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
color: Colors.transparent,
|
||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
onLongPress: onLongPress,
|
||||
|
|
|
|||
|
|
@ -212,7 +212,7 @@ class MangaImageCardListTileWidget extends ConsumerWidget {
|
|||
child: Material(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
color: Colors.transparent,
|
||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
pushToMangaReaderDetail(
|
||||
|
|
@ -258,7 +258,7 @@ class MangaImageCardListTileWidget extends ConsumerWidget {
|
|||
Material(
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
color: Colors.transparent,
|
||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Image(
|
||||
height: 55,
|
||||
width: 40,
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ final class GetChapterPagesProvider
|
|||
}
|
||||
}
|
||||
|
||||
String _$getChapterPagesHash() => r'544311ac02b1034b938bb5f85e97fe34683c26c7';
|
||||
String _$getChapterPagesHash() => r'593f5af68761ff44d50fb3667d6717edf58769d7';
|
||||
|
||||
final class GetChapterPagesFamily extends $Family
|
||||
with $FunctionalFamilyOverride<FutureOr<GetChapterPagesModel>, Chapter> {
|
||||
|
|
|
|||
Loading…
Reference in a new issue