diff --git a/data/lib/service/ball_score/ball_score_service.dart b/data/lib/service/ball_score/ball_score_service.dart index 9eda93a2..51f25bd7 100644 --- a/data/lib/service/ball_score/ball_score_service.dart +++ b/data/lib/service/ball_score/ball_score_service.dart @@ -116,12 +116,21 @@ class BallScoreService { } } - Stream> streamBallScoresByInningIds( - List inningIds, - ) { - if (inningIds.isEmpty) return const Stream.empty(); - return _ballScoreCollection - .where(FireStoreConst.inningId, whereIn: inningIds) + Stream> streamBallScoresByInningIds({ + required List inningIds, + int? limit, + }) { + if (inningIds.isEmpty) return Stream.value([]); + + var query = + _ballScoreCollection.where(FireStoreConst.inningId, whereIn: inningIds); + if (limit != null) { + query = query + .orderBy(FireStoreConst.scoreTime, descending: true) + .limit(limit); + } + + return query .snapshots() .map( (event) => event.docChanges diff --git a/data/lib/service/match/match_service.dart b/data/lib/service/match/match_service.dart index 3e23def7..981a519f 100644 --- a/data/lib/service/match/match_service.dart +++ b/data/lib/service/match/match_service.dart @@ -150,7 +150,10 @@ class MatchService { .catchError((error, stack) => throw AppError.fromError(error, stack)); } - Stream> streamUserRelatedMatches(String userId) { + Stream> streamUserRelatedMatches({ + required String userId, + int limit = 10, + }) { final filter = Filter.or( Filter(FireStoreConst.createdBy, isEqualTo: userId), Filter(FireStoreConst.players, arrayContains: userId), @@ -159,6 +162,8 @@ class MatchService { return _matchCollection .where(filter) + .orderBy(FieldPath.documentId) + .limit(limit) .snapshots() .asyncMap((snapshot) async { return await Future.wait( @@ -186,15 +191,19 @@ class MatchService { }).handleError((error, stack) => throw AppError.fromError(error, stack)); } - Stream> streamMatchesByTeamId(String teamId) { + Stream> streamMatchesByTeamId({ + required String teamId, + int limit = 10, + }) { return _matchCollection .where(FireStoreConst.teamIds, arrayContains: teamId) + .orderBy(FieldPath.documentId) + .limit(limit) .snapshots() .asyncMap((snapshot) async { return await Future.wait( snapshot.docs.map((mainDoc) async { final match = mainDoc.data(); - final List teams = await getTeamsList(match.teams); return match.copyWith(teams: teams); }).toList(), @@ -202,7 +211,7 @@ class MatchService { }).handleError((error, stack) => throw AppError.fromError(error, stack)); } - Stream> streamActiveRunningMatches() { + Stream> streamActiveRunningMatches({int limit = 10}) { final DateTime now = DateTime.now(); final DateTime oneAndHalfHoursAgo = now.subtract(Duration(hours: 1, minutes: 30)); @@ -215,6 +224,7 @@ class MatchService { ); return _matchCollection .where(filter) + .limit(limit) .snapshots() .asyncMap((snapshot) async { return await Future.wait( @@ -228,7 +238,7 @@ class MatchService { }).handleError((error, stack) => throw AppError.fromError(error, stack)); } - Stream> streamUpcomingMatches() { + Stream> streamUpcomingMatches({int limit = 10}) { final DateTime now = DateTime.now(); final startOfDay = DateTime(now.year, now.month, now.day); final DateTime aMonthAfter = DateTime(now.year, now.month + 1, now.day); @@ -249,6 +259,7 @@ class MatchService { ); return _matchCollection .where(filter) + .limit(limit) .snapshots() .asyncMap((snapshot) async { return await Future.wait( @@ -655,7 +666,7 @@ class MatchService { Stream> streamMatchesByIds(List matchIds) { try { - if (matchIds.isEmpty) return Stream.empty(); + if (matchIds.isEmpty) return Stream.value([]); return _matchCollection .where(FieldPath.documentId, whereIn: matchIds) .snapshots() diff --git a/data/lib/service/team/team_service.dart b/data/lib/service/team/team_service.dart index c3ef2008..5cdf2810 100644 --- a/data/lib/service/team/team_service.dart +++ b/data/lib/service/team/team_service.dart @@ -87,20 +87,25 @@ class TeamService { }).catchError((error, stack) => throw AppError.fromError(error, stack)); } - Stream> streamUserRelatedTeams(String userId) { + Stream> streamUserRelatedTeams({ + required String userId, + int limit = 10, + }) { final currentPlayer = TeamPlayer(id: userId); final playerContains = [ currentPlayer.copyWith(role: TeamPlayerRole.admin).toJson(), currentPlayer.copyWith(role: TeamPlayerRole.player).toJson(), ]; - final filter = Filter.or( Filter(FireStoreConst.createdBy, isEqualTo: userId), Filter(FireStoreConst.teamPlayers, arrayContainsAny: playerContains), ); + return _teamsCollection .where(filter) + .orderBy(FieldPath.documentId) + .limit(limit) .snapshots() .asyncMap((snapshot) async { final teams = await Future.wait( diff --git a/data/lib/utils/constant/firestore_constant.dart b/data/lib/utils/constant/firestore_constant.dart index 6bd17f84..052d7c2f 100644 --- a/data/lib/utils/constant/firestore_constant.dart +++ b/data/lib/utils/constant/firestore_constant.dart @@ -50,6 +50,7 @@ class FireStoreConst { static const String batsmanId = "batsman_id"; static const String wicketTakerId = "wicket_taker_id"; static const String playerOutId = "player_out_id"; + static const String scoreTime = "score_time"; // teams field const static const String players = "players"; diff --git a/khelo/lib/ui/flow/matches/match_detail/components/match_detail_commentary_view.dart b/khelo/lib/ui/flow/matches/match_detail/components/match_detail_commentary_view.dart index 601824fa..49b269a5 100644 --- a/khelo/lib/ui/flow/matches/match_detail/components/match_detail_commentary_view.dart +++ b/khelo/lib/ui/flow/matches/match_detail/components/match_detail_commentary_view.dart @@ -10,10 +10,13 @@ import 'package:khelo/ui/flow/matches/match_detail/components/commentary_ball_su import 'package:khelo/ui/flow/matches/match_detail/components/commentary_over_overview.dart'; import 'package:khelo/ui/flow/matches/match_detail/components/final_score_view.dart'; import 'package:khelo/ui/flow/matches/match_detail/match_detail_tab_view_model.dart'; +import 'package:style/callback/on_visible_callback.dart'; import 'package:style/extensions/context_extensions.dart'; import 'package:style/indicator/progress_indicator.dart'; import 'package:style/text/app_text_style.dart'; +import '../../../../../domain/extensions/widget_extension.dart'; + class MatchDetailCommentaryView extends ConsumerWidget { const MatchDetailCommentaryView({super.key}); @@ -39,10 +42,38 @@ class MatchDetailCommentaryView extends ConsumerWidget { } return (state.overList.isNotEmpty) - ? ListView( + ? ListView.builder( padding: context.mediaQueryPadding + const EdgeInsets.only(bottom: 24), - children: _buildCommentaryList(context, state), + itemCount: 1 + state.overList.length + 1, + itemBuilder: (context, index) { + if (index == 0) { + return const Padding( + padding: EdgeInsets.symmetric(horizontal: 16.0), + child: FinalScoreView(), + ); + } + if (index < state.overList.length + 1) { + final overSummaryIndex = state.overList.length - index; + final overSummary = state.overList[overSummaryIndex]; + final nextOverSummary = + state.overList.elementAtOrNull(overSummaryIndex + 1); + final team = _getTeamByTeamId(state, overSummary.team_id); + + return _buildCommentaryItem( + context, + overSummary: overSummary, + team: team, + nextOverSummary: nextOverSummary, + ); + } + return OnVisibleCallback( + onVisible: () => runPostFrame(notifier.loadBallScores), + child: (state.loadingBallScoreMore && state.overList.isNotEmpty) + ? const Center(child: AppProgressIndicator()) + : const SizedBox(), + ); + }, ) : EmptyScreen( title: context.l10n.match_detail_match_not_started_error_title, @@ -51,61 +82,65 @@ class MatchDetailCommentaryView extends ConsumerWidget { ); } - List _buildCommentaryList( - BuildContext context, - MatchDetailTabState state, - ) { + Widget _buildCommentaryItem( + BuildContext context, { + required OverSummary overSummary, + required TeamModel? team, + OverSummary? nextOverSummary, + }) { List children = []; - children.add(const Padding( - padding: EdgeInsets.symmetric(horizontal: 16.0), - child: FinalScoreView(), - )); - for (int index = state.overList.length - 1; index >= 0; index--) { - final overSummary = state.overList[index]; - final team = _getTeamByTeamId(state, overSummary.team_id); - - final nextOverSummary = state.overList.elementAtOrNull(index + 1); - if (nextOverSummary != null && - nextOverSummary.overNumber != overSummary.overNumber) { - children.add(Padding( + // Add BowlerSummaryView if applicable + if (nextOverSummary != null && + nextOverSummary.overNumber != overSummary.overNumber) { + children.add( + Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), child: BowlerSummaryView( bowlerSummary: nextOverSummary.bowlerStatAtStart, isForBowlerIntro: true, ), - )); - } + ), + ); + } - if ((overSummary.balls.lastOrNull?.isLegalDelivery() ?? false) && - overSummary.balls.lastOrNull?.ball_number == 6 && - nextOverSummary?.inning_id == overSummary.inning_id) { - children - .add(CommentaryOverOverview(overSummary: overSummary, team: team)); - } else if (nextOverSummary != null && - nextOverSummary.inning_id != overSummary.inning_id) { - children.addAll([ - _inningOverview(context, - teamName: team?.name ?? "", targetRun: overSummary.totalRuns + 1), - CommentaryOverOverview(overSummary: overSummary, team: team), - ]); - } + // Add CommentaryOverOverview or InningOverview if applicable + if ((overSummary.balls.lastOrNull?.isLegalDelivery() ?? false) && + overSummary.balls.lastOrNull?.ball_number == 6 && + nextOverSummary?.inning_id == overSummary.inning_id) { + children + .add(CommentaryOverOverview(overSummary: overSummary, team: team)); + } else if (nextOverSummary != null && + nextOverSummary.inning_id != overSummary.inning_id) { + children.addAll([ + _inningOverview( + context, + teamName: team?.name ?? "", + targetRun: overSummary.totalRuns + 1, + ), + CommentaryOverOverview(overSummary: overSummary, team: team), + ]); + } - for (final ball in overSummary.balls.reversed) { - children.addAll([ - CommentaryBallSummary( - ball: ball, - overSummary: overSummary, - showBallScore: - ball.is_four || ball.is_six || ball.wicket_taker_id != null, - ), - if (ball != overSummary.balls.first) ...[ - Divider(color: context.colorScheme.outline), + // Add CommentaryBallSummary for each ball + children.addAll( + overSummary.balls.reversed.map( + (ball) => Column( + children: [ + CommentaryBallSummary( + ball: ball, + overSummary: overSummary, + showBallScore: + ball.is_four || ball.is_six || ball.wicket_taker_id != null, + ), + if (ball != overSummary.balls.first) + Divider(color: context.colorScheme.outline), ], - ]); - } - } - return children; + ), + ), + ); + + return Column(children: children); } TeamModel? _getTeamByTeamId(MatchDetailTabState state, String teamId) { diff --git a/khelo/lib/ui/flow/matches/match_detail/match_detail_tab_view_model.dart b/khelo/lib/ui/flow/matches/match_detail/match_detail_tab_view_model.dart index b907f640..2cb4f2d7 100644 --- a/khelo/lib/ui/flow/matches/match_detail/match_detail_tab_view_model.dart +++ b/khelo/lib/ui/flow/matches/match_detail/match_detail_tab_view_model.dart @@ -48,6 +48,8 @@ class MatchDetailTabViewNotifier extends StateNotifier { this._ballScoreService, ) : super(const MatchDetailTabState()); + int _ballsLoaded = 0; + void setData(String matchId) { _matchId = matchId; _loadMatchesAndInning(); @@ -79,13 +81,12 @@ class MatchDetailTabViewNotifier extends StateNotifier { true, ); state = state.copyWith( - highlightTeamId: state.highlightTeamId ?? match.teams.first.team.id, - allInnings: innings, - error: null); + highlightTeamId: state.highlightTeamId ?? match.teams.first.team.id, + allInnings: innings, + error: null, + ); - if (!state.ballScoreQueryListenerSet) { - _loadBallScores(); - } + loadBallScores(); }, onError: (e) { debugPrint( "MatchDetailTabViewNotifier: error while loading match and inning -> $e"); @@ -98,24 +99,30 @@ class MatchDetailTabViewNotifier extends StateNotifier { } } - void _loadBallScores() { + void loadBallScores() { if (state.allInnings.isEmpty) { state = state.copyWith(loading: false); return; } - state = state.copyWith(ballScoreQueryListenerSet: true); - + if (state.loadingBallScoreMore) return; + state = state.copyWith(loadingBallScoreMore: _ballsLoaded > 0); + final inningIds = state.allInnings.map((inning) => inning.id).toList(); _ballScoreStreamSubscription = _ballScoreService - .streamBallScoresByInningIds(state.allInnings.map((e) => e.id).toList()) + .streamBallScoresByInningIds( + inningIds: inningIds, + limit: _ballsLoaded + 12, + ) .listen( (scores) { + _ballsLoaded = scores.length; + final sortedList = scores.toList(); sortedList.sort((a, b) => (a.ballScore.score_time ?? a.ballScore.time)?.compareTo( b.ballScore.score_time ?? b.ballScore.time ?? DateTime.now()) ?? 0); - final overList = state.overList.toList(); + final List overList = []; for (final score in sortedList) { if (score.type == DocumentChangeType.added) { final overSummary = @@ -139,7 +146,12 @@ class MatchDetailTabViewNotifier extends StateNotifier { } overList.sort((a, b) => a.time.compareTo(b.time)); - state = state.copyWith(overList: overList, loading: false, error: null); + state = state.copyWith( + overList: overList, + loadingBallScoreMore: false, + loading: false, + error: null, + ); changeHighlightFilter(); }, onError: (e, stack) { @@ -156,10 +168,10 @@ class MatchDetailTabViewNotifier extends StateNotifier { bool isUndo = false, }) { if (isUndo) { - final overToUpdate = overList.firstWhere((element) => + final overToUpdate = overList.firstWhereOrNull((element) => element.overNumber == ball.over_number && element.inning_id == ball.inning_id); - return overToUpdate.removeBall(ball); + return overToUpdate?.removeBall(ball); } BatsmanSummary striker = _configureBatsman( @@ -351,7 +363,6 @@ class MatchDetailTabViewNotifier extends StateNotifier { } Future _cancelStreamSubscription() async { - state = state.copyWith(ballScoreQueryListenerSet: false); await _matchStreamSubscription?.cancel(); await _ballScoreStreamSubscription?.cancel(); } @@ -399,7 +410,7 @@ class MatchDetailTabState with _$MatchDetailTabState { @Default([]) List filteredHighlight, @Default([]) List expandedInningsScorecard, @Default(false) bool loading, - @Default(false) bool ballScoreQueryListenerSet, + @Default(false) bool loadingBallScoreMore, @Default(HighlightFilterOption.all) HighlightFilterOption highlightFilterOption, }) = _MatchDetailTabState; diff --git a/khelo/lib/ui/flow/matches/match_detail/match_detail_tab_view_model.freezed.dart b/khelo/lib/ui/flow/matches/match_detail/match_detail_tab_view_model.freezed.dart index 1adf69e7..32608086 100644 --- a/khelo/lib/ui/flow/matches/match_detail/match_detail_tab_view_model.freezed.dart +++ b/khelo/lib/ui/flow/matches/match_detail/match_detail_tab_view_model.freezed.dart @@ -29,7 +29,7 @@ mixin _$MatchDetailTabState { List get expandedInningsScorecard => throw _privateConstructorUsedError; bool get loading => throw _privateConstructorUsedError; - bool get ballScoreQueryListenerSet => throw _privateConstructorUsedError; + bool get loadingBallScoreMore => throw _privateConstructorUsedError; HighlightFilterOption get highlightFilterOption => throw _privateConstructorUsedError; @@ -58,7 +58,7 @@ abstract class $MatchDetailTabStateCopyWith<$Res> { List filteredHighlight, List expandedInningsScorecard, bool loading, - bool ballScoreQueryListenerSet, + bool loadingBallScoreMore, HighlightFilterOption highlightFilterOption}); $MatchModelCopyWith<$Res>? get match; @@ -90,7 +90,7 @@ class _$MatchDetailTabStateCopyWithImpl<$Res, $Val extends MatchDetailTabState> Object? filteredHighlight = null, Object? expandedInningsScorecard = null, Object? loading = null, - Object? ballScoreQueryListenerSet = null, + Object? loadingBallScoreMore = null, Object? highlightFilterOption = null, }) { return _then(_value.copyWith( @@ -136,9 +136,9 @@ class _$MatchDetailTabStateCopyWithImpl<$Res, $Val extends MatchDetailTabState> ? _value.loading : loading // ignore: cast_nullable_to_non_nullable as bool, - ballScoreQueryListenerSet: null == ballScoreQueryListenerSet - ? _value.ballScoreQueryListenerSet - : ballScoreQueryListenerSet // ignore: cast_nullable_to_non_nullable + loadingBallScoreMore: null == loadingBallScoreMore + ? _value.loadingBallScoreMore + : loadingBallScoreMore // ignore: cast_nullable_to_non_nullable as bool, highlightFilterOption: null == highlightFilterOption ? _value.highlightFilterOption @@ -182,7 +182,7 @@ abstract class _$$MatchDetailTabStateImplCopyWith<$Res> List filteredHighlight, List expandedInningsScorecard, bool loading, - bool ballScoreQueryListenerSet, + bool loadingBallScoreMore, HighlightFilterOption highlightFilterOption}); @override @@ -213,7 +213,7 @@ class __$$MatchDetailTabStateImplCopyWithImpl<$Res> Object? filteredHighlight = null, Object? expandedInningsScorecard = null, Object? loading = null, - Object? ballScoreQueryListenerSet = null, + Object? loadingBallScoreMore = null, Object? highlightFilterOption = null, }) { return _then(_$MatchDetailTabStateImpl( @@ -259,9 +259,9 @@ class __$$MatchDetailTabStateImplCopyWithImpl<$Res> ? _value.loading : loading // ignore: cast_nullable_to_non_nullable as bool, - ballScoreQueryListenerSet: null == ballScoreQueryListenerSet - ? _value.ballScoreQueryListenerSet - : ballScoreQueryListenerSet // ignore: cast_nullable_to_non_nullable + loadingBallScoreMore: null == loadingBallScoreMore + ? _value.loadingBallScoreMore + : loadingBallScoreMore // ignore: cast_nullable_to_non_nullable as bool, highlightFilterOption: null == highlightFilterOption ? _value.highlightFilterOption @@ -286,7 +286,7 @@ class _$MatchDetailTabStateImpl implements _MatchDetailTabState { final List filteredHighlight = const [], final List expandedInningsScorecard = const [], this.loading = false, - this.ballScoreQueryListenerSet = false, + this.loadingBallScoreMore = false, this.highlightFilterOption = HighlightFilterOption.all}) : _allInnings = allInnings, _overList = overList, @@ -349,14 +349,14 @@ class _$MatchDetailTabStateImpl implements _MatchDetailTabState { final bool loading; @override @JsonKey() - final bool ballScoreQueryListenerSet; + final bool loadingBallScoreMore; @override @JsonKey() final HighlightFilterOption highlightFilterOption; @override String toString() { - return 'MatchDetailTabState(error: $error, match: $match, allInnings: $allInnings, highlightTeamId: $highlightTeamId, showTeamSelectionSheet: $showTeamSelectionSheet, showHighlightOptionSelectionSheet: $showHighlightOptionSelectionSheet, selectedTab: $selectedTab, overList: $overList, filteredHighlight: $filteredHighlight, expandedInningsScorecard: $expandedInningsScorecard, loading: $loading, ballScoreQueryListenerSet: $ballScoreQueryListenerSet, highlightFilterOption: $highlightFilterOption)'; + return 'MatchDetailTabState(error: $error, match: $match, allInnings: $allInnings, highlightTeamId: $highlightTeamId, showTeamSelectionSheet: $showTeamSelectionSheet, showHighlightOptionSelectionSheet: $showHighlightOptionSelectionSheet, selectedTab: $selectedTab, overList: $overList, filteredHighlight: $filteredHighlight, expandedInningsScorecard: $expandedInningsScorecard, loading: $loading, loadingBallScoreMore: $loadingBallScoreMore, highlightFilterOption: $highlightFilterOption)'; } @override @@ -384,9 +384,8 @@ class _$MatchDetailTabStateImpl implements _MatchDetailTabState { const DeepCollectionEquality().equals( other._expandedInningsScorecard, _expandedInningsScorecard) && (identical(other.loading, loading) || other.loading == loading) && - (identical(other.ballScoreQueryListenerSet, - ballScoreQueryListenerSet) || - other.ballScoreQueryListenerSet == ballScoreQueryListenerSet) && + (identical(other.loadingBallScoreMore, loadingBallScoreMore) || + other.loadingBallScoreMore == loadingBallScoreMore) && (identical(other.highlightFilterOption, highlightFilterOption) || other.highlightFilterOption == highlightFilterOption)); } @@ -405,7 +404,7 @@ class _$MatchDetailTabStateImpl implements _MatchDetailTabState { const DeepCollectionEquality().hash(_filteredHighlight), const DeepCollectionEquality().hash(_expandedInningsScorecard), loading, - ballScoreQueryListenerSet, + loadingBallScoreMore, highlightFilterOption); /// Create a copy of MatchDetailTabState @@ -431,7 +430,7 @@ abstract class _MatchDetailTabState implements MatchDetailTabState { final List filteredHighlight, final List expandedInningsScorecard, final bool loading, - final bool ballScoreQueryListenerSet, + final bool loadingBallScoreMore, final HighlightFilterOption highlightFilterOption}) = _$MatchDetailTabStateImpl; @@ -458,7 +457,7 @@ abstract class _MatchDetailTabState implements MatchDetailTabState { @override bool get loading; @override - bool get ballScoreQueryListenerSet; + bool get loadingBallScoreMore; @override HighlightFilterOption get highlightFilterOption; diff --git a/khelo/lib/ui/flow/matches/match_list_screen.dart b/khelo/lib/ui/flow/matches/match_list_screen.dart index c10c0ae4..52d98f41 100644 --- a/khelo/lib/ui/flow/matches/match_list_screen.dart +++ b/khelo/lib/ui/flow/matches/match_list_screen.dart @@ -6,9 +6,11 @@ import 'package:khelo/components/error_screen.dart'; import 'package:khelo/components/match_detail_cell.dart'; import 'package:khelo/domain/extensions/context_extensions.dart'; import 'package:khelo/ui/app_route.dart'; +import 'package:style/callback/on_visible_callback.dart'; import 'package:style/extensions/context_extensions.dart'; import 'package:style/indicator/progress_indicator.dart'; +import '../../../domain/extensions/widget_extension.dart'; import 'match_list_view_model.dart'; class MatchListScreen extends ConsumerStatefulWidget { @@ -60,32 +62,38 @@ class _MatchListScreenState extends ConsumerState ); } - return (state.matches != null && state.matches!.isNotEmpty) + return (state.matches.isNotEmpty) ? ListView.separated( padding: context.mediaQueryPadding + const EdgeInsets.all(16), - itemCount: state.matches!.length, - separatorBuilder: (context, index) { - return const SizedBox(height: 16); - }, + itemCount: state.matches.length + 1, + separatorBuilder: (context, index) => const SizedBox(height: 16), itemBuilder: (context, index) { - final match = state.matches![index]; - return MatchDetailCell( - match: match, - showActionButtons: match.created_by == state.currentUserId, - onTap: () => - AppRoute.matchDetailTab(matchId: match.id).push(context), - onActionTap: () { - if (match.match_status == MatchStatus.yetToStart) { - AppRoute.addMatch(matchId: match.id).push(context); - } else if (match.match_status == MatchStatus.running) { - if (match.toss_decision == null || - match.toss_winner_id == null) { - AppRoute.addTossDetail(matchId: match.id).push(context); - } else { - AppRoute.scoreBoard(matchId: match.id).push(context); + if (index < state.matches.length) { + final match = state.matches[index]; + return MatchDetailCell( + match: match, + showActionButtons: match.created_by == state.currentUserId, + onTap: () => + AppRoute.matchDetailTab(matchId: match.id).push(context), + onActionTap: () { + if (match.match_status == MatchStatus.yetToStart) { + AppRoute.addMatch(matchId: match.id).push(context); + } else if (match.match_status == MatchStatus.running) { + if (match.toss_decision == null || + match.toss_winner_id == null) { + AppRoute.addTossDetail(matchId: match.id).push(context); + } else { + AppRoute.scoreBoard(matchId: match.id).push(context); + } } - } - }, + }, + ); + } + return OnVisibleCallback( + onVisible: () => runPostFrame(notifier.loadMatches), + child: (state.loading && state.matches.isNotEmpty) + ? const Center(child: AppProgressIndicator()) + : const SizedBox(), ); }, ) diff --git a/khelo/lib/ui/flow/matches/match_list_view_model.dart b/khelo/lib/ui/flow/matches/match_list_view_model.dart index a7280723..14262666 100644 --- a/khelo/lib/ui/flow/matches/match_list_view_model.dart +++ b/khelo/lib/ui/flow/matches/match_list_view_model.dart @@ -39,16 +39,22 @@ class MatchListViewNotifier extends StateNotifier { } Future loadMatches() async { - if (state.currentUserId == null) { - return; - } + if (state.currentUserId == null) return; + if (state.loading) return; + _matchesStreamSubscription?.cancel(); - state = state.copyWith(loading: true); + state = state.copyWith(loading: state.matches.isEmpty); try { _matchesStreamSubscription = _matchService - .streamUserRelatedMatches(state.currentUserId ?? '') - .listen((matches) { - state = state.copyWith(matches: matches, loading: false, error: null); + .streamUserRelatedMatches( + userId: state.currentUserId!, + limit: state.matches.length + 10, + ).listen((matches) { + state = state.copyWith( + matches: matches, + loading: false, + error: null, + ); }, onError: (e) { state = state.copyWith(loading: false, error: e); debugPrint("MatchListViewNotifier: error while load matches -> $e"); @@ -71,7 +77,7 @@ class MatchListViewState with _$MatchListViewState { const factory MatchListViewState({ Object? error, String? currentUserId, - List? matches, + @Default([]) List matches, @Default(false) bool loading, }) = _MatchListViewState; } diff --git a/khelo/lib/ui/flow/matches/match_list_view_model.freezed.dart b/khelo/lib/ui/flow/matches/match_list_view_model.freezed.dart index a6d20fa2..3e9fc133 100644 --- a/khelo/lib/ui/flow/matches/match_list_view_model.freezed.dart +++ b/khelo/lib/ui/flow/matches/match_list_view_model.freezed.dart @@ -18,7 +18,7 @@ final _privateConstructorUsedError = UnsupportedError( mixin _$MatchListViewState { Object? get error => throw _privateConstructorUsedError; String? get currentUserId => throw _privateConstructorUsedError; - List? get matches => throw _privateConstructorUsedError; + List get matches => throw _privateConstructorUsedError; bool get loading => throw _privateConstructorUsedError; /// Create a copy of MatchListViewState @@ -37,7 +37,7 @@ abstract class $MatchListViewStateCopyWith<$Res> { $Res call( {Object? error, String? currentUserId, - List? matches, + List matches, bool loading}); } @@ -58,7 +58,7 @@ class _$MatchListViewStateCopyWithImpl<$Res, $Val extends MatchListViewState> $Res call({ Object? error = freezed, Object? currentUserId = freezed, - Object? matches = freezed, + Object? matches = null, Object? loading = null, }) { return _then(_value.copyWith( @@ -67,10 +67,10 @@ class _$MatchListViewStateCopyWithImpl<$Res, $Val extends MatchListViewState> ? _value.currentUserId : currentUserId // ignore: cast_nullable_to_non_nullable as String?, - matches: freezed == matches + matches: null == matches ? _value.matches : matches // ignore: cast_nullable_to_non_nullable - as List?, + as List, loading: null == loading ? _value.loading : loading // ignore: cast_nullable_to_non_nullable @@ -90,7 +90,7 @@ abstract class _$$MatchListViewStateImplCopyWith<$Res> $Res call( {Object? error, String? currentUserId, - List? matches, + List matches, bool loading}); } @@ -109,7 +109,7 @@ class __$$MatchListViewStateImplCopyWithImpl<$Res> $Res call({ Object? error = freezed, Object? currentUserId = freezed, - Object? matches = freezed, + Object? matches = null, Object? loading = null, }) { return _then(_$MatchListViewStateImpl( @@ -118,10 +118,10 @@ class __$$MatchListViewStateImplCopyWithImpl<$Res> ? _value.currentUserId : currentUserId // ignore: cast_nullable_to_non_nullable as String?, - matches: freezed == matches + matches: null == matches ? _value._matches : matches // ignore: cast_nullable_to_non_nullable - as List?, + as List, loading: null == loading ? _value.loading : loading // ignore: cast_nullable_to_non_nullable @@ -136,7 +136,7 @@ class _$MatchListViewStateImpl implements _MatchListViewState { const _$MatchListViewStateImpl( {this.error, this.currentUserId, - final List? matches, + final List matches = const [], this.loading = false}) : _matches = matches; @@ -144,14 +144,13 @@ class _$MatchListViewStateImpl implements _MatchListViewState { final Object? error; @override final String? currentUserId; - final List? _matches; + final List _matches; @override - List? get matches { - final value = _matches; - if (value == null) return null; + @JsonKey() + List get matches { if (_matches is EqualUnmodifiableListView) return _matches; // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(value); + return EqualUnmodifiableListView(_matches); } @override @@ -197,7 +196,7 @@ abstract class _MatchListViewState implements MatchListViewState { const factory _MatchListViewState( {final Object? error, final String? currentUserId, - final List? matches, + final List matches, final bool loading}) = _$MatchListViewStateImpl; @override @@ -205,7 +204,7 @@ abstract class _MatchListViewState implements MatchListViewState { @override String? get currentUserId; @override - List? get matches; + List get matches; @override bool get loading; diff --git a/khelo/lib/ui/flow/score_board/score_board_view_model.dart b/khelo/lib/ui/flow/score_board/score_board_view_model.dart index 0e115b78..df51db80 100644 --- a/khelo/lib/ui/flow/score_board/score_board_view_model.dart +++ b/khelo/lib/ui/flow/score_board/score_board_view_model.dart @@ -96,7 +96,7 @@ class ScoreBoardViewNotifier extends StateNotifier { String otherInningId, ) { return _ballScoreService.streamBallScoresByInningIds( - [currentInningId, otherInningId]).handleError((e) { + inningIds: [currentInningId, otherInningId]).handleError((e) { throw AppError.fromError(e); }); } diff --git a/khelo/lib/ui/flow/team/detail/components/team_detail_match_content.dart b/khelo/lib/ui/flow/team/detail/components/team_detail_match_content.dart index 5c8fc43d..dc5de68c 100644 --- a/khelo/lib/ui/flow/team/detail/components/team_detail_match_content.dart +++ b/khelo/lib/ui/flow/team/detail/components/team_detail_match_content.dart @@ -7,6 +7,10 @@ import 'package:khelo/components/match_detail_cell.dart'; import 'package:khelo/domain/extensions/context_extensions.dart'; import 'package:khelo/ui/app_route.dart'; import 'package:khelo/ui/flow/team/detail/team_detail_view_model.dart'; +import 'package:style/callback/on_visible_callback.dart'; +import 'package:style/indicator/progress_indicator.dart'; + +import '../../../../../domain/extensions/widget_extension.dart'; class TeamDetailMatchContent extends ConsumerWidget { const TeamDetailMatchContent({super.key}); @@ -17,29 +21,38 @@ class TeamDetailMatchContent extends ConsumerWidget { final isAdminOrOwner = state.team?.isAdminOrOwner(state.currentUserId) ?? false; - if (state.matches != null && state.matches!.isNotEmpty) { + if (state.matches.isNotEmpty) { return ListView.separated( padding: const EdgeInsets.all(16), - itemCount: state.matches?.length ?? 0, + itemCount: state.matches.length + 1, itemBuilder: (context, index) { - final match = state.matches![index]; - return MatchDetailCell( - match: match, - showActionButtons: isAdminOrOwner, - onTap: () => - AppRoute.matchDetailTab(matchId: match.id).push(context), - onActionTap: () { - if (match.match_status == MatchStatus.yetToStart) { - AppRoute.addMatch(matchId: match.id).push(context); - } else { - if (match.toss_decision == null || - match.toss_winner_id == null) { - AppRoute.addTossDetail(matchId: match.id).push(context); + if (index < state.matches.length) { + final match = state.matches[index]; + return MatchDetailCell( + match: match, + showActionButtons: isAdminOrOwner, + onTap: () => + AppRoute.matchDetailTab(matchId: match.id).push(context), + onActionTap: () { + if (match.match_status == MatchStatus.yetToStart) { + AppRoute.addMatch(matchId: match.id).push(context); } else { - AppRoute.scoreBoard(matchId: match.id).push(context); + if (match.toss_decision == null || + match.toss_winner_id == null) { + AppRoute.addTossDetail(matchId: match.id).push(context); + } else { + AppRoute.scoreBoard(matchId: match.id).push(context); + } } - } - }, + }, + ); + } + return OnVisibleCallback( + onVisible: () => runPostFrame( + ref.read(teamDetailStateProvider.notifier).loadData), + child: (state.loading && state.matches.isNotEmpty) + ? const Center(child: AppProgressIndicator()) + : const SizedBox(), ); }, separatorBuilder: (context, index) => const SizedBox(height: 16), diff --git a/khelo/lib/ui/flow/team/detail/components/team_detail_stat_content.dart b/khelo/lib/ui/flow/team/detail/components/team_detail_stat_content.dart index 38818e42..b2e59804 100644 --- a/khelo/lib/ui/flow/team/detail/components/team_detail_stat_content.dart +++ b/khelo/lib/ui/flow/team/detail/components/team_detail_stat_content.dart @@ -19,9 +19,8 @@ class TeamDetailStatContent extends ConsumerWidget { final state = ref.watch(teamDetailStateProvider); if (state.matches - ?.where((element) => element.match_status == MatchStatus.finish) - .isEmpty ?? - true) { + .where((element) => element.match_status == MatchStatus.finish) + .isEmpty) { return EmptyScreen( title: context.l10n.team_detail_empty_stat_title, description: context.l10n.team_detail_empty_stat_description_text, diff --git a/khelo/lib/ui/flow/team/detail/team_detail_view_model.dart b/khelo/lib/ui/flow/team/detail/team_detail_view_model.dart index 78b7bdb7..75d99c35 100644 --- a/khelo/lib/ui/flow/team/detail/team_detail_view_model.dart +++ b/khelo/lib/ui/flow/team/detail/team_detail_view_model.dart @@ -36,11 +36,17 @@ class TeamDetailViewNotifier extends StateNotifier { void loadData() { if (_teamId == null) return; + if (state.loading) return; _teamStreamSubscription?.cancel(); state = - state.copyWith(loading: state.team == null || state.matches == null); - final teamCombiner = combineLatest2(_teamService.streamTeamById(_teamId!), - _matchService.streamMatchesByTeamId(_teamId!)); + state.copyWith(loading: state.team == null || state.matches.isEmpty); + final teamCombiner = combineLatest2( + _teamService.streamTeamById(_teamId!), + _matchService.streamMatchesByTeamId( + teamId: _teamId!, + limit: state.matches.length + 10, + ), + ); _teamStreamSubscription = teamCombiner.listen((data) { state = state.copyWith( team: data.$1, @@ -73,7 +79,7 @@ class TeamDetailState with _$TeamDetailState { Object? error, TeamModel? team, String? currentUserId, - List? matches, + @Default([]) List matches, @Default(0) int selectedTab, @Default(false) bool loading, }) = _TeamDetailState; diff --git a/khelo/lib/ui/flow/team/detail/team_detail_view_model.freezed.dart b/khelo/lib/ui/flow/team/detail/team_detail_view_model.freezed.dart index d3114e00..ce87bb4b 100644 --- a/khelo/lib/ui/flow/team/detail/team_detail_view_model.freezed.dart +++ b/khelo/lib/ui/flow/team/detail/team_detail_view_model.freezed.dart @@ -19,7 +19,7 @@ mixin _$TeamDetailState { Object? get error => throw _privateConstructorUsedError; TeamModel? get team => throw _privateConstructorUsedError; String? get currentUserId => throw _privateConstructorUsedError; - List? get matches => throw _privateConstructorUsedError; + List get matches => throw _privateConstructorUsedError; int get selectedTab => throw _privateConstructorUsedError; bool get loading => throw _privateConstructorUsedError; @@ -40,7 +40,7 @@ abstract class $TeamDetailStateCopyWith<$Res> { {Object? error, TeamModel? team, String? currentUserId, - List? matches, + List matches, int selectedTab, bool loading}); @@ -65,7 +65,7 @@ class _$TeamDetailStateCopyWithImpl<$Res, $Val extends TeamDetailState> Object? error = freezed, Object? team = freezed, Object? currentUserId = freezed, - Object? matches = freezed, + Object? matches = null, Object? selectedTab = null, Object? loading = null, }) { @@ -79,10 +79,10 @@ class _$TeamDetailStateCopyWithImpl<$Res, $Val extends TeamDetailState> ? _value.currentUserId : currentUserId // ignore: cast_nullable_to_non_nullable as String?, - matches: freezed == matches + matches: null == matches ? _value.matches : matches // ignore: cast_nullable_to_non_nullable - as List?, + as List, selectedTab: null == selectedTab ? _value.selectedTab : selectedTab // ignore: cast_nullable_to_non_nullable @@ -121,7 +121,7 @@ abstract class _$$TeamDetailStateImplCopyWith<$Res> {Object? error, TeamModel? team, String? currentUserId, - List? matches, + List matches, int selectedTab, bool loading}); @@ -145,7 +145,7 @@ class __$$TeamDetailStateImplCopyWithImpl<$Res> Object? error = freezed, Object? team = freezed, Object? currentUserId = freezed, - Object? matches = freezed, + Object? matches = null, Object? selectedTab = null, Object? loading = null, }) { @@ -159,10 +159,10 @@ class __$$TeamDetailStateImplCopyWithImpl<$Res> ? _value.currentUserId : currentUserId // ignore: cast_nullable_to_non_nullable as String?, - matches: freezed == matches + matches: null == matches ? _value._matches : matches // ignore: cast_nullable_to_non_nullable - as List?, + as List, selectedTab: null == selectedTab ? _value.selectedTab : selectedTab // ignore: cast_nullable_to_non_nullable @@ -182,7 +182,7 @@ class _$TeamDetailStateImpl implements _TeamDetailState { {this.error, this.team, this.currentUserId, - final List? matches, + final List matches = const [], this.selectedTab = 0, this.loading = false}) : _matches = matches; @@ -193,14 +193,13 @@ class _$TeamDetailStateImpl implements _TeamDetailState { final TeamModel? team; @override final String? currentUserId; - final List? _matches; + final List _matches; @override - List? get matches { - final value = _matches; - if (value == null) return null; + @JsonKey() + List get matches { if (_matches is EqualUnmodifiableListView) return _matches; // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(value); + return EqualUnmodifiableListView(_matches); } @override @@ -255,7 +254,7 @@ abstract class _TeamDetailState implements TeamDetailState { {final Object? error, final TeamModel? team, final String? currentUserId, - final List? matches, + final List matches, final int selectedTab, final bool loading}) = _$TeamDetailStateImpl; @@ -266,7 +265,7 @@ abstract class _TeamDetailState implements TeamDetailState { @override String? get currentUserId; @override - List? get matches; + List get matches; @override int get selectedTab; @override diff --git a/khelo/lib/ui/flow/team/team_list_screen.dart b/khelo/lib/ui/flow/team/team_list_screen.dart index 5cb15a5f..892195e7 100644 --- a/khelo/lib/ui/flow/team/team_list_screen.dart +++ b/khelo/lib/ui/flow/team/team_list_screen.dart @@ -11,9 +11,11 @@ import 'package:khelo/components/team_detail_cell.dart'; import 'package:khelo/domain/extensions/context_extensions.dart'; import 'package:khelo/ui/app_route.dart'; import 'package:khelo/ui/flow/team/team_list_view_model.dart'; +import 'package:style/callback/on_visible_callback.dart'; import 'package:style/extensions/context_extensions.dart'; import 'package:style/indicator/progress_indicator.dart'; +import '../../../domain/extensions/widget_extension.dart'; import '../../../gen/assets.gen.dart'; class TeamListScreen extends ConsumerStatefulWidget { @@ -75,18 +77,26 @@ class _TeamListScreenState extends ConsumerState ) { return (state.filteredTeams.isNotEmpty) ? ListView.separated( - itemCount: state.filteredTeams.length, + itemCount: state.filteredTeams.length + 1, padding: const EdgeInsets.fromLTRB(16, 16, 16, 8) + context.mediaQueryPadding, separatorBuilder: (context, index) => Divider(color: context.colorScheme.outline), itemBuilder: (context, index) { - final team = state.filteredTeams[index]; - return TeamDetailCell( - team: team, - showMoreOptionButton: team.isAdminOrOwner(state.currentUserId), - onTap: () => AppRoute.teamDetail(teamId: team.id).push(context), - onMoreOptionTap: () => _moreActionButton(context, team), + if (index < state.filteredTeams.length) { + final team = state.filteredTeams[index]; + return TeamDetailCell( + team: team, + showMoreOptionButton: team.isAdminOrOwner(state.currentUserId), + onTap: () => AppRoute.teamDetail(teamId: team.id).push(context), + onMoreOptionTap: () => _moreActionButton(context, team), + ); + } + return OnVisibleCallback( + onVisible: () => runPostFrame(notifier.loadTeamList), + child: (state.loading && state.teams.isNotEmpty) + ? const Center(child: AppProgressIndicator()) + : const SizedBox(), ); }, ) diff --git a/khelo/lib/ui/flow/team/team_list_view_model.dart b/khelo/lib/ui/flow/team/team_list_view_model.dart index 9ebfdb45..ff919957 100644 --- a/khelo/lib/ui/flow/team/team_list_view_model.dart +++ b/khelo/lib/ui/flow/team/team_list_view_model.dart @@ -36,19 +36,25 @@ class TeamListViewNotifier extends StateNotifier { _teamsStreamSubscription?.cancel(); } state = state.copyWith(currentUserId: userId); - loadTeamList(); } Future loadTeamList() async { if (state.currentUserId == null) return; + if (state.loading) return; _teamsStreamSubscription?.cancel(); state = state.copyWith(loading: state.teams.isEmpty); try { _teamsStreamSubscription = _teamService - .streamUserRelatedTeams(state.currentUserId!) - .listen((teams) { - state = state.copyWith(teams: teams, loading: false, error: null); + .streamUserRelatedTeams( + userId: state.currentUserId!, + limit: state.teams.length + 10, + ).listen((teams) { + state = state.copyWith( + teams: {...state.teams, ...teams}.toList(), + loading: false, + error: null, + ); _filterTeamList(); }, onError: (e) { state = state.copyWith(loading: false, error: e); @@ -114,7 +120,7 @@ class TeamListViewState with _$TeamListViewState { String? currentUserId, @Default([]) List teams, @Default([]) List filteredTeams, - @Default(true) bool loading, + @Default(false) bool loading, @Default(TeamFilterOption.all) TeamFilterOption selectedFilter, }) = _TeamListViewState; } diff --git a/khelo/lib/ui/flow/team/team_list_view_model.freezed.dart b/khelo/lib/ui/flow/team/team_list_view_model.freezed.dart index e9a95f2d..aaab8371 100644 --- a/khelo/lib/ui/flow/team/team_list_view_model.freezed.dart +++ b/khelo/lib/ui/flow/team/team_list_view_model.freezed.dart @@ -178,7 +178,7 @@ class _$TeamListViewStateImpl implements _TeamListViewState { this.currentUserId, final List teams = const [], final List filteredTeams = const [], - this.loading = true, + this.loading = false, this.selectedFilter = TeamFilterOption.all}) : _teams = teams, _filteredTeams = filteredTeams;