mirror of
https://github.com/kodjodevf/mangayomi.git
synced 2026-05-24 04:22:18 +00:00
MangasListState previously stored selected manga IDs as List<int>. Every visible library card called .contains() on that list once per rebuild to determine its highlight state, making each check O(n) in the number of selected items. The provider's own update/selectAll/ selectSome methods also used .contains() and .remove() on a List. Change the state type to Set<int> throughout, making all membership checks O(1). Updated all consumers: library_gridview_widget, library_listview_widget, library_app_bar, library_dialogs, and MangasSetIsReadState.
441 lines
22 KiB
Dart
441 lines
22 KiB
Dart
import 'dart:typed_data';
|
|
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/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';
|
|
import 'package:mangayomi/modules/widgets/manga_image_card_widget.dart';
|
|
|
|
class LibraryGridViewWidget extends StatefulWidget {
|
|
final bool isCoverOnlyGrid;
|
|
final bool isComfortableGrid;
|
|
final Set<int> mangaIdsList;
|
|
final List<Manga> entriesManga;
|
|
final bool language;
|
|
final bool downloadedChapter;
|
|
final bool continueReaderBtn;
|
|
final bool localSource;
|
|
final ItemType itemType;
|
|
const LibraryGridViewWidget({
|
|
super.key,
|
|
required this.entriesManga,
|
|
required this.isCoverOnlyGrid,
|
|
this.isComfortableGrid = false,
|
|
required this.language,
|
|
required this.downloadedChapter,
|
|
required this.continueReaderBtn,
|
|
required this.mangaIdsList,
|
|
required this.localSource,
|
|
required this.itemType,
|
|
});
|
|
|
|
@override
|
|
State<LibraryGridViewWidget> createState() => _LibraryGridViewWidgetState();
|
|
}
|
|
|
|
class _LibraryGridViewWidgetState extends State<LibraryGridViewWidget> {
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Consumer(
|
|
builder: (context, ref, child) {
|
|
final isLongPressed = ref.watch(isLongPressedStateProvider);
|
|
final itemType = widget.itemType;
|
|
|
|
final gridSize = ref.watch(
|
|
libraryGridSizeStateProvider(itemType: itemType),
|
|
);
|
|
return GridViewWidget(
|
|
gridSize: gridSize,
|
|
childAspectRatio: widget.isComfortableGrid ? 0.642 : 0.69,
|
|
itemCount: widget.entriesManga.length,
|
|
itemBuilder: (context, index) {
|
|
final entry = widget.entriesManga[index];
|
|
|
|
return Builder(
|
|
builder: (context) {
|
|
bool isLocalArchive = entry.isLocalArchive ?? false;
|
|
return Padding(
|
|
padding: const EdgeInsets.all(2),
|
|
child: CoverViewWidget(
|
|
isLongPressed: widget.mangaIdsList.contains(entry.id),
|
|
bottomTextWidget: BottomTextWidget(
|
|
maxLines: 1,
|
|
text: entry.name!,
|
|
isComfortableGrid: widget.isComfortableGrid,
|
|
),
|
|
isComfortableGrid: widget.isComfortableGrid,
|
|
image: entry.customCoverImage != null
|
|
? MemoryImage(entry.customCoverImage as Uint8List)
|
|
as ImageProvider
|
|
: CustomExtendedNetworkImageProvider(
|
|
toImgUrl(
|
|
entry.customCoverFromTracker ??
|
|
entry.imageUrl ??
|
|
"",
|
|
),
|
|
headers: entry.isLocalArchive!
|
|
? null
|
|
: ref.watch(
|
|
headersProvider(
|
|
source: entry.source!,
|
|
lang: entry.lang!,
|
|
sourceId: entry.sourceId,
|
|
),
|
|
),
|
|
),
|
|
onTap: () async {
|
|
if (isLongPressed) {
|
|
ref
|
|
.read(mangasListStateProvider.notifier)
|
|
.update(entry);
|
|
} else {
|
|
await pushToMangaReaderDetail(
|
|
ref: ref,
|
|
archiveId: isLocalArchive ? entry.id : null,
|
|
context: context,
|
|
lang: entry.lang!,
|
|
mangaM: entry,
|
|
source: entry.source!,
|
|
sourceId: entry.sourceId,
|
|
);
|
|
if (context.mounted) {
|
|
ref.invalidate(
|
|
getAllMangaWithoutCategoriesStreamProvider(
|
|
itemType: widget.itemType,
|
|
),
|
|
);
|
|
ref.invalidate(
|
|
getAllMangaStreamProvider(
|
|
categoryId: null,
|
|
itemType: widget.itemType,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
},
|
|
onLongPress: () {
|
|
_handleLongOrSecondaryTap(isLongPressed, ref, entry);
|
|
},
|
|
onSecondaryTap: () {
|
|
_handleLongOrSecondaryTap(isLongPressed, ref, entry);
|
|
},
|
|
children: [
|
|
Stack(
|
|
children: [
|
|
Positioned(
|
|
top: 0,
|
|
left: 0,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(5),
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
borderRadius: BorderRadius.circular(3),
|
|
color: context.primaryColor,
|
|
),
|
|
child: Row(
|
|
children: [
|
|
if (widget.localSource && isLocalArchive)
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
borderRadius: const BorderRadius.only(
|
|
topLeft: Radius.circular(3),
|
|
bottomLeft: Radius.circular(3),
|
|
),
|
|
color: Theme.of(context).hintColor,
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.only(
|
|
left: 3,
|
|
right: 3,
|
|
),
|
|
child: Text(
|
|
"Local",
|
|
style: TextStyle(
|
|
color: context
|
|
.dynamicBlackWhiteColor,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.only(right: 5),
|
|
child: Consumer(
|
|
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);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
return Row(
|
|
children: [
|
|
if (nbrDown.isNotEmpty &&
|
|
widget.downloadedChapter)
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
borderRadius:
|
|
const BorderRadius.only(
|
|
topLeft:
|
|
Radius.circular(
|
|
3,
|
|
),
|
|
bottomLeft:
|
|
Radius.circular(
|
|
3,
|
|
),
|
|
),
|
|
color: Theme.of(
|
|
context,
|
|
).secondaryHeaderColor,
|
|
),
|
|
child: Padding(
|
|
padding:
|
|
const EdgeInsets.only(
|
|
left: 3,
|
|
right: 3,
|
|
),
|
|
child: Text(
|
|
nbrDown.length.toString(),
|
|
),
|
|
),
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.only(
|
|
left: 3,
|
|
),
|
|
child: Text(
|
|
entry.chapters
|
|
.where(
|
|
(element) =>
|
|
!element.isRead!,
|
|
)
|
|
.length
|
|
.toString(),
|
|
style: TextStyle(
|
|
color: context
|
|
.dynamicBlackWhiteColor,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
if (widget.language && entry.lang!.isNotEmpty)
|
|
Positioned(
|
|
top: 0,
|
|
right: 0,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(5),
|
|
child: Container(
|
|
color: context.themeData.cardColor,
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
borderRadius: const BorderRadius.only(
|
|
topLeft: Radius.circular(3),
|
|
bottomLeft: Radius.circular(3),
|
|
),
|
|
color: Theme.of(context).hintColor,
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.only(
|
|
left: 3,
|
|
right: 3,
|
|
),
|
|
child: Text(
|
|
entry.lang!.toUpperCase(),
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
if (!widget.isComfortableGrid && !widget.isCoverOnlyGrid)
|
|
BottomTextWidget(text: entry.name!),
|
|
if (widget.continueReaderBtn)
|
|
Positioned(
|
|
bottom: 0,
|
|
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,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
void _handleLongOrSecondaryTap(
|
|
bool isLongPressed,
|
|
WidgetRef ref,
|
|
Manga entry,
|
|
) {
|
|
if (!isLongPressed) {
|
|
ref.read(mangasListStateProvider.notifier).update(entry);
|
|
ref.read(isLongPressedStateProvider.notifier).update(!isLongPressed);
|
|
} else {
|
|
ref.read(mangasListStateProvider.notifier).update(entry);
|
|
}
|
|
}
|
|
}
|