From 525eeea3acdf2e7a9470c8bc63527cc8a8d1efbc Mon Sep 17 00:00:00 2001 From: NBA2K1 <78034913+NBA2K1@users.noreply.github.com> Date: Sat, 18 Apr 2026 23:00:56 +0200 Subject: [PATCH] Use shared library index for manga cards Replace per-widget Isar StreamBuilder usage with a single shared library index managed in MangaHomeScreen. - Subscribe once to manga updates and build a name -> Manga map - Pass libraryManga down to card widgets instead of querying per item - Remove StreamBuilder logic from MangaImageCardWidget and list tile - Use library data for cover, tracker image, and favorite state - Add favorite overlay indicator based on libraryManga - Clean up redundant filtering and improve performance This reduces rebuild overhead and avoids multiple database listeners per list/grid item. --- lib/modules/manga/home/manga_home_screen.dart | 33 ++ .../widgets/manga_image_card_widget.dart | 408 ++++++++---------- 2 files changed, 216 insertions(+), 225 deletions(-) diff --git a/lib/modules/manga/home/manga_home_screen.dart b/lib/modules/manga/home/manga_home_screen.dart index abd85246..bee14c24 100644 --- a/lib/modules/manga/home/manga_home_screen.dart +++ b/lib/modules/manga/home/manga_home_screen.dart @@ -2,8 +2,10 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; +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/settings.dart'; import 'package:mangayomi/models/source.dart'; @@ -67,6 +69,8 @@ class _MangaHomeScreenState extends ConsumerState { late bool isLocal = source.name == "local" && source.lang == ""; late List filters = isLocal ? [] : getFilterList(source: source); final List _mangaList = []; + late StreamSubscription> _mangaStreamSub; + Map _libraryIndex = {}; List _types(BuildContext context) { final l10n = l10nLocalizations(context)!; return [ @@ -120,10 +124,28 @@ class _MangaHomeScreenState extends ConsumerState { void initState() { super.initState(); _scrollController.addListener(_onScroll); + _mangaStreamSub = isar.mangas + .filter() + .sourceEqualTo(source.name) + .langEqualTo(source.lang) + .watch(fireImmediately: true) + .listen((mangas) { + if (mounted) { + setState(() { + _libraryIndex = { + for (final m in mangas.where( + (e) => e.sourceId == null || e.sourceId == source.id, + )) + if (m.name != null) m.name!: m, + }; + }); + } + }); } @override void dispose() { + _mangaStreamSub.cancel(); _scrollController.removeListener(_onScroll); _scrollController.dispose(); _textEditingController.dispose(); @@ -571,6 +593,8 @@ class _MangaHomeScreenState extends ConsumerState { itemType: source.itemType, manga: _mangaList[index], source: source, + libraryManga: + _libraryIndex[_mangaList[index].name], ); }, ) @@ -598,6 +622,9 @@ class _MangaHomeScreenState extends ConsumerState { manga: _mangaList[index], source: source, isComfortableGrid: isComfortableGrid, + libraryManga: + _libraryIndex[_mangaList[index] + .name], ); }, ); @@ -700,12 +727,14 @@ class MangaHomeImageCard extends ConsumerStatefulWidget { final ItemType itemType; final Source source; final bool isComfortableGrid; + final Manga? libraryManga; const MangaHomeImageCard({ super.key, required this.manga, required this.source, required this.itemType, required this.isComfortableGrid, + this.libraryManga, }); @override @@ -720,6 +749,7 @@ class _MangaHomeImageCardState extends ConsumerState { source: widget.source, itemType: widget.itemType, isComfortableGrid: widget.isComfortableGrid, + libraryManga: widget.libraryManga, ); } } @@ -728,11 +758,13 @@ class MangaHomeImageCardListTile extends ConsumerStatefulWidget { final MManga manga; final ItemType itemType; final Source source; + final Manga? libraryManga; const MangaHomeImageCardListTile({ super.key, required this.manga, required this.source, required this.itemType, + this.libraryManga, }); @override @@ -748,6 +780,7 @@ class _MangaHomeImageCardListTileState getMangaDetail: widget.manga, source: widget.source, itemType: widget.itemType, + libraryManga: widget.libraryManga, ); } } diff --git a/lib/modules/widgets/manga_image_card_widget.dart b/lib/modules/widgets/manga_image_card_widget.dart index 99c098cc..fa1cca02 100644 --- a/lib/modules/widgets/manga_image_card_widget.dart +++ b/lib/modules/widgets/manga_image_card_widget.dart @@ -22,6 +22,7 @@ class MangaImageCardWidget extends ConsumerWidget { final ItemType itemType; final bool isComfortableGrid; final MManga? getMangaDetail; + final Manga? libraryManga; const MangaImageCardWidget({ required this.source, @@ -29,57 +30,150 @@ class MangaImageCardWidget extends ConsumerWidget { required this.getMangaDetail, required this.isComfortableGrid, required this.itemType, + this.libraryManga, }); @override Widget build(BuildContext context, WidgetRef ref) { - return StreamBuilder( - stream: isar.mangas - .filter() - .langEqualTo(source.lang) - .nameEqualTo(getMangaDetail!.name) - .sourceEqualTo(source.name) - .watch(fireImmediately: true), - builder: (context, snapshot) { - bool hasData = snapshot.hasData; - final mangaList = hasData - ? snapshot.data! - .where( - (element) => element.sourceId == null - ? true - : element.sourceId == source.id, - ) - .toList() - : []; - hasData = hasData && mangaList.isNotEmpty; - return CoverViewWidget( - bottomTextWidget: BottomTextWidget( - maxLines: 1, - text: getMangaDetail!.name!, - isComfortableGrid: isComfortableGrid, - ), - isComfortableGrid: isComfortableGrid, - image: hasData && mangaList.first.customCoverImage != null - ? MemoryImage(mangaList.first.customCoverImage as Uint8List) - as ImageProvider - : CustomExtendedNetworkImageProvider( - toImgUrl( - hasData - ? mangaList.first.customCoverFromTracker ?? - mangaList.first.imageUrl ?? - "" - : getMangaDetail!.imageUrl ?? "", - ), - headers: ref.watch( - headersProvider( - source: source.name!, - lang: source.lang!, - sourceId: source.id, - ), - ), - cache: true, - cacheMaxAge: const Duration(days: 7), + final hasData = libraryManga != null; + return CoverViewWidget( + bottomTextWidget: BottomTextWidget( + maxLines: 1, + text: getMangaDetail!.name!, + isComfortableGrid: isComfortableGrid, + ), + isComfortableGrid: isComfortableGrid, + image: hasData && libraryManga!.customCoverImage != null + ? MemoryImage(libraryManga!.customCoverImage as Uint8List) + as ImageProvider + : CustomExtendedNetworkImageProvider( + toImgUrl( + hasData + ? libraryManga!.customCoverFromTracker ?? + libraryManga!.imageUrl ?? + "" + : getMangaDetail!.imageUrl ?? "", + ), + headers: ref.watch( + headersProvider( + source: source.name!, + lang: source.lang!, + sourceId: source.id, ), + ), + cache: true, + cacheMaxAge: const Duration(days: 7), + ), + onTap: () => pushToMangaReaderDetail( + ref: ref, + context: context, + getManga: getMangaDetail!, + lang: source.lang!, + source: source.name!, + itemType: itemType, + sourceId: source.id, + ), + onLongPress: () => pushToMangaReaderDetail( + ref: ref, + context: context, + getManga: getMangaDetail!, + lang: source.lang!, + source: source.name!, + itemType: itemType, + addToFavourite: true, + sourceId: source.id, + ), + onSecondaryTap: () => pushToMangaReaderDetail( + ref: ref, + context: context, + getManga: getMangaDetail!, + lang: source.lang!, + source: source.name!, + itemType: itemType, + addToFavourite: true, + sourceId: source.id, + ), + children: [ + Container( + color: hasData && libraryManga!.favorite! + ? Colors.black.withValues(alpha: 0.5) + : null, + ), + if (hasData && libraryManga!.favorite!) + Positioned( + top: 0, + left: 0, + child: Padding( + padding: const EdgeInsets.all(4), + child: Container( + decoration: BoxDecoration( + color: context.primaryColor, + borderRadius: BorderRadius.circular(5), + ), + child: Padding( + padding: const EdgeInsets.all(4), + child: Icon( + Icons.collections_bookmark_outlined, + size: 16, + color: context.dynamicWhiteBlackColor, + ), + ), + ), + ), + ), + if (!isComfortableGrid) + BottomTextWidget( + isTorrent: source.isTorrent, + text: getMangaDetail!.name!, + ), + ], + ); + } +} + +class MangaImageCardListTileWidget extends ConsumerWidget { + final Source source; + final ItemType itemType; + final MManga? getMangaDetail; + final Manga? libraryManga; + + const MangaImageCardListTileWidget({ + required this.source, + super.key, + required this.itemType, + required this.getMangaDetail, + this.libraryManga, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final hasData = libraryManga != null; + final image = hasData && libraryManga!.customCoverImage != null + ? MemoryImage(libraryManga!.customCoverImage as Uint8List) + as ImageProvider + : CustomExtendedNetworkImageProvider( + toImgUrl( + hasData + ? libraryManga!.customCoverFromTracker ?? + libraryManga!.imageUrl ?? + "" + : getMangaDetail!.imageUrl ?? "", + ), + headers: ref.watch( + headersProvider( + source: source.name!, + lang: source.lang!, + sourceId: source.id, + ), + ), + ); + return Padding( + padding: const EdgeInsets.all(8.0), + child: Material( + borderRadius: BorderRadius.circular(5), + color: Colors.transparent, + clipBehavior: Clip.antiAlias, + child: InkWell( onTap: () { pushToMangaReaderDetail( ref: ref, @@ -115,18 +209,46 @@ class MangaImageCardWidget extends ConsumerWidget { sourceId: source.id, ); }, - children: [ - Container( - color: hasData && mangaList.first.favorite! - ? Colors.black.withValues(alpha: 0.5) - : null, - ), - if (hasData && mangaList.first.favorite!) - Positioned( - top: 0, - left: 0, - child: Padding( - padding: const EdgeInsets.all(4), + child: Row( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Stack( + children: [ + Material( + borderRadius: BorderRadius.circular(5), + color: Colors.transparent, + clipBehavior: Clip.antiAlias, + child: Image( + height: 55, + width: 40, + fit: BoxFit.cover, + image: image, + ), + ), + Container( + height: 55, + width: 40, + color: hasData && libraryManga!.favorite! + ? Colors.black.withValues(alpha: 0.5) + : null, + ), + ], + ), + ), + Expanded( + child: Text( + getMangaDetail!.name!, + maxLines: 2, + style: TextStyle( + overflow: TextOverflow.ellipsis, + color: context.textColor, + ), + ), + ), + if (hasData && libraryManga!.favorite!) + Padding( + padding: const EdgeInsets.all(8.0), child: Container( decoration: BoxDecoration( color: context.primaryColor, @@ -142,174 +264,10 @@ class MangaImageCardWidget extends ConsumerWidget { ), ), ), - ), - if (!isComfortableGrid) - BottomTextWidget( - isTorrent: source.isTorrent, - text: getMangaDetail!.name!, - ), - ], - ); - }, - ); - } -} - -class MangaImageCardListTileWidget extends ConsumerWidget { - final Source source; - final ItemType itemType; - final MManga? getMangaDetail; - - const MangaImageCardListTileWidget({ - required this.source, - super.key, - required this.itemType, - required this.getMangaDetail, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return StreamBuilder( - stream: isar.mangas - .filter() - .langEqualTo(source.lang) - .nameEqualTo(getMangaDetail!.name) - .sourceEqualTo(source.name) - .watch(fireImmediately: true), - builder: (context, snapshot) { - bool hasData = snapshot.hasData; - final mangaList = hasData - ? snapshot.data! - .where( - (element) => element.sourceId == null - ? true - : element.sourceId == source.id, - ) - .toList() - : []; - hasData = hasData && mangaList.isNotEmpty; - final image = hasData && mangaList.first.customCoverImage != null - ? MemoryImage(mangaList.first.customCoverImage as Uint8List) - as ImageProvider - : CustomExtendedNetworkImageProvider( - toImgUrl( - hasData - ? mangaList.first.customCoverFromTracker ?? - mangaList.first.imageUrl ?? - "" - : getMangaDetail!.imageUrl ?? "", - ), - headers: ref.watch( - headersProvider( - source: source.name!, - lang: source.lang!, - sourceId: source.id, - ), - ), - ); - return Padding( - padding: const EdgeInsets.all(8.0), - child: Material( - borderRadius: BorderRadius.circular(5), - color: Colors.transparent, - clipBehavior: Clip.antiAlias, - child: InkWell( - onTap: () { - pushToMangaReaderDetail( - ref: ref, - context: context, - getManga: getMangaDetail!, - lang: source.lang!, - source: source.name!, - itemType: itemType, - sourceId: source.id, - ); - }, - onLongPress: () { - pushToMangaReaderDetail( - ref: ref, - context: context, - getManga: getMangaDetail!, - lang: source.lang!, - source: source.name!, - itemType: itemType, - addToFavourite: true, - sourceId: source.id, - ); - }, - onSecondaryTap: () { - pushToMangaReaderDetail( - ref: ref, - context: context, - getManga: getMangaDetail!, - lang: source.lang!, - source: source.name!, - itemType: itemType, - addToFavourite: true, - sourceId: source.id, - ); - }, - child: Row( - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: Stack( - children: [ - Material( - borderRadius: BorderRadius.circular(5), - color: Colors.transparent, - clipBehavior: Clip.antiAlias, - child: Image( - height: 55, - width: 40, - fit: BoxFit.cover, - image: image, - ), - ), - Container( - height: 55, - width: 40, - color: hasData && mangaList.first.favorite! - ? Colors.black.withValues(alpha: 0.5) - : null, - ), - ], - ), - ), - Expanded( - child: Text( - getMangaDetail!.name!, - maxLines: 2, - style: TextStyle( - overflow: TextOverflow.ellipsis, - color: context.textColor, - ), - ), - ), - if (hasData && mangaList.first.favorite!) - Padding( - padding: const EdgeInsets.all(8.0), - child: Container( - decoration: BoxDecoration( - color: context.primaryColor, - borderRadius: BorderRadius.circular(5), - ), - child: Padding( - padding: const EdgeInsets.all(4), - child: Icon( - Icons.collections_bookmark_outlined, - size: 16, - color: context.dynamicWhiteBlackColor, - ), - ), - ), - ), - ], - ), - ), + ], ), - ); - }, + ), + ), ); } }