mangayomi-mirror/lib/modules/library/widgets/library_gridview_widget.dart
NBA2K1 81123dc3cb Change the MangasListStateProvider to Set<int>
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.
2026-04-12 23:03:18 +02:00

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