diff --git a/khelo/assets/locales/app_en.arb b/khelo/assets/locales/app_en.arb index c91daae3..069eb986 100644 --- a/khelo/assets/locales/app_en.arb +++ b/khelo/assets/locales/app_en.arb @@ -321,6 +321,7 @@ "add_team_player_role_title": "Playing role", "add_team_player_add_via_contact_title": "Add via Contact", "add_team_players_text": "Players", + "add_team_already_added": "Team already added", "add_team_add_hint_text": "Added user will be shown here, tap on '+' button to add team member.", "scan_qr_code": "Scan QR code", diff --git a/khelo/lib/ui/app_route.dart b/khelo/lib/ui/app_route.dart index 4ea1a7b4..34a93ca3 100644 --- a/khelo/lib/ui/app_route.dart +++ b/khelo/lib/ui/app_route.dart @@ -26,6 +26,7 @@ import 'package:khelo/ui/flow/team/add_team_member/contact_selection/contact_sel import 'package:khelo/ui/flow/team/detail/make_admin/make_team_admin_screen.dart'; import 'package:khelo/ui/flow/team/detail/team_detail_screen.dart'; import 'package:khelo/ui/flow/team/scanner/scanner_screen.dart'; +import 'package:khelo/ui/flow/team/scanner/scanner_view_model.dart'; import 'package:khelo/ui/flow/team/search_team/search_team_screen.dart'; import 'package:khelo/ui/flow/tournament/add/add_tournament_screen.dart'; import 'package:khelo/ui/flow/tournament/detail/members/tournament_detail_members_screen.dart'; @@ -210,9 +211,9 @@ class AppRoute { ); static AppRoute matchSelection({required String tournamentId}) => AppRoute( - pathMatchSelection, - builder: (_) => MatchSelectionScreen(tournamentId: tournamentId), - ); + pathMatchSelection, + builder: (_) => MatchSelectionScreen(tournamentId: tournamentId), + ); static AppRoute tournamentDetail({required String tournamentId}) => AppRoute( pathTournamentDetail, @@ -301,9 +302,15 @@ class AppRoute { pathEditProfile, builder: (_) => EditProfileScreen(isToCreateAccount: isToCreateAccount)); - static AppRoute teamDetail({required String teamId}) => + static AppRoute teamDetail({ + required String teamId, + bool showAddButton = false, + }) => AppRoute(pathTeamDetail, - builder: (_) => TeamDetailScreen(teamId: teamId)); + builder: (_) => TeamDetailScreen( + teamId: teamId, + showAddButton: showAddButton, + )); static AppRoute userDetail({ required String userId, @@ -315,10 +322,16 @@ class AppRoute { showAddButton: showAddButton, )); - static AppRoute scannerScreen({required List addedMembers}) => + static AppRoute scannerScreen({ + required List addedIds, + ScanTarget target = ScanTarget.player, + }) => AppRoute( pathScannerScreen, - builder: (_) => ScannerScreen(addedMembers: addedMembers), + builder: (_) => ScannerScreen( + addedIds: addedIds, + target: target, + ), ); static AppRoute qrCodeView( diff --git a/khelo/lib/ui/flow/team/add_team_member/add_team_member_screen.dart b/khelo/lib/ui/flow/team/add_team_member/add_team_member_screen.dart index b3ff5670..5f14b0ee 100644 --- a/khelo/lib/ui/flow/team/add_team_member/add_team_member_screen.dart +++ b/khelo/lib/ui/flow/team/add_team_member/add_team_member_screen.dart @@ -60,9 +60,9 @@ class _AddTeamMemberScreenState extends ConsumerState { actionButton( context, onPressed: () async { - final user = await AppRoute.scannerScreen( - addedMembers: notifier.getMemberIds()) - .push(context); + final user = + await AppRoute.scannerScreen(addedIds: notifier.getMemberIds()) + .push(context); if (context.mounted && user != null) notifier.selectUser(user); }, icon: SvgPicture.asset( diff --git a/khelo/lib/ui/flow/team/detail/team_detail_screen.dart b/khelo/lib/ui/flow/team/detail/team_detail_screen.dart index 8bdf0e7e..01c3ff90 100644 --- a/khelo/lib/ui/flow/team/detail/team_detail_screen.dart +++ b/khelo/lib/ui/flow/team/detail/team_detail_screen.dart @@ -15,6 +15,7 @@ import 'package:khelo/ui/flow/team/detail/components/team_detail_member_content. import 'package:khelo/ui/flow/team/detail/team_detail_view_model.dart'; import 'package:style/button/action_button.dart'; import 'package:style/button/more_option_button.dart'; +import 'package:style/button/secondary_button.dart'; import 'package:style/button/tab_button.dart'; import 'package:style/extensions/context_extensions.dart'; import 'package:style/indicator/progress_indicator.dart'; @@ -25,8 +26,13 @@ import 'components/team_detail_stat_content.dart'; class TeamDetailScreen extends ConsumerStatefulWidget { final String teamId; + final bool showAddButton; - const TeamDetailScreen({super.key, required this.teamId}); + const TeamDetailScreen({ + super.key, + required this.teamId, + required this.showAddButton, + }); @override ConsumerState createState() => _TeamDetailScreenState(); @@ -66,14 +72,20 @@ class _TeamDetailScreenState extends ConsumerState { size: 24, color: context.colorScheme.textPrimary, )), - actions: (state.team?.isAdminOrOwner(state.currentUserId) ?? false) - ? [ - moreOptionButton( - context, - onPressed: () => _moreActionButton(context, notifier, state), - ), - ] - : null, + actions: [ + if (widget.showAddButton) + SecondaryButton( + context.l10n.common_add_title, + enabled: !state.loading, + onPressed: () => context.pop(state.team), + ), + if (!widget.showAddButton && + (state.team?.isAdminOrOwner(state.currentUserId) ?? false)) + moreOptionButton( + context, + onPressed: () => _moreActionButton(context, notifier, state), + ), + ], body: Builder(builder: (context) { return _body(context, state); }), diff --git a/khelo/lib/ui/flow/team/scanner/scanner_screen.dart b/khelo/lib/ui/flow/team/scanner/scanner_screen.dart index ea212da5..1cfdd2cc 100644 --- a/khelo/lib/ui/flow/team/scanner/scanner_screen.dart +++ b/khelo/lib/ui/flow/team/scanner/scanner_screen.dart @@ -1,5 +1,6 @@ import 'dart:math'; +import 'package:data/api/team/team_model.dart'; import 'package:data/api/user/user_models.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -22,9 +23,14 @@ import '../../../../components/app_page.dart'; import '../../../../components/error_snackbar.dart'; class ScannerScreen extends ConsumerStatefulWidget { - final List addedMembers; + final List addedIds; + final ScanTarget target; - const ScannerScreen({super.key, required this.addedMembers}); + const ScannerScreen({ + super.key, + required this.addedIds, + this.target = ScanTarget.player, + }); @override ConsumerState createState() => _ScannerScreenState(); @@ -36,21 +42,37 @@ class _ScannerScreenState extends ConsumerState { late final ScannerStateNotifier _notifier; void _observeBarcode() { - ref.listen(scannerStateNotifierProvider.select((value) => value.userId), - (prev, userId) async { - if (widget.addedMembers.contains(userId)) { - showSnackBar(context, context.l10n.add_team_member_already_added); - } else if (userId.isNotEmpty) { - final user = - await AppRoute.userDetail(userId: userId, showAddButton: true) - .push(context); - if (mounted) { - context.pop(user); - } + ref.listen(scannerStateNotifierProvider.select((value) => value.scannedId), + (prev, scannedId) async { + if (widget.addedIds.contains(scannedId)) { + showSnackBar( + context, + widget.target == ScanTarget.team + ? context.l10n.add_team_already_added + : context.l10n.add_team_member_already_added); + } else if (scannedId.isNotEmpty) { + _handleScanTarget(widget.target, scannedId); } }); } + void _handleScanTarget(ScanTarget target, String scannedId) async { + switch (widget.target) { + case ScanTarget.team: + final team = + await AppRoute.teamDetail(teamId: scannedId, showAddButton: true) + .push(context); + if (mounted) context.pop(team); + break; + case ScanTarget.player: + final user = + await AppRoute.userDetail(userId: scannedId, showAddButton: true) + .push(context); + if (mounted) context.pop(user); + break; + } + } + void _observeError() { ref.listen(scannerStateNotifierProvider.select((value) => value.error), (prev, error) { @@ -63,7 +85,7 @@ class _ScannerScreenState extends ConsumerState { @override void initState() { _notifier = ref.read(scannerStateNotifierProvider.notifier); - runPostFrame(() => _notifier.setData(widget.addedMembers)); + runPostFrame(() => _notifier.setData(widget.addedIds)); super.initState(); } diff --git a/khelo/lib/ui/flow/team/scanner/scanner_view_model.dart b/khelo/lib/ui/flow/team/scanner/scanner_view_model.dart index 9c51dbb7..19605076 100644 --- a/khelo/lib/ui/flow/team/scanner/scanner_view_model.dart +++ b/khelo/lib/ui/flow/team/scanner/scanner_view_model.dart @@ -15,15 +15,15 @@ final scannerStateNotifierProvider = }); class ScannerStateNotifier extends StateNotifier { - List _addedMembers = []; + List _addedIds = []; StreamSubscription? _subscription; ScannerStateNotifier() : super(const ScannerState()) { _checkCameraPermission(); } - void setData(List addedMembers) { - _addedMembers = addedMembers; + void setData(List addedIds) { + _addedIds = addedIds; } Future _checkCameraPermission() async { @@ -62,11 +62,11 @@ class ScannerStateNotifier extends StateNotifier { _subscription = state.controller?.scannedDataStream.listen( (event) { final code = event.code; - if (!_addedMembers.contains(code)) { + if (!_addedIds.contains(code)) { _removeListeners(); } state = state.copyWith( - userId: code ?? "", + scannedId: code ?? "", ); }, onError: (e) { @@ -93,8 +93,10 @@ class ScannerState with _$ScannerState { const factory ScannerState({ Object? error, QRViewController? controller, - @Default('') String userId, + @Default('') String scannedId, @Default(false) bool flashOn, @Default(false) bool hasPermission, }) = _ScannerState; } + +enum ScanTarget { player, team } diff --git a/khelo/lib/ui/flow/team/scanner/scanner_view_model.freezed.dart b/khelo/lib/ui/flow/team/scanner/scanner_view_model.freezed.dart index 7a7aa982..ec475810 100644 --- a/khelo/lib/ui/flow/team/scanner/scanner_view_model.freezed.dart +++ b/khelo/lib/ui/flow/team/scanner/scanner_view_model.freezed.dart @@ -18,7 +18,7 @@ final _privateConstructorUsedError = UnsupportedError( mixin _$ScannerState { Object? get error => throw _privateConstructorUsedError; QRViewController? get controller => throw _privateConstructorUsedError; - String get userId => throw _privateConstructorUsedError; + String get scannedId => throw _privateConstructorUsedError; bool get flashOn => throw _privateConstructorUsedError; bool get hasPermission => throw _privateConstructorUsedError; @@ -38,7 +38,7 @@ abstract class $ScannerStateCopyWith<$Res> { $Res call( {Object? error, QRViewController? controller, - String userId, + String scannedId, bool flashOn, bool hasPermission}); } @@ -60,7 +60,7 @@ class _$ScannerStateCopyWithImpl<$Res, $Val extends ScannerState> $Res call({ Object? error = freezed, Object? controller = freezed, - Object? userId = null, + Object? scannedId = null, Object? flashOn = null, Object? hasPermission = null, }) { @@ -70,9 +70,9 @@ class _$ScannerStateCopyWithImpl<$Res, $Val extends ScannerState> ? _value.controller : controller // ignore: cast_nullable_to_non_nullable as QRViewController?, - userId: null == userId - ? _value.userId - : userId // ignore: cast_nullable_to_non_nullable + scannedId: null == scannedId + ? _value.scannedId + : scannedId // ignore: cast_nullable_to_non_nullable as String, flashOn: null == flashOn ? _value.flashOn @@ -97,7 +97,7 @@ abstract class _$$ScannerStateImplCopyWith<$Res> $Res call( {Object? error, QRViewController? controller, - String userId, + String scannedId, bool flashOn, bool hasPermission}); } @@ -117,7 +117,7 @@ class __$$ScannerStateImplCopyWithImpl<$Res> $Res call({ Object? error = freezed, Object? controller = freezed, - Object? userId = null, + Object? scannedId = null, Object? flashOn = null, Object? hasPermission = null, }) { @@ -127,9 +127,9 @@ class __$$ScannerStateImplCopyWithImpl<$Res> ? _value.controller : controller // ignore: cast_nullable_to_non_nullable as QRViewController?, - userId: null == userId - ? _value.userId - : userId // ignore: cast_nullable_to_non_nullable + scannedId: null == scannedId + ? _value.scannedId + : scannedId // ignore: cast_nullable_to_non_nullable as String, flashOn: null == flashOn ? _value.flashOn @@ -149,7 +149,7 @@ class _$ScannerStateImpl implements _ScannerState { const _$ScannerStateImpl( {this.error, this.controller, - this.userId = '', + this.scannedId = '', this.flashOn = false, this.hasPermission = false}); @@ -159,7 +159,7 @@ class _$ScannerStateImpl implements _ScannerState { final QRViewController? controller; @override @JsonKey() - final String userId; + final String scannedId; @override @JsonKey() final bool flashOn; @@ -169,7 +169,7 @@ class _$ScannerStateImpl implements _ScannerState { @override String toString() { - return 'ScannerState(error: $error, controller: $controller, userId: $userId, flashOn: $flashOn, hasPermission: $hasPermission)'; + return 'ScannerState(error: $error, controller: $controller, scannedId: $scannedId, flashOn: $flashOn, hasPermission: $hasPermission)'; } @override @@ -180,7 +180,8 @@ class _$ScannerStateImpl implements _ScannerState { const DeepCollectionEquality().equals(other.error, error) && (identical(other.controller, controller) || other.controller == controller) && - (identical(other.userId, userId) || other.userId == userId) && + (identical(other.scannedId, scannedId) || + other.scannedId == scannedId) && (identical(other.flashOn, flashOn) || other.flashOn == flashOn) && (identical(other.hasPermission, hasPermission) || other.hasPermission == hasPermission)); @@ -191,7 +192,7 @@ class _$ScannerStateImpl implements _ScannerState { runtimeType, const DeepCollectionEquality().hash(error), controller, - userId, + scannedId, flashOn, hasPermission); @@ -208,7 +209,7 @@ abstract class _ScannerState implements ScannerState { const factory _ScannerState( {final Object? error, final QRViewController? controller, - final String userId, + final String scannedId, final bool flashOn, final bool hasPermission}) = _$ScannerStateImpl; @@ -217,7 +218,7 @@ abstract class _ScannerState implements ScannerState { @override QRViewController? get controller; @override - String get userId; + String get scannedId; @override bool get flashOn; @override diff --git a/khelo/lib/ui/flow/team/search_team/search_team_screen.dart b/khelo/lib/ui/flow/team/search_team/search_team_screen.dart index 3e375662..bfaef046 100644 --- a/khelo/lib/ui/flow/team/search_team/search_team_screen.dart +++ b/khelo/lib/ui/flow/team/search_team/search_team_screen.dart @@ -22,6 +22,8 @@ import 'package:style/widgets/rounded_check_box.dart'; import '../../../../components/create_team_cell.dart'; import '../../../../gen/assets.gen.dart'; +import '../../../app_route.dart'; +import '../scanner/scanner_view_model.dart'; class SearchTeamScreen extends ConsumerStatefulWidget { final List? excludedIds; @@ -58,6 +60,23 @@ class _SearchTeamScreenState extends ConsumerState { return AppPage( title: context.l10n.search_team_screen_title, actions: [ + actionButton( + context, + onPressed: () async { + final team = await AppRoute.scannerScreen( + target: ScanTarget.team, + addedIds: widget.excludedIds ?? [], + ).push(context); + if (context.mounted && team != null) notifier.onTeamCellTap(team); + }, + icon: SvgPicture.asset( + Assets.images.icQrCode, + colorFilter: ColorFilter.mode( + context.colorScheme.textPrimary, + BlendMode.srcIn, + ), + ), + ), actionButton( context, onPressed: state.selectedTeam != null diff --git a/khelo/lib/ui/flow/tournament/team_selection/team_selection_screen.dart b/khelo/lib/ui/flow/tournament/team_selection/team_selection_screen.dart index 5c9877a9..db3fd58d 100644 --- a/khelo/lib/ui/flow/tournament/team_selection/team_selection_screen.dart +++ b/khelo/lib/ui/flow/tournament/team_selection/team_selection_screen.dart @@ -19,6 +19,8 @@ import '../../../../components/error_screen.dart'; import '../../../../components/error_snackbar.dart'; import '../../../../domain/extensions/widget_extension.dart'; import '../../../../gen/assets.gen.dart'; +import '../../../app_route.dart'; +import '../../team/scanner/scanner_view_model.dart'; import '../../team/search_team/components/team_member_sheet.dart'; import 'component/team_profile_cell.dart'; @@ -52,6 +54,23 @@ class _TeamSelectionScreenState extends ConsumerState { return AppPage( title: context.l10n.team_selection_screen_title, actions: [ + actionButton( + context, + onPressed: () async { + final team = await AppRoute.scannerScreen( + target: ScanTarget.team, + addedIds: widget.selectedTeams?.map((e) => e.id).toList() ?? [], + ).push(context); + if (context.mounted && team != null) notifier.onTeamCellTap(team); + }, + icon: SvgPicture.asset( + Assets.images.icQrCode, + colorFilter: ColorFilter.mode( + context.colorScheme.textPrimary, + BlendMode.srcIn, + ), + ), + ), actionButton( context, onPressed: () async {