From 98b3121c04440a511c0bf2fafdfc04f90d9eaffc Mon Sep 17 00:00:00 2001 From: NBA2K1 <78034913+NBA2K1@users.noreply.github.com> Date: Sun, 19 Apr 2026 01:42:51 +0200 Subject: [PATCH] Move tracker lookup to parent Replace per-card Isar tracking streams with a single subscription in TrackerSectionScreen. - Build a mediaId -> Track index at the screen level - Pass resolved Track? into TrackerLibraryImageCard - Convert TrackerLibraryImageCard to a plain StatelessWidget - Remove StreamBuilder, ConsumerStatefulWidget, and keep-alive boilerplate - Keep the same UI while reducing widget-level DB work This follows the same parent-index pattern used elsewhere and makes the tracker library view lighter and easier to maintain. --- .../tracker_library/tracker_library_card.dart | 193 ++++++++---------- .../tracker_section_screen.dart | 53 +++-- 2 files changed, 121 insertions(+), 125 deletions(-) diff --git a/lib/modules/tracker_library/tracker_library_card.dart b/lib/modules/tracker_library/tracker_library_card.dart index 91bc631a..489c329b 100644 --- a/lib/modules/tracker_library/tracker_library_card.dart +++ b/lib/modules/tracker_library/tracker_library_card.dart @@ -1,7 +1,4 @@ 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/manga.dart'; import 'package:mangayomi/models/track.dart'; import 'package:mangayomi/models/track_search.dart'; @@ -11,141 +8,113 @@ import 'package:mangayomi/utils/cached_network.dart'; import 'package:mangayomi/utils/constant.dart'; import 'package:mangayomi/utils/extensions/build_context_extensions.dart'; -class TrackerLibraryImageCard extends ConsumerStatefulWidget { +class TrackerLibraryImageCard extends StatelessWidget { final TrackSearch track; final ItemType itemType; + final Track? libraryTrack; const TrackerLibraryImageCard({ super.key, required this.track, required this.itemType, + this.libraryTrack, }); - @override - ConsumerState createState() => - _TrackerLibraryImageCardState(); -} - -class _TrackerLibraryImageCardState - extends ConsumerState - with AutomaticKeepAliveClientMixin { @override Widget build(BuildContext context) { - super.build(context); - final trackData = widget.track; - return StreamBuilder( - stream: isar.tracks - .filter() - .mangaIdIsNotNull() - .mediaIdEqualTo(trackData.mediaId) - .itemTypeEqualTo(widget.itemType) - .watch(fireImmediately: true), - builder: (context, snapshot) { - final hasData = snapshot.hasData && snapshot.data!.isNotEmpty; - return GestureDetector( - onTap: () => _showCard(context, snapshot.data?.firstOrNull?.mangaId), - child: Padding( - padding: const EdgeInsets.only(left: 10), - child: Stack( - children: [ - SizedBox( - width: 110, - child: Column( - children: [ - Builder( - builder: (context) { - return ClipRRect( - borderRadius: BorderRadius.circular(5), - child: Stack( + final hasData = libraryTrack != null; + return GestureDetector( + onTap: () => _showCard(context, libraryTrack?.mangaId), + child: Padding( + padding: const EdgeInsets.only(left: 10), + child: Stack( + children: [ + SizedBox( + width: 110, + child: Column( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(5), + child: Stack( + children: [ + cachedNetworkImage( + imageUrl: toImgUrl(track.coverUrl ?? ""), + width: 110, + height: 150, + fit: BoxFit.cover, + ), + Positioned( + top: 0, + right: 0, + child: Text.rich( + TextSpan( + style: TextStyle( + background: Paint() + ..color = Theme.of(context) + .scaffoldBackgroundColor + .withValues(alpha: 0.75) + ..strokeWidth = 20.0 + ..strokeJoin = StrokeJoin.round + ..style = PaintingStyle.stroke, + ), children: [ - cachedNetworkImage( - imageUrl: toImgUrl(trackData.coverUrl ?? ""), - width: 110, - height: 150, - fit: BoxFit.cover, + WidgetSpan( + child: Icon( + Icons.star, + color: context.primaryColor, + ), ), - Positioned( - top: 0, - right: 0, - child: Text.rich( - TextSpan( - style: TextStyle( - background: Paint() - ..color = Theme.of(context) - .scaffoldBackgroundColor - .withValues(alpha: 0.75) - ..strokeWidth = 20.0 - ..strokeJoin = StrokeJoin.round - ..style = PaintingStyle.stroke, - ), - children: [ - WidgetSpan( - child: Icon( - Icons.star, - color: context.primaryColor, - ), - ), - TextSpan( - text: " ${trackData.score ?? "?"}", - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - ), - ], - ), + TextSpan( + text: " ${track.score ?? "?"}", + style: const TextStyle( + fontWeight: FontWeight.bold, ), ), ], ), - ); - }, - ), - BottomTextWidget( - fontSize: 12.0, - text: trackData.title!, - isLoading: true, - textColor: Theme.of(context).textTheme.bodyLarge!.color, - isComfortableGrid: true, - ), - ], - ), - ), - Container( - width: 110, - height: 150, - color: hasData ? Colors.black.withValues(alpha: 0.7) : null, - ), - if (hasData) - Positioned( - top: 0, - left: 0, - child: Padding( - padding: const EdgeInsets.all(4), - child: Icon( - Icons.collections_bookmark, - color: context.primaryColor, - ), + ), + ), + ], ), ), - ], + BottomTextWidget( + fontSize: 12.0, + text: track.title!, + isLoading: true, + textColor: Theme.of(context).textTheme.bodyLarge!.color, + isComfortableGrid: true, + ), + ], + ), ), - ), - ); - }, + Container( + width: 110, + height: 150, + color: hasData ? Colors.black.withValues(alpha: 0.7) : null, + ), + if (hasData) + Positioned( + top: 0, + left: 0, + child: Padding( + padding: const EdgeInsets.all(4), + child: Icon( + Icons.collections_bookmark, + color: context.primaryColor, + ), + ), + ), + ], + ), + ), ); } void _showCard(BuildContext context, int? mangaId) { showDialog( context: context, - builder: (context) => TrackerItemCard( - track: widget.track, - itemType: widget.itemType, - mangaId: mangaId, - ), + builder: (context) => + TrackerItemCard(track: track, itemType: itemType, mangaId: mangaId), ); } - - @override - bool get wantKeepAlive => true; } diff --git a/lib/modules/tracker_library/tracker_section_screen.dart b/lib/modules/tracker_library/tracker_section_screen.dart index 2c3f883c..60c7c458 100644 --- a/lib/modules/tracker_library/tracker_section_screen.dart +++ b/lib/modules/tracker_library/tracker_section_screen.dart @@ -1,5 +1,9 @@ +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:hive_flutter/adapters.dart'; +import 'package:isar_community/isar.dart'; +import 'package:mangayomi/main.dart'; +import 'package:mangayomi/models/track.dart'; import 'package:mangayomi/models/track_search.dart'; import 'package:mangayomi/modules/tracker_library/tracker_library_card.dart'; import 'package:mangayomi/modules/tracker_library/tracker_library_section.dart'; @@ -19,23 +23,52 @@ class _TrackerSectionScreenState extends State { String _errorMessage = ""; bool _isLoading = true; List _tracks = []; + late StreamSubscription> _trackStreamSub; + Map _trackIndex = {}; @override void initState() { super.initState(); _fetchData(); + _subscribeToTracks(); + } + + void _subscribeToTracks() { + _trackStreamSub = isar.tracks + .filter() + .itemTypeEqualTo(widget.section.itemType) + .watch(fireImmediately: true) + .listen((tracks) { + if (mounted) { + setState(() { + _trackIndex = { + for (final t in tracks) + if (t.mediaId != null) t.mediaId!: t, + }; + }); + } + }); } @override void didUpdateWidget(covariant TrackerSectionScreen oldWidget) { super.didUpdateWidget(oldWidget); + if (oldWidget.section.itemType != widget.section.itemType) { + _trackStreamSub.cancel(); + _subscribeToTracks(); + } _fetchData(); } + @override + void dispose() { + _trackStreamSub.cancel(); + super.dispose(); + } + @override Widget build(BuildContext context) { final l10n = l10nLocalizations(context)!; - return Scaffold( body: SizedBox( height: 260, @@ -57,9 +90,11 @@ class _TrackerSectionScreenState extends State { scrollDirection: Axis.horizontal, itemCount: _tracks.length, itemBuilder: (context, index) { + final track = _tracks[index]; return TrackerLibraryImageCard( - track: _tracks[index], + track: track, itemType: widget.section.itemType, + libraryTrack: _trackIndex[track.mediaId], ); }, ); @@ -78,17 +113,13 @@ class _TrackerSectionScreenState extends State { final box = await Hive.openBox("tracker_library"); final key = "${widget.section.syncId}-${widget.section.itemType.name}-${widget.section.name}"; - if (_checkCache(box, key)) { - return; - } + if (_checkCache(box, key)) return; try { _errorMessage = ""; _tracks = await widget.section.func() ?? []; box.put(key, _tracks); if (mounted) { - setState(() { - _isLoading = false; - }); + setState(() => _isLoading = false); } } catch (e) { if (mounted) { @@ -106,11 +137,7 @@ class _TrackerSectionScreenState extends State { if (temp is List) { _errorMessage = ""; _tracks = temp; - if (mounted) { - setState(() { - _isLoading = false; - }); - } + if (mounted) setState(() => _isLoading = false); return true; } }