From 007d85b1d2fc17d7c91ab788a21afd2c41fec216 Mon Sep 17 00:00:00 2001 From: Mayank Variya Date: Fri, 11 Oct 2024 15:46:07 +0530 Subject: [PATCH 1/5] implement tournament detail --- data/lib/api/tournament/tournament_model.dart | 4 + .../tournament/tournament_model.freezed.dart | 58 ++++- data/lib/service/match/match_service.dart | 14 ++ data/lib/service/team/team_service.dart | 12 ++ .../tournament/tournament_service.dart | 72 ++++++- khelo/assets/images/ic_location.svg | 6 +- khelo/assets/locales/app_en.arb | 14 ++ khelo/lib/components/image_avatar.dart | 3 + khelo/lib/ui/app_route.dart | 11 + .../detail/tournament_detail_screen.dart | 176 +++++++++++++++ .../detail/tournament_detail_view_model.dart | 57 +++++ .../tournament_detail_view_model.freezed.dart | 200 ++++++++++++++++++ .../tournament/tournament_list_screen.dart | 4 +- 13 files changed, 616 insertions(+), 15 deletions(-) create mode 100644 khelo/lib/ui/flow/tournament/detail/tournament_detail_screen.dart create mode 100644 khelo/lib/ui/flow/tournament/detail/tournament_detail_view_model.dart create mode 100644 khelo/lib/ui/flow/tournament/detail/tournament_detail_view_model.freezed.dart diff --git a/data/lib/api/tournament/tournament_model.dart b/data/lib/api/tournament/tournament_model.dart index 07b9c4c7..568291b2 100644 --- a/data/lib/api/tournament/tournament_model.dart +++ b/data/lib/api/tournament/tournament_model.dart @@ -6,6 +6,7 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import '../../converter/timestamp_json_converter.dart'; import '../match/match_model.dart'; import '../team/team_model.dart'; +import '../user/user_models.dart'; part 'tournament_model.freezed.dart'; @@ -47,8 +48,11 @@ class TournamentModel with _$TournamentModel { @freezed class TournamentMember with _$TournamentMember { + @JsonSerializable(explicitToJson: true) const factory TournamentMember({ required String id, + @JsonKey(includeToJson: false, includeFromJson: false) + @Default(UserModel(id: '')) UserModel user, @Default(TournamentMemberRole.admin) TournamentMemberRole role, }) = _TournamentMember; diff --git a/data/lib/api/tournament/tournament_model.freezed.dart b/data/lib/api/tournament/tournament_model.freezed.dart index 9eba15b3..ed37d162 100644 --- a/data/lib/api/tournament/tournament_model.freezed.dart +++ b/data/lib/api/tournament/tournament_model.freezed.dart @@ -518,6 +518,8 @@ TournamentMember _$TournamentMemberFromJson(Map json) { /// @nodoc mixin _$TournamentMember { String get id => throw _privateConstructorUsedError; + @JsonKey(includeToJson: false, includeFromJson: false) + UserModel get user => throw _privateConstructorUsedError; TournamentMemberRole get role => throw _privateConstructorUsedError; /// Serializes this TournamentMember to a JSON map. @@ -536,7 +538,12 @@ abstract class $TournamentMemberCopyWith<$Res> { TournamentMember value, $Res Function(TournamentMember) then) = _$TournamentMemberCopyWithImpl<$Res, TournamentMember>; @useResult - $Res call({String id, TournamentMemberRole role}); + $Res call( + {String id, + @JsonKey(includeToJson: false, includeFromJson: false) UserModel user, + TournamentMemberRole role}); + + $UserModelCopyWith<$Res> get user; } /// @nodoc @@ -555,6 +562,7 @@ class _$TournamentMemberCopyWithImpl<$Res, $Val extends TournamentMember> @override $Res call({ Object? id = null, + Object? user = null, Object? role = null, }) { return _then(_value.copyWith( @@ -562,12 +570,26 @@ class _$TournamentMemberCopyWithImpl<$Res, $Val extends TournamentMember> ? _value.id : id // ignore: cast_nullable_to_non_nullable as String, + user: null == user + ? _value.user + : user // ignore: cast_nullable_to_non_nullable + as UserModel, role: null == role ? _value.role : role // ignore: cast_nullable_to_non_nullable as TournamentMemberRole, ) as $Val); } + + /// Create a copy of TournamentMember + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $UserModelCopyWith<$Res> get user { + return $UserModelCopyWith<$Res>(_value.user, (value) { + return _then(_value.copyWith(user: value) as $Val); + }); + } } /// @nodoc @@ -578,7 +600,13 @@ abstract class _$$TournamentMemberImplCopyWith<$Res> __$$TournamentMemberImplCopyWithImpl<$Res>; @override @useResult - $Res call({String id, TournamentMemberRole role}); + $Res call( + {String id, + @JsonKey(includeToJson: false, includeFromJson: false) UserModel user, + TournamentMemberRole role}); + + @override + $UserModelCopyWith<$Res> get user; } /// @nodoc @@ -595,6 +623,7 @@ class __$$TournamentMemberImplCopyWithImpl<$Res> @override $Res call({ Object? id = null, + Object? user = null, Object? role = null, }) { return _then(_$TournamentMemberImpl( @@ -602,6 +631,10 @@ class __$$TournamentMemberImplCopyWithImpl<$Res> ? _value.id : id // ignore: cast_nullable_to_non_nullable as String, + user: null == user + ? _value.user + : user // ignore: cast_nullable_to_non_nullable + as UserModel, role: null == role ? _value.role : role // ignore: cast_nullable_to_non_nullable @@ -611,10 +644,14 @@ class __$$TournamentMemberImplCopyWithImpl<$Res> } /// @nodoc -@JsonSerializable() + +@JsonSerializable(explicitToJson: true) class _$TournamentMemberImpl implements _TournamentMember { const _$TournamentMemberImpl( - {required this.id, this.role = TournamentMemberRole.admin}); + {required this.id, + @JsonKey(includeToJson: false, includeFromJson: false) + this.user = const UserModel(id: ''), + this.role = TournamentMemberRole.admin}); factory _$TournamentMemberImpl.fromJson(Map json) => _$$TournamentMemberImplFromJson(json); @@ -622,12 +659,15 @@ class _$TournamentMemberImpl implements _TournamentMember { @override final String id; @override + @JsonKey(includeToJson: false, includeFromJson: false) + final UserModel user; + @override @JsonKey() final TournamentMemberRole role; @override String toString() { - return 'TournamentMember(id: $id, role: $role)'; + return 'TournamentMember(id: $id, user: $user, role: $role)'; } @override @@ -636,12 +676,13 @@ class _$TournamentMemberImpl implements _TournamentMember { (other.runtimeType == runtimeType && other is _$TournamentMemberImpl && (identical(other.id, id) || other.id == id) && + (identical(other.user, user) || other.user == user) && (identical(other.role, role) || other.role == role)); } @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => Object.hash(runtimeType, id, role); + int get hashCode => Object.hash(runtimeType, id, user, role); /// Create a copy of TournamentMember /// with the given fields replaced by the non-null parameter values. @@ -663,6 +704,8 @@ class _$TournamentMemberImpl implements _TournamentMember { abstract class _TournamentMember implements TournamentMember { const factory _TournamentMember( {required final String id, + @JsonKey(includeToJson: false, includeFromJson: false) + final UserModel user, final TournamentMemberRole role}) = _$TournamentMemberImpl; factory _TournamentMember.fromJson(Map json) = @@ -671,6 +714,9 @@ abstract class _TournamentMember implements TournamentMember { @override String get id; @override + @JsonKey(includeToJson: false, includeFromJson: false) + UserModel get user; + @override TournamentMemberRole get role; /// Create a copy of TournamentMember diff --git a/data/lib/service/match/match_service.dart b/data/lib/service/match/match_service.dart index a16cf454..cbd227ef 100644 --- a/data/lib/service/match/match_service.dart +++ b/data/lib/service/match/match_service.dart @@ -464,4 +464,18 @@ class MatchService { throw AppError.fromError(error, stack); } } + + //Helper Methods + Future> getMatchesByIds(List matchIds) async { + try { + final List matches = []; + await Future.forEach(matchIds, (matchId) async { + final match = await getMatchById(matchId); + matches.add(match); + }); + return matches; + } catch (error, stack) { + throw AppError.fromError(error, stack); + } + } } diff --git a/data/lib/service/team/team_service.dart b/data/lib/service/team/team_service.dart index 4460b7ce..1d119eaf 100644 --- a/data/lib/service/team/team_service.dart +++ b/data/lib/service/team/team_service.dart @@ -267,6 +267,18 @@ class TeamService { } } + Future> getTeamsByIds(List teamIds) async { + try { + final teamList = await _teamsCollection + .where(FieldPath.documentId, whereIn: teamIds) + .get() + .then((value) => value.docs.map((e) => e.data()).toList()); + return teamList; + } catch (error, stack) { + throw AppError.fromError(error, stack); + } + } + Stream> streamUserRelatedTeamsByUserId(String userId) { final currentPlayer = TeamPlayer(id: userId); diff --git a/data/lib/service/tournament/tournament_service.dart b/data/lib/service/tournament/tournament_service.dart index 093b66ff..55bdfab9 100644 --- a/data/lib/service/tournament/tournament_service.dart +++ b/data/lib/service/tournament/tournament_service.dart @@ -4,17 +4,34 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../api/tournament/tournament_model.dart'; import '../../errors/app_error.dart'; import '../../utils/constant/firestore_constant.dart'; +import '../match/match_service.dart'; +import '../team/team_service.dart'; +import '../user/user_service.dart'; -final tournamentServiceProvider = - Provider((ref) => TournamentService(FirebaseFirestore.instance)); +final tournamentServiceProvider = Provider( + (ref) => TournamentService( + fireStore: FirebaseFirestore.instance, + teamService: ref.read(teamServiceProvider), + matchService: ref.read(matchServiceProvider), + userService: ref.read(userServiceProvider), + ), +); class TournamentService { - final FirebaseFirestore _firestore; + final FirebaseFirestore fireStore; + final TeamService teamService; + final MatchService matchService; + final UserService userService; - TournamentService(this._firestore); + TournamentService({ + required this.fireStore, + required this.teamService, + required this.matchService, + required this.userService, + }); CollectionReference get _tournamentCollection => - _firestore.collection(FireStoreConst.tournamentCollection).withConverter( + fireStore.collection(FireStoreConst.tournamentCollection).withConverter( fromFirestore: TournamentModel.fromFireStore, toFirestore: (TournamentModel tournament, _) => tournament.toJson(), ); @@ -49,4 +66,49 @@ class TournamentService { .map((event) => event.docs.map((e) => e.data()).toList()) .handleError((error, stack) => throw AppError.fromError(error, stack)); } + + Stream streamTournamentById(String tournamentId) { + return _tournamentCollection + .doc(tournamentId) + .snapshots() + .asyncMap((snapshot) async { + if (snapshot.data() == null) { + return TournamentModel( + id: tournamentId, + name: '', + created_by: '', + type: TournamentType.other, + start_date: DateTime.now(), + ); + } else { + var tournament = snapshot.data()!; + final teamIds = tournament.team_ids; + final matchIds = tournament.match_ids; + + if (teamIds.isNotEmpty) { + final teams = await teamService.getTeamsByIds(teamIds); + tournament = tournament.copyWith(teams: teams); + } + + if (matchIds.isNotEmpty) { + final matches = await matchService.getMatchesByIds(matchIds); + tournament = tournament.copyWith(matches: matches); + } + + if (tournament.members.isNotEmpty) { + final memberIds = tournament.members.map((e) => e.id).toList(); + final users = await userService.getUsersByIds(memberIds); + + final members = tournament.members.map((member) { + final user = users.firstWhere((element) => element.id == member.id); + return member.copyWith(user: user); + }).toList(); + + tournament = tournament.copyWith(members: members); + } + + return tournament; + } + }).handleError((error, stack) => throw AppError.fromError(error, stack)); + } } diff --git a/khelo/assets/images/ic_location.svg b/khelo/assets/images/ic_location.svg index 39251a6f..a5bb8734 100644 --- a/khelo/assets/images/ic_location.svg +++ b/khelo/assets/images/ic_location.svg @@ -1,4 +1,4 @@ - - - + + + diff --git a/khelo/assets/locales/app_en.arb b/khelo/assets/locales/app_en.arb index 40b5e33b..4506adf3 100644 --- a/khelo/assets/locales/app_en.arb +++ b/khelo/assets/locales/app_en.arb @@ -164,6 +164,20 @@ } }, + "@_TOURNAMENT_DETAIL": { + }, + "tournament_detail_not_found_title": "No Tournament found", + "tournament_detail_not_found_description": "The tournament you are looking for does not available.", + "tournament_detail_start_from_title": "Start from {date}", + "@tournament_detail_start_from_title": { + "description": "Start from {date}", + "placeholders": { + "date": { + "type": "String" + } + } + }, + "@_TOURNAMENT_TYPE":{ }, "tournament_type_knock_out": "Knockout", diff --git a/khelo/lib/components/image_avatar.dart b/khelo/lib/components/image_avatar.dart index ef8dbcb7..fce92815 100644 --- a/khelo/lib/components/image_avatar.dart +++ b/khelo/lib/components/image_avatar.dart @@ -8,6 +8,7 @@ class ImageAvatar extends StatelessWidget { final double size; final String? imageUrl; final String initial; + final Border? border; final Color? foregroundColor; final Color? backgroundColor; @@ -15,6 +16,7 @@ class ImageAvatar extends StatelessWidget { super.key, this.size = 50, this.imageUrl, + this.border, required this.initial, this.foregroundColor, this.backgroundColor, @@ -29,6 +31,7 @@ class ImageAvatar extends StatelessWidget { alignment: Alignment.center, decoration: BoxDecoration( shape: BoxShape.circle, + border: border, color: backgroundColor ?? context.colorScheme.containerHigh, ), child: imageUrl == null diff --git a/khelo/lib/ui/app_route.dart b/khelo/lib/ui/app_route.dart index 4b90d3f6..16abb666 100644 --- a/khelo/lib/ui/app_route.dart +++ b/khelo/lib/ui/app_route.dart @@ -32,6 +32,7 @@ import 'flow/main/main_screen.dart'; import 'flow/settings/support/contact_support_screen.dart'; import 'flow/sign_in/sign_in_with_phone/sign_in_with_phone_screen.dart'; import 'flow/team/user_detail/user_detail_screen.dart'; +import 'flow/tournament/detail/tournament_detail_screen.dart'; class AppRoute { static const pathPhoneNumberVerification = '/phone-number-verification'; @@ -55,6 +56,7 @@ class AppRoute { static const pathViewAll = "/view-all"; static const pathContactSelection = "/contact-selection"; static const pathAddTournament = "/add-tournament"; + static const pathTournamentDetail = "/tournament-detail"; final String path; final String? name; @@ -172,6 +174,11 @@ class AppRoute { builder: (_) => const AddTournamentScreen(), ); + static AppRoute tournamentDetail({required String tournamentId}) => AppRoute( + pathTournamentDetail, + builder: (_) => TournamentDetailScreen(tournamentId: tournamentId), + ); + static AppRoute matchDetailTab({required String matchId}) => AppRoute( pathMatchDetailTab, builder: (_) => MatchDetailTabScreen(matchId: matchId), @@ -330,6 +337,10 @@ class AppRoute { path: pathAddTournament, builder: (context, state) => state.widget(context), ), + GoRoute( + path: pathTournamentDetail, + builder: (context, state) => state.widget(context), + ), GoRoute( path: pathMatchDetailTab, builder: (context, state) => state.widget(context), diff --git a/khelo/lib/ui/flow/tournament/detail/tournament_detail_screen.dart b/khelo/lib/ui/flow/tournament/detail/tournament_detail_screen.dart new file mode 100644 index 00000000..4624b9de --- /dev/null +++ b/khelo/lib/ui/flow/tournament/detail/tournament_detail_screen.dart @@ -0,0 +1,176 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:data/api/tournament/tournament_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:khelo/components/empty_screen.dart'; +import 'package:khelo/components/image_avatar.dart'; +import 'package:khelo/domain/extensions/context_extensions.dart'; +import 'package:khelo/domain/formatter/date_formatter.dart'; +import 'package:khelo/ui/flow/tournament/detail/tournament_detail_view_model.dart'; +import 'package:style/extensions/context_extensions.dart'; +import 'package:style/indicator/progress_indicator.dart'; +import 'package:style/text/app_text_style.dart'; + +import '../../../../components/app_page.dart'; +import '../../../../components/error_screen.dart'; +import '../../../../domain/extensions/widget_extension.dart'; +import '../../../../gen/assets.gen.dart'; + +class TournamentDetailScreen extends ConsumerStatefulWidget { + final String tournamentId; + + const TournamentDetailScreen({ + super.key, + required this.tournamentId, + }); + + @override + ConsumerState createState() => + _TournamentDetailScreenState(); +} + +class _TournamentDetailScreenState + extends ConsumerState { + late TournamentDetailStateViewNotifier notifier; + + @override + void initState() { + super.initState(); + notifier = ref.read(tournamentDetailStateProvider.notifier); + runPostFrame(() => notifier.setData(widget.tournamentId)); + } + + @override + Widget build(BuildContext context) { + final state = ref.watch(tournamentDetailStateProvider); + return AppPage( + body: Builder(builder: (context) { + return _body(context, state); + }), + ); + } + + Widget _body(BuildContext context, TournamentDetailState state) { + if (state.loading) { + return const Center(child: AppProgressIndicator()); + } + if (state.error != null) { + return ErrorScreen( + error: state.error, + onRetryTap: notifier.loadTournament, + ); + } + + if (state.tournament == null) { + return EmptyScreen( + title: context.l10n.tournament_detail_not_found_title, + description: context.l10n.tournament_detail_not_found_description, + isShowButton: false, + ); + } + + return CustomScrollView( + slivers: [ + SliverAppBar( + expandedHeight: 300, + backgroundColor: context.colorScheme.surface, + pinned: true, + flexibleSpace: _flexibleTitle(context, state.tournament!), + actions: [], + ), + SliverToBoxAdapter( + child: SizedBox(height: 16 + context.mediaQueryPadding.bottom), + ), + SliverToBoxAdapter( + child: _content(context, state), + ) + ], + ); + } + + Widget _content(BuildContext context, TournamentDetailState state) { + return Column(); + } + + Widget _flexibleTitle(BuildContext context, TournamentModel tournament) { + return FlexibleSpaceBar( + background: Stack( + children: [ + Container( + decoration: BoxDecoration( + color: context.colorScheme.containerLow, + image: (tournament.banner_img_url != null) + ? DecorationImage( + image: CachedNetworkImageProvider( + tournament.banner_img_url ?? ''), + fit: BoxFit.fill, + ) + : null, + ), + child: (tournament.banner_img_url == null) + ? Center( + child: SvgPicture.asset( + Assets.images.icTournaments, + colorFilter: ColorFilter.mode( + context.colorScheme.textPrimary, + BlendMode.srcIn, + ), + ), + ) + : null, + ), + Positioned( + left: 16, + bottom: 24, + child: _profileView(context, tournament), + ), + ], + ), + ); + } + + Widget _profileView(BuildContext context, TournamentModel tournament) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ImageAvatar( + initial: tournament.name.characters.first.toUpperCase(), + size: 80, + imageUrl: tournament.profile_img_url, + border: Border.all( + color: Colors.white, + width: 1.5, + ), + backgroundColor: context.colorScheme.primary, + ), + const SizedBox(height: 16), + Text( + tournament.name, + style: AppTextStyle.header1.copyWith(color: Colors.white), + ), + const SizedBox(height: 4), + Row( + children: [ + SvgPicture.asset( + Assets.images.icCalendar, + height: 24, + width: 24, + colorFilter: ColorFilter.mode( + Colors.white, + BlendMode.srcIn, + ), + ), + const SizedBox(width: 8), + Text( + context.l10n.tournament_detail_start_from_title(tournament + .start_date + .format(context, DateFormatType.dayMonth)), + style: AppTextStyle.body1.copyWith(color: Colors.white), + ), + ], + ) + ], + ); + } +} diff --git a/khelo/lib/ui/flow/tournament/detail/tournament_detail_view_model.dart b/khelo/lib/ui/flow/tournament/detail/tournament_detail_view_model.dart new file mode 100644 index 00000000..ced65490 --- /dev/null +++ b/khelo/lib/ui/flow/tournament/detail/tournament_detail_view_model.dart @@ -0,0 +1,57 @@ +import 'dart:async'; + +import 'package:data/api/tournament/tournament_model.dart'; +import 'package:data/service/tournament/tournament_service.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'tournament_detail_view_model.freezed.dart'; + +final tournamentDetailStateProvider = StateNotifierProvider.autoDispose< + TournamentDetailStateViewNotifier, TournamentDetailState>( + (ref) => + TournamentDetailStateViewNotifier(ref.read(tournamentServiceProvider)), +); + +class TournamentDetailStateViewNotifier + extends StateNotifier { + final TournamentService _tournamentService; + StreamSubscription? _tournamentSubscription; + + TournamentDetailStateViewNotifier(this._tournamentService) + : super(const TournamentDetailState()); + + String? _tournamentId; + + void setData(String tournamentId) { + _tournamentId = tournamentId; + loadTournament(); + } + + void loadTournament() async { + if (_tournamentId == null) return; + _tournamentSubscription?.cancel(); + + state = state.copyWith(loading: true); + + _tournamentSubscription = _tournamentService + .streamTournamentById(_tournamentId!) + .listen((tournament) { + state = state.copyWith(tournament: tournament, loading: false); + }, onError: (e) { + state = state.copyWith(error: e, loading: false); + debugPrint( + "TournamentListViewNotifier: error while loading tournament list -> $e"); + }); + } +} + +@freezed +class TournamentDetailState with _$TournamentDetailState { + const factory TournamentDetailState({ + @Default(null) TournamentModel? tournament, + @Default(false) bool loading, + Object? error, + }) = _TournamentDetailState; +} diff --git a/khelo/lib/ui/flow/tournament/detail/tournament_detail_view_model.freezed.dart b/khelo/lib/ui/flow/tournament/detail/tournament_detail_view_model.freezed.dart new file mode 100644 index 00000000..2ee503e2 --- /dev/null +++ b/khelo/lib/ui/flow/tournament/detail/tournament_detail_view_model.freezed.dart @@ -0,0 +1,200 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'tournament_detail_view_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$TournamentDetailState { + TournamentModel? get tournament => throw _privateConstructorUsedError; + bool get loading => throw _privateConstructorUsedError; + Object? get error => throw _privateConstructorUsedError; + + /// Create a copy of TournamentDetailState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $TournamentDetailStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $TournamentDetailStateCopyWith<$Res> { + factory $TournamentDetailStateCopyWith(TournamentDetailState value, + $Res Function(TournamentDetailState) then) = + _$TournamentDetailStateCopyWithImpl<$Res, TournamentDetailState>; + @useResult + $Res call({TournamentModel? tournament, bool loading, Object? error}); + + $TournamentModelCopyWith<$Res>? get tournament; +} + +/// @nodoc +class _$TournamentDetailStateCopyWithImpl<$Res, + $Val extends TournamentDetailState> + implements $TournamentDetailStateCopyWith<$Res> { + _$TournamentDetailStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of TournamentDetailState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? tournament = freezed, + Object? loading = null, + Object? error = freezed, + }) { + return _then(_value.copyWith( + tournament: freezed == tournament + ? _value.tournament + : tournament // ignore: cast_nullable_to_non_nullable + as TournamentModel?, + loading: null == loading + ? _value.loading + : loading // ignore: cast_nullable_to_non_nullable + as bool, + error: freezed == error ? _value.error : error, + ) as $Val); + } + + /// Create a copy of TournamentDetailState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $TournamentModelCopyWith<$Res>? get tournament { + if (_value.tournament == null) { + return null; + } + + return $TournamentModelCopyWith<$Res>(_value.tournament!, (value) { + return _then(_value.copyWith(tournament: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$TournamentDetailStateImplCopyWith<$Res> + implements $TournamentDetailStateCopyWith<$Res> { + factory _$$TournamentDetailStateImplCopyWith( + _$TournamentDetailStateImpl value, + $Res Function(_$TournamentDetailStateImpl) then) = + __$$TournamentDetailStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({TournamentModel? tournament, bool loading, Object? error}); + + @override + $TournamentModelCopyWith<$Res>? get tournament; +} + +/// @nodoc +class __$$TournamentDetailStateImplCopyWithImpl<$Res> + extends _$TournamentDetailStateCopyWithImpl<$Res, + _$TournamentDetailStateImpl> + implements _$$TournamentDetailStateImplCopyWith<$Res> { + __$$TournamentDetailStateImplCopyWithImpl(_$TournamentDetailStateImpl _value, + $Res Function(_$TournamentDetailStateImpl) _then) + : super(_value, _then); + + /// Create a copy of TournamentDetailState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? tournament = freezed, + Object? loading = null, + Object? error = freezed, + }) { + return _then(_$TournamentDetailStateImpl( + tournament: freezed == tournament + ? _value.tournament + : tournament // ignore: cast_nullable_to_non_nullable + as TournamentModel?, + loading: null == loading + ? _value.loading + : loading // ignore: cast_nullable_to_non_nullable + as bool, + error: freezed == error ? _value.error : error, + )); + } +} + +/// @nodoc + +class _$TournamentDetailStateImpl implements _TournamentDetailState { + const _$TournamentDetailStateImpl( + {this.tournament = null, this.loading = false, this.error}); + + @override + @JsonKey() + final TournamentModel? tournament; + @override + @JsonKey() + final bool loading; + @override + final Object? error; + + @override + String toString() { + return 'TournamentDetailState(tournament: $tournament, loading: $loading, error: $error)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$TournamentDetailStateImpl && + (identical(other.tournament, tournament) || + other.tournament == tournament) && + (identical(other.loading, loading) || other.loading == loading) && + const DeepCollectionEquality().equals(other.error, error)); + } + + @override + int get hashCode => Object.hash(runtimeType, tournament, loading, + const DeepCollectionEquality().hash(error)); + + /// Create a copy of TournamentDetailState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$TournamentDetailStateImplCopyWith<_$TournamentDetailStateImpl> + get copyWith => __$$TournamentDetailStateImplCopyWithImpl< + _$TournamentDetailStateImpl>(this, _$identity); +} + +abstract class _TournamentDetailState implements TournamentDetailState { + const factory _TournamentDetailState( + {final TournamentModel? tournament, + final bool loading, + final Object? error}) = _$TournamentDetailStateImpl; + + @override + TournamentModel? get tournament; + @override + bool get loading; + @override + Object? get error; + + /// Create a copy of TournamentDetailState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$TournamentDetailStateImplCopyWith<_$TournamentDetailStateImpl> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/khelo/lib/ui/flow/tournament/tournament_list_screen.dart b/khelo/lib/ui/flow/tournament/tournament_list_screen.dart index 7c40e06a..85c43c86 100644 --- a/khelo/lib/ui/flow/tournament/tournament_list_screen.dart +++ b/khelo/lib/ui/flow/tournament/tournament_list_screen.dart @@ -7,6 +7,7 @@ import 'package:khelo/components/app_page.dart'; import 'package:khelo/domain/extensions/context_extensions.dart'; import 'package:khelo/domain/extensions/enum_extensions.dart'; import 'package:khelo/domain/formatter/date_formatter.dart'; +import 'package:khelo/ui/app_route.dart'; import 'package:khelo/ui/flow/tournament/tournament_list_view_model.dart'; import 'package:style/animations/on_tap_scale.dart'; import 'package:style/extensions/context_extensions.dart'; @@ -122,7 +123,8 @@ class _TournamentListScreenState extends ConsumerState Widget _tournamentItem(BuildContext context, TournamentModel tournament) { return OnTapScale( - onTap: () {}, + onTap: () => + AppRoute.tournamentDetail(tournamentId: tournament.id).push(context), child: Container( padding: const EdgeInsets.all(16), margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), From 1d51d72bbf06f7ba4e0f4ff4e5e58ef007caade3 Mon Sep 17 00:00:00 2001 From: Mayank Variya Date: Fri, 11 Oct 2024 17:17:31 +0530 Subject: [PATCH 2/5] Add tabs --- khelo/assets/locales/app_en.arb | 5 + .../components/sliver_header_delegate.dart | 32 ++++ .../detail/tournament_detail_screen.dart | 139 +++++++++++++----- .../detail/tournament_detail_view_model.dart | 12 ++ .../tournament_detail_view_model.freezed.dart | 40 ++++- .../tournament/tournament_list_screen.dart | 33 +---- 6 files changed, 190 insertions(+), 71 deletions(-) create mode 100644 khelo/lib/ui/flow/tournament/components/sliver_header_delegate.dart diff --git a/khelo/assets/locales/app_en.arb b/khelo/assets/locales/app_en.arb index 4506adf3..5fb4164b 100644 --- a/khelo/assets/locales/app_en.arb +++ b/khelo/assets/locales/app_en.arb @@ -177,6 +177,11 @@ } } }, + "tournament_detail_overview_tab": "Overview", + "tournament_detail_teams_tab": "Teams", + "tournament_detail_matches_tab": "Matches", + "tournament_detail_points_table_tab": "Points Table", + "tournament_detail_stats_tab": "Stats", "@_TOURNAMENT_TYPE":{ }, diff --git a/khelo/lib/ui/flow/tournament/components/sliver_header_delegate.dart b/khelo/lib/ui/flow/tournament/components/sliver_header_delegate.dart new file mode 100644 index 00000000..cc96330b --- /dev/null +++ b/khelo/lib/ui/flow/tournament/components/sliver_header_delegate.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:style/extensions/context_extensions.dart'; + +class SliverPersistentDelegate extends SliverPersistentHeaderDelegate { + final Widget child; + + SliverPersistentDelegate({Key? key, required this.child}); + + @override + Widget build( + BuildContext context, + double shrinkOffset, + bool overlapsContent, + ) { + return Container( + color: context.colorScheme.surface, + alignment: Alignment.centerLeft, + child: child, + ); + } + + @override + bool shouldRebuild(SliverPersistentDelegate oldDelegate) { + return child != oldDelegate.child; + } + + @override + double get maxExtent => 50; + + @override + double get minExtent => 50; +} diff --git a/khelo/lib/ui/flow/tournament/detail/tournament_detail_screen.dart b/khelo/lib/ui/flow/tournament/detail/tournament_detail_screen.dart index 4624b9de..a14ba613 100644 --- a/khelo/lib/ui/flow/tournament/detail/tournament_detail_screen.dart +++ b/khelo/lib/ui/flow/tournament/detail/tournament_detail_screen.dart @@ -7,7 +7,10 @@ import 'package:khelo/components/empty_screen.dart'; import 'package:khelo/components/image_avatar.dart'; import 'package:khelo/domain/extensions/context_extensions.dart'; import 'package:khelo/domain/formatter/date_formatter.dart'; +import 'package:khelo/ui/flow/tournament/components/sliver_header_delegate.dart'; import 'package:khelo/ui/flow/tournament/detail/tournament_detail_view_model.dart'; +import 'package:style/button/more_option_button.dart'; +import 'package:style/button/tab_button.dart'; import 'package:style/extensions/context_extensions.dart'; import 'package:style/indicator/progress_indicator.dart'; import 'package:style/text/app_text_style.dart'; @@ -33,11 +36,19 @@ class TournamentDetailScreen extends ConsumerStatefulWidget { class _TournamentDetailScreenState extends ConsumerState { late TournamentDetailStateViewNotifier notifier; + late PageController _controller; + + int get _selectedTab => _controller.hasClients + ? _controller.page?.round() ?? 0 + : _controller.initialPage; @override void initState() { super.initState(); notifier = ref.read(tournamentDetailStateProvider.notifier); + _controller = PageController( + initialPage: ref.read(tournamentDetailStateProvider).selectedTab, + ); runPostFrame(() => notifier.setData(widget.tournamentId)); } @@ -73,16 +84,24 @@ class _TournamentDetailScreenState return CustomScrollView( slivers: [ SliverAppBar( + pinned: true, expandedHeight: 300, backgroundColor: context.colorScheme.surface, - pinned: true, flexibleSpace: _flexibleTitle(context, state.tournament!), - actions: [], + actions: [ + moreOptionButton(context), + ], ), SliverToBoxAdapter( child: SizedBox(height: 16 + context.mediaQueryPadding.bottom), ), - SliverToBoxAdapter( + SliverPersistentHeader( + pinned: true, + delegate: SliverPersistentDelegate( + child: _tabSelection(context), + ), + ), + SliverFillRemaining( child: _content(context, state), ) ], @@ -90,46 +109,96 @@ class _TournamentDetailScreenState } Widget _content(BuildContext context, TournamentDetailState state) { - return Column(); + return PageView( + controller: _controller, + onPageChanged: notifier.onTabChange, + children: const [ + //Add Tab view + ], + ); } - Widget _flexibleTitle(BuildContext context, TournamentModel tournament) { - return FlexibleSpaceBar( - background: Stack( - children: [ - Container( - decoration: BoxDecoration( - color: context.colorScheme.containerLow, - image: (tournament.banner_img_url != null) - ? DecorationImage( - image: CachedNetworkImageProvider( - tournament.banner_img_url ?? ''), - fit: BoxFit.fill, - ) - : null, + Widget _tabSelection(BuildContext context) { + final tabs = [ + context.l10n.tournament_detail_overview_tab, + context.l10n.tournament_detail_teams_tab, + context.l10n.tournament_detail_matches_tab, + context.l10n.tournament_detail_points_table_tab, + context.l10n.tournament_detail_stats_tab, + ]; + + return SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 12), + scrollDirection: Axis.horizontal, + child: Row( + children: List.generate( + tabs.length, + (index) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: TabButton( + tabs[index], + selected: index == _selectedTab, + onTap: () { + _controller.jumpToPage(index); + }, ), - child: (tournament.banner_img_url == null) - ? Center( - child: SvgPicture.asset( - Assets.images.icTournaments, - colorFilter: ColorFilter.mode( - context.colorScheme.textPrimary, - BlendMode.srcIn, - ), - ), - ) - : null, ), - Positioned( - left: 16, - bottom: 24, - child: _profileView(context, tournament), - ), - ], + ), ), ); } + Widget _flexibleTitle(BuildContext context, TournamentModel tournament) { + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + final isCollapsed = constraints.biggest.height < 150; + + return FlexibleSpaceBar( + title: isCollapsed + ? Text( + tournament.name, + style: AppTextStyle.header2.copyWith( + color: context.colorScheme.textPrimary, + ), + ) + : null, + background: Stack( + children: [ + Container( + decoration: BoxDecoration( + color: context.colorScheme.containerLow, + image: (tournament.banner_img_url != null) + ? DecorationImage( + image: CachedNetworkImageProvider( + tournament.banner_img_url ?? ''), + fit: BoxFit.fill, + ) + : null, + ), + child: (tournament.banner_img_url == null) + ? Center( + child: SvgPicture.asset( + Assets.images.icTournaments, + colorFilter: ColorFilter.mode( + context.colorScheme.textPrimary, + BlendMode.srcIn, + ), + ), + ) + : null, + ), + if (!isCollapsed) + Positioned( + left: 16, + bottom: 24, + child: _profileView(context, tournament), + ), + ], + ), + ); + }); + } + Widget _profileView(BuildContext context, TournamentModel tournament) { return Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/khelo/lib/ui/flow/tournament/detail/tournament_detail_view_model.dart b/khelo/lib/ui/flow/tournament/detail/tournament_detail_view_model.dart index ced65490..3969801b 100644 --- a/khelo/lib/ui/flow/tournament/detail/tournament_detail_view_model.dart +++ b/khelo/lib/ui/flow/tournament/detail/tournament_detail_view_model.dart @@ -45,6 +45,17 @@ class TournamentDetailStateViewNotifier "TournamentListViewNotifier: error while loading tournament list -> $e"); }); } + + void onTabChange(int tab) { + if (state.selectedTab != tab) { + state = state.copyWith(selectedTab: tab); + } + } + @override + void dispose() { + _tournamentSubscription?.cancel(); + super.dispose(); + } } @freezed @@ -52,6 +63,7 @@ class TournamentDetailState with _$TournamentDetailState { const factory TournamentDetailState({ @Default(null) TournamentModel? tournament, @Default(false) bool loading, + @Default(0) int selectedTab, Object? error, }) = _TournamentDetailState; } diff --git a/khelo/lib/ui/flow/tournament/detail/tournament_detail_view_model.freezed.dart b/khelo/lib/ui/flow/tournament/detail/tournament_detail_view_model.freezed.dart index 2ee503e2..cfeb2014 100644 --- a/khelo/lib/ui/flow/tournament/detail/tournament_detail_view_model.freezed.dart +++ b/khelo/lib/ui/flow/tournament/detail/tournament_detail_view_model.freezed.dart @@ -18,6 +18,7 @@ final _privateConstructorUsedError = UnsupportedError( mixin _$TournamentDetailState { TournamentModel? get tournament => throw _privateConstructorUsedError; bool get loading => throw _privateConstructorUsedError; + int get selectedTab => throw _privateConstructorUsedError; Object? get error => throw _privateConstructorUsedError; /// Create a copy of TournamentDetailState @@ -33,7 +34,11 @@ abstract class $TournamentDetailStateCopyWith<$Res> { $Res Function(TournamentDetailState) then) = _$TournamentDetailStateCopyWithImpl<$Res, TournamentDetailState>; @useResult - $Res call({TournamentModel? tournament, bool loading, Object? error}); + $Res call( + {TournamentModel? tournament, + bool loading, + int selectedTab, + Object? error}); $TournamentModelCopyWith<$Res>? get tournament; } @@ -56,6 +61,7 @@ class _$TournamentDetailStateCopyWithImpl<$Res, $Res call({ Object? tournament = freezed, Object? loading = null, + Object? selectedTab = null, Object? error = freezed, }) { return _then(_value.copyWith( @@ -67,6 +73,10 @@ class _$TournamentDetailStateCopyWithImpl<$Res, ? _value.loading : loading // ignore: cast_nullable_to_non_nullable as bool, + selectedTab: null == selectedTab + ? _value.selectedTab + : selectedTab // ignore: cast_nullable_to_non_nullable + as int, error: freezed == error ? _value.error : error, ) as $Val); } @@ -95,7 +105,11 @@ abstract class _$$TournamentDetailStateImplCopyWith<$Res> __$$TournamentDetailStateImplCopyWithImpl<$Res>; @override @useResult - $Res call({TournamentModel? tournament, bool loading, Object? error}); + $Res call( + {TournamentModel? tournament, + bool loading, + int selectedTab, + Object? error}); @override $TournamentModelCopyWith<$Res>? get tournament; @@ -117,6 +131,7 @@ class __$$TournamentDetailStateImplCopyWithImpl<$Res> $Res call({ Object? tournament = freezed, Object? loading = null, + Object? selectedTab = null, Object? error = freezed, }) { return _then(_$TournamentDetailStateImpl( @@ -128,6 +143,10 @@ class __$$TournamentDetailStateImplCopyWithImpl<$Res> ? _value.loading : loading // ignore: cast_nullable_to_non_nullable as bool, + selectedTab: null == selectedTab + ? _value.selectedTab + : selectedTab // ignore: cast_nullable_to_non_nullable + as int, error: freezed == error ? _value.error : error, )); } @@ -137,7 +156,10 @@ class __$$TournamentDetailStateImplCopyWithImpl<$Res> class _$TournamentDetailStateImpl implements _TournamentDetailState { const _$TournamentDetailStateImpl( - {this.tournament = null, this.loading = false, this.error}); + {this.tournament = null, + this.loading = false, + this.selectedTab = 0, + this.error}); @override @JsonKey() @@ -146,11 +168,14 @@ class _$TournamentDetailStateImpl implements _TournamentDetailState { @JsonKey() final bool loading; @override + @JsonKey() + final int selectedTab; + @override final Object? error; @override String toString() { - return 'TournamentDetailState(tournament: $tournament, loading: $loading, error: $error)'; + return 'TournamentDetailState(tournament: $tournament, loading: $loading, selectedTab: $selectedTab, error: $error)'; } @override @@ -161,11 +186,13 @@ class _$TournamentDetailStateImpl implements _TournamentDetailState { (identical(other.tournament, tournament) || other.tournament == tournament) && (identical(other.loading, loading) || other.loading == loading) && + (identical(other.selectedTab, selectedTab) || + other.selectedTab == selectedTab) && const DeepCollectionEquality().equals(other.error, error)); } @override - int get hashCode => Object.hash(runtimeType, tournament, loading, + int get hashCode => Object.hash(runtimeType, tournament, loading, selectedTab, const DeepCollectionEquality().hash(error)); /// Create a copy of TournamentDetailState @@ -182,6 +209,7 @@ abstract class _TournamentDetailState implements TournamentDetailState { const factory _TournamentDetailState( {final TournamentModel? tournament, final bool loading, + final int selectedTab, final Object? error}) = _$TournamentDetailStateImpl; @override @@ -189,6 +217,8 @@ abstract class _TournamentDetailState implements TournamentDetailState { @override bool get loading; @override + int get selectedTab; + @override Object? get error; /// Create a copy of TournamentDetailState diff --git a/khelo/lib/ui/flow/tournament/tournament_list_screen.dart b/khelo/lib/ui/flow/tournament/tournament_list_screen.dart index 85c43c86..e8b6ed53 100644 --- a/khelo/lib/ui/flow/tournament/tournament_list_screen.dart +++ b/khelo/lib/ui/flow/tournament/tournament_list_screen.dart @@ -17,6 +17,7 @@ import 'package:style/text/app_text_style.dart'; import '../../../components/empty_screen.dart'; import '../../../components/error_screen.dart'; import '../../../gen/assets.gen.dart'; +import 'components/sliver_header_delegate.dart'; class TournamentListScreen extends ConsumerStatefulWidget { const TournamentListScreen({super.key}); @@ -97,7 +98,7 @@ class _TournamentListScreenState extends ConsumerState slivers: [ SliverPersistentHeader( pinned: true, - delegate: SliverAppbarDelegate( + delegate: SliverPersistentDelegate( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Text( @@ -228,33 +229,3 @@ class _TournamentListScreenState extends ConsumerState ])); } } - -class SliverAppbarDelegate extends SliverPersistentHeaderDelegate { - final Widget child; - - SliverAppbarDelegate({Key? key, required this.child}); - - @override - Widget build( - BuildContext context, - double shrinkOffset, - bool overlapsContent, - ) { - return Container( - color: context.colorScheme.surface, - alignment: Alignment.centerLeft, - child: child, - ); - } - - @override - bool shouldRebuild(SliverAppbarDelegate oldDelegate) { - return child != oldDelegate.child; - } - - @override - double get maxExtent => 50; - - @override - double get minExtent => 50; -} From 2697e5c6f0338069f4521c207c4fc8fa47b66c8b Mon Sep 17 00:00:00 2001 From: Mayank Variya Date: Fri, 11 Oct 2024 17:30:04 +0530 Subject: [PATCH 3/5] minor changes --- .../components/sliver_header_delegate.dart | 11 +++++--- .../detail/tournament_detail_screen.dart | 27 +++++++++++-------- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/khelo/lib/ui/flow/tournament/components/sliver_header_delegate.dart b/khelo/lib/ui/flow/tournament/components/sliver_header_delegate.dart index cc96330b..495a47e5 100644 --- a/khelo/lib/ui/flow/tournament/components/sliver_header_delegate.dart +++ b/khelo/lib/ui/flow/tournament/components/sliver_header_delegate.dart @@ -3,8 +3,13 @@ import 'package:style/extensions/context_extensions.dart'; class SliverPersistentDelegate extends SliverPersistentHeaderDelegate { final Widget child; + final double size; - SliverPersistentDelegate({Key? key, required this.child}); + SliverPersistentDelegate({ + Key? key, + required this.child, + this.size = 50, + }); @override Widget build( @@ -25,8 +30,8 @@ class SliverPersistentDelegate extends SliverPersistentHeaderDelegate { } @override - double get maxExtent => 50; + double get maxExtent => size; @override - double get minExtent => 50; + double get minExtent => size; } diff --git a/khelo/lib/ui/flow/tournament/detail/tournament_detail_screen.dart b/khelo/lib/ui/flow/tournament/detail/tournament_detail_screen.dart index a14ba613..1d999d02 100644 --- a/khelo/lib/ui/flow/tournament/detail/tournament_detail_screen.dart +++ b/khelo/lib/ui/flow/tournament/detail/tournament_detail_screen.dart @@ -92,13 +92,11 @@ class _TournamentDetailScreenState moreOptionButton(context), ], ), - SliverToBoxAdapter( - child: SizedBox(height: 16 + context.mediaQueryPadding.bottom), - ), SliverPersistentHeader( pinned: true, delegate: SliverPersistentDelegate( child: _tabSelection(context), + size: 60, ), ), SliverFillRemaining( @@ -155,10 +153,14 @@ class _TournamentDetailScreenState return FlexibleSpaceBar( title: isCollapsed - ? Text( - tournament.name, - style: AppTextStyle.header2.copyWith( - color: context.colorScheme.textPrimary, + ? AnimatedOpacity( + opacity: isCollapsed ? 1 : 0, + duration: const Duration(milliseconds: 100), + child: Text( + tournament.name, + style: AppTextStyle.header2.copyWith( + color: context.colorScheme.textPrimary, + ), ), ) : null, @@ -187,12 +189,15 @@ class _TournamentDetailScreenState ) : null, ), - if (!isCollapsed) - Positioned( - left: 16, - bottom: 24, + Positioned( + left: 16, + bottom: 24, + child: AnimatedOpacity( + opacity: isCollapsed ? 0 : 1, + duration: const Duration(milliseconds: 100), child: _profileView(context, tournament), ), + ), ], ), ); From 56af43c1eec4880cb9ffe999284e9fffe7d50d43 Mon Sep 17 00:00:00 2001 From: Mayank Variya Date: Fri, 11 Oct 2024 18:46:55 +0530 Subject: [PATCH 4/5] minor changes --- data/lib/service/match/match_service.dart | 11 ++---- data/lib/service/team/team_service.dart | 3 +- .../tournament/tournament_service.dart | 38 +++++++++---------- khelo/assets/locales/app_en.arb | 2 +- .../detail/tournament_detail_view_model.dart | 2 +- 5 files changed, 26 insertions(+), 30 deletions(-) diff --git a/data/lib/service/match/match_service.dart b/data/lib/service/match/match_service.dart index cbd227ef..11b5dae7 100644 --- a/data/lib/service/match/match_service.dart +++ b/data/lib/service/match/match_service.dart @@ -465,15 +465,12 @@ class MatchService { } } - //Helper Methods Future> getMatchesByIds(List matchIds) async { try { - final List matches = []; - await Future.forEach(matchIds, (matchId) async { - final match = await getMatchById(matchId); - matches.add(match); - }); - return matches; + return await _matchCollection + .where(FieldPath.documentId, whereIn: matchIds) + .get() + .then((value) => value.docs.map((e) => e.data()).toList()); } catch (error, stack) { throw AppError.fromError(error, stack); } diff --git a/data/lib/service/team/team_service.dart b/data/lib/service/team/team_service.dart index 1d119eaf..3741ead9 100644 --- a/data/lib/service/team/team_service.dart +++ b/data/lib/service/team/team_service.dart @@ -269,11 +269,10 @@ class TeamService { Future> getTeamsByIds(List teamIds) async { try { - final teamList = await _teamsCollection + return await _teamsCollection .where(FieldPath.documentId, whereIn: teamIds) .get() .then((value) => value.docs.map((e) => e.data()).toList()); - return teamList; } catch (error, stack) { throw AppError.fromError(error, stack); } diff --git a/data/lib/service/tournament/tournament_service.dart b/data/lib/service/tournament/tournament_service.dart index 55bdfab9..f45076cc 100644 --- a/data/lib/service/tournament/tournament_service.dart +++ b/data/lib/service/tournament/tournament_service.dart @@ -10,28 +10,28 @@ import '../user/user_service.dart'; final tournamentServiceProvider = Provider( (ref) => TournamentService( - fireStore: FirebaseFirestore.instance, - teamService: ref.read(teamServiceProvider), - matchService: ref.read(matchServiceProvider), - userService: ref.read(userServiceProvider), + FirebaseFirestore.instance, + ref.read(teamServiceProvider), + ref.read(matchServiceProvider), + ref.read(userServiceProvider), ), ); class TournamentService { - final FirebaseFirestore fireStore; - final TeamService teamService; - final MatchService matchService; - final UserService userService; - - TournamentService({ - required this.fireStore, - required this.teamService, - required this.matchService, - required this.userService, - }); + final FirebaseFirestore _firestore; + final TeamService _teamService; + final MatchService _matchService; + final UserService _userService; + + TournamentService( + this._firestore, + this._teamService, + this._matchService, + this._userService, + ); CollectionReference get _tournamentCollection => - fireStore.collection(FireStoreConst.tournamentCollection).withConverter( + _firestore.collection(FireStoreConst.tournamentCollection).withConverter( fromFirestore: TournamentModel.fromFireStore, toFirestore: (TournamentModel tournament, _) => tournament.toJson(), ); @@ -86,18 +86,18 @@ class TournamentService { final matchIds = tournament.match_ids; if (teamIds.isNotEmpty) { - final teams = await teamService.getTeamsByIds(teamIds); + final teams = await _teamService.getTeamsByIds(teamIds); tournament = tournament.copyWith(teams: teams); } if (matchIds.isNotEmpty) { - final matches = await matchService.getMatchesByIds(matchIds); + final matches = await _matchService.getMatchesByIds(matchIds); tournament = tournament.copyWith(matches: matches); } if (tournament.members.isNotEmpty) { final memberIds = tournament.members.map((e) => e.id).toList(); - final users = await userService.getUsersByIds(memberIds); + final users = await _userService.getUsersByIds(memberIds); final members = tournament.members.map((member) { final user = users.firstWhere((element) => element.id == member.id); diff --git a/khelo/assets/locales/app_en.arb b/khelo/assets/locales/app_en.arb index 5fb4164b..ae1fb96f 100644 --- a/khelo/assets/locales/app_en.arb +++ b/khelo/assets/locales/app_en.arb @@ -167,7 +167,7 @@ "@_TOURNAMENT_DETAIL": { }, "tournament_detail_not_found_title": "No Tournament found", - "tournament_detail_not_found_description": "The tournament you are looking for does not available.", + "tournament_detail_not_found_description": "The tournament you are looking for is not available.", "tournament_detail_start_from_title": "Start from {date}", "@tournament_detail_start_from_title": { "description": "Start from {date}", diff --git a/khelo/lib/ui/flow/tournament/detail/tournament_detail_view_model.dart b/khelo/lib/ui/flow/tournament/detail/tournament_detail_view_model.dart index 3969801b..d1d53714 100644 --- a/khelo/lib/ui/flow/tournament/detail/tournament_detail_view_model.dart +++ b/khelo/lib/ui/flow/tournament/detail/tournament_detail_view_model.dart @@ -42,7 +42,7 @@ class TournamentDetailStateViewNotifier }, onError: (e) { state = state.copyWith(error: e, loading: false); debugPrint( - "TournamentListViewNotifier: error while loading tournament list -> $e"); + "TournamentDetailStateViewNotifier: error while loading tournament list -> $e"); }); } From d22f175c7cd9ea373cd8529492ed1686512afd11 Mon Sep 17 00:00:00 2001 From: sidhdhi canopas <122426509+cp-sidhdhi-p@users.noreply.github.com> Date: Mon, 14 Oct 2024 09:12:27 +0530 Subject: [PATCH 5/5] Improve home screen content (#118) * show matches based on updated_at field * remove unused package * fix error prone implementation for date * revert unwanted change --- data/lib/api/match/match_model.dart | 1 + data/lib/api/match/match_model.freezed.dart | 39 ++++++-- data/lib/api/match/match_model.g.dart | 4 + data/lib/service/match/match_service.dart | 96 ++++++++++++++++++- .../utils/constant/firestore_constant.dart | 4 +- khelo/lib/ui/flow/home/home_view_model.dart | 42 +++----- .../add_match/add_match_view_model.dart | 1 + style/lib/pickers/date_and_time_picker.dart | 6 +- 8 files changed, 150 insertions(+), 43 deletions(-) diff --git a/data/lib/api/match/match_model.dart b/data/lib/api/match/match_model.dart index 66e2dd25..6832450b 100644 --- a/data/lib/api/match/match_model.dart +++ b/data/lib/api/match/match_model.dart @@ -50,6 +50,7 @@ class MatchModel with _$MatchModel { String? toss_winner_id, String? current_playing_team_id, RevisedTarget? revised_target, + @TimeStampJsonConverter() DateTime? updated_at, }) = _MatchModel; factory MatchModel.fromJson(Map json) => diff --git a/data/lib/api/match/match_model.freezed.dart b/data/lib/api/match/match_model.freezed.dart index 2050de3e..963d511e 100644 --- a/data/lib/api/match/match_model.freezed.dart +++ b/data/lib/api/match/match_model.freezed.dart @@ -56,6 +56,8 @@ mixin _$MatchModel { String? get toss_winner_id => throw _privateConstructorUsedError; String? get current_playing_team_id => throw _privateConstructorUsedError; RevisedTarget? get revised_target => throw _privateConstructorUsedError; + @TimeStampJsonConverter() + DateTime? get updated_at => throw _privateConstructorUsedError; /// Serializes this MatchModel to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -107,7 +109,8 @@ abstract class $MatchModelCopyWith<$Res> { TossDecision? toss_decision, String? toss_winner_id, String? current_playing_team_id, - RevisedTarget? revised_target}); + RevisedTarget? revised_target, + @TimeStampJsonConverter() DateTime? updated_at}); $UserModelCopyWith<$Res>? get referee; $RevisedTargetCopyWith<$Res>? get revised_target; @@ -159,6 +162,7 @@ class _$MatchModelCopyWithImpl<$Res, $Val extends MatchModel> Object? toss_winner_id = freezed, Object? current_playing_team_id = freezed, Object? revised_target = freezed, + Object? updated_at = freezed, }) { return _then(_value.copyWith( id: null == id @@ -285,6 +289,10 @@ class _$MatchModelCopyWithImpl<$Res, $Val extends MatchModel> ? _value.revised_target : revised_target // ignore: cast_nullable_to_non_nullable as RevisedTarget?, + updated_at: freezed == updated_at + ? _value.updated_at + : updated_at // ignore: cast_nullable_to_non_nullable + as DateTime?, ) as $Val); } @@ -359,7 +367,8 @@ abstract class _$$MatchModelImplCopyWith<$Res> TossDecision? toss_decision, String? toss_winner_id, String? current_playing_team_id, - RevisedTarget? revised_target}); + RevisedTarget? revised_target, + @TimeStampJsonConverter() DateTime? updated_at}); @override $UserModelCopyWith<$Res>? get referee; @@ -411,6 +420,7 @@ class __$$MatchModelImplCopyWithImpl<$Res> Object? toss_winner_id = freezed, Object? current_playing_team_id = freezed, Object? revised_target = freezed, + Object? updated_at = freezed, }) { return _then(_$MatchModelImpl( id: null == id @@ -537,6 +547,10 @@ class __$$MatchModelImplCopyWithImpl<$Res> ? _value.revised_target : revised_target // ignore: cast_nullable_to_non_nullable as RevisedTarget?, + updated_at: freezed == updated_at + ? _value.updated_at + : updated_at // ignore: cast_nullable_to_non_nullable + as DateTime?, )); } } @@ -579,7 +593,8 @@ class _$MatchModelImpl implements _MatchModel { this.toss_decision, this.toss_winner_id, this.current_playing_team_id, - this.revised_target}) + this.revised_target, + @TimeStampJsonConverter() this.updated_at}) : _teams = teams, _players = players, _team_ids = team_ids, @@ -764,10 +779,13 @@ class _$MatchModelImpl implements _MatchModel { final String? current_playing_team_id; @override final RevisedTarget? revised_target; + @override + @TimeStampJsonConverter() + final DateTime? updated_at; @override String toString() { - return 'MatchModel(id: $id, teams: $teams, match_type: $match_type, number_of_over: $number_of_over, over_per_bowler: $over_per_bowler, players: $players, team_ids: $team_ids, team_creator_ids: $team_creator_ids, power_play_overs1: $power_play_overs1, power_play_overs2: $power_play_overs2, power_play_overs3: $power_play_overs3, city: $city, ground: $ground, start_time: $start_time, start_at: $start_at, ball_type: $ball_type, pitch_type: $pitch_type, created_by: $created_by, umpires: $umpires, scorers: $scorers, commentators: $commentators, referee: $referee, umpire_ids: $umpire_ids, scorer_ids: $scorer_ids, commentator_ids: $commentator_ids, referee_id: $referee_id, match_status: $match_status, toss_decision: $toss_decision, toss_winner_id: $toss_winner_id, current_playing_team_id: $current_playing_team_id, revised_target: $revised_target)'; + return 'MatchModel(id: $id, teams: $teams, match_type: $match_type, number_of_over: $number_of_over, over_per_bowler: $over_per_bowler, players: $players, team_ids: $team_ids, team_creator_ids: $team_creator_ids, power_play_overs1: $power_play_overs1, power_play_overs2: $power_play_overs2, power_play_overs3: $power_play_overs3, city: $city, ground: $ground, start_time: $start_time, start_at: $start_at, ball_type: $ball_type, pitch_type: $pitch_type, created_by: $created_by, umpires: $umpires, scorers: $scorers, commentators: $commentators, referee: $referee, umpire_ids: $umpire_ids, scorer_ids: $scorer_ids, commentator_ids: $commentator_ids, referee_id: $referee_id, match_status: $match_status, toss_decision: $toss_decision, toss_winner_id: $toss_winner_id, current_playing_team_id: $current_playing_team_id, revised_target: $revised_target, updated_at: $updated_at)'; } @override @@ -828,7 +846,9 @@ class _$MatchModelImpl implements _MatchModel { other.current_playing_team_id, current_playing_team_id) || other.current_playing_team_id == current_playing_team_id) && (identical(other.revised_target, revised_target) || - other.revised_target == revised_target)); + other.revised_target == revised_target) && + (identical(other.updated_at, updated_at) || + other.updated_at == updated_at)); } @JsonKey(includeFromJson: false, includeToJson: false) @@ -865,7 +885,8 @@ class _$MatchModelImpl implements _MatchModel { toss_decision, toss_winner_id, current_playing_team_id, - revised_target + revised_target, + updated_at ]); /// Create a copy of MatchModel @@ -920,7 +941,8 @@ abstract class _MatchModel implements MatchModel { final TossDecision? toss_decision, final String? toss_winner_id, final String? current_playing_team_id, - final RevisedTarget? revised_target}) = _$MatchModelImpl; + final RevisedTarget? revised_target, + @TimeStampJsonConverter() final DateTime? updated_at}) = _$MatchModelImpl; factory _MatchModel.fromJson(Map json) = _$MatchModelImpl.fromJson; @@ -992,6 +1014,9 @@ abstract class _MatchModel implements MatchModel { String? get current_playing_team_id; @override RevisedTarget? get revised_target; + @override + @TimeStampJsonConverter() + DateTime? get updated_at; /// Create a copy of MatchModel /// with the given fields replaced by the non-null parameter values. diff --git a/data/lib/api/match/match_model.g.dart b/data/lib/api/match/match_model.g.dart index 0e8731de..3e141a21 100644 --- a/data/lib/api/match/match_model.g.dart +++ b/data/lib/api/match/match_model.g.dart @@ -68,6 +68,8 @@ _$MatchModelImpl _$$MatchModelImplFromJson(Map json) => _$MatchModelImpl( ? null : RevisedTarget.fromJson( Map.from(json['revised_target'] as Map)), + updated_at: _$JsonConverterFromJson( + json['updated_at'], const TimeStampJsonConverter().fromJson), ); Map _$$MatchModelImplToJson(_$MatchModelImpl instance) => @@ -100,6 +102,8 @@ Map _$$MatchModelImplToJson(_$MatchModelImpl instance) => 'toss_winner_id': instance.toss_winner_id, 'current_playing_team_id': instance.current_playing_team_id, 'revised_target': instance.revised_target?.toJson(), + 'updated_at': _$JsonConverterToJson( + instance.updated_at, const TimeStampJsonConverter().toJson), }; const _$MatchTypeEnumMap = { diff --git a/data/lib/service/match/match_service.dart b/data/lib/service/match/match_service.dart index c559cc62..73d46df2 100644 --- a/data/lib/service/match/match_service.dart +++ b/data/lib/service/match/match_service.dart @@ -70,6 +70,7 @@ class MatchService { pitch_type: PitchType.turf, created_by: '', match_status: MatchStatus.running, + updated_at: DateTime.now(), ); } @@ -163,8 +164,81 @@ class MatchService { }).handleError((error, stack) => throw AppError.fromError(error, stack)); } - Stream> streamMatches() { - return _matchCollection.snapshots().asyncMap((snapshot) async { + Stream> streamActiveRunningMatches() { + final DateTime now = DateTime.now(); + final DateTime oneAndHalfHoursAgo = + now.subtract(Duration(hours: 1, minutes: 30)); + + final Timestamp timestamp = Timestamp.fromDate(oneAndHalfHoursAgo); + + final filter = Filter.and( + Filter(FireStoreConst.matchStatus, isEqualTo: MatchStatus.running.value), + Filter(FireStoreConst.updatedAt, isGreaterThan: timestamp), + ); + return _matchCollection + .where(filter) + .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(), + ); + }).handleError((error, stack) => throw AppError.fromError(error, stack)); + } + + Stream> streamUpcomingMatches() { + 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); + + final Timestamp timestampAfterMonth = Timestamp.fromDate(aMonthAfter); + final Timestamp timestampNow = Timestamp.fromDate(startOfDay); + + final filter = Filter.and( + Filter( + FireStoreConst.matchStatus, + isEqualTo: MatchStatus.yetToStart.value, + ), + Filter(FireStoreConst.startAt, isGreaterThanOrEqualTo: timestampNow), + Filter( + FireStoreConst.startAt, + isLessThanOrEqualTo: timestampAfterMonth, + ), + ); + return _matchCollection + .where(filter) + .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(), + ); + }).handleError((error, stack) => throw AppError.fromError(error, stack)); + } + + Stream> streamFinishedMatches() { + final DateTime now = DateTime.now(); + final DateTime fifteenDaysAgo = + DateTime(now.year, now.month, now.day).subtract(Duration(days: 15)); + + final Timestamp timestamp = Timestamp.fromDate(fifteenDaysAgo); + + final filter = Filter.and( + Filter(FireStoreConst.matchStatus, isEqualTo: MatchStatus.finish.value), + Filter(FireStoreConst.updatedAt, isGreaterThan: timestamp), + ); + return _matchCollection + .where(filter) + .snapshots() + .asyncMap((snapshot) async { return await Future.wait( snapshot.docs.map((mainDoc) async { final match = mainDoc.data(); @@ -272,6 +346,7 @@ class MatchService { FireStoreConst.tossWinnerId: tossWinnerId, FireStoreConst.tossDecision: tossDecision.value, FireStoreConst.currentPlayingTeamId: currentPlayingTeam, + FireStoreConst.updatedAt: DateTime.now(), }; await matchRef.update(tossDetails); @@ -286,6 +361,7 @@ class MatchService { final Map matchStatus = { FireStoreConst.matchStatus: status.value, + FireStoreConst.updatedAt: DateTime.now(), }; await matchRef.update(matchStatus); @@ -347,6 +423,7 @@ class MatchService { transaction.update(matchRef, { FireStoreConst.teams: [battingTeam.toJson(), bowlingTeam.toJson()], + FireStoreConst.updatedAt: DateTime.now(), }); } catch (error, stack) { throw AppError.fromError(error, stack); @@ -360,7 +437,10 @@ class MatchService { try { final matchRef = _matchCollection.doc(matchId); - await matchRef.update({FireStoreConst.currentPlayingTeamId: teamId}); + await matchRef.update({ + FireStoreConst.currentPlayingTeamId: teamId, + FireStoreConst.updatedAt: DateTime.now(), + }); } catch (error, stack) { throw AppError.fromError(error, stack); } @@ -404,7 +484,10 @@ class MatchService { transaction.update( matchRef, - {FireStoreConst.teams: updatedTeams.map((e) => e.toJson())}, + { + FireStoreConst.teams: updatedTeams.map((e) => e.toJson()).toList(), + FireStoreConst.updatedAt: DateTime.now(), + }, ); }); } catch (error, stack) { @@ -417,7 +500,10 @@ class MatchService { required RevisedTarget revisedTarget, }) async { await _matchCollection.doc(matchId).update( - {FireStoreConst.revisedTarget: revisedTarget.toJson()}, + { + FireStoreConst.revisedTarget: revisedTarget.toJson(), + FireStoreConst.updatedAt: DateTime.now(), + }, ); } diff --git a/data/lib/utils/constant/firestore_constant.dart b/data/lib/utils/constant/firestore_constant.dart index 278e4c96..4014a894 100644 --- a/data/lib/utils/constant/firestore_constant.dart +++ b/data/lib/utils/constant/firestore_constant.dart @@ -26,6 +26,8 @@ class FireStoreConst { static const String teamIds = "team_ids"; static const String teamCreatorIds = "team_creator_ids"; static const String revisedTarget = "revised_target"; + static const String updatedAt = "updated_at"; + static const String startAt = "start_at"; // innings field const static const String matchId = "match_id"; @@ -56,7 +58,7 @@ class FireStoreConst { // tournament field const static const String members = "members"; - } +} class DataConfig { static late DataConfig _instance; diff --git a/khelo/lib/ui/flow/home/home_view_model.dart b/khelo/lib/ui/flow/home/home_view_model.dart index cf26ea0f..72be2d20 100644 --- a/khelo/lib/ui/flow/home/home_view_model.dart +++ b/khelo/lib/ui/flow/home/home_view_model.dart @@ -1,9 +1,9 @@ import 'dart:async'; -import 'package:collection/collection.dart'; import 'package:data/api/match/match_model.dart'; import 'package:data/service/match/match_service.dart'; import 'package:data/storage/app_preferences.dart'; +import 'package:data/utils/combine_latest.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -39,12 +39,20 @@ class HomeViewNotifier extends StateNotifier { _streamSubscription?.cancel(); state = state.copyWith(loading: state.matches.isEmpty); - _streamSubscription = _matchService.streamMatches().listen( - (matches) { - final groupMatches = _groupMatches(matches); + final matchCombiner = combineLatest3( + _matchService.streamActiveRunningMatches(), + _matchService.streamUpcomingMatches(), + _matchService.streamFinishedMatches()); + + _streamSubscription = matchCombiner.listen( + (allMatches) { state = state.copyWith( - matches: matches, - groupMatches: groupMatches, + matches: [...allMatches.$1, ...allMatches.$2, ...allMatches.$3], + groupMatches: { + MatchStatusLabel.live: allMatches.$1, + MatchStatusLabel.upcoming: allMatches.$2, + MatchStatusLabel.finished: allMatches.$3, + }, loading: false, error: null, ); @@ -56,28 +64,6 @@ class HomeViewNotifier extends StateNotifier { ); } - Map> _groupMatches( - List matches) { - final groupedMatches = groupBy(matches, (match) { - switch (match.match_status) { - case MatchStatus.running: - return MatchStatusLabel.live; - case MatchStatus.yetToStart: - return MatchStatusLabel.upcoming; - case MatchStatus.abandoned: - case MatchStatus.finish: - return MatchStatusLabel.finished; - } - }); - return { - MatchStatusLabel.live: groupedMatches[MatchStatusLabel.live] ?? [], - MatchStatusLabel.upcoming: - groupedMatches[MatchStatusLabel.upcoming] ?? [], - MatchStatusLabel.finished: - groupedMatches[MatchStatusLabel.finished] ?? [], - }; - } - @override void dispose() { _streamSubscription?.cancel(); diff --git a/khelo/lib/ui/flow/matches/add_match/add_match_view_model.dart b/khelo/lib/ui/flow/matches/add_match/add_match_view_model.dart index 00cebc51..d1609308 100644 --- a/khelo/lib/ui/flow/matches/add_match/add_match_view_model.dart +++ b/khelo/lib/ui/flow/matches/add_match/add_match_view_model.dart @@ -186,6 +186,7 @@ class AddMatchViewNotifier extends StateNotifier { ground: ground, start_time: state.matchTime, start_at: state.matchTime, + updated_at: DateTime.now(), created_by: _currentUserId ?? state.teamA?.created_by ?? "INVALID ID", ball_type: state.ballType, pitch_type: state.pitchType, diff --git a/style/lib/pickers/date_and_time_picker.dart b/style/lib/pickers/date_and_time_picker.dart index 4357d614..cdadf2e9 100644 --- a/style/lib/pickers/date_and_time_picker.dart +++ b/style/lib/pickers/date_and_time_picker.dart @@ -9,12 +9,14 @@ Future selectDate( required DateTime initialDate, required Function(DateTime) onDateSelected, }) async { + final now = DateTime.now(); + showDatePicker( context: context, helpText: helpText, initialDate: initialDate, - firstDate: DateTime.now(), - lastDate: DateTime(DateTime.now().year + 1), + firstDate: initialDate.isBefore(now) ? initialDate : now, + lastDate: DateTime(now.year + 1, now.month, now.day), builder: (context, child) { return Theme( data: context.brightness == Brightness.dark