From 3a5f64abffa9530ba34e2c4efffb7e5d6296f4af Mon Sep 17 00:00:00 2001 From: ale24dev Date: Tue, 1 Oct 2024 22:12:10 -0400 Subject: [PATCH 01/30] fix: starting app flow --- .../xcshareddata/IDEWorkspaceChecks.plist | 8 - .../xcshareddata/WorkspaceSettings.xcsettings | 8 - lib/main.dart | 17 +-- lib/src/app.dart | 20 +-- .../core/di/dependency_injection.config.dart | 6 +- lib/src/features/game/cubit/game_cubit.dart | 26 +++- lib/src/features/home/home_screen.dart | 3 +- lib/src/features/ranking/leaderboard.dart | 6 - .../features/splash/loading_profile_data.dart | 2 +- lib/src/features/splash/splash_screen.dart | 140 ++++++++++++------ lib/src/router/router.dart | 2 +- pubspec.lock | 10 +- 12 files changed, 140 insertions(+), 108 deletions(-) delete mode 100644 ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist delete mode 100644 ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d9810..0000000 --- a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100644 index f9b0d7c..0000000 --- a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - PreviewsEnabled - - - diff --git a/lib/main.dart b/lib/main.dart index 98cbeb5..0180f8c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,13 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:my_app/src/app.dart'; import 'package:my_app/src/core/di/dependency_injection.dart'; -import 'package:my_app/src/features/auth/cubit/auth_cubit.dart'; -import 'package:my_app/src/features/game/cubit/game_cubit.dart'; -import 'package:my_app/src/features/player/cubit/player_cubit.dart'; -import 'package:my_app/src/features/ranking/cubit/ranking_cubit.dart'; -import 'package:my_app/src/features/splash/cubit/app_cubit.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; Future main() async { @@ -21,15 +15,6 @@ Future main() async { configureDependencies(); runApp( - MultiBlocProvider( - providers: [ - BlocProvider(create: (_) => getIt.get()), - BlocProvider(create: (_) => getIt.get()), - BlocProvider(create: (_) => getIt.get()), - BlocProvider(create: (_) => getIt.get()), - BlocProvider(create: (_) => getIt.get()..loadRanking()), - ], - child: const MyApp(), - ), + const MyApp(), ); } diff --git a/lib/src/app.dart b/lib/src/app.dart index 16d4afc..2e2e422 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -1,11 +1,13 @@ -import 'dart:developer'; - import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:my_app/l10n/l10n.dart'; import 'package:my_app/src/core/di/dependency_injection.dart'; import 'package:my_app/src/core/ui/theme.dart'; import 'package:my_app/src/features/auth/cubit/auth_cubit.dart'; +import 'package:my_app/src/features/game/cubit/game_cubit.dart'; +import 'package:my_app/src/features/player/cubit/player_cubit.dart'; +import 'package:my_app/src/features/ranking/cubit/ranking_cubit.dart'; +import 'package:my_app/src/features/splash/cubit/app_cubit.dart'; import 'package:my_app/src/router/router.dart'; class MyApp extends StatelessWidget { @@ -14,13 +16,13 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { final theme = AppTheme(); - return MultiBlocListener( - listeners: [ - BlocListener( - listener: (context, state) { - log('Initializaing AuthCubit...'); - }, - ), + return MultiBlocProvider( + providers: [ + BlocProvider(create: (_) => getIt.get()), + BlocProvider(create: (_) => getIt.get()), + BlocProvider(create: (_) => getIt.get()), + BlocProvider(create: (_) => getIt.get()), + BlocProvider(create: (_) => getIt.get()), ], child: MaterialApp.router( theme: theme.light, diff --git a/lib/src/core/di/dependency_injection.config.dart b/lib/src/core/di/dependency_injection.config.dart index bc3f256..0531f8c 100644 --- a/lib/src/core/di/dependency_injection.config.dart +++ b/lib/src/core/di/dependency_injection.config.dart @@ -44,15 +44,15 @@ extension GetItInjectableX on _i174.GetIt { () => _i427.AuthRepository(gh<_i454.SupabaseClient>())); gh.singleton<_i63.RouterController>( () => _i63.RouterController(gh<_i454.SupabaseClient>())); - gh.singleton<_i34.GameRepository>(() => _i34.GameRepository( + gh.singleton<_i34.RankingRepository>(() => _i34.RankingRepository( gh<_i330.SupabaseServiceImpl>(), gh<_i454.SupabaseClient>(), )); - gh.singleton<_i406.PlayerRepository>(() => _i406.PlayerRepository( + gh.singleton<_i34.GameRepository>(() => _i34.GameRepository( gh<_i330.SupabaseServiceImpl>(), gh<_i454.SupabaseClient>(), )); - gh.singleton<_i34.RankingRepository>(() => _i34.RankingRepository( + gh.singleton<_i406.PlayerRepository>(() => _i406.PlayerRepository( gh<_i330.SupabaseServiceImpl>(), gh<_i454.SupabaseClient>(), )); diff --git a/lib/src/features/game/cubit/game_cubit.dart b/lib/src/features/game/cubit/game_cubit.dart index f7b82b0..1aaa388 100644 --- a/lib/src/features/game/cubit/game_cubit.dart +++ b/lib/src/features/game/cubit/game_cubit.dart @@ -79,6 +79,11 @@ class GameCubit extends Cubit { payload.newRecord['player_number2'] as int?, ); + final gameStatus = getGameStatusById( + state.listGameStatus, + payload.newRecord['status'] as int, + ); + if (checkIfPlayerIsInGame(playerNumber1, playerNumber2)) { getLastGame(); } @@ -168,7 +173,12 @@ class GameCubit extends Cubit { return false; } - emit(state.copyWith(stateStatus: GameStateStatus.cancel, game: null)); + emit( + state.copyWith( + stateStatus: GameStateStatus.cancel, + game: oldState.game, + ), + ); await Future.delayed(1.seconds, refresh); return success!; @@ -216,17 +226,17 @@ class GameCubit extends Cubit { emit(state.copyWith(listGameStatus: listGameStatus)); } - Future setUserPlayer(Player player) async{ + Future setUserPlayer(Player player) async { emit(state.copyWith(player: player)); _listenPlayerNumberChanges(); } - GameStatus getGameStatusByStatus( + GameStatus getGameStatusById( List listGameStatus, - StatusEnum status, + int status, ) { - return listGameStatus.firstWhere((element) => element.status == status); + return listGameStatus.firstWhere((element) => element.id == status); } bool checkIfPlayerIsInGame( @@ -238,8 +248,10 @@ class GameCubit extends Cubit { } void refresh() { - emit(const GameState().copyWith(player: state.player)); - _gameRepository.dispose(); + emit( + const GameState() + .copyWith(player: state.player, listGameStatus: state.listGameStatus), + ); getLastGame(); } } diff --git a/lib/src/features/home/home_screen.dart b/lib/src/features/home/home_screen.dart index 0e8c343..8c9d551 100644 --- a/lib/src/features/home/home_screen.dart +++ b/lib/src/features/home/home_screen.dart @@ -5,6 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:my_app/src/features/game/cubit/game_cubit.dart'; import 'package:my_app/src/features/home/widgets/user_header_info.dart'; +import 'package:my_app/src/features/ranking/cubit/ranking_cubit.dart'; import 'package:my_app/src/features/ranking/leaderboard.dart'; import 'package:my_app/src/features/home/widgets/search_game_section.dart'; import 'package:my_app/src/router/router.dart'; @@ -20,7 +21,7 @@ class _HomeScreenState extends State { @override void initState() { context.read().getLastGame(); - + context.read().loadRanking(); super.initState(); } diff --git a/lib/src/features/ranking/leaderboard.dart b/lib/src/features/ranking/leaderboard.dart index 68546d9..975365e 100644 --- a/lib/src/features/ranking/leaderboard.dart +++ b/lib/src/features/ranking/leaderboard.dart @@ -17,12 +17,6 @@ class LeaderboardWidget extends StatefulWidget { } class _LeaderboardWidgetState extends State { - @override - void initState() { - context.read().loadRanking(); - super.initState(); - } - @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; diff --git a/lib/src/features/splash/loading_profile_data.dart b/lib/src/features/splash/loading_profile_data.dart index c9ce05e..855db80 100644 --- a/lib/src/features/splash/loading_profile_data.dart +++ b/lib/src/features/splash/loading_profile_data.dart @@ -21,7 +21,7 @@ class LoadingProfileData extends StatefulWidget { class _LoadingProfileDataState extends State { @override Widget build(BuildContext context) { - final gameCubit = context.watch(); + final gameCubit = context.read(); log('LOADING PROFILE DATA'); return Scaffold( body: BlocListener( diff --git a/lib/src/features/splash/splash_screen.dart b/lib/src/features/splash/splash_screen.dart index b561e04..8f4ee3a 100644 --- a/lib/src/features/splash/splash_screen.dart +++ b/lib/src/features/splash/splash_screen.dart @@ -1,14 +1,18 @@ +import 'dart:async'; import 'dart:developer'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gutter/flutter_gutter.dart'; -import 'package:my_app/src/core/ui/typography.dart'; -import 'package:my_app/src/core/utils/widgets/generic_button.dart'; +import 'package:go_router/go_router.dart'; +import 'package:lottie/lottie.dart'; +import 'package:my_app/resources/resources.dart'; +import 'package:my_app/src/core/utils/object_extensions.dart'; import 'package:my_app/src/features/game/cubit/game_cubit.dart'; +import 'package:my_app/src/features/player/cubit/player_cubit.dart'; import 'package:my_app/src/features/splash/cubit/app_cubit.dart'; import 'package:my_app/src/router/router.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; +import 'package:sized_context/sized_context.dart'; class SplashScreen extends StatefulWidget { const SplashScreen({super.key}); @@ -18,61 +22,103 @@ class SplashScreen extends StatefulWidget { } class _SplashScreenState extends State { + final Completer _appInitCompleter = Completer(); + final Completer _profileDataCompleter = Completer(); + @override void initState() { - // final client = Supabase.instance.client; + super.initState(); context.read().initialize(); + _waitForInitialization(); + } - // client.auth.onAuthStateChange.listen((authSupState) { - // log('AUTH message'); - - // final route = switch (authSupState.event) { - // AuthChangeEvent.initialSession => AppRoutes.initProfileData, - // AuthChangeEvent.signedIn => AppRoutes.initProfileData, - // AuthChangeEvent.signedOut => AppRoutes.auth, - // _ => AppRoutes.home, - // }; - // WidgetsFlutterBinding.ensureInitialized().addPostFrameCallback((_) { - // log(route); - // Navigator.pushReplacementNamed(context, route); - // }); - // }); - - super.initState(); + Future _waitForInitialization() async { + await Future.wait([ + _appInitCompleter.future, + _profileDataCompleter.future, + ]); + WidgetsFlutterBinding.ensureInitialized().addPostFrameCallback((_) { + context.goNamed(AppRoute.home.name); + }); } @override Widget build(BuildContext context) { + final gameCubit = context.read(); + return Scaffold( - body: BlocBuilder( - builder: (context, state) { - if (state.isSuccess && state.initialized) { - context.read().setGameStatus(state.gameStatus); - } - if (state.isError) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Center( - child: Text('An error has occurred'), - ), - const Gutter(), - GenericButton( - widget: Text( - 'Retry', - style: AppTextStyle().body.copyWith(color: Colors.white), + body: MultiBlocListener( + listeners: [ + BlocListener( + listener: (context, state) { + if (state.isSuccess && state.initialized) { + context.read().setGameStatus(state.gameStatus); + if (!_appInitCompleter.isCompleted) { + _appInitCompleter.complete(); + } + } + if (state.isError) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Error'), + content: const Text('An error has occurred'), + actions: [ + TextButton( + onPressed: () { + context.read().initialize(); + Navigator.of(context).pop(); + }, + child: const Text('Retry'), + ), + ], ), - onPressed: () { - context.read().initialize(); - }, + ); + } + }, + ), + BlocListener( + listenWhen: (previous, current) => + previous.status != current.status, + listener: (context, state) { + log('PlayerCubit: ${state.status}'); + + if (state.status == PlayerStatus.success) { + if (gameCubit.state.player == null) { + context.read().setUserPlayer(state.player!); + } + + if (gameCubit.state.game.isNull) { + if (!_profileDataCompleter.isCompleted) { + _profileDataCompleter.complete(); + } + } + } + }, + ), + ], + child: BlocBuilder( + builder: (context, state) { + if (state.isLoading) { + return SizedBox( + width: context.widthPx, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + LottieBuilder.asset( + AppImages.appLoading, + height: 100, + fit: BoxFit.cover, + ), + const GutterSmall(), + const Text('Loading...'), + ], ), - ], - ); - } - return const Center( - child: CircularProgressIndicator(), - ); - }, + ); + } + return Container(); + }, + ), ), ); } diff --git a/lib/src/router/router.dart b/lib/src/router/router.dart index b4564fd..32932df 100644 --- a/lib/src/router/router.dart +++ b/lib/src/router/router.dart @@ -31,7 +31,7 @@ final class RouterController { final currentSession = _client.auth.currentSession; router = GoRouter( debugLogDiagnostics: kDebugMode, - initialLocation: currentSession.isNull ? '/auth' : '/init-profile-data', + initialLocation: currentSession.isNull ? '/auth' : '/splash', navigatorKey: rootNavigatorKey, redirect: (context, state) { final routeName = _listenAuthChanges(_client, context); diff --git a/pubspec.lock b/pubspec.lock index 2df4a1e..c832361 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -286,6 +286,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.1" + equatable: + dependency: "direct main" + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted + version: "2.0.5" fake_async: dependency: transitive description: @@ -1208,7 +1216,7 @@ packages: source: hosted version: "3.1.2" uuid: - dependency: transitive + dependency: "direct main" description: name: uuid sha256: f33d6bb662f0e4f79dcd7ada2e6170f3b3a2530c28fc41f49a411ddedd576a77 From 0a9e956fa3156ee2f0d609234c218c397568291c Mon Sep 17 00:00:00 2001 From: ale24dev Date: Tue, 1 Oct 2024 23:30:21 -0400 Subject: [PATCH 02/30] fix: cancel game --- lib/src/features/game/cubit/game_cubit.dart | 20 ++++++------------- .../features/game/data/game_repository.dart | 4 ++-- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/lib/src/features/game/cubit/game_cubit.dart b/lib/src/features/game/cubit/game_cubit.dart index 1aaa388..ad0b4d4 100644 --- a/lib/src/features/game/cubit/game_cubit.dart +++ b/lib/src/features/game/cubit/game_cubit.dart @@ -79,11 +79,6 @@ class GameCubit extends Cubit { payload.newRecord['player_number2'] as int?, ); - final gameStatus = getGameStatusById( - state.listGameStatus, - payload.newRecord['status'] as int, - ); - if (checkIfPlayerIsInGame(playerNumber1, playerNumber2)) { getLastGame(); } @@ -132,10 +127,7 @@ class GameCubit extends Cubit { }); } - Future findOrCreateGame( - // Player player, - ) async { - /// If the last game is already finished + Future findOrCreateGame() async { if (!state.isGameFinished) return; emit( state.copyWith( @@ -156,7 +148,6 @@ class GameCubit extends Cubit { } Future cancelSearchGame(Player player) async { - final oldState = state; emit(state.copyWith(stateStatus: GameStateStatus.loading)); final result = await _gameRepository.cancelSearchGame(player); @@ -176,7 +167,6 @@ class GameCubit extends Cubit { emit( state.copyWith( stateStatus: GameStateStatus.cancel, - game: oldState.game, ), ); @@ -249,9 +239,11 @@ class GameCubit extends Cubit { void refresh() { emit( - const GameState() - .copyWith(player: state.player, listGameStatus: state.listGameStatus), + const GameState().copyWith( + player: state.player, + listGameStatus: state.listGameStatus, + game: null, + ), ); - getLastGame(); } } diff --git a/lib/src/features/game/data/game_repository.dart b/lib/src/features/game/data/game_repository.dart index 030b7ce..1166ea8 100644 --- a/lib/src/features/game/data/game_repository.dart +++ b/lib/src/features/game/data/game_repository.dart @@ -26,7 +26,7 @@ class GameRepository extends GameDataSource { table: 'RPC create_game', request: () => // _client.rpc('create_game', params: {'player_id': player.id}), - _client.rpc('find_or_create_game', params: {'player_id': player.id}), + _client.rpc('find_or_create_game', params: {'p_player_id': player.id}), queryOption: QueryOption.insert, fromJsonParse: Game.fromJson, ); @@ -94,7 +94,7 @@ class GameRepository extends GameDataSource { return _supabaseServiceImpl.query( table: 'RPC cancel_search_game', request: () => - _client.rpc('cancel_search_game', params: {'player_id': player.id}), + _client.rpc('cancel_search_game', params: {'p_player_id': player.id}), queryOption: QueryOption.insert, ); } From 8e1341f355c90bcd0c27ceadd97357aef60670f6 Mon Sep 17 00:00:00 2001 From: ale24dev Date: Wed, 2 Oct 2024 01:39:30 -0400 Subject: [PATCH 03/30] add: logout flow --- .../core/di/dependency_injection.config.dart | 13 +++- lib/src/core/di/dependency_injection.dart | 2 +- lib/src/core/di/modules/modules.dart | 8 ++ lib/src/features/auth/cubit/auth_cubit.dart | 28 +++++-- .../features/auth/data/auth_repository.dart | 9 +++ lib/src/features/game/cubit/game_cubit.dart | 6 +- lib/src/features/home/home_screen.dart | 23 +++++- .../home/widgets/search_game_section.dart | 78 ++++++++++++------- .../home/widgets/user_header_info.dart | 38 +-------- .../features/home/widgets/user_points.dart | 55 +++++++++++++ .../features/player/cubit/player_cubit.dart | 1 - .../features/splash/loading_profile_data.dart | 6 +- lib/src/features/splash/splash_screen.dart | 1 - lib/src/router/router.dart | 22 +----- pubspec.lock | 2 +- pubspec.yaml | 2 + 16 files changed, 194 insertions(+), 100 deletions(-) create mode 100644 lib/src/core/di/modules/modules.dart create mode 100644 lib/src/features/home/widgets/user_points.dart diff --git a/lib/src/core/di/dependency_injection.config.dart b/lib/src/core/di/dependency_injection.config.dart index 0531f8c..4447bc0 100644 --- a/lib/src/core/di/dependency_injection.config.dart +++ b/lib/src/core/di/dependency_injection.config.dart @@ -10,6 +10,7 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'package:get_it/get_it.dart' as _i174; import 'package:injectable/injectable.dart' as _i526; +import 'package:my_app/src/core/di/modules/modules.dart' as _i560; import 'package:my_app/src/core/interceptor.dart' as _i330; import 'package:my_app/src/core/supabase/client.dart' as _i880; import 'package:my_app/src/features/auth/cubit/auth_cubit.dart' as _i992; @@ -24,20 +25,26 @@ import 'package:my_app/src/features/ranking/data/ranking_repository.dart' as _i34; import 'package:my_app/src/features/splash/cubit/app_cubit.dart' as _i1038; import 'package:my_app/src/router/router.dart' as _i63; +import 'package:shared_preferences/shared_preferences.dart' as _i460; import 'package:supabase_flutter/supabase_flutter.dart' as _i454; extension GetItInjectableX on _i174.GetIt { // initializes the registration of main-scope dependencies inside of GetIt - _i174.GetIt init({ + Future<_i174.GetIt> init({ String? environment, _i526.EnvironmentFilter? environmentFilter, - }) { + }) async { final gh = _i526.GetItHelper( this, environment, environmentFilter, ); + final modules = _$Modules(); final supabaseModule = _$SupabaseModule(); + await gh.factoryAsync<_i460.SharedPreferences>( + () => modules.prefs, + preResolve: true, + ); gh.singleton<_i330.SupabaseServiceImpl>(() => _i330.SupabaseServiceImpl()); gh.lazySingleton<_i454.SupabaseClient>(() => supabaseModule.client); gh.singleton<_i427.AuthRepository>( @@ -79,4 +86,6 @@ extension GetItInjectableX on _i174.GetIt { } } +class _$Modules extends _i560.Modules {} + class _$SupabaseModule extends _i880.SupabaseModule {} diff --git a/lib/src/core/di/dependency_injection.dart b/lib/src/core/di/dependency_injection.dart index a291cf3..daf1f85 100644 --- a/lib/src/core/di/dependency_injection.dart +++ b/lib/src/core/di/dependency_injection.dart @@ -5,4 +5,4 @@ import 'package:my_app/src/core/di/dependency_injection.config.dart'; final getIt = GetIt.instance; @InjectableInit() -GetIt configureDependencies() => getIt.init(); +Future configureDependencies() => getIt.init(); diff --git a/lib/src/core/di/modules/modules.dart b/lib/src/core/di/modules/modules.dart new file mode 100644 index 0000000..4fe0e06 --- /dev/null +++ b/lib/src/core/di/modules/modules.dart @@ -0,0 +1,8 @@ +import 'package:injectable/injectable.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +@module +abstract class Modules { + @preResolve + Future get prefs => SharedPreferences.getInstance(); +} diff --git a/lib/src/features/auth/cubit/auth_cubit.dart b/lib/src/features/auth/cubit/auth_cubit.dart index f1bc7ee..edeebdf 100644 --- a/lib/src/features/auth/cubit/auth_cubit.dart +++ b/lib/src/features/auth/cubit/auth_cubit.dart @@ -1,5 +1,3 @@ -import 'dart:developer'; - import 'package:bloc/bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:injectable/injectable.dart'; @@ -37,8 +35,8 @@ class AuthCubit extends Cubit { ), AuthChangeEvent.signedOut => emit( state.copyWith( - authStatus: AuthStatus.authenticated, - user: authSupState.session!.user, + authStatus: AuthStatus.success, + user: null, ), ), _ => emit(state), @@ -76,7 +74,27 @@ class AuthCubit extends Cubit { errorMessage: 'Error signin', ), ), - (success) => emit(const AuthState(authStatus: AuthStatus.authenticated)), + (success) => emit( + const AuthState( + authStatus: AuthStatus.authenticated, + ), + ), + ); + } + + Future logout() async { + emit(const AuthState(authStatus: AuthStatus.loading)); + + final result = await _authRepository.logout(); + + result.fold( + (error) => emit( + const AuthState( + authStatus: AuthStatus.error, + errorMessage: 'Error signin', + ), + ), + (success) => emit(const AuthState(authStatus: AuthStatus.success)), ); } diff --git a/lib/src/features/auth/data/auth_repository.dart b/lib/src/features/auth/data/auth_repository.dart index 565495c..30dea6b 100644 --- a/lib/src/features/auth/data/auth_repository.dart +++ b/lib/src/features/auth/data/auth_repository.dart @@ -34,6 +34,15 @@ class AuthRepository { } } + Future> logout() async { + try { + await _client.auth.signOut(); + return right(true); + } catch (e) { + return left(Exception('Error sign up')); + } + } + Future> refreshToken() async { try { final refreshToken = _client.auth.currentSession?.refreshToken; diff --git a/lib/src/features/game/cubit/game_cubit.dart b/lib/src/features/game/cubit/game_cubit.dart index ad0b4d4..74d82b7 100644 --- a/lib/src/features/game/cubit/game_cubit.dart +++ b/lib/src/features/game/cubit/game_cubit.dart @@ -27,7 +27,6 @@ class GameCubit extends Cubit { this._gameRepository, this._playerRepository, ) : super(const GameState()) { - log('Initializing GameCubit...'); _listenGame(); } @@ -59,7 +58,6 @@ class GameCubit extends Cubit { void _listenGame() { final game = Game.empty(); final myChannel = _client.channel('games_channel'); - log('Game Database Changes Listen On'); myChannel .onPostgresChanges( @@ -246,4 +244,8 @@ class GameCubit extends Cubit { ), ); } + + void dispose() { + emit(const GameState()); + } } diff --git a/lib/src/features/home/home_screen.dart b/lib/src/features/home/home_screen.dart index 8c9d551..e31d8bd 100644 --- a/lib/src/features/home/home_screen.dart +++ b/lib/src/features/home/home_screen.dart @@ -1,14 +1,19 @@ +// ignore_for_file: use_build_context_synchronously + import 'dart:developer'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; +import 'package:my_app/src/core/di/dependency_injection.dart'; +import 'package:my_app/src/core/utils/object_extensions.dart'; import 'package:my_app/src/features/game/cubit/game_cubit.dart'; import 'package:my_app/src/features/home/widgets/user_header_info.dart'; import 'package:my_app/src/features/ranking/cubit/ranking_cubit.dart'; import 'package:my_app/src/features/ranking/leaderboard.dart'; import 'package:my_app/src/features/home/widgets/search_game_section.dart'; import 'package:my_app/src/router/router.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @@ -22,12 +27,28 @@ class _HomeScreenState extends State { void initState() { context.read().getLastGame(); context.read().loadRanking(); + + getIt.get().auth.onAuthStateChange.listen((authSupState) { + final route = switch (authSupState.event) { + AuthChangeEvent.initialSession => + authSupState.session.isNull ? AppRoute.auth.name : AppRoute.home.name, + AuthChangeEvent.signedIn => AppRoute.home.name, + AuthChangeEvent.signedOut => AppRoute.auth.name, + _ => null + }; + if (mounted) { + if (route != null) context.goNamed(route); + + if (authSupState.event == AuthChangeEvent.signedOut) { + context.read().dispose(); + } + } + }); super.initState(); } @override Widget build(BuildContext context) { - log('HOME PAGE'); return Scaffold( body: BlocBuilder( builder: (context, state) { diff --git a/lib/src/features/home/widgets/search_game_section.dart b/lib/src/features/home/widgets/search_game_section.dart index 93fcd52..f5c9428 100644 --- a/lib/src/features/home/widgets/search_game_section.dart +++ b/lib/src/features/home/widgets/search_game_section.dart @@ -2,11 +2,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gutter/flutter_gutter.dart'; import 'package:go_router/go_router.dart'; -import 'package:my_app/resources/resources.dart'; import 'package:my_app/src/core/ui/theme.dart'; import 'package:my_app/src/core/ui/typography.dart'; +import 'package:my_app/src/features/auth/cubit/auth_cubit.dart'; +import 'package:my_app/src/features/auth/views/auth_screen.dart'; import 'package:my_app/src/features/game/cubit/game_cubit.dart'; import 'package:my_app/src/router/router.dart'; +import 'package:sized_context/sized_context.dart'; class SearchGameSection extends StatefulWidget { const SearchGameSection({super.key}); @@ -21,7 +23,8 @@ class _SearchGameSectionState extends State { @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.symmetric(vertical: 40), + padding: + EdgeInsets.symmetric(vertical: 40, horizontal: context.widthPx * .05), child: BlocBuilder( builder: (context, state) { if (state.isGameStarted && !_hasNavigated) { @@ -31,38 +34,59 @@ class _SearchGameSectionState extends State { ); } return Row( - mainAxisAlignment: MainAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - GestureDetector( - onTap: () { - context.goNamed(AppRoute.searchGame.name); - }, - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary, - borderRadius: BorderRadius.circular(5), - boxShadow: AppTheme.defaultShadow, + Expanded( + child: GestureDetector( + onTap: () => context.read().logout(), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.logout), + const GutterTiny(), + Text( + 'Exit', + style: AppTextStyle().body.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ], ), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 30, - vertical: 10, + ), + ), + Expanded( + child: GestureDetector( + onTap: () { + context.goNamed(AppRoute.searchGame.name); + }, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + borderRadius: BorderRadius.circular(5), + boxShadow: AppTheme.defaultShadow, ), - child: Row( - children: [ - const Icon(Icons.play_arrow, color: Colors.white), - const GutterTiny(), - Text( - 'Play', - style: AppTextStyle() - .textButton - .copyWith(color: Colors.white), - ), - ], + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 30, + vertical: 10, + ), + child: Row( + children: [ + const Icon(Icons.play_arrow, color: Colors.white), + const GutterTiny(), + Text( + 'Play', + style: AppTextStyle() + .textButton + .copyWith(color: Colors.white), + ), + ], + ), ), ), ), ), + const Expanded(child: SizedBox.shrink()), ], ); }, diff --git a/lib/src/features/home/widgets/user_header_info.dart b/lib/src/features/home/widgets/user_header_info.dart index 953cc28..9e91238 100644 --- a/lib/src/features/home/widgets/user_header_info.dart +++ b/lib/src/features/home/widgets/user_header_info.dart @@ -6,6 +6,7 @@ import 'package:my_app/src/core/ui/device.dart'; import 'package:my_app/src/core/ui/typography.dart'; import 'package:my_app/src/core/utils/widgets/cache_widget.dart'; import 'package:my_app/src/features/game/cubit/game_cubit.dart'; +import 'package:my_app/src/features/home/widgets/user_points.dart'; import 'package:my_app/src/features/player/data/model/player.dart'; import 'package:my_app/src/features/ranking/cubit/ranking_cubit.dart'; import 'package:my_app/src/features/ranking/uitls/ranking_utils.dart'; @@ -78,42 +79,7 @@ class _UserHeaderInfoState extends State { ), ], ), - Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(50), - ), - child: Padding( - padding: const EdgeInsets.all(8), - child: Row( - children: [ - Image.asset( - AppImages.gamePoints, - height: 20, - ), - const GutterTiny(), - BlocBuilder( - builder: (context, state) { - if (state.isLoading) { - return const CircularProgressIndicator.adaptive(); - } - - final ranking = RankingUtils.getRankingByPlayerId( - state.ranking, - player.id, - ); - return Text( - '${ranking.points}', - style: AppTextStyle() - .body - .copyWith(fontWeight: FontWeight.bold), - ); - }, - ), - ], - ), - ), - ), + UserPoints(player: player), ], ), ], diff --git a/lib/src/features/home/widgets/user_points.dart b/lib/src/features/home/widgets/user_points.dart new file mode 100644 index 0000000..bae39b3 --- /dev/null +++ b/lib/src/features/home/widgets/user_points.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_gutter/flutter_gutter.dart'; +import 'package:my_app/resources/resources.dart'; +import 'package:my_app/src/core/ui/typography.dart'; +import 'package:my_app/src/features/player/data/model/player.dart'; +import 'package:my_app/src/features/ranking/cubit/ranking_cubit.dart'; +import 'package:my_app/src/features/ranking/uitls/ranking_utils.dart'; + +class UserPoints extends StatelessWidget { + const UserPoints({ + required this.player, super.key, + }); + + final Player player; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(50), + ), + child: Padding( + padding: const EdgeInsets.all(8), + child: Row( + children: [ + Image.asset( + AppImages.gamePoints, + height: 20, + ), + const GutterTiny(), + BlocBuilder( + builder: (context, state) { + if (state.isLoading) { + return const CircularProgressIndicator.adaptive(); + } + + final ranking = RankingUtils.getRankingByPlayerId( + state.ranking, + player.id, + ); + return Text( + '${ranking.points}', + style: + AppTextStyle().body.copyWith(fontWeight: FontWeight.bold), + ); + }, + ), + ], + ), + ), + ); + } +} diff --git a/lib/src/features/player/cubit/player_cubit.dart b/lib/src/features/player/cubit/player_cubit.dart index 888e711..249705f 100644 --- a/lib/src/features/player/cubit/player_cubit.dart +++ b/lib/src/features/player/cubit/player_cubit.dart @@ -45,7 +45,6 @@ class PlayerCubit extends Cubit { state.copyWith(status: PlayerStatus.error), ), (player) { emit(state.copyWith(player: player, status: PlayerStatus.success)); - log('Termine'); }); }); } diff --git a/lib/src/features/splash/loading_profile_data.dart b/lib/src/features/splash/loading_profile_data.dart index 855db80..28e5dff 100644 --- a/lib/src/features/splash/loading_profile_data.dart +++ b/lib/src/features/splash/loading_profile_data.dart @@ -22,24 +22,22 @@ class _LoadingProfileDataState extends State { @override Widget build(BuildContext context) { final gameCubit = context.read(); - log('LOADING PROFILE DATA'); return Scaffold( body: BlocListener( listenWhen: (previous, current) => previous.status != current.status, listener: (context, state) { - log('PlayerCubit: ${state.status}'); if (state.status == PlayerStatus.success) { if (gameCubit.state.player == null) { context.read().setUserPlayer(state.player!); } - if (gameCubit.state.game.isNull) { + // if (gameCubit.state.game.isNull) { WidgetsFlutterBinding.ensureInitialized() .addPostFrameCallback((_) { context.goNamed(AppRoute.home.name); }); - } + // } } }, child: Center( diff --git a/lib/src/features/splash/splash_screen.dart b/lib/src/features/splash/splash_screen.dart index 8f4ee3a..d6bea0c 100644 --- a/lib/src/features/splash/splash_screen.dart +++ b/lib/src/features/splash/splash_screen.dart @@ -81,7 +81,6 @@ class _SplashScreenState extends State { listenWhen: (previous, current) => previous.status != current.status, listener: (context, state) { - log('PlayerCubit: ${state.status}'); if (state.status == PlayerStatus.success) { if (gameCubit.state.player == null) { diff --git a/lib/src/router/router.dart b/lib/src/router/router.dart index 32932df..533dee0 100644 --- a/lib/src/router/router.dart +++ b/lib/src/router/router.dart @@ -1,3 +1,5 @@ +// ignore_for_file: use_build_context_synchronously + import 'dart:developer'; import 'package:flutter/cupertino.dart'; @@ -34,10 +36,7 @@ final class RouterController { initialLocation: currentSession.isNull ? '/auth' : '/splash', navigatorKey: rootNavigatorKey, redirect: (context, state) { - final routeName = _listenAuthChanges(_client, context); - - log('Route: $routeName'); - return routeName; + return null; }, routes: [ GoRoute( @@ -110,21 +109,6 @@ final class RouterController { late final GoRouter router; } -String? _listenAuthChanges(SupabaseClient client, BuildContext context) { - String? route; - client.auth.onAuthStateChange.listen((authSupState) { - log('AAAAAA: ${authSupState.event}'); - route = switch (authSupState.event) { - AuthChangeEvent.initialSession => - authSupState.session.isNull ? AppRoute.auth.name : AppRoute.home.name, - AuthChangeEvent.signedIn => AppRoute.home.name, - AuthChangeEvent.signedOut => AppRoute.auth.name, - _ => null - }; - }); - return route; -} - /// Adaptive route that uses cupertino pages on iOS and macos, no transitions on web and material on the rest of the /// platforms. Page adaptivePageRoute({ diff --git a/pubspec.lock b/pubspec.lock index c832361..2f1a88a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -859,7 +859,7 @@ packages: source: hosted version: "0.28.0" shared_preferences: - dependency: transitive + dependency: "direct main" description: name: shared_preferences sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051" diff --git a/pubspec.yaml b/pubspec.yaml index b6d2267..7367978 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -31,7 +31,9 @@ dependencies: sized_context: ^1.0.0+4 spider: ^4.2.2 + supabase_flutter: ^2.6.0 + shared_preferences: ^2.3.2 uuid: ^4.5.0 From 5bbb55482b561c4d595a94f521e310994ab42aff Mon Sep 17 00:00:00 2001 From: ale24dev Date: Wed, 2 Oct 2024 02:04:59 -0400 Subject: [PATCH 04/30] add: theme to app --- lib/src/app.dart | 2 +- lib/src/features/auth/views/auth_screen.dart | 1 + lib/src/features/game/game_screen.dart | 7 +++-- .../features/home/widgets/game_section.dart | 28 +++++++++++++++++-- lib/src/features/home/widgets/otp_fields.dart | 15 ++++++---- .../home/widgets/play_number_card.dart | 15 +++++++++- .../home/widgets/select_secret_number.dart | 7 +++-- .../features/home/widgets/versus_section.dart | 1 + .../features/ranking/widgets/rank_card.dart | 14 ++++++++-- lib/src/features/splash/splash_screen.dart | 8 ++++-- 10 files changed, 78 insertions(+), 20 deletions(-) diff --git a/lib/src/app.dart b/lib/src/app.dart index 2e2e422..3febcea 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -25,7 +25,7 @@ class MyApp extends StatelessWidget { BlocProvider(create: (_) => getIt.get()), ], child: MaterialApp.router( - theme: theme.light, + theme: theme.dark, darkTheme: theme.dark, routerConfig: RouterController(getIt.get()).router, localizationsDelegates: AppLocalizations.localizationsDelegates, diff --git a/lib/src/features/auth/views/auth_screen.dart b/lib/src/features/auth/views/auth_screen.dart index ff452b2..057ce45 100644 --- a/lib/src/features/auth/views/auth_screen.dart +++ b/lib/src/features/auth/views/auth_screen.dart @@ -56,6 +56,7 @@ class _AuthScreenState extends State { style: AppTextStyle().body.copyWith( fontFamily: AppTextStyle.secondaryFontFamily, fontSize: 30, + color: Theme.of(context).colorScheme.onSurface, ), ), const GutterLarge(), diff --git a/lib/src/features/game/game_screen.dart b/lib/src/features/game/game_screen.dart index 5b8de70..34d94a4 100644 --- a/lib/src/features/game/game_screen.dart +++ b/lib/src/features/game/game_screen.dart @@ -166,9 +166,10 @@ class _GameScreenState extends State { const GutterTiny(), Text( '${isWinner ? '+' : ''}$resultPoints', - style: AppTextStyle() - .body - .copyWith(fontWeight: FontWeight.bold), + style: AppTextStyle().body.copyWith( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onSurface, + ), ), ], ), diff --git a/lib/src/features/home/widgets/game_section.dart b/lib/src/features/home/widgets/game_section.dart index f1aa085..af39b80 100644 --- a/lib/src/features/home/widgets/game_section.dart +++ b/lib/src/features/home/widgets/game_section.dart @@ -69,6 +69,7 @@ class _GameSectionState extends State { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; final ownPlayerNumber = widget.game.getOwnPlayerNumber(player); if (ownPlayerNumber.isTurn && timer == null) { @@ -86,9 +87,17 @@ class _GameSectionState extends State { Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - Text('Time left: ', style: AppTextStyle().body), + Text( + 'Time left: ', + style: AppTextStyle().body.copyWith( + color: colorScheme.onSurface, + ), + ), Text( '${(timeLeft ~/ 60).toString().padLeft(2, '0')}:${(timeLeft % 60).toString().padLeft(2, '0')}', + style: AppTextStyle().body.copyWith( + color: colorScheme.onSurface, + ), ), ], ), @@ -110,12 +119,16 @@ class _PlayList extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; return ListView( padding: EdgeInsets.zero, children: [ Text( 'Previous attempts'.toUpperCase(), - style: AppTextStyle().body.copyWith(fontWeight: FontWeight.w600), + style: AppTextStyle().body.copyWith( + fontWeight: FontWeight.w600, + color: colorScheme.onSurface, + ), ), const GutterSmall(), BlocBuilder( @@ -129,7 +142,16 @@ class _PlayList extends StatelessWidget { } return Column( children: state.listAttempts.isEmpty - ? [const Text('No attempts')] + ? [ + Center( + child: Text( + 'No attempts', + style: AppTextStyle().body.copyWith( + color: colorScheme.onSurface, + ), + ), + ), + ] : state.listAttempts.asMap().entries.map((entry) { final index = state.listAttempts.length - entry.key; final value = entry.value; diff --git a/lib/src/features/home/widgets/otp_fields.dart b/lib/src/features/home/widgets/otp_fields.dart index 0bd2216..8b41bf5 100644 --- a/lib/src/features/home/widgets/otp_fields.dart +++ b/lib/src/features/home/widgets/otp_fields.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:my_app/src/core/ui/typography.dart'; class OTPFields extends StatefulWidget { const OTPFields({ @@ -8,7 +9,6 @@ class OTPFields extends StatefulWidget { this.fieldWidth = 50, this.fieldHeight, this.margin = const EdgeInsets.symmetric(horizontal: 5), - this.borderFilledColor = Colors.black, this.allowRepetitions = true, }); @@ -17,7 +17,6 @@ class OTPFields extends StatefulWidget { final double fieldWidth; final double? fieldHeight; final EdgeInsets margin; - final Color borderFilledColor; final bool allowRepetitions; @override @@ -86,6 +85,7 @@ class OTPFieldsState extends State { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; return Row( mainAxisAlignment: MainAxisAlignment.center, children: List.generate(widget.length, (index) { @@ -97,20 +97,23 @@ class OTPFieldsState extends State { controller: controllers[index], focusNode: focusNodes[index], maxLength: 1, + style: AppTextStyle().body.copyWith( + fontFamily: AppTextStyle.secondaryFontFamily, + color: Theme.of(context).colorScheme.onSurface, + ), textAlign: TextAlign.center, keyboardType: TextInputType.number, decoration: InputDecoration( + fillColor: colorScheme.surfaceContainer, counterText: '', border: OutlineInputBorder( borderSide: BorderSide( - color: - isFilled[index] ? widget.borderFilledColor : Colors.grey, + color: isFilled[index] ? colorScheme.onSurface : Colors.grey, ), ), enabledBorder: OutlineInputBorder( borderSide: BorderSide( - color: - isFilled[index] ? widget.borderFilledColor : Colors.grey, + color: isFilled[index] ? colorScheme.onSurface : Colors.grey, ), ), ), diff --git a/lib/src/features/home/widgets/play_number_card.dart b/lib/src/features/home/widgets/play_number_card.dart index 5aea72a..f59b2f1 100644 --- a/lib/src/features/home/widgets/play_number_card.dart +++ b/lib/src/features/home/widgets/play_number_card.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gutter/flutter_gutter.dart'; import 'package:my_app/src/core/ui/theme.dart'; +import 'package:my_app/src/core/ui/typography.dart'; import 'package:my_app/src/features/game/data/model/attempt.dart'; class PlayNumberCard extends StatelessWidget { @@ -16,7 +17,19 @@ class PlayNumberCard extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 15), child: Row( children: [ - Text(index.toString()), + Container( + decoration: BoxDecoration( + color: colorScheme.primary, + shape: BoxShape.circle, + ), + child: Padding( + padding: const EdgeInsets.all(10), + child: Text( + index.toString(), + style: AppTextStyle().body.copyWith(color: Colors.black), + ), + ), + ), const GutterLarge(), Expanded( child: Container( diff --git a/lib/src/features/home/widgets/select_secret_number.dart b/lib/src/features/home/widgets/select_secret_number.dart index 5448753..60b309d 100644 --- a/lib/src/features/home/widgets/select_secret_number.dart +++ b/lib/src/features/home/widgets/select_secret_number.dart @@ -66,8 +66,11 @@ class _SelectSecretNumberState extends State { Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return AlertDialog( - title: - Text('Select your secret number', style: AppTextStyle().dialogTitle), + title: Text( + 'Select your secret number', + style: + AppTextStyle().dialogTitle.copyWith(color: colorScheme.onSurface), + ), content: Column( mainAxisSize: MainAxisSize.min, children: [ diff --git a/lib/src/features/home/widgets/versus_section.dart b/lib/src/features/home/widgets/versus_section.dart index a0467a4..9c6321e 100644 --- a/lib/src/features/home/widgets/versus_section.dart +++ b/lib/src/features/home/widgets/versus_section.dart @@ -70,6 +70,7 @@ class VersusSection extends StatelessWidget { ), const Spacer(), IconButton( + color: Colors.black, onPressed: () { _exitGame(context); }, diff --git a/lib/src/features/ranking/widgets/rank_card.dart b/lib/src/features/ranking/widgets/rank_card.dart index 164cf21..3163e9a 100644 --- a/lib/src/features/ranking/widgets/rank_card.dart +++ b/lib/src/features/ranking/widgets/rank_card.dart @@ -1,5 +1,7 @@ +import 'package:flex_color_scheme/flex_color_scheme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gutter/flutter_gutter.dart'; +import 'package:my_app/src/core/ui/theme.dart'; import 'package:my_app/src/core/ui/typography.dart'; import 'package:my_app/src/features/ranking/data/model/ranking.dart'; import 'package:sized_context/sized_context.dart'; @@ -16,12 +18,17 @@ class RankCard extends StatelessWidget { @override Widget build(BuildContext context) { + final theme = Theme.of(context); + final isDarkTheme = theme.brightness == Brightness.dark; + return Padding( padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2), child: Container( width: context.widthPx, decoration: BoxDecoration( - color: colorScheme.secondary.withOpacity(.7), + color: isDarkTheme + ? colorScheme.primary.darken(4) + : colorScheme.secondary.withOpacity(.7), borderRadius: BorderRadius.circular(20), ), child: Padding( @@ -35,7 +42,10 @@ class RankCard extends StatelessWidget { ), child: Padding( padding: const EdgeInsets.all(12), - child: Text(ranking.position.toString()), + child: Text( + ranking.position.toString(), + style: AppTextStyle().body.copyWith(color: Colors.black), + ), ), ), const GutterMedium(), diff --git a/lib/src/features/splash/splash_screen.dart b/lib/src/features/splash/splash_screen.dart index d6bea0c..eff851f 100644 --- a/lib/src/features/splash/splash_screen.dart +++ b/lib/src/features/splash/splash_screen.dart @@ -7,6 +7,7 @@ import 'package:flutter_gutter/flutter_gutter.dart'; import 'package:go_router/go_router.dart'; import 'package:lottie/lottie.dart'; import 'package:my_app/resources/resources.dart'; +import 'package:my_app/src/core/ui/typography.dart'; import 'package:my_app/src/core/utils/object_extensions.dart'; import 'package:my_app/src/features/game/cubit/game_cubit.dart'; import 'package:my_app/src/features/player/cubit/player_cubit.dart'; @@ -47,6 +48,7 @@ class _SplashScreenState extends State { final gameCubit = context.read(); return Scaffold( + backgroundColor: Colors.white, body: MultiBlocListener( listeners: [ BlocListener( @@ -81,7 +83,6 @@ class _SplashScreenState extends State { listenWhen: (previous, current) => previous.status != current.status, listener: (context, state) { - if (state.status == PlayerStatus.success) { if (gameCubit.state.player == null) { context.read().setUserPlayer(state.player!); @@ -110,7 +111,10 @@ class _SplashScreenState extends State { fit: BoxFit.cover, ), const GutterSmall(), - const Text('Loading...'), + Text( + 'Loading...', + style: AppTextStyle().body.copyWith(color: Colors.black), + ), ], ), ); From 61568f969ce9f529383c9561b718e4701a0ab43a Mon Sep 17 00:00:00 2001 From: ale24dev Date: Wed, 2 Oct 2024 03:20:22 -0400 Subject: [PATCH 05/30] add: users signUp --- lib/src/app.dart | 1 + lib/src/features/auth/views/auth_screen.dart | 64 +++++-- .../features/auth/views/signup_screen.dart | 164 ++++++++++++++++++ .../features/ranking/widgets/rank_card.dart | 27 +-- .../features/splash/loading_profile_data.dart | 1 - lib/src/router/router.dart | 34 ++-- 6 files changed, 251 insertions(+), 40 deletions(-) create mode 100644 lib/src/features/auth/views/signup_screen.dart diff --git a/lib/src/app.dart b/lib/src/app.dart index 3febcea..e2a008d 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -25,6 +25,7 @@ class MyApp extends StatelessWidget { BlocProvider(create: (_) => getIt.get()), ], child: MaterialApp.router( + debugShowCheckedModeBanner: false, theme: theme.dark, darkTheme: theme.dark, routerConfig: RouterController(getIt.get()).router, diff --git a/lib/src/features/auth/views/auth_screen.dart b/lib/src/features/auth/views/auth_screen.dart index 057ce45..5a1508b 100644 --- a/lib/src/features/auth/views/auth_screen.dart +++ b/lib/src/features/auth/views/auth_screen.dart @@ -43,6 +43,7 @@ class _AuthScreenState extends State { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; return Scaffold( body: Padding( padding: context.responsiveContentPadding, @@ -77,7 +78,7 @@ class _AuthScreenState extends State { WidgetsFlutterBinding.ensureInitialized() .addPostFrameCallback( (_) { - context.goNamed(AppRoute.initProfileData.name); + context.goNamed(AppRoute.splash.name); }, ); } @@ -96,25 +97,52 @@ class _AuthScreenState extends State { }, ); } - return GenericButton( - onPressed: !enableButton - ? null - : () { - context.read().signIn( - email: _usernameController - .text.parseStringToEmail, - password: _passwordController.text, - ); - }, - width: context.widthPx, - widget: state.authStatus.isLoading - ? const CircularProgressIndicator.adaptive() - : Text( - 'SignIn', + return Column( + children: [ + GenericButton( + onPressed: !enableButton + ? null + : () { + context.read().signIn( + email: _usernameController + .text.parseStringToEmail, + password: _passwordController.text, + ); + }, + width: context.widthPx, + widget: state.authStatus.isLoading + ? const CircularProgressIndicator.adaptive() + : Text( + 'SignIn', + style: AppTextStyle() + .textButton + .copyWith(color: Colors.white), + ), + ), + const GutterSmall(), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Don't have an account?", style: AppTextStyle() - .textButton - .copyWith(color: Colors.white), + .body + .copyWith(color: colorScheme.onSurface), ), + TextButton( + onPressed: () { + context.goNamed(AppRoute.signUp.name); + }, + child: Text( + 'Create account', + style: AppTextStyle().body.copyWith( + color: colorScheme.primary, + ), + ), + ), + ], + ), + ], ); }, ), diff --git a/lib/src/features/auth/views/signup_screen.dart b/lib/src/features/auth/views/signup_screen.dart new file mode 100644 index 0000000..fd36497 --- /dev/null +++ b/lib/src/features/auth/views/signup_screen.dart @@ -0,0 +1,164 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gutter/flutter_gutter.dart'; +import 'package:go_router/go_router.dart'; +import 'package:my_app/src/core/extensions/string.dart'; +import 'package:my_app/src/core/ui/device.dart'; +import 'package:my_app/src/core/ui/typography.dart'; +import 'package:my_app/src/core/utils/widgets/generic_button.dart'; +import 'package:my_app/src/core/utils/widgets/generic_text_field.dart'; +import 'package:my_app/src/features/auth/cubit/auth_cubit.dart'; +import 'package:my_app/src/router/router.dart'; +import 'package:sized_context/sized_context.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class SignUpScreen extends StatefulWidget { + const SignUpScreen({super.key}); + + @override + State createState() => _SignUpScreenState(); +} + +class _SignUpScreenState extends State { + final TextEditingController _usernameController = TextEditingController(); + final TextEditingController _passwordController = TextEditingController(); + final TextEditingController _confirmPasswordController = + TextEditingController(); + + bool enableButton = false; + + @override + void initState() { + _usernameController.addListener(_updateButtonState); + _passwordController.addListener(_updateButtonState); + _confirmPasswordController.addListener(_updateButtonState); + super.initState(); + } + + void _updateButtonState() { + setState(() { + enableButton = _usernameController.text.isNotEmpty && + _passwordController.text.isNotEmpty && + _confirmPasswordController.text.isNotEmpty && + _passwordController.text == _confirmPasswordController.text; + }); + } + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return Scaffold( + body: Padding( + padding: context.responsiveContentPadding, + child: SizedBox( + width: context.widthPx, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'SignUp', + style: AppTextStyle().body.copyWith( + fontFamily: AppTextStyle.secondaryFontFamily, + fontSize: 30, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + const GutterLarge(), + GenericTextField( + labelText: 'Username', + controller: _usernameController, + ), + const GutterTiny(), + GenericTextField( + obscureText: true, + labelText: 'Password', + controller: _passwordController, + ), + const GutterTiny(), + GenericTextField( + obscureText: true, + labelText: 'Confirm Password', + controller: _confirmPasswordController, + ), + const Gutter(), + BlocBuilder( + builder: (context, state) { + if (state.authStatus.isAuthenticated) { + WidgetsFlutterBinding.ensureInitialized() + .addPostFrameCallback( + (_) { + context.goNamed(AppRoute.splash.name); + }, + ); + } + if (state.authStatus.isError) { + WidgetsFlutterBinding.ensureInitialized() + .addPostFrameCallback( + (_) { + context.read().refresh(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: + Text(state.errorMessage ?? 'An error occurred'), + backgroundColor: Colors.red, + ), + ); + }, + ); + } + return Column( + children: [ + GenericButton( + onPressed: !enableButton + ? null + : () { + context.read().signUp( + email: _usernameController + .text.parseStringToEmail, + password: _passwordController.text, + ); + }, + width: context.widthPx, + widget: state.authStatus.isLoading + ? const CircularProgressIndicator.adaptive() + : Text( + 'SignUp', + style: AppTextStyle() + .textButton + .copyWith(color: Colors.white), + ), + ), + const GutterSmall(), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Already have an account? ', + style: AppTextStyle() + .body + .copyWith(color: colorScheme.onSurface), + ), + TextButton( + onPressed: () { + context.goNamed(AppRoute.auth.name); + }, + child: Text( + 'SignIn', + style: AppTextStyle().body.copyWith( + color: + Theme.of(context).colorScheme.primary, + ), + ), + ), + ], + ), + ], + ); + }, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/src/features/ranking/widgets/rank_card.dart b/lib/src/features/ranking/widgets/rank_card.dart index 3163e9a..d739769 100644 --- a/lib/src/features/ranking/widgets/rank_card.dart +++ b/lib/src/features/ranking/widgets/rank_card.dart @@ -35,16 +35,23 @@ class RankCard extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 5), child: Row( children: [ - Container( - decoration: const BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - ), - child: Padding( - padding: const EdgeInsets.all(12), - child: Text( - ranking.position.toString(), - style: AppTextStyle().body.copyWith(color: Colors.black), + Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Container( + height: 40, + width: 40, + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + child: Padding( + padding: const EdgeInsets.all(2), + child: Center( + child: Text( + ranking.position.toString(), + style: AppTextStyle().body.copyWith(color: Colors.black), + ), + ), ), ), ), diff --git a/lib/src/features/splash/loading_profile_data.dart b/lib/src/features/splash/loading_profile_data.dart index 28e5dff..fff7036 100644 --- a/lib/src/features/splash/loading_profile_data.dart +++ b/lib/src/features/splash/loading_profile_data.dart @@ -26,7 +26,6 @@ class _LoadingProfileDataState extends State { body: BlocListener( listenWhen: (previous, current) => previous.status != current.status, listener: (context, state) { - if (state.status == PlayerStatus.success) { if (gameCubit.state.player == null) { context.read().setUserPlayer(state.player!); diff --git a/lib/src/router/router.dart b/lib/src/router/router.dart index 533dee0..3afa2b9 100644 --- a/lib/src/router/router.dart +++ b/lib/src/router/router.dart @@ -9,6 +9,7 @@ import 'package:go_router/go_router.dart'; import 'package:injectable/injectable.dart'; import 'package:my_app/src/core/utils/object_extensions.dart'; import 'package:my_app/src/features/auth/views/auth_screen.dart'; +import 'package:my_app/src/features/auth/views/signup_screen.dart'; import 'package:my_app/src/features/game/game_screen.dart'; import 'package:my_app/src/features/game/search_game_screen.dart'; import 'package:my_app/src/features/home/home_screen.dart'; @@ -18,8 +19,9 @@ import 'package:supabase_flutter/supabase_flutter.dart'; enum AppRoute { splash, - initProfileData, + // initProfileData, auth, + signUp, home, game, searchGame, @@ -59,6 +61,16 @@ final class RouterController { ); }, ), + GoRoute( + path: '/signUp', + name: AppRoute.signUp.name, + pageBuilder: (context, state) { + return adaptivePageRoute( + key: ValueKey(state.pageKey.value), + child: const SignUpScreen(), + ); + }, + ), GoRoute( path: '/home', name: AppRoute.home.name, @@ -91,16 +103,16 @@ final class RouterController { ), ], ), - GoRoute( - path: '/init-profile-data', - name: AppRoute.initProfileData.name, - pageBuilder: (context, state) { - return adaptivePageRoute( - key: ValueKey(state.pageKey.value), - child: const LoadingProfileData(), - ); - }, - ), + // GoRoute( + // path: '/init-profile-data', + // name: AppRoute.initProfileData.name, + // pageBuilder: (context, state) { + // return adaptivePageRoute( + // key: ValueKey(state.pageKey.value), + // child: const LoadingProfileData(), + // ); + // }, + // ), ], ); } From 8596f68fb7d6a8d198037daeb1e602552d13b90d Mon Sep 17 00:00:00 2001 From: ale24dev Date: Thu, 3 Oct 2024 02:34:07 -0400 Subject: [PATCH 06/30] feat/ improve new attempt edge function --- .../functions/create-new-attempt/index.ts | 80 ++++++++++++++++--- 1 file changed, 71 insertions(+), 9 deletions(-) diff --git a/supabase/functions/create-new-attempt/index.ts b/supabase/functions/create-new-attempt/index.ts index 0dc2dec..18e77fd 100644 --- a/supabase/functions/create-new-attempt/index.ts +++ b/supabase/functions/create-new-attempt/index.ts @@ -48,7 +48,7 @@ function countBullsAndCows(playerNumber: number[], opponentNumber: number[]) { // Función para verificar el game, player numbers, y actualizar el intento async function verifyAndInsertAttempt(supabase: any, gameId: number, playerId: number, p_number: number[]) { // Variables estáticas que representan las columnas en las consultas - const player = "id, username, avatar_url"; + const player = "id, username, avatar_url, is_bot"; const playerNumber = `id, player(${player}), number, time_left, is_turn, started_time`; const gameStatus = "id, status"; const gameColumns = ` @@ -87,20 +87,24 @@ async function verifyAndInsertAttempt(supabase: any, gameId: number, playerId: n bulls, // Número de toros cows, // Número de vacas }); - console.log("Opponent Player Number:", opponent); - console.log("Own Player Number:", ownPlayerNumber); // Intercambiar el turno de los jugadores + swapTurns(supabase, ownPlayerNumber, opponent, gameId); +} + +// Función para intercambiar turnos y verificar si el oponente es un bot +async function swapTurns(supabase: any, ownPlayerNumber: any, opponent: any, gameId: number) { + console.log("Starting turn swap..."); + if (ownPlayerNumber && opponent) { + console.log("Updating own player number..."); + // Asegúrate de que started_time sea un timestamp válido - console.log("ownPlayerNumber.started_time:", ownPlayerNumber.started_time); const startedTimeInMillis = new Date(ownPlayerNumber.started_time).getTime(); - console.log("startedTimeInMillis:", startedTimeInMillis); const elapsedTime = Math.floor((Date.now() - startedTimeInMillis) / 1000); // Convertir a segundos const newTimeLeft = Math.max(ownPlayerNumber.time_left - elapsedTime, 0); - console.log("Elapsed Time:", elapsedTime); - console.log("newTimeLeft:", newTimeLeft); + console.log(`Elapsed time: ${elapsedTime}, New time left for own player: ${newTimeLeft}`); const { error: updateTimeLeftOwnPlayerError } = await supabase .from("player_number") @@ -115,10 +119,13 @@ async function verifyAndInsertAttempt(supabase: any, gameId: number, playerId: n if (updateTimeLeftOwnPlayerError) { throw new Error(`Error updating own player time left: ${updateTimeLeftOwnPlayerError.message}`); } + console.log("Own player time left updated successfully."); } else { - console.error("started_time is not valid:", ownPlayerNumber.started_time); throw new Error("Invalid started_time value."); } + + console.log("Updating opponent player number..."); + const { error: updateTimeLeftOpponentError } = await supabase .from("player_number") .update({ @@ -131,7 +138,62 @@ async function verifyAndInsertAttempt(supabase: any, gameId: number, playerId: n if (updateTimeLeftOpponentError) { throw new Error(`Error updating opponent time left: ${updateTimeLeftOpponentError.message}`); } + console.log("Opponent player time left updated successfully."); + + // Verificar si el oponente es un bot + if (opponent.player.is_bot) { + console.log("Opponent is a bot, waiting for the bot's turn..."); + + // Insertar intento para el bot + const { error: botAttemptError } = await supabase.from("attempt").insert({ + player: opponent.player.id, + game: gameId, + number: [], // Número vacío + bulls: 0, // Bulls para el bot + cows: 0, // Cows para el bot + }); + + if (botAttemptError) { + throw new Error(`Error inserting bot attempt: ${botAttemptError.message}`); + } + console.log("Bot attempt inserted successfully."); + + // Esperar un tiempo aleatorio entre 10 y 30 segundos + const randomDelay = Math.floor(Math.random() * (30000 - 10000 + 1)) + 10000; // Entre 10 y 30 segundos + console.log(`Waiting for ${randomDelay / 1000} seconds for the bot's turn...`); + await new Promise((resolve) => setTimeout(resolve, randomDelay)); + + console.log("Reverting opponent turn..."); + const { error: revertTurnError } = await supabase + .from("player_number") + .update({ + is_turn: false, + started_time: null, + }) + .eq("id", opponent.id); + + if (revertTurnError) { + throw new Error(`Error reverting opponent turn: ${revertTurnError.message}`); + } + console.log("Opponent turn reverted successfully."); + + console.log("Updating own turn after bot's turn..."); + + const { error: revertOwnTurnError } = await supabase + .from("player_number") + .update({ + is_turn: true, + started_time: new Date().toISOString(), + }) + .eq("id", ownPlayerNumber.id); + + if (revertOwnTurnError) { + throw new Error(`Error reverting own turn: ${revertOwnTurnError.message}`); + } + console.log("Own turn updated successfully after bot's turn."); + } } + // Servidor Deno.serve(async (req) => { try { @@ -143,4 +205,4 @@ Deno.serve(async (req) => { } catch (err) { return new Response(String(err?.message ?? err), { status: 500 }); } -}); +}); \ No newline at end of file From 421ec3d222dd7fb47e280e7eb93e86561b145b8a Mon Sep 17 00:00:00 2001 From: ale24dev Date: Thu, 3 Oct 2024 02:37:37 -0400 Subject: [PATCH 07/30] fix: color name --- lib/src/features/game/game_screen.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/src/features/game/game_screen.dart b/lib/src/features/game/game_screen.dart index 34d94a4..7989077 100644 --- a/lib/src/features/game/game_screen.dart +++ b/lib/src/features/game/game_screen.dart @@ -144,12 +144,16 @@ class _GameScreenState extends State { Text.rich( TextSpan( text: 'The secret number was: ', - style: AppTextStyle().body, + style: AppTextStyle().body.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ), children: [ TextSpan( text: rival.number!.parseNumberListToString, style: AppTextStyle().body.copyWith( fontWeight: FontWeight.bold, + color: + Theme.of(context).colorScheme.onSurface, ), ), ], From 232fd0d474eb2aa54de1a0899e16c4814e89334d Mon Sep 17 00:00:00 2001 From: ale24dev Date: Thu, 3 Oct 2024 03:30:29 -0400 Subject: [PATCH 08/30] feat: update ranking to in live --- lib/src/core/services/ranking_datasource.dart | 4 ++ .../features/ranking/cubit/ranking_cubit.dart | 10 +++- .../ranking/data/ranking_repository.dart | 23 ++++++++++ lib/src/features/ranking/leaderboard.dart | 46 ++++++++++++------- .../features/ranking/mocks/ranking_mocks.dart | 18 ++++++++ pubspec.lock | 8 ++++ pubspec.yaml | 2 + 7 files changed, 92 insertions(+), 19 deletions(-) create mode 100644 lib/src/features/ranking/mocks/ranking_mocks.dart diff --git a/lib/src/core/services/ranking_datasource.dart b/lib/src/core/services/ranking_datasource.dart index f444aa1..b54c2ba 100644 --- a/lib/src/core/services/ranking_datasource.dart +++ b/lib/src/core/services/ranking_datasource.dart @@ -4,4 +4,8 @@ import 'package:my_app/src/features/ranking/data/model/ranking.dart'; abstract class RankingDatasource { Future?>> getRanking(); + + void listenRanking( + void Function() callback, + ); } diff --git a/lib/src/features/ranking/cubit/ranking_cubit.dart b/lib/src/features/ranking/cubit/ranking_cubit.dart index 7ff4ea7..6b701a2 100644 --- a/lib/src/features/ranking/cubit/ranking_cubit.dart +++ b/lib/src/features/ranking/cubit/ranking_cubit.dart @@ -9,13 +9,19 @@ part 'ranking_cubit.freezed.dart'; @injectable class RankingCubit extends Cubit { - RankingCubit(this._rankingRepository) : super(const RankingState()); + RankingCubit(this._rankingRepository) : super(const RankingState()) { + _listenRankingChanges(); + } final RankingRepository _rankingRepository; + void _listenRankingChanges() { + _rankingRepository.listenRanking(loadRanking); + } + void loadRanking() { emit(state.copyWith(status: RankingStateStatus.loading)); - + _rankingRepository.getRanking().then((result) { result.fold( (error) => emit(state.copyWith(status: RankingStateStatus.error)), diff --git a/lib/src/features/ranking/data/ranking_repository.dart b/lib/src/features/ranking/data/ranking_repository.dart index 6db2b97..c555600 100644 --- a/lib/src/features/ranking/data/ranking_repository.dart +++ b/lib/src/features/ranking/data/ranking_repository.dart @@ -1,3 +1,5 @@ +import 'dart:developer'; + import 'package:fpdart/fpdart.dart'; import 'package:injectable/injectable.dart'; import 'package:my_app/src/core/exceptions.dart'; @@ -25,4 +27,25 @@ class RankingRepository extends RankingDatasource { fromJsonParse: rankingsFromJson, ); } + + @override + void listenRanking( + void Function() callback, + ) { + final myChannel = _client.channel('ranking_channel'); + log('Ranking Database Changes Listen On'); + + myChannel + .onPostgresChanges( + event: PostgresChangeEvent.update, + schema: 'public', + table: 'ranking', + callback: (payload) { + log('Ranking Database change detected'); + + return callback(); + }, + ) + .subscribe(); + } } diff --git a/lib/src/features/ranking/leaderboard.dart b/lib/src/features/ranking/leaderboard.dart index 975365e..96e4240 100644 --- a/lib/src/features/ranking/leaderboard.dart +++ b/lib/src/features/ranking/leaderboard.dart @@ -3,9 +3,11 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gutter/flutter_gutter.dart'; import 'package:my_app/src/core/ui/typography.dart'; import 'package:my_app/src/features/ranking/cubit/ranking_cubit.dart'; +import 'package:my_app/src/features/ranking/mocks/ranking_mocks.dart'; import 'package:my_app/src/features/ranking/widgets/rank_card.dart'; import 'package:my_app/src/features/ranking/widgets/top_three_players.dart'; import 'package:sized_context/sized_context.dart'; +import 'package:skeletonizer/skeletonizer.dart'; class LeaderboardWidget extends StatefulWidget { const LeaderboardWidget({ @@ -20,14 +22,18 @@ class _LeaderboardWidgetState extends State { @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; - return BlocBuilder( - builder: (context, state) { - if (state.isLoading) { - return const Center(child: CircularProgressIndicator.adaptive()); - } + return BlocConsumer( + listener: (context, state) { if (state.isError) { - return const Center(child: Text('Error')); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Error'), + ), + ); } + }, + builder: (context, state) { + final rankings = state.isLoading ? rankingMock : state.ranking; return Column( children: [ const GutterLarge(), @@ -40,22 +46,28 @@ class _LeaderboardWidgetState extends State { ), ), const GutterSmall(), - TopThreePlayers(state.ranking), + Skeletonizer( + enabled: state.isLoading, + child: TopThreePlayers(rankings), + ), Expanded( child: Padding( padding: EdgeInsets.symmetric(horizontal: context.widthPx * .1), child: Stack( children: [ - ListView.builder( - itemCount: state.ranking.length, - itemBuilder: (context, index) { - final ranking = state.ranking[index]; - if (index <= 2) return const SizedBox.shrink(); - return RankCard( - colorScheme: colorScheme, - ranking: ranking, - ); - }, + Skeletonizer( + enabled: state.isLoading, + child: ListView.builder( + itemCount: rankings.length, + itemBuilder: (context, index) { + final ranking = rankings[index]; + if (index <= 2) return const SizedBox.shrink(); + return RankCard( + colorScheme: colorScheme, + ranking: ranking, + ); + }, + ), ), IgnorePointer( child: Opacity( diff --git a/lib/src/features/ranking/mocks/ranking_mocks.dart b/lib/src/features/ranking/mocks/ranking_mocks.dart new file mode 100644 index 0000000..d868e31 --- /dev/null +++ b/lib/src/features/ranking/mocks/ranking_mocks.dart @@ -0,0 +1,18 @@ +import 'package:my_app/src/features/player/data/model/player.dart'; +import 'package:my_app/src/features/ranking/data/model/ranking.dart'; + +final rankingMock = List.generate(20, (index) { + return Ranking( + id: index, + gamesLoss: index, + gamesWon: index, + minimumAttempts: index, + position: index, + player: Player( + id: index.toString(), + username: 'Player $index', + avatarUrl: 'https://example.com/avatar$index.png', + ), + points: 80, + ); +}); diff --git a/pubspec.lock b/pubspec.lock index 2f1a88a..c1db22e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -954,6 +954,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0+4" + skeletonizer: + dependency: "direct main" + description: + name: skeletonizer + sha256: "3b202e4fa9c49b017d368fb0e570d4952bcd19972b67b2face071bdd68abbfae" + url: "https://pub.dev" + source: hosted + version: "1.4.2" sky_engine: dependency: transitive description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 7367978..ad1892d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -26,6 +26,8 @@ dependencies: logger: ^2.3.0 go_router: ^14.2.3 + skeletonizer: ^1.4.2 + lottie: ^3.1.2 rxdart: ^0.28.0 From 923e0938d1f7639ec3c1eb9ed60c1f27fed5f4ef Mon Sep 17 00:00:00 2001 From: ale24dev Date: Thu, 3 Oct 2024 03:49:00 -0400 Subject: [PATCH 09/30] feat/ logout user confirmation --- .../home/widgets/search_game_section.dart | 134 +++++++++++------- 1 file changed, 85 insertions(+), 49 deletions(-) diff --git a/lib/src/features/home/widgets/search_game_section.dart b/lib/src/features/home/widgets/search_game_section.dart index f5c9428..8fbc6eb 100644 --- a/lib/src/features/home/widgets/search_game_section.dart +++ b/lib/src/features/home/widgets/search_game_section.dart @@ -1,3 +1,5 @@ +// ignore_for_file: inference_failure_on_function_invocation + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gutter/flutter_gutter.dart'; @@ -5,7 +7,6 @@ import 'package:go_router/go_router.dart'; import 'package:my_app/src/core/ui/theme.dart'; import 'package:my_app/src/core/ui/typography.dart'; import 'package:my_app/src/features/auth/cubit/auth_cubit.dart'; -import 'package:my_app/src/features/auth/views/auth_screen.dart'; import 'package:my_app/src/features/game/cubit/game_cubit.dart'; import 'package:my_app/src/router/router.dart'; import 'package:sized_context/sized_context.dart'; @@ -33,64 +34,99 @@ class _SearchGameSectionState extends State { (_) => context.goNamed(AppRoute.game.name), ); } - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: GestureDetector( - onTap: () => context.read().logout(), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.logout), - const GutterTiny(), - Text( - 'Exit', - style: AppTextStyle().body.copyWith( - color: Theme.of(context).colorScheme.onSurface, - ), - ), - ], + return SizedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: GestureDetector( + onTap: () => _showLogoutConfirmationDialog(context), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.logout), + const GutterTiny(), + Text( + 'Exit', + style: AppTextStyle().body.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ], + ), ), ), - ), - Expanded( - child: GestureDetector( - onTap: () { - context.goNamed(AppRoute.searchGame.name); - }, - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary, - borderRadius: BorderRadius.circular(5), - boxShadow: AppTheme.defaultShadow, - ), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 30, - vertical: 10, + Expanded( + child: GestureDetector( + onTap: () { + context.goNamed(AppRoute.searchGame.name); + }, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + borderRadius: BorderRadius.circular(5), + boxShadow: AppTheme.defaultShadow, ), - child: Row( - children: [ - const Icon(Icons.play_arrow, color: Colors.white), - const GutterTiny(), - Text( - 'Play', - style: AppTextStyle() - .textButton - .copyWith(color: Colors.white), - ), - ], + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.play_arrow, color: Colors.white), + const GutterTiny(), + Text( + 'Play', + style: AppTextStyle() + .textButton + .copyWith(color: Colors.white), + ), + ], + ), ), ), ), ), - ), - const Expanded(child: SizedBox.shrink()), - ], + const Expanded(child: SizedBox.shrink()), + ], + ), ); }, ), ); } + + void _showLogoutConfirmationDialog(BuildContext context) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Confirm Logout'), + content: const Text('Are you sure you want to logout?'), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text( + 'Cancel', + style: AppTextStyle().textButton.copyWith(color: Colors.white), + ), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(); + context.read().logout(); + }, + child: Text( + 'Logout', + style: AppTextStyle() + .textButton + .copyWith(color: Theme.of(context).colorScheme.primary), + ), + ), + ], + ); + }, + ); + } } From 7ab2ae6c5da989ace734c12c0b9957303ace60dc Mon Sep 17 00:00:00 2001 From: ale24dev Date: Thu, 3 Oct 2024 16:32:08 -0400 Subject: [PATCH 10/30] fix: create game when a game is finished --- lib/src/features/game/cubit/game_cubit.dart | 2 +- lib/src/features/game/cubit/game_state.dart | 2 +- lib/src/features/ranking/cubit/ranking_cubit.dart | 2 +- lib/src/features/ranking/leaderboard.dart | 6 +++++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/src/features/game/cubit/game_cubit.dart b/lib/src/features/game/cubit/game_cubit.dart index 74d82b7..9ffadf5 100644 --- a/lib/src/features/game/cubit/game_cubit.dart +++ b/lib/src/features/game/cubit/game_cubit.dart @@ -126,7 +126,7 @@ class GameCubit extends Cubit { } Future findOrCreateGame() async { - if (!state.isGameFinished) return; + if (!state.canCreateGame) return; emit( state.copyWith( stateStatus: GameStateStatus.searchingGame, diff --git a/lib/src/features/game/cubit/game_state.dart b/lib/src/features/game/cubit/game_state.dart index 671372a..72972b7 100644 --- a/lib/src/features/game/cubit/game_state.dart +++ b/lib/src/features/game/cubit/game_state.dart @@ -26,5 +26,5 @@ class GameState with _$GameState { game.isNotNull && game!.isInSelectingSecretsNumbers; bool get isGameInProgress => game.isNotNull && game!.isInProgress; bool get isGameStarted => isInSelectingSecretsNumbers || isGameInProgress; - bool get isGameFinished => game.isNotNull && game!.isFinished; + bool get canCreateGame => game.isNull || (game.isNotNull && game!.isFinished); } diff --git a/lib/src/features/ranking/cubit/ranking_cubit.dart b/lib/src/features/ranking/cubit/ranking_cubit.dart index 6b701a2..15ad0c4 100644 --- a/lib/src/features/ranking/cubit/ranking_cubit.dart +++ b/lib/src/features/ranking/cubit/ranking_cubit.dart @@ -21,7 +21,7 @@ class RankingCubit extends Cubit { void loadRanking() { emit(state.copyWith(status: RankingStateStatus.loading)); - + _rankingRepository.getRanking().then((result) { result.fold( (error) => emit(state.copyWith(status: RankingStateStatus.error)), diff --git a/lib/src/features/ranking/leaderboard.dart b/lib/src/features/ranking/leaderboard.dart index 96e4240..32cea73 100644 --- a/lib/src/features/ranking/leaderboard.dart +++ b/lib/src/features/ranking/leaderboard.dart @@ -78,10 +78,14 @@ class _LeaderboardWidgetState extends State { begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ - colorScheme.primary.withOpacity(.05), + colorScheme.primary, + colorScheme.primary.withOpacity(0), + colorScheme.primary.withOpacity(0), colorScheme.primary, ], stops: const [ + -5, + 0.2, 0.5, 1, ], From a8178b835c808b1a694a0a9185375daf5cfed828 Mon Sep 17 00:00:00 2001 From: ale24dev Date: Thu, 3 Oct 2024 16:38:56 -0400 Subject: [PATCH 11/30] fix: debounce time to ranking changes --- .../ranking/data/ranking_repository.dart | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/src/features/ranking/data/ranking_repository.dart b/lib/src/features/ranking/data/ranking_repository.dart index c555600..c76a1c3 100644 --- a/lib/src/features/ranking/data/ranking_repository.dart +++ b/lib/src/features/ranking/data/ranking_repository.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:developer'; import 'package:fpdart/fpdart.dart'; @@ -6,6 +7,7 @@ import 'package:my_app/src/core/exceptions.dart'; import 'package:my_app/src/core/interceptor.dart'; import 'package:my_app/src/core/services/ranking_datasource.dart'; import 'package:my_app/src/core/supabase/query_supabase.dart'; +import 'package:my_app/src/core/ui/extensions.dart'; import 'package:my_app/src/features/ranking/data/model/ranking.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; @@ -15,6 +17,8 @@ class RankingRepository extends RankingDatasource { final SupabaseServiceImpl _supabaseServiceImpl; final SupabaseClient _client; + Timer? _debounceTimer; + @override Future?>> getRanking() async { return _supabaseServiceImpl.query>( @@ -43,7 +47,19 @@ class RankingRepository extends RankingDatasource { callback: (payload) { log('Ranking Database change detected'); - return callback(); + // Si no hay debounce activo, ejecuta inmediatamente + if (_debounceTimer == null || !_debounceTimer!.isActive) { + callback(); // Ejecuta el callback inmediatamente + } + + // Cancelar el temporizador anterior si existe + _debounceTimer?.cancel(); + + // Iniciar un nuevo temporizador de debounce + _debounceTimer = Timer(5.seconds, () { + log('5 segundos de inactividad, reiniciando debounce'); + // Aquí puedes realizar acciones adicionales si es necesario. + }); }, ) .subscribe(); From d88076c0988a6f1456034399c573e68736c53ce9 Mon Sep 17 00:00:00 2001 From: ale24dev Date: Thu, 3 Oct 2024 16:40:26 -0400 Subject: [PATCH 12/30] update gitIgnore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 12e9467..9cc77ea 100644 --- a/.gitignore +++ b/.gitignore @@ -111,3 +111,6 @@ app.*.map.json !.idea/dictionaries/ !.idea/runConfigurations/ .env +scripts/fix_seed_sql.sh +scripts/run_dump.sh +Makefile From 4a3333a2261f58a861652dc25d9a7c82a1be7136 Mon Sep 17 00:00:00 2001 From: ale24dev Date: Fri, 4 Oct 2024 00:53:16 -0400 Subject: [PATCH 13/30] add: check current time --- lib/src/core/services/game_datasource.dart | 2 + lib/src/core/services/player_datasource.dart | 4 +- lib/src/core/supabase/query_supabase.dart | 2 +- lib/src/features/game/cubit/game_cubit.dart | 26 +++- .../game/cubit/game_cubit.freezed.dart | 36 ++++- lib/src/features/game/cubit/game_state.dart | 1 + .../features/game/data/game_repository.dart | 17 ++- lib/src/features/game/game_screen.dart | 2 +- .../features/home/widgets/game_section.dart | 131 ++++++++++-------- .../player/data/model/player_number.dart | 13 ++ .../player/data/model/player_number.g.dart | 8 ++ .../player/data/player_repository.dart | 14 +- .../player/domain/player_number_realtime.dart | 25 ++++ pubspec.lock | 8 ++ pubspec.yaml | 1 + .../functions/create-new-attempt/index.ts | 10 +- 16 files changed, 216 insertions(+), 84 deletions(-) create mode 100644 lib/src/features/player/domain/player_number_realtime.dart diff --git a/lib/src/core/services/game_datasource.dart b/lib/src/core/services/game_datasource.dart index e854d0f..3012daf 100644 --- a/lib/src/core/services/game_datasource.dart +++ b/lib/src/core/services/game_datasource.dart @@ -20,4 +20,6 @@ abstract class GameDataSource { ); Future> insertAttempt(Attempt attempt); + + Future> getServerTime(); } diff --git a/lib/src/core/services/player_datasource.dart b/lib/src/core/services/player_datasource.dart index 9226fd0..05fe295 100644 --- a/lib/src/core/services/player_datasource.dart +++ b/lib/src/core/services/player_datasource.dart @@ -2,7 +2,7 @@ import 'package:fpdart/fpdart.dart'; import 'package:my_app/src/core/exceptions.dart'; import 'package:my_app/src/features/player/data/model/player.dart'; import 'package:my_app/src/features/player/data/model/player_number.dart'; -import 'package:my_app/src/features/player/data/player_repository.dart'; +import 'package:my_app/src/features/player/domain/player_number_realtime.dart'; abstract class PlayerDatasource { Future> getPlayerById(String id); @@ -18,6 +18,6 @@ abstract class PlayerDatasource { void listenPlayerNumberChanges( String playerId, - void Function(PlayerNumberCallbackData) callback, + void Function(PlayerNumberRealtime) callback, ); } diff --git a/lib/src/core/supabase/query_supabase.dart b/lib/src/core/supabase/query_supabase.dart index 46d2f1f..a959b60 100644 --- a/lib/src/core/supabase/query_supabase.dart +++ b/lib/src/core/supabase/query_supabase.dart @@ -3,7 +3,7 @@ abstract class QuerySupabase { static String get player => 'id, username, avatar_url'; static String get playerNumber => - 'id, player($player), number, is_turn, time_left'; + 'id, player($player), number, is_turn, time_left, started_time, finish_time'; static String get game => ''' id, status!inner($gameStatus), player_number1!inner($playerNumber), player_number2!inner($playerNumber), winner($player) diff --git a/lib/src/features/game/cubit/game_cubit.dart b/lib/src/features/game/cubit/game_cubit.dart index 9ffadf5..7ebdafd 100644 --- a/lib/src/features/game/cubit/game_cubit.dart +++ b/lib/src/features/game/cubit/game_cubit.dart @@ -38,10 +38,13 @@ class GameCubit extends Cubit { void _listenPlayerNumberChanges() { _playerRepository.listenPlayerNumberChanges(state.player!.id, (callbackData) { - final (isTurn, timeLeft) = callbackData; - final ownPlayerNumber = state.game! - .getOwnPlayerNumber(state.player!) - .copyWith(isTurn: isTurn, timeLeft: timeLeft); + final ownPlayerNumber = + state.game!.getOwnPlayerNumber(state.player!).copyWith( + isTurn: callbackData.isTurn, + // timeLeft: timeLeft, + startedTime: callbackData.startedTime, + finishTime: callbackData.finishTime, + ); final game = state.game!.copyWith( playerNumber1: state.game!.playerNumber1!.id == ownPlayerNumber.id @@ -116,6 +119,8 @@ class GameCubit extends Cubit { return; } + await getServerTime(); + emit(state.copyWith(stateStatus: GameStateStatus.success, game: game)); await Future.delayed(Duration.zero, () { @@ -145,6 +150,19 @@ class GameCubit extends Cubit { }); } + Future getServerTime() async { + await _gameRepository.getServerTime().then((value) { + value.fold( + (error) => emit(state.copyWith(stateStatus: GameStateStatus.error)), + (time) { + emit( + state.copyWith(serverTime: time), + ); + }, + ); + }); + } + Future cancelSearchGame(Player player) async { emit(state.copyWith(stateStatus: GameStateStatus.loading)); final result = await _gameRepository.cancelSearchGame(player); diff --git a/lib/src/features/game/cubit/game_cubit.freezed.dart b/lib/src/features/game/cubit/game_cubit.freezed.dart index ff75b0a..6d7c92a 100644 --- a/lib/src/features/game/cubit/game_cubit.freezed.dart +++ b/lib/src/features/game/cubit/game_cubit.freezed.dart @@ -22,6 +22,7 @@ mixin _$GameState { List get listAttempts => throw _privateConstructorUsedError; bool get selectSecretNumberShowed => throw _privateConstructorUsedError; Player? get player => throw _privateConstructorUsedError; + DateTime? get serverTime => throw _privateConstructorUsedError; /// Create a copy of GameState /// with the given fields replaced by the non-null parameter values. @@ -41,7 +42,8 @@ abstract class $GameStateCopyWith<$Res> { List listGameStatus, List listAttempts, bool selectSecretNumberShowed, - Player? player}); + Player? player, + DateTime? serverTime}); } /// @nodoc @@ -65,6 +67,7 @@ class _$GameStateCopyWithImpl<$Res, $Val extends GameState> Object? listAttempts = null, Object? selectSecretNumberShowed = null, Object? player = freezed, + Object? serverTime = freezed, }) { return _then(_value.copyWith( game: freezed == game @@ -91,6 +94,10 @@ class _$GameStateCopyWithImpl<$Res, $Val extends GameState> ? _value.player : player // ignore: cast_nullable_to_non_nullable as Player?, + serverTime: freezed == serverTime + ? _value.serverTime + : serverTime // ignore: cast_nullable_to_non_nullable + as DateTime?, ) as $Val); } } @@ -109,7 +116,8 @@ abstract class _$$GameStateImplCopyWith<$Res> List listGameStatus, List listAttempts, bool selectSecretNumberShowed, - Player? player}); + Player? player, + DateTime? serverTime}); } /// @nodoc @@ -131,6 +139,7 @@ class __$$GameStateImplCopyWithImpl<$Res> Object? listAttempts = null, Object? selectSecretNumberShowed = null, Object? player = freezed, + Object? serverTime = freezed, }) { return _then(_$GameStateImpl( game: freezed == game @@ -157,6 +166,10 @@ class __$$GameStateImplCopyWithImpl<$Res> ? _value.player : player // ignore: cast_nullable_to_non_nullable as Player?, + serverTime: freezed == serverTime + ? _value.serverTime + : serverTime // ignore: cast_nullable_to_non_nullable + as DateTime?, )); } } @@ -170,7 +183,8 @@ class _$GameStateImpl extends _GameState { final List listGameStatus = const [], final List listAttempts = const [], this.selectSecretNumberShowed = false, - this.player}) + this.player, + this.serverTime}) : _listGameStatus = listGameStatus, _listAttempts = listAttempts, super._(); @@ -203,10 +217,12 @@ class _$GameStateImpl extends _GameState { final bool selectSecretNumberShowed; @override final Player? player; + @override + final DateTime? serverTime; @override String toString() { - return 'GameState(game: $game, stateStatus: $stateStatus, listGameStatus: $listGameStatus, listAttempts: $listAttempts, selectSecretNumberShowed: $selectSecretNumberShowed, player: $player)'; + return 'GameState(game: $game, stateStatus: $stateStatus, listGameStatus: $listGameStatus, listAttempts: $listAttempts, selectSecretNumberShowed: $selectSecretNumberShowed, player: $player, serverTime: $serverTime)'; } @override @@ -224,7 +240,9 @@ class _$GameStateImpl extends _GameState { (identical( other.selectSecretNumberShowed, selectSecretNumberShowed) || other.selectSecretNumberShowed == selectSecretNumberShowed) && - (identical(other.player, player) || other.player == player)); + (identical(other.player, player) || other.player == player) && + (identical(other.serverTime, serverTime) || + other.serverTime == serverTime)); } @override @@ -235,7 +253,8 @@ class _$GameStateImpl extends _GameState { const DeepCollectionEquality().hash(_listGameStatus), const DeepCollectionEquality().hash(_listAttempts), selectSecretNumberShowed, - player); + player, + serverTime); /// Create a copy of GameState /// with the given fields replaced by the non-null parameter values. @@ -253,7 +272,8 @@ abstract class _GameState extends GameState { final List listGameStatus, final List listAttempts, final bool selectSecretNumberShowed, - final Player? player}) = _$GameStateImpl; + final Player? player, + final DateTime? serverTime}) = _$GameStateImpl; const _GameState._() : super._(); @override @@ -268,6 +288,8 @@ abstract class _GameState extends GameState { bool get selectSecretNumberShowed; @override Player? get player; + @override + DateTime? get serverTime; /// Create a copy of GameState /// with the given fields replaced by the non-null parameter values. diff --git a/lib/src/features/game/cubit/game_state.dart b/lib/src/features/game/cubit/game_state.dart index 72972b7..ca75f1e 100644 --- a/lib/src/features/game/cubit/game_state.dart +++ b/lib/src/features/game/cubit/game_state.dart @@ -11,6 +11,7 @@ class GameState with _$GameState { @Default([]) final List listAttempts, @Default(false) final bool selectSecretNumberShowed, Player? player, + DateTime? serverTime, }) = _GameState; const GameState._(); diff --git a/lib/src/features/game/data/game_repository.dart b/lib/src/features/game/data/game_repository.dart index 1166ea8..0a5aaac 100644 --- a/lib/src/features/game/data/game_repository.dart +++ b/lib/src/features/game/data/game_repository.dart @@ -26,7 +26,8 @@ class GameRepository extends GameDataSource { table: 'RPC create_game', request: () => // _client.rpc('create_game', params: {'player_id': player.id}), - _client.rpc('find_or_create_game', params: {'p_player_id': player.id}), + _client + .rpc('find_or_create_game', params: {'p_player_id': player.id}), queryOption: QueryOption.insert, fromJsonParse: Game.fromJson, ); @@ -98,4 +99,18 @@ class GameRepository extends GameDataSource { queryOption: QueryOption.insert, ); } + + @override + Future> getServerTime() async { + final response = await _supabaseServiceImpl.query( + table: 'RPC get_server_time', + request: () => _client.rpc('get_server_time'), + queryOption: QueryOption.select, + ); + + return response.fold(left, (time) { + final dateTime = DateTime.parse(time!); + return right(dateTime); + }); + } } diff --git a/lib/src/features/game/game_screen.dart b/lib/src/features/game/game_screen.dart index 7989077..279d4b0 100644 --- a/lib/src/features/game/game_screen.dart +++ b/lib/src/features/game/game_screen.dart @@ -44,7 +44,7 @@ class _GameScreenState extends State { return Column( children: [ VersusSection(game: state.game!), - Expanded(child: GameSection(game: state.game!)), + Expanded(child: GameSection(gameState: state)), ], ); }, diff --git a/lib/src/features/home/widgets/game_section.dart b/lib/src/features/home/widgets/game_section.dart index af39b80..d0383f0 100644 --- a/lib/src/features/home/widgets/game_section.dart +++ b/lib/src/features/home/widgets/game_section.dart @@ -1,9 +1,9 @@ import 'dart:async'; +import 'dart:developer'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:my_app/src/core/extensions/string.dart'; -import 'package:my_app/src/core/utils/object_extensions.dart'; -import 'package:my_app/src/features/game/game_screen.dart'; import 'package:my_app/src/features/player/cubit/player_cubit.dart'; import 'package:my_app/src/features/player/data/model/player.dart'; import 'package:my_app/src/features/game/data/model/game.dart'; @@ -17,66 +17,52 @@ import 'package:my_app/src/features/game/data/model/attempt.dart'; import 'package:flutter_gutter/flutter_gutter.dart'; import 'package:my_app/src/core/utils/utils.dart'; -class GameSection extends StatefulWidget { - const GameSection({required this.game, super.key}); +class GameSection extends HookWidget { + const GameSection({required this.gameState, super.key}); - final Game game; + final GameState gameState; @override - State createState() => _GameSectionState(); -} - -class _GameSectionState extends State { - late Player player; - late Player rival; - late int timeLeft; - Timer? timer; + Widget build(BuildContext context) { + final player = context.read().state.player!; + final game = gameState.game!; + final rival = game.getRivalPlayerNumber(player).player; + final ownPlayerNumber = game.getOwnPlayerNumber(player); + + final initialTimeLeft = useState( + calculateInitialTimeLeft( + ownPlayerNumber.startedTime, + ownPlayerNumber.finishTime, + ), + ); - @override - void initState() { - super.initState(); - player = context.read().state.player!; - rival = widget.game.getRivalPlayerNumber(player).player; - final ownPlayerNumber = widget.game.getOwnPlayerNumber(player); - timeLeft = ownPlayerNumber.timeLeft; - if (ownPlayerNumber.isTurn) { - startTimer(rival); - } - } + final isFirstRender = useState(true); - void startTimer(Player playerWinner) { - timer = Timer.periodic(const Duration(seconds: 1), (timer) { - setState(() { - if (timeLeft > 0) { - timeLeft--; - } else { - timer.cancel(); + useEffect( + () { + Timer? timer; + if (ownPlayerNumber.isTurn) { + if (isFirstRender.value) { + initialTimeLeft.value = calculateInitialTimeLeft( + ownPlayerNumber.startedTime, + ownPlayerNumber.finishTime, + ); + isFirstRender.value = false; + } + timer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (initialTimeLeft.value > 0) { + initialTimeLeft.value--; + } else { + timer.cancel(); + } + }); } - }); - }); - } - - void stopTimer() { - timer?.cancel(); - timer = null; - } - - @override - void dispose() { - stopTimer(); - super.dispose(); - } + return timer?.cancel; + }, + [ownPlayerNumber.isTurn], + ); - @override - Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; - final ownPlayerNumber = widget.game.getOwnPlayerNumber(player); - - if (ownPlayerNumber.isTurn && timer == null) { - startTimer(rival); - } else if (!ownPlayerNumber.isTurn && timer != null) { - stopTimer(); - } return Padding( padding: context.responsiveContentPadding, @@ -94,7 +80,7 @@ class _GameSectionState extends State { ), ), Text( - '${(timeLeft ~/ 60).toString().padLeft(2, '0')}:${(timeLeft % 60).toString().padLeft(2, '0')}', + '${(initialTimeLeft.value ~/ 60).toString().padLeft(2, '0')}:${(initialTimeLeft.value % 60).toString().padLeft(2, '0')}', style: AppTextStyle().body.copyWith( color: colorScheme.onSurface, ), @@ -104,7 +90,7 @@ class _GameSectionState extends State { const Expanded(child: _PlayList()), const Gutter(), _SendNumberSection( - game: widget.game, + game: game, canSendNumber: ownPlayerNumber.isTurn, ), const GutterLarge(), @@ -112,6 +98,36 @@ class _GameSectionState extends State { ), ); } + + int calculateInitialTimeLeft(DateTime? startedTime, DateTime? finishedTime) { + // Retorna 0 si no hay tiempo para calcular + if (startedTime == null || finishedTime == null) { + return 0; + } + + // Obtén el tiempo actual del servidor + final currentTime = gameState.serverTime ?? + DateTime.now(); // Usa el servidor, o la hora local como respaldo + + // Asegúrate de que ambas horas están en UTC + final currentTimeUtc = currentTime.toUtc(); + final finishedTimeUtc = finishedTime.toUtc(); + + // Lógica para verificar si el tiempo actual ha pasado el tiempo final + if (currentTimeUtc.isAfter(finishedTimeUtc)) { + return 0; // Retorna 0 si el tiempo ha pasado + } + + log('finishedTime: $finishedTimeUtc'); + log('currentTime: $currentTimeUtc'); + + // Calcula la diferencia en segundos + final timeLeft = finishedTimeUtc.difference(currentTimeUtc).inSeconds; + + log('Initial time left: $timeLeft seconds'); + + return timeLeft; + } } class _PlayList extends StatelessWidget { @@ -169,7 +185,6 @@ class _SendNumberSection extends StatefulWidget { const _SendNumberSection({required this.game, required this.canSendNumber}); final Game game; - final bool canSendNumber; @override @@ -209,7 +224,7 @@ class __SendNumberSectionState extends State<_SendNumberSection> { if (!isValidNumber) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( - content: Text('Introduce a valid number'), + content: Text('Introduce un número válido'), backgroundColor: Colors.red, ), ); diff --git a/lib/src/features/player/data/model/player_number.dart b/lib/src/features/player/data/model/player_number.dart index e30331d..e6ff41c 100644 --- a/lib/src/features/player/data/model/player_number.dart +++ b/lib/src/features/player/data/model/player_number.dart @@ -12,12 +12,15 @@ class PlayerNumber extends Equatable with TableInterface { required this.player, required this.isTurn, required this.timeLeft, + this.startedTime, + this.finishTime, this.number, }); factory PlayerNumber.empty() => PlayerNumber( id: 0, player: Player.empty(), + startedTime: DateTime.now(), number: const [], isTurn: false, timeLeft: 0, @@ -29,6 +32,12 @@ class PlayerNumber extends Equatable with TableInterface { final Player player; final bool isTurn; final int timeLeft; + + /// Started time of the turn + final DateTime? startedTime; + + /// Finished time of the turn + final DateTime? finishTime; final List? number; @override @@ -52,6 +61,8 @@ extension PlayerNumberX on PlayerNumber { List? number, bool? isTurn, int? timeLeft, + DateTime? startedTime, + DateTime? finishTime, }) { return PlayerNumber( id: id ?? this.id, @@ -59,6 +70,8 @@ extension PlayerNumberX on PlayerNumber { number: number ?? this.number, isTurn: isTurn ?? this.isTurn, timeLeft: timeLeft ?? this.timeLeft, + startedTime: startedTime ?? this.startedTime, + finishTime: finishTime ?? this.finishTime, ); } diff --git a/lib/src/features/player/data/model/player_number.g.dart b/lib/src/features/player/data/model/player_number.g.dart index eaefd0e..96cee9b 100644 --- a/lib/src/features/player/data/model/player_number.g.dart +++ b/lib/src/features/player/data/model/player_number.g.dart @@ -11,6 +11,12 @@ PlayerNumber _$PlayerNumberFromJson(Map json) => PlayerNumber( player: Player.fromJson(json['player'] as Map), isTurn: json['is_turn'] as bool, timeLeft: (json['time_left'] as num).toInt(), + startedTime: json['started_time'] == null + ? null + : DateTime.parse(json['started_time'] as String), + finishTime: json['finish_time'] == null + ? null + : DateTime.parse(json['finish_time'] as String), number: (json['number'] as List?) ?.map((e) => (e as num).toInt()) .toList(), @@ -22,5 +28,7 @@ Map _$PlayerNumberToJson(PlayerNumber instance) => 'player': instance.player.toJson(), 'is_turn': instance.isTurn, 'time_left': instance.timeLeft, + 'started_time': instance.startedTime?.toIso8601String(), + 'finish_time': instance.finishTime?.toIso8601String(), 'number': instance.number, }; diff --git a/lib/src/features/player/data/player_repository.dart b/lib/src/features/player/data/player_repository.dart index 176491f..e317490 100644 --- a/lib/src/features/player/data/player_repository.dart +++ b/lib/src/features/player/data/player_repository.dart @@ -10,9 +10,14 @@ import 'package:my_app/src/core/interceptor.dart'; import 'package:my_app/src/core/services/player_datasource.dart'; import 'package:my_app/src/features/player/data/model/player.dart'; import 'package:my_app/src/features/player/data/model/player_number.dart'; +import 'package:my_app/src/features/player/domain/player_number_realtime.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; -typedef PlayerNumberCallbackData = (bool isTurn, int timeLeft); +typedef PlayerNumberCallbackData = ( + bool isTurn, + int timeLeft, + DateTime? startedTime +); @singleton class PlayerRepository extends PlayerDatasource { @@ -24,7 +29,7 @@ class PlayerRepository extends PlayerDatasource { @override void listenPlayerNumberChanges( String playerId, - void Function(PlayerNumberCallbackData) callback, + void Function(PlayerNumberRealtime) callback, ) { final myChannel = _client.channel('player_number_channel'); log('Game Database Changes Listen On'); @@ -39,9 +44,8 @@ class PlayerRepository extends PlayerDatasource { if (playerId != payload.newRecord['player']) return; log(payload.toString()); - final isTurn = payload.newRecord['is_turn'] as bool; - final timeLeft = payload.newRecord['time_left'] as int; - return callback((isTurn, timeLeft)); + + return callback(PlayerNumberRealtime.fromJson(payload.newRecord)); }, ) .subscribe(); diff --git a/lib/src/features/player/domain/player_number_realtime.dart b/lib/src/features/player/domain/player_number_realtime.dart new file mode 100644 index 0000000..654a438 --- /dev/null +++ b/lib/src/features/player/domain/player_number_realtime.dart @@ -0,0 +1,25 @@ +class PlayerNumberRealtime { + PlayerNumberRealtime({ + required this.isTurn, + // required this.timeLeft, + required this.startedTime, + required this.finishTime, + }); + + factory PlayerNumberRealtime.fromJson(Map json) { + return PlayerNumberRealtime( + isTurn: json['is_turn'] as bool, + startedTime: json['started_time'] == null + ? DateTime.now() + : DateTime.parse(json['started_time'] as String), + finishTime: json['finish_time'] == null + ? DateTime.now() + : DateTime.parse(json['finish_time'] as String), + ); + } + + final bool isTurn; + // final int timeLeft; + final DateTime startedTime; + final DateTime finishTime; +} diff --git a/pubspec.lock b/pubspec.lock index c1db22e..843f2eb 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -387,6 +387,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + flutter_hooks: + dependency: "direct main" + description: + name: flutter_hooks + sha256: cde36b12f7188c85286fba9b38cc5a902e7279f36dd676967106c041dc9dde70 + url: "https://pub.dev" + source: hosted + version: "0.20.5" flutter_localizations: dependency: "direct main" description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index ad1892d..1262292 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -25,6 +25,7 @@ dependencies: intl: ^0.19.0 logger: ^2.3.0 go_router: ^14.2.3 + flutter_hooks: ^0.20.5 skeletonizer: ^1.4.2 diff --git a/supabase/functions/create-new-attempt/index.ts b/supabase/functions/create-new-attempt/index.ts index 18e77fd..d84ae7b 100644 --- a/supabase/functions/create-new-attempt/index.ts +++ b/supabase/functions/create-new-attempt/index.ts @@ -110,8 +110,8 @@ async function swapTurns(supabase: any, ownPlayerNumber: any, opponent: any, gam .from("player_number") .update({ is_turn: false, - started_time: null, - time_left: newTimeLeft, // Asegurarse de que no sea null + started_time: new Date().toISOString(), + // time_left: newTimeLeft, // Asegurarse de que no sea null }) .eq("id", ownPlayerNumber.id); @@ -130,7 +130,7 @@ async function swapTurns(supabase: any, ownPlayerNumber: any, opponent: any, gam .from("player_number") .update({ is_turn: true, - started_time: new Date().toISOString(), + // started_time: new Date().toISOString(), }) .eq("id", opponent.id); @@ -168,7 +168,7 @@ async function swapTurns(supabase: any, ownPlayerNumber: any, opponent: any, gam .from("player_number") .update({ is_turn: false, - started_time: null, + started_time: new Date().toISOString(), }) .eq("id", opponent.id); @@ -183,7 +183,7 @@ async function swapTurns(supabase: any, ownPlayerNumber: any, opponent: any, gam .from("player_number") .update({ is_turn: true, - started_time: new Date().toISOString(), + // started_time: new Date().toISOString(), }) .eq("id", ownPlayerNumber.id); From 1522f31b85d832b1ea5bf8585ca2a173067e2e59 Mon Sep 17 00:00:00 2001 From: ale24dev Date: Fri, 4 Oct 2024 01:27:44 -0400 Subject: [PATCH 14/30] add: surrender game --- lib/src/core/services/game_datasource.dart | 5 +++++ lib/src/features/game/cubit/game_cubit.dart | 15 +++++++++++++++ lib/src/features/game/data/game_repository.dart | 12 ++++++++++++ lib/src/features/home/widgets/versus_section.dart | 4 ++-- 4 files changed, 34 insertions(+), 2 deletions(-) diff --git a/lib/src/core/services/game_datasource.dart b/lib/src/core/services/game_datasource.dart index 3012daf..708df11 100644 --- a/lib/src/core/services/game_datasource.dart +++ b/lib/src/core/services/game_datasource.dart @@ -22,4 +22,9 @@ abstract class GameDataSource { Future> insertAttempt(Attempt attempt); Future> getServerTime(); + + Future> surrenderGame( + Game game, + Player player, + ); } diff --git a/lib/src/features/game/cubit/game_cubit.dart b/lib/src/features/game/cubit/game_cubit.dart index 7ebdafd..4a477ed 100644 --- a/lib/src/features/game/cubit/game_cubit.dart +++ b/lib/src/features/game/cubit/game_cubit.dart @@ -150,6 +150,21 @@ class GameCubit extends Cubit { }); } + Future surrender() async { + await _gameRepository + .surrenderGame(state.game!, state.player!) + .then((value) { + value.fold( + (error) => emit(state.copyWith(stateStatus: GameStateStatus.error)), + (_) { + emit( + state.copyWith(stateStatus: GameStateStatus.success), + ); + }, + ); + }); + } + Future getServerTime() async { await _gameRepository.getServerTime().then((value) { value.fold( diff --git a/lib/src/features/game/data/game_repository.dart b/lib/src/features/game/data/game_repository.dart index 0a5aaac..616c05b 100644 --- a/lib/src/features/game/data/game_repository.dart +++ b/lib/src/features/game/data/game_repository.dart @@ -113,4 +113,16 @@ class GameRepository extends GameDataSource { return right(dateTime); }); } + + @override + Future> surrenderGame(Game game, Player player) { + return _supabaseServiceImpl.query( + table: 'RPC surrender_game', + request: () => _client.rpc( + 'surrender_game', + params: {'game_id': game.id, 'surrendering_player_id': player.id}, + ), + queryOption: QueryOption.select, + ); + } } diff --git a/lib/src/features/home/widgets/versus_section.dart b/lib/src/features/home/widgets/versus_section.dart index 9c6321e..3631cc5 100644 --- a/lib/src/features/home/widgets/versus_section.dart +++ b/lib/src/features/home/widgets/versus_section.dart @@ -88,7 +88,7 @@ class VersusSection extends StatelessWidget { builder: (context) { return AlertDialog.adaptive( title: const Text('Are you sure?'), - content: const Text('Do you want to exit the game?'), + content: const Text('Do you want to surrender the game?'), actions: [ TextButton( onPressed: () { @@ -98,7 +98,7 @@ class VersusSection extends StatelessWidget { ), TextButton( onPressed: () { - context.read().refresh(); + context.read().surrender(); Navigator.of(context).pop(); }, child: const Text('Yes'), From fdae861a756c8cb095ddc25133d4184bb52bb551 Mon Sep 17 00:00:00 2001 From: ale24dev Date: Fri, 4 Oct 2024 01:53:45 -0400 Subject: [PATCH 15/30] show: shimmer loading on attempts --- .../game/domain/mocks/attempt_mock.dart | 17 +++++++ .../features/home/widgets/game_section.dart | 48 ++++++++++--------- 2 files changed, 43 insertions(+), 22 deletions(-) create mode 100644 lib/src/features/game/domain/mocks/attempt_mock.dart diff --git a/lib/src/features/game/domain/mocks/attempt_mock.dart b/lib/src/features/game/domain/mocks/attempt_mock.dart new file mode 100644 index 0000000..dcbd474 --- /dev/null +++ b/lib/src/features/game/domain/mocks/attempt_mock.dart @@ -0,0 +1,17 @@ +import 'package:my_app/src/features/game/data/model/attempt.dart'; +import 'package:my_app/src/features/game/data/model/game.dart'; +import 'package:my_app/src/features/game/data/model/game_status.dart'; +import 'package:my_app/src/features/player/data/model/player.dart'; + +List getAttemptsMock(int quantity) { + return List.generate(quantity, (index) { + return Attempt( + id: 0, + game: Game(id: 0, status: GameStatus.empty()), + bulls: 0, + cows: 0, + number: [1, 2, 3, 4], + player: Player.empty(), + ); + }); +} diff --git a/lib/src/features/home/widgets/game_section.dart b/lib/src/features/home/widgets/game_section.dart index d0383f0..7355f4c 100644 --- a/lib/src/features/home/widgets/game_section.dart +++ b/lib/src/features/home/widgets/game_section.dart @@ -4,8 +4,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:my_app/src/core/extensions/string.dart'; +import 'package:my_app/src/features/game/domain/mocks/attempt_mock.dart'; import 'package:my_app/src/features/player/cubit/player_cubit.dart'; -import 'package:my_app/src/features/player/data/model/player.dart'; import 'package:my_app/src/features/game/data/model/game.dart'; import 'package:my_app/src/features/game/widgets/game_turn_widget.dart'; import 'package:my_app/src/core/ui/typography.dart'; @@ -16,6 +16,7 @@ import 'package:my_app/src/features/game/cubit/game_cubit.dart'; import 'package:my_app/src/features/game/data/model/attempt.dart'; import 'package:flutter_gutter/flutter_gutter.dart'; import 'package:my_app/src/core/utils/utils.dart'; +import 'package:skeletonizer/skeletonizer.dart'; class GameSection extends HookWidget { const GameSection({required this.gameState, super.key}); @@ -26,7 +27,7 @@ class GameSection extends HookWidget { Widget build(BuildContext context) { final player = context.read().state.player!; final game = gameState.game!; - final rival = game.getRivalPlayerNumber(player).player; + // final rival = game.getRivalPlayerNumber(player).player; final ownPlayerNumber = game.getOwnPlayerNumber(player); final initialTimeLeft = useState( @@ -149,30 +150,33 @@ class _PlayList extends StatelessWidget { const GutterSmall(), BlocBuilder( builder: (context, state) { - if (state.isLoading) { - return const Center(child: CircularProgressIndicator()); - } - if (state.isError) { // log('Error'); } - return Column( - children: state.listAttempts.isEmpty - ? [ - Center( - child: Text( - 'No attempts', - style: AppTextStyle().body.copyWith( - color: colorScheme.onSurface, - ), + + final attempts = state.isLoading + ? getAttemptsMock(state.listAttempts.length + 1) + : state.listAttempts; + return Skeletonizer( + enabled: state.isLoading, + child: Column( + children: attempts.isEmpty + ? [ + Center( + child: Text( + 'No attempts', + style: AppTextStyle().body.copyWith( + color: colorScheme.onSurface, + ), + ), ), - ), - ] - : state.listAttempts.asMap().entries.map((entry) { - final index = state.listAttempts.length - entry.key; - final value = entry.value; - return PlayNumberCard(attempt: value, index: index); - }).toList(), + ] + : attempts.asMap().entries.map((entry) { + final index = attempts.length - entry.key; + final value = entry.value; + return PlayNumberCard(attempt: value, index: index); + }).toList(), + ), ); }, ), From 7bd9396c3a4cc600870a4b2da40b609f7de76346 Mon Sep 17 00:00:00 2001 From: ale24dev Date: Fri, 4 Oct 2024 03:05:11 -0400 Subject: [PATCH 16/30] reduce attempts to win on bot --- supabase/functions/create-new-attempt/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/supabase/functions/create-new-attempt/index.ts b/supabase/functions/create-new-attempt/index.ts index d84ae7b..2bf4a51 100644 --- a/supabase/functions/create-new-attempt/index.ts +++ b/supabase/functions/create-new-attempt/index.ts @@ -49,7 +49,7 @@ function countBullsAndCows(playerNumber: number[], opponentNumber: number[]) { async function verifyAndInsertAttempt(supabase: any, gameId: number, playerId: number, p_number: number[]) { // Variables estáticas que representan las columnas en las consultas const player = "id, username, avatar_url, is_bot"; - const playerNumber = `id, player(${player}), number, time_left, is_turn, started_time`; + const playerNumber = `id, player(${player}), number, time_left, is_turn, started_time, attempts_to_win`; const gameStatus = "id, status"; const gameColumns = ` id, status!inner(${gameStatus}), player_number1!inner(${playerNumber}), player_number2!inner(${playerNumber}), winner(${player}) @@ -169,6 +169,7 @@ async function swapTurns(supabase: any, ownPlayerNumber: any, opponent: any, gam .update({ is_turn: false, started_time: new Date().toISOString(), + attempts_to_win: opponent.attempts_to_win - 1 }) .eq("id", opponent.id); From ce3cef568c6d8288eac2125c05e02775e85d07da Mon Sep 17 00:00:00 2001 From: ale24dev Date: Fri, 4 Oct 2024 03:05:30 -0400 Subject: [PATCH 17/30] fix: constraints on elements --- lib/src/features/home/widgets/play_number_card.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/src/features/home/widgets/play_number_card.dart b/lib/src/features/home/widgets/play_number_card.dart index f59b2f1..5d1611d 100644 --- a/lib/src/features/home/widgets/play_number_card.dart +++ b/lib/src/features/home/widgets/play_number_card.dart @@ -18,12 +18,13 @@ class PlayNumberCard extends StatelessWidget { child: Row( children: [ Container( + height: 30, + width: 30, decoration: BoxDecoration( color: colorScheme.primary, shape: BoxShape.circle, ), - child: Padding( - padding: const EdgeInsets.all(10), + child: Center( child: Text( index.toString(), style: AppTextStyle().body.copyWith(color: Colors.black), @@ -72,14 +73,13 @@ class _NumberPlay extends StatelessWidget { return Padding( padding: const EdgeInsets.symmetric(horizontal: 5), child: Container( + height: 45, + width: 30, decoration: BoxDecoration( border: Border.all(color: Theme.of(context).colorScheme.primary), borderRadius: BorderRadius.circular(5), ), - child: Padding( - padding: const EdgeInsets.all(8), - child: Text(number), - ), + child: Center(child: Text(number)), ), ); } From 915148eaf38e1a0d1228054496b5f7ec39d83303 Mon Sep 17 00:00:00 2001 From: ale24dev Date: Fri, 4 Oct 2024 03:12:50 -0400 Subject: [PATCH 18/30] set focus to the first field after send an attempt --- lib/src/features/home/widgets/otp_fields.dart | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/src/features/home/widgets/otp_fields.dart b/lib/src/features/home/widgets/otp_fields.dart index 8b41bf5..322f19c 100644 --- a/lib/src/features/home/widgets/otp_fields.dart +++ b/lib/src/features/home/widgets/otp_fields.dart @@ -68,11 +68,6 @@ class OTPFieldsState extends State { widget.onChanged(otp); } - // bool _hasRepetitions(List values) { - // final uniqueValues = values.where((e) => e.isNotEmpty).toSet(); - // return uniqueValues.length != values.where((e) => e.isNotEmpty).length; - // } - void clearFields() { setState(() { for (var i = 0; i < widget.length; i++) { @@ -81,6 +76,8 @@ class OTPFieldsState extends State { isFilled[i] = false; } }); + // Llevar el foco al primer campo de texto + FocusScope.of(context).requestFocus(focusNodes[0]); } @override From 78999818cfa21c0f9764522b54937e4ae822b888 Mon Sep 17 00:00:00 2001 From: ale24dev Date: Fri, 4 Oct 2024 22:48:05 -0400 Subject: [PATCH 19/30] add: internationalization app --- lib/l10n/arb/app_en.arb | 72 +++++++++++++++++-- lib/l10n/arb/app_es.arb | 72 +++++++++++++++++-- lib/src/features/auth/views/auth_screen.dart | 17 +++-- .../features/auth/views/signup_screen.dart | 21 +++--- lib/src/features/game/game_screen.dart | 15 ++-- lib/src/features/game/search_game_screen.dart | 5 +- .../game/widgets/game_turn_widget.dart | 5 +- .../features/home/widgets/game_section.dart | 11 +-- .../home/widgets/play_number_card.dart | 5 +- .../home/widgets/search_game_section.dart | 13 ++-- .../home/widgets/select_secret_number.dart | 14 ++-- .../home/widgets/user_header_info.dart | 3 +- .../features/home/widgets/versus_section.dart | 11 +-- lib/src/features/ranking/leaderboard.dart | 7 +- .../features/splash/loading_profile_data.dart | 3 +- lib/src/features/splash/splash_screen.dart | 7 +- 16 files changed, 210 insertions(+), 71 deletions(-) diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index a5484a0..5c713b1 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1,7 +1,67 @@ { - "@@locale": "en", - "counterAppBarTitle": "Counter", - "@counterAppBarTitle": { - "description": "Text shown in the AppBar of the Counter Page" - } -} \ No newline at end of file + "signUp": "Sign Up", + "username": "Username", + "password": "Password", + "confirmPassword": "Confirm Password", + "alreadyHaveAccount": "Already have an account?", + "dontHaveAccount": "Don't have an account?", + "createAccount": "Create an account", + "signIn": "Sign In", + "waitingForRival": "Waiting for rival", + "waitingForRivalDescription": "Please wait for your rival to add the secret number", + "congratulations": "Congratulations!!!", + "youLose": "You Lose", + "theSecretNumberWas": "The secret number was: ", + "accept": "Accept", + "pleaseWait": "Please wait...", + "error": "Error", + "retry": "Retry", + "email": "Email", + "signUpSuccess": "Signed up successfully!", + "signUpError": "Error signing up. Please try again.", + "anErrorOccurred": "An error occurred", + + "searchingForPlayers": "Searching for players", + "cancel": "Cancel", + + "yourTurn": "Your turn", + "rivalTurn": "Rival turn", + + "timeLeft": "Time left: ", + "previousAttempts": "Previous attempts", + "noAttempts": "No attempts", + "introduceValidNumber": "Introduce un número válido", + + "cow": "Cow", + "cowPlay": "C", + "bull": "Bull", + "bullPlay": "B", + + "exit": "Exit", + "play": "Play", + "confirmLogout": "Confirm Logout", + "logoutConfirmation": "Are you sure you want to logout?", + "logout": "Logout", + + "timeRemaining": "Time remaining: {time}", + "@timeRemaining": { + "placeholders": { + "time": { + "type": "int" + } + } + }, + "selectSecretNumber": "Select your secret number", + "note": "Note: If you don't select a number in the time you will lose the game", + "send": "Send", + "validNumber": "Introduce a valid number", + "rank": "Rank", + + "areYouSure": "Are you sure?", + "doYouWantToSurrender": "Do you want to surrender?", + "no": "No", + "yes": "Yes", + + "leaderboard": "Leaderboard", + "loading": "Loading..." + } \ No newline at end of file diff --git a/lib/l10n/arb/app_es.arb b/lib/l10n/arb/app_es.arb index f1405f0..f0f44f4 100644 --- a/lib/l10n/arb/app_es.arb +++ b/lib/l10n/arb/app_es.arb @@ -1,7 +1,67 @@ { - "@@locale": "es", - "counterAppBarTitle": "Contador", - "@counterAppBarTitle": { - "description": "Texto mostrado en la AppBar de la página del contador" - } -} \ No newline at end of file + "signUp": "Registrarse", + "username": "Nombre de usuario", + "password": "Contraseña", + "confirmPassword": "Confirmar contraseña", + "alreadyHaveAccount": "¿Ya tienes una cuenta?", + "dontHaveAccount": "¿No tienes una cuenta?", + "createAccount": "Crear una cuenta", + "signIn": "Iniciar sesión", + "waitingForRival": "Esperando al rival", + "waitingForRivalDescription": "Por favor, espera a que tu rival añada el número secreto", + "congratulations": "¡Felicidades!", + "youLose": "Has perdido", + "theSecretNumberWas": "El número secreto era: ", + "accept": "Aceptar", + "pleaseWait": "Por favor espera...", + "error": "Error", + "retry": "Reintentar", + "email": "Correo electrónico", + "signUpSuccess": "¡Registro exitoso!", + "signUpError": "Error al registrarse. Por favor, inténtalo de nuevo.", + "anErrorOccurred": "Ocurrió un error", + + "searchingForPlayers": "Buscando jugadores", + "cancel": "Cancelar", + + "yourTurn": "Tu turno", + "rivalTurn": "Turno del rival", + + "timeLeft": "Tiempo restante: ", + "previousAttempts": "Intentos anteriores", + "noAttempts": "No hay intentos", + "introduceValidNumber": "Introduce un número válido", + + "cow": "Vaca", + "cowPlay": "V", + "bull": "Toro", + "bullPlay": "T", + + "exit": "Salir", + "play": "Jugar", + "confirmLogout": "Confirmar Cierre de Sesión", + "logoutConfirmation": "¿Estás seguro de que deseas cerrar sesión?", + "logout": "Cerrar Sesión", + + "timeRemaining": "Tiempo restante: {time}", + "@timeRemaining": { + "placeholders": { + "time": { + "type": "int" + } + } + }, + "selectSecretNumber": "Selecciona tu número secreto", + "note": "Nota: Si no seleccionas un número en el tiempo, perderás el juego", + "send": "Enviar", + "validNumber": "Introduce un número válido", + "rank": "Posición", + + "areYouSure": "¿Estás seguro?", + "doYouWantToSurrender": "¿Quieres rendirte?", + "no": "No", + "yes": "Sí", + + "leaderboard": "Tabla de posiciones", + "loading": "Cargando..." + } \ No newline at end of file diff --git a/lib/src/features/auth/views/auth_screen.dart b/lib/src/features/auth/views/auth_screen.dart index 5a1508b..1a38975 100644 --- a/lib/src/features/auth/views/auth_screen.dart +++ b/lib/src/features/auth/views/auth_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gutter/flutter_gutter.dart'; import 'package:go_router/go_router.dart'; +import 'package:my_app/l10n/l10n.dart'; import 'package:my_app/src/core/extensions/string.dart'; import 'package:my_app/src/core/ui/device.dart'; import 'package:my_app/src/core/ui/typography.dart'; @@ -53,7 +54,7 @@ class _AuthScreenState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - 'SignIn', + context.l10n.signIn, style: AppTextStyle().body.copyWith( fontFamily: AppTextStyle.secondaryFontFamily, fontSize: 30, @@ -62,13 +63,13 @@ class _AuthScreenState extends State { ), const GutterLarge(), GenericTextField( - labelText: 'Username', + labelText: context.l10n.username, controller: _usernameController, ), const GutterTiny(), GenericTextField( obscureText: true, - labelText: 'Password', + labelText: context.l10n.password, controller: _passwordController, ), const Gutter(), @@ -89,8 +90,10 @@ class _AuthScreenState extends State { context.read().refresh(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: - Text(state.errorMessage ?? 'An error occurred'), + content: Text( + state.errorMessage ?? + context.l10n.anErrorOccurred, + ), backgroundColor: Colors.red, ), ); @@ -124,7 +127,7 @@ class _AuthScreenState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - "Don't have an account?", + context.l10n.dontHaveAccount, style: AppTextStyle() .body .copyWith(color: colorScheme.onSurface), @@ -134,7 +137,7 @@ class _AuthScreenState extends State { context.goNamed(AppRoute.signUp.name); }, child: Text( - 'Create account', + context.l10n.createAccount, style: AppTextStyle().body.copyWith( color: colorScheme.primary, ), diff --git a/lib/src/features/auth/views/signup_screen.dart b/lib/src/features/auth/views/signup_screen.dart index fd36497..443ad76 100644 --- a/lib/src/features/auth/views/signup_screen.dart +++ b/lib/src/features/auth/views/signup_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gutter/flutter_gutter.dart'; import 'package:go_router/go_router.dart'; +import 'package:my_app/l10n/l10n.dart'; import 'package:my_app/src/core/extensions/string.dart'; import 'package:my_app/src/core/ui/device.dart'; import 'package:my_app/src/core/ui/typography.dart'; @@ -55,7 +56,7 @@ class _SignUpScreenState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - 'SignUp', + context.l10n.signUp, style: AppTextStyle().body.copyWith( fontFamily: AppTextStyle.secondaryFontFamily, fontSize: 30, @@ -64,19 +65,19 @@ class _SignUpScreenState extends State { ), const GutterLarge(), GenericTextField( - labelText: 'Username', + labelText: context.l10n.username, controller: _usernameController, ), const GutterTiny(), GenericTextField( obscureText: true, - labelText: 'Password', + labelText: context.l10n.password, controller: _passwordController, ), const GutterTiny(), GenericTextField( obscureText: true, - labelText: 'Confirm Password', + labelText: context.l10n.password, controller: _confirmPasswordController, ), const Gutter(), @@ -97,8 +98,10 @@ class _SignUpScreenState extends State { context.read().refresh(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: - Text(state.errorMessage ?? 'An error occurred'), + content: Text( + state.errorMessage ?? + context.l10n.anErrorOccurred, + ), backgroundColor: Colors.red, ), ); @@ -121,7 +124,7 @@ class _SignUpScreenState extends State { widget: state.authStatus.isLoading ? const CircularProgressIndicator.adaptive() : Text( - 'SignUp', + context.l10n.signUp, style: AppTextStyle() .textButton .copyWith(color: Colors.white), @@ -132,7 +135,7 @@ class _SignUpScreenState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - 'Already have an account? ', + context.l10n.alreadyHaveAccount, style: AppTextStyle() .body .copyWith(color: colorScheme.onSurface), @@ -142,7 +145,7 @@ class _SignUpScreenState extends State { context.goNamed(AppRoute.auth.name); }, child: Text( - 'SignIn', + context.l10n.signIn, style: AppTextStyle().body.copyWith( color: Theme.of(context).colorScheme.primary, diff --git a/lib/src/features/game/game_screen.dart b/lib/src/features/game/game_screen.dart index 279d4b0..7cbfa5a 100644 --- a/lib/src/features/game/game_screen.dart +++ b/lib/src/features/game/game_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gutter/flutter_gutter.dart'; import 'package:go_router/go_router.dart'; import 'package:lottie/lottie.dart'; +import 'package:my_app/l10n/l10n.dart'; import 'package:my_app/resources/resources.dart'; import 'package:my_app/src/core/extensions/list.dart'; import 'package:my_app/src/core/ui/typography.dart'; @@ -107,10 +108,9 @@ class _GameScreenState extends State { builder: (dialogContext) { _dialogContext = dialogContext; _isDialogOpen = true; - return const AlertDialog.adaptive( - title: Text('Waiting for rival'), - content: - Text('Please wait for your rival to add the secret number'), + return AlertDialog.adaptive( + title: Text(context.l10n.waitingForRival), + content: Text(context.l10n.waitingForRivalDescription), ); }, ); @@ -122,7 +122,8 @@ class _GameScreenState extends State { void _showWinner(Game game, Player player, PlayerNumber rival) { final winner = game.winner; final isWinner = winner?.id == player.id; - final title = isWinner ? 'Congratulations!!!' : 'You Lose'; + final title = + isWinner ? context.l10n.congratulations : context.l10n.youLose; final imageResult = isWinner ? AppImages.winGame : AppImages.loseGame; final resultPoints = GameUtils.calculateResultPoints( wonCurrentGame: isWinner, @@ -143,7 +144,7 @@ class _GameScreenState extends State { if (!isWinner) Text.rich( TextSpan( - text: 'The secret number was: ', + text: context.l10n.theSecretNumberWas, style: AppTextStyle().body.copyWith( color: Theme.of(context).colorScheme.onSurface, ), @@ -186,7 +187,7 @@ class _GameScreenState extends State { context.goNamed(AppRoute.home.name); context.read().refresh(); }, - child: const Text('Accept'), + child: Text(context.l10n.accept), ), ], ); diff --git a/lib/src/features/game/search_game_screen.dart b/lib/src/features/game/search_game_screen.dart index 83ddf47..1046aff 100644 --- a/lib/src/features/game/search_game_screen.dart +++ b/lib/src/features/game/search_game_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gutter/flutter_gutter.dart'; import 'package:go_router/go_router.dart'; import 'package:lottie/lottie.dart'; +import 'package:my_app/l10n/l10n.dart'; import 'package:my_app/resources/resources.dart'; import 'package:my_app/src/core/ui/typography.dart'; import 'package:my_app/src/core/utils/widgets/generic_button.dart'; @@ -46,7 +47,7 @@ class _SearchGameScreenState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - 'Searching for players', + context.l10n.searchingForPlayers, textAlign: TextAlign.center, style: AppTextStyle().bodyLarge.copyWith( color: Theme.of(context).colorScheme.onSurface, @@ -63,7 +64,7 @@ class _SearchGameScreenState extends State { padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 20), child: Text( - 'Cancel', + context.l10n.cancel, style: AppTextStyle().body.copyWith(color: Colors.white), ), ), diff --git a/lib/src/features/game/widgets/game_turn_widget.dart b/lib/src/features/game/widgets/game_turn_widget.dart index da11b2f..36fc634 100644 --- a/lib/src/features/game/widgets/game_turn_widget.dart +++ b/lib/src/features/game/widgets/game_turn_widget.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:my_app/l10n/l10n.dart'; import 'package:my_app/src/core/ui/typography.dart'; import 'package:my_app/src/features/player/data/model/player_number.dart'; @@ -20,7 +21,9 @@ class GameTurnWidget extends StatelessWidget { child: Padding( padding: const EdgeInsets.all(5), child: Text( - ownPlayerNumber.isTurn ? 'Your turn' : 'Rival turn', + ownPlayerNumber.isTurn + ? context.l10n.yourTurn + : context.l10n.rivalTurn, style: AppTextStyle().body.copyWith(color: Colors.white), ), ), diff --git a/lib/src/features/home/widgets/game_section.dart b/lib/src/features/home/widgets/game_section.dart index 7355f4c..4d53e6b 100644 --- a/lib/src/features/home/widgets/game_section.dart +++ b/lib/src/features/home/widgets/game_section.dart @@ -3,6 +3,7 @@ import 'dart:developer'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:my_app/l10n/l10n.dart'; import 'package:my_app/src/core/extensions/string.dart'; import 'package:my_app/src/features/game/domain/mocks/attempt_mock.dart'; import 'package:my_app/src/features/player/cubit/player_cubit.dart'; @@ -75,7 +76,7 @@ class GameSection extends HookWidget { mainAxisAlignment: MainAxisAlignment.end, children: [ Text( - 'Time left: ', + context.l10n.timeLeft, style: AppTextStyle().body.copyWith( color: colorScheme.onSurface, ), @@ -141,7 +142,7 @@ class _PlayList extends StatelessWidget { padding: EdgeInsets.zero, children: [ Text( - 'Previous attempts'.toUpperCase(), + context.l10n.previousAttempts.toUpperCase(), style: AppTextStyle().body.copyWith( fontWeight: FontWeight.w600, color: colorScheme.onSurface, @@ -164,7 +165,7 @@ class _PlayList extends StatelessWidget { ? [ Center( child: Text( - 'No attempts', + context.l10n.noAttempts, style: AppTextStyle().body.copyWith( color: colorScheme.onSurface, ), @@ -227,8 +228,8 @@ class __SendNumberSectionState extends State<_SendNumberSection> { final isValidNumber = Utils.isValidPlayerNumber(otpValue); if (!isValidNumber) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Introduce un número válido'), + SnackBar( + content: Text(context.l10n.introduceValidNumber), backgroundColor: Colors.red, ), ); diff --git a/lib/src/features/home/widgets/play_number_card.dart b/lib/src/features/home/widgets/play_number_card.dart index 5d1611d..d1f6bd8 100644 --- a/lib/src/features/home/widgets/play_number_card.dart +++ b/lib/src/features/home/widgets/play_number_card.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_gutter/flutter_gutter.dart'; +import 'package:my_app/l10n/l10n.dart'; import 'package:my_app/src/core/ui/theme.dart'; import 'package:my_app/src/core/ui/typography.dart'; import 'package:my_app/src/features/game/data/model/attempt.dart'; @@ -49,9 +50,9 @@ class PlayNumberCard extends StatelessWidget { return _NumberPlay(attempt.number[index].toString()); }), const Gutter(), - Text('${attempt.cows}V'), + Text('${attempt.cows}${context.l10n.cowPlay}'), const Gutter(), - Text('${attempt.bulls}T'), + Text('${attempt.bulls}${context.l10n.bullPlay}'), ], ), ), diff --git a/lib/src/features/home/widgets/search_game_section.dart b/lib/src/features/home/widgets/search_game_section.dart index 8fbc6eb..77decec 100644 --- a/lib/src/features/home/widgets/search_game_section.dart +++ b/lib/src/features/home/widgets/search_game_section.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gutter/flutter_gutter.dart'; import 'package:go_router/go_router.dart'; +import 'package:my_app/l10n/l10n.dart'; import 'package:my_app/src/core/ui/theme.dart'; import 'package:my_app/src/core/ui/typography.dart'; import 'package:my_app/src/features/auth/cubit/auth_cubit.dart'; @@ -47,7 +48,7 @@ class _SearchGameSectionState extends State { const Icon(Icons.logout), const GutterTiny(), Text( - 'Exit', + context.l10n.logout, style: AppTextStyle().body.copyWith( color: Theme.of(context).colorScheme.onSurface, ), @@ -75,7 +76,7 @@ class _SearchGameSectionState extends State { const Icon(Icons.play_arrow, color: Colors.white), const GutterTiny(), Text( - 'Play', + context.l10n.play, style: AppTextStyle() .textButton .copyWith(color: Colors.white), @@ -100,15 +101,15 @@ class _SearchGameSectionState extends State { context: context, builder: (BuildContext context) { return AlertDialog( - title: const Text('Confirm Logout'), - content: const Text('Are you sure you want to logout?'), + title: Text(context.l10n.confirmLogout), + content: Text(context.l10n.logoutConfirmation), actions: [ TextButton( onPressed: () { Navigator.of(context).pop(); }, child: Text( - 'Cancel', + context.l10n.cancel, style: AppTextStyle().textButton.copyWith(color: Colors.white), ), ), @@ -118,7 +119,7 @@ class _SearchGameSectionState extends State { context.read().logout(); }, child: Text( - 'Logout', + context.l10n.logout, style: AppTextStyle() .textButton .copyWith(color: Theme.of(context).colorScheme.primary), diff --git a/lib/src/features/home/widgets/select_secret_number.dart b/lib/src/features/home/widgets/select_secret_number.dart index 60b309d..613d3be 100644 --- a/lib/src/features/home/widgets/select_secret_number.dart +++ b/lib/src/features/home/widgets/select_secret_number.dart @@ -1,8 +1,8 @@ import 'dart:async'; -import 'dart:developer'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gutter/flutter_gutter.dart'; +import 'package:my_app/l10n/l10n.dart'; import 'package:my_app/src/core/extensions/string.dart'; import 'package:my_app/src/core/ui/typography.dart'; import 'package:my_app/src/core/utils/utils.dart'; @@ -67,7 +67,7 @@ class _SelectSecretNumberState extends State { final colorScheme = Theme.of(context).colorScheme; return AlertDialog( title: Text( - 'Select your secret number', + context.l10n.selectSecretNumber, style: AppTextStyle().dialogTitle.copyWith(color: colorScheme.onSurface), ), @@ -79,7 +79,7 @@ class _SelectSecretNumberState extends State { ), const SizedBox(height: 16), Text( - 'Time remaining: $_start seconds', + context.l10n.timeRemaining(_start), style: AppTextStyle().body.copyWith( fontWeight: FontWeight.w600, color: _start <= 10 ? Colors.red : colorScheme.onSurface, @@ -88,7 +88,7 @@ class _SelectSecretNumberState extends State { const GutterSmall(), Text( textAlign: TextAlign.center, - "Note: If you don't select a number in the time you will lose the game", + context.l10n.note, style: AppTextStyle().body.copyWith(color: Colors.red, fontSize: 12), ), @@ -97,7 +97,7 @@ class _SelectSecretNumberState extends State { loading: context.read().state.isLoading, width: double.infinity, widget: Text( - 'Send', + context.l10n.send, style: AppTextStyle().body.copyWith(color: Colors.white), ), onPressed: secretNumber.length != 4 @@ -107,8 +107,8 @@ class _SelectSecretNumberState extends State { Utils.isValidPlayerNumber(secretNumber); if (!isValidNumber) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Introduce a valid number'), + SnackBar( + content: Text(context.l10n.introduceValidNumber), backgroundColor: Colors.red, ), ); diff --git a/lib/src/features/home/widgets/user_header_info.dart b/lib/src/features/home/widgets/user_header_info.dart index 9e91238..ccda81d 100644 --- a/lib/src/features/home/widgets/user_header_info.dart +++ b/lib/src/features/home/widgets/user_header_info.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gutter/flutter_gutter.dart'; +import 'package:my_app/l10n/l10n.dart'; import 'package:my_app/resources/resources.dart'; import 'package:my_app/src/core/ui/device.dart'; import 'package:my_app/src/core/ui/typography.dart'; @@ -70,7 +71,7 @@ class _UserHeaderInfoState extends State { player.id, ); return Text( - 'Rank: ${ranking.position}', + '${context.l10n.rank}: ${ranking.position}', style: AppTextStyle().body.copyWith(fontSize: 12), ); }, diff --git a/lib/src/features/home/widgets/versus_section.dart b/lib/src/features/home/widgets/versus_section.dart index 3631cc5..2b46d58 100644 --- a/lib/src/features/home/widgets/versus_section.dart +++ b/lib/src/features/home/widgets/versus_section.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gutter/flutter_gutter.dart'; +import 'package:my_app/l10n/l10n.dart'; import 'package:my_app/src/core/ui/colors.dart'; import 'package:my_app/src/core/ui/typography.dart'; import 'package:my_app/src/core/utils/widgets/cache_widget.dart'; @@ -59,7 +60,7 @@ class VersusSection extends StatelessWidget { rival.player.id, ); return Text( - 'Rank: ${rivalRanking.position}', + '${context.l10n.rank}: ${rivalRanking.position}', style: AppTextStyle() .body .copyWith(fontSize: 12, color: Colors.black45), @@ -87,21 +88,21 @@ class VersusSection extends StatelessWidget { context: context, builder: (context) { return AlertDialog.adaptive( - title: const Text('Are you sure?'), - content: const Text('Do you want to surrender the game?'), + title: Text(context.l10n.areYouSure), + content: Text(context.l10n.doYouWantToSurrender), actions: [ TextButton( onPressed: () { Navigator.of(context).pop(); }, - child: const Text('No'), + child: Text(context.l10n.no), ), TextButton( onPressed: () { context.read().surrender(); Navigator.of(context).pop(); }, - child: const Text('Yes'), + child: Text(context.l10n.yes), ), ], ); diff --git a/lib/src/features/ranking/leaderboard.dart b/lib/src/features/ranking/leaderboard.dart index 32cea73..7261bfc 100644 --- a/lib/src/features/ranking/leaderboard.dart +++ b/lib/src/features/ranking/leaderboard.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gutter/flutter_gutter.dart'; +import 'package:my_app/l10n/l10n.dart'; import 'package:my_app/src/core/ui/typography.dart'; import 'package:my_app/src/features/ranking/cubit/ranking_cubit.dart'; import 'package:my_app/src/features/ranking/mocks/ranking_mocks.dart'; @@ -26,8 +27,8 @@ class _LeaderboardWidgetState extends State { listener: (context, state) { if (state.isError) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Error'), + SnackBar( + content: Text(context.l10n.anErrorOccurred), ), ); } @@ -38,7 +39,7 @@ class _LeaderboardWidgetState extends State { children: [ const GutterLarge(), Text( - 'Leaderboard', + context.l10n.leaderboard, style: AppTextStyle().bodyLarge.copyWith( fontWeight: FontWeight.w600, fontSize: 35, diff --git a/lib/src/features/splash/loading_profile_data.dart b/lib/src/features/splash/loading_profile_data.dart index fff7036..cb33804 100644 --- a/lib/src/features/splash/loading_profile_data.dart +++ b/lib/src/features/splash/loading_profile_data.dart @@ -5,6 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gutter/flutter_gutter.dart'; import 'package:go_router/go_router.dart'; import 'package:lottie/lottie.dart'; +import 'package:my_app/l10n/l10n.dart'; import 'package:my_app/resources/resources.dart'; import 'package:my_app/src/core/utils/object_extensions.dart'; import 'package:my_app/src/features/game/cubit/game_cubit.dart'; @@ -49,7 +50,7 @@ class _LoadingProfileDataState extends State { fit: BoxFit.cover, ), const GutterSmall(), - const Text('Loading...'), + Text(context.l10n.loading), ], ), ), diff --git a/lib/src/features/splash/splash_screen.dart b/lib/src/features/splash/splash_screen.dart index eff851f..b6cdfc7 100644 --- a/lib/src/features/splash/splash_screen.dart +++ b/lib/src/features/splash/splash_screen.dart @@ -6,6 +6,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gutter/flutter_gutter.dart'; import 'package:go_router/go_router.dart'; import 'package:lottie/lottie.dart'; +import 'package:my_app/l10n/l10n.dart'; import 'package:my_app/resources/resources.dart'; import 'package:my_app/src/core/ui/typography.dart'; import 'package:my_app/src/core/utils/object_extensions.dart'; @@ -63,15 +64,15 @@ class _SplashScreenState extends State { showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Error'), - content: const Text('An error has occurred'), + title: Text(context.l10n.error), + content: Text(context.l10n.anErrorOccurred), actions: [ TextButton( onPressed: () { context.read().initialize(); Navigator.of(context).pop(); }, - child: const Text('Retry'), + child: Text(context.l10n.retry), ), ], ), From 0e87440c8a81acd6d74515483f9ec7d8509774de Mon Sep 17 00:00:00 2001 From: ale24dev Date: Sat, 5 Oct 2024 01:17:57 -0400 Subject: [PATCH 20/30] add: change profile image --- lib/l10n/arb/app_en.arb | 6 +- lib/l10n/arb/app_es.arb | 6 +- lib/src/app.dart | 2 + lib/src/core/constants.dart | 6 +- .../core/di/dependency_injection.config.dart | 11 ++ lib/src/core/services/player_datasource.dart | 5 + .../core/services/settings_datasource.dart | 2 + .../home/widgets/search_game_section.dart | 68 ++----- .../home/widgets/user_header_info.dart | 8 +- .../features/player/cubit/player_cubit.dart | 22 ++- .../player/data/player_repository.dart | 20 +++ .../settings/cubit/settings_cubit.dart | 15 ++ .../cubit/settings_cubit.freezed.dart | 166 ++++++++++++++++++ .../settings/cubit/settings_state.dart | 16 ++ .../settings/data/profile_images.dart | 13 ++ .../settings/data/settings_repository.dart | 12 ++ .../settings/mocks/settings_mock.dart | 24 +++ .../features/settings/settings_screen.dart | 80 +++++++++ .../widgets/change_profile_image_dialog.dart | 119 +++++++++++++ lib/src/features/splash/cubit/app_cubit.dart | 1 - lib/src/router/router.dart | 25 ++- 21 files changed, 552 insertions(+), 75 deletions(-) create mode 100644 lib/src/core/services/settings_datasource.dart create mode 100644 lib/src/features/settings/cubit/settings_cubit.dart create mode 100644 lib/src/features/settings/cubit/settings_cubit.freezed.dart create mode 100644 lib/src/features/settings/cubit/settings_state.dart create mode 100644 lib/src/features/settings/data/profile_images.dart create mode 100644 lib/src/features/settings/data/settings_repository.dart create mode 100644 lib/src/features/settings/mocks/settings_mock.dart create mode 100644 lib/src/features/settings/settings_screen.dart create mode 100644 lib/src/features/settings/widgets/change_profile_image_dialog.dart diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 5c713b1..1c1c3d2 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -63,5 +63,9 @@ "yes": "Yes", "leaderboard": "Leaderboard", - "loading": "Loading..." + "loading": "Loading...", + + "settings": "Settings", + + "changeProfileImage": "Change profile image" } \ No newline at end of file diff --git a/lib/l10n/arb/app_es.arb b/lib/l10n/arb/app_es.arb index f0f44f4..1f390ae 100644 --- a/lib/l10n/arb/app_es.arb +++ b/lib/l10n/arb/app_es.arb @@ -63,5 +63,9 @@ "yes": "Sí", "leaderboard": "Tabla de posiciones", - "loading": "Cargando..." + "loading": "Cargando...", + + "settings": "Configuración", + + "changeProfileImage": "Cambiar imagen de perfil" } \ No newline at end of file diff --git a/lib/src/app.dart b/lib/src/app.dart index e2a008d..b00c526 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -7,6 +7,7 @@ import 'package:my_app/src/features/auth/cubit/auth_cubit.dart'; import 'package:my_app/src/features/game/cubit/game_cubit.dart'; import 'package:my_app/src/features/player/cubit/player_cubit.dart'; import 'package:my_app/src/features/ranking/cubit/ranking_cubit.dart'; +import 'package:my_app/src/features/settings/cubit/settings_cubit.dart'; import 'package:my_app/src/features/splash/cubit/app_cubit.dart'; import 'package:my_app/src/router/router.dart'; @@ -23,6 +24,7 @@ class MyApp extends StatelessWidget { BlocProvider(create: (_) => getIt.get()), BlocProvider(create: (_) => getIt.get()), BlocProvider(create: (_) => getIt.get()), + BlocProvider(create: (_) => getIt.get()), ], child: MaterialApp.router( debugShowCheckedModeBanner: false, diff --git a/lib/src/core/constants.dart b/lib/src/core/constants.dart index 976d308..32073f2 100644 --- a/lib/src/core/constants.dart +++ b/lib/src/core/constants.dart @@ -1,5 +1,9 @@ // ignore_for_file: non_constant_identifier_names -abstract class Constants{ +abstract class Constants { static double BUTTON_HEIGHT = 60; + + static String apiUrl = 'https://vtxedgyoqydehqzgcpwb.supabase.co'; + + static String publicStorageUrl = '$apiUrl/storage/v1/object/public'; } diff --git a/lib/src/core/di/dependency_injection.config.dart b/lib/src/core/di/dependency_injection.config.dart index 4447bc0..d9df460 100644 --- a/lib/src/core/di/dependency_injection.config.dart +++ b/lib/src/core/di/dependency_injection.config.dart @@ -12,6 +12,7 @@ import 'package:get_it/get_it.dart' as _i174; import 'package:injectable/injectable.dart' as _i526; import 'package:my_app/src/core/di/modules/modules.dart' as _i560; import 'package:my_app/src/core/interceptor.dart' as _i330; +import 'package:my_app/src/core/services/settings_datasource.dart' as _i94; import 'package:my_app/src/core/supabase/client.dart' as _i880; import 'package:my_app/src/features/auth/cubit/auth_cubit.dart' as _i992; import 'package:my_app/src/features/auth/data/auth_repository.dart' as _i427; @@ -23,6 +24,10 @@ import 'package:my_app/src/features/player/data/player_repository.dart' import 'package:my_app/src/features/ranking/cubit/ranking_cubit.dart' as _i931; import 'package:my_app/src/features/ranking/data/ranking_repository.dart' as _i34; +import 'package:my_app/src/features/settings/cubit/settings_cubit.dart' + as _i303; +import 'package:my_app/src/features/settings/data/settings_repository.dart' + as _i621; import 'package:my_app/src/features/splash/cubit/app_cubit.dart' as _i1038; import 'package:my_app/src/router/router.dart' as _i63; import 'package:shared_preferences/shared_preferences.dart' as _i460; @@ -63,11 +68,17 @@ extension GetItInjectableX on _i174.GetIt { gh<_i330.SupabaseServiceImpl>(), gh<_i454.SupabaseClient>(), )); + gh.singleton<_i94.SettingsDatasource>(() => _i621.SettingsRepository( + gh<_i330.SupabaseServiceImpl>(), + gh<_i454.SupabaseClient>(), + )); gh.factory<_i584.GameCubit>(() => _i584.GameCubit( gh<_i454.SupabaseClient>(), gh<_i34.GameRepository>(), gh<_i406.PlayerRepository>(), )); + gh.factory<_i303.SettingsCubit>( + () => _i303.SettingsCubit(gh<_i94.SettingsDatasource>())); gh.factory<_i931.RankingCubit>( () => _i931.RankingCubit(gh<_i34.RankingRepository>())); gh.factory<_i126.PlayerCubit>(() => _i126.PlayerCubit( diff --git a/lib/src/core/services/player_datasource.dart b/lib/src/core/services/player_datasource.dart index 05fe295..1cced5b 100644 --- a/lib/src/core/services/player_datasource.dart +++ b/lib/src/core/services/player_datasource.dart @@ -20,4 +20,9 @@ abstract class PlayerDatasource { String playerId, void Function(PlayerNumberRealtime) callback, ); + + Future> setProfileImage( + Player player, + String imageUrl, + ); } diff --git a/lib/src/core/services/settings_datasource.dart b/lib/src/core/services/settings_datasource.dart new file mode 100644 index 0000000..5296f4e --- /dev/null +++ b/lib/src/core/services/settings_datasource.dart @@ -0,0 +1,2 @@ + +abstract class SettingsDatasource {} diff --git a/lib/src/features/home/widgets/search_game_section.dart b/lib/src/features/home/widgets/search_game_section.dart index 77decec..ab3ae20 100644 --- a/lib/src/features/home/widgets/search_game_section.dart +++ b/lib/src/features/home/widgets/search_game_section.dart @@ -39,21 +39,16 @@ class _SearchGameSectionState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + /// Element created to center the play button Expanded( - child: GestureDetector( - onTap: () => _showLogoutConfirmationDialog(context), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.logout), - const GutterTiny(), - Text( - context.l10n.logout, - style: AppTextStyle().body.copyWith( - color: Theme.of(context).colorScheme.onSurface, - ), - ), - ], + child: Align( + alignment: Alignment.centerLeft, + child: GestureDetector( + onTap: () {}, + child: const Icon( + Icons.settings, + color: Colors.transparent, + ), ), ), ), @@ -87,7 +82,15 @@ class _SearchGameSectionState extends State { ), ), ), - const Expanded(child: SizedBox.shrink()), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: GestureDetector( + onTap: () => context.goNamed(AppRoute.settings.name), + child: const Icon(Icons.settings), + ), + ), + ), ], ), ); @@ -95,39 +98,4 @@ class _SearchGameSectionState extends State { ), ); } - - void _showLogoutConfirmationDialog(BuildContext context) { - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: Text(context.l10n.confirmLogout), - content: Text(context.l10n.logoutConfirmation), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text( - context.l10n.cancel, - style: AppTextStyle().textButton.copyWith(color: Colors.white), - ), - ), - TextButton( - onPressed: () { - Navigator.of(context).pop(); - context.read().logout(); - }, - child: Text( - context.l10n.logout, - style: AppTextStyle() - .textButton - .copyWith(color: Theme.of(context).colorScheme.primary), - ), - ), - ], - ); - }, - ); - } } diff --git a/lib/src/features/home/widgets/user_header_info.dart b/lib/src/features/home/widgets/user_header_info.dart index ccda81d..54f4f78 100644 --- a/lib/src/features/home/widgets/user_header_info.dart +++ b/lib/src/features/home/widgets/user_header_info.dart @@ -24,15 +24,10 @@ class UserHeaderInfo extends StatefulWidget { class _UserHeaderInfoState extends State { late Player player; - @override - void initState() { - player = context.read().state.player!; - - super.initState(); - } @override Widget build(BuildContext context) { + player = context.watch().state.player!; return Container( width: context.widthPx, color: Theme.of(context).colorScheme.primary, @@ -49,6 +44,7 @@ class _UserHeaderInfoState extends State { CacheWidget( size: const Size(60, 60), imageUrl: player.avatarUrl, + fit: BoxFit.cover, ), const GutterSmall(), Column( diff --git a/lib/src/features/player/cubit/player_cubit.dart b/lib/src/features/player/cubit/player_cubit.dart index 249705f..317b169 100644 --- a/lib/src/features/player/cubit/player_cubit.dart +++ b/lib/src/features/player/cubit/player_cubit.dart @@ -1,5 +1,3 @@ -import 'dart:developer'; - import 'package:bloc/bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:injectable/injectable.dart'; @@ -85,6 +83,26 @@ class PlayerCubit extends Cubit { }); } + Future setProfileImage(String imageUrl) async { + emit(state.copyWith(status: PlayerStatus.loading)); + + await _playerRepository + .setProfileImage(state.player!, imageUrl) + .then((result) { + result.fold( + (error) => emit( + state.copyWith(status: PlayerStatus.error), + ), + (player) => emit( + state.copyWith( + player: player, + status: PlayerStatus.success, + ), + ), + ); + }); + } + void refresh() { emit(const PlayerState()); } diff --git a/lib/src/features/player/data/player_repository.dart b/lib/src/features/player/data/player_repository.dart index e317490..4dda9c8 100644 --- a/lib/src/features/player/data/player_repository.dart +++ b/lib/src/features/player/data/player_repository.dart @@ -8,6 +8,7 @@ import 'package:my_app/src/core/exceptions.dart'; import 'package:my_app/src/core/extensions/list.dart'; import 'package:my_app/src/core/interceptor.dart'; import 'package:my_app/src/core/services/player_datasource.dart'; +import 'package:my_app/src/core/supabase/query_supabase.dart'; import 'package:my_app/src/features/player/data/model/player.dart'; import 'package:my_app/src/features/player/data/model/player_number.dart'; import 'package:my_app/src/features/player/domain/player_number_realtime.dart'; @@ -104,4 +105,23 @@ class PlayerRepository extends PlayerDatasource { fromJsonParse: PlayerNumber.fromJson, ); } + + @override + Future> setProfileImage( + Player player, + String imageUrl, + ) { + return _supabaseServiceImpl.query( + table: 'setProfileImage', + request: () => _client + .from('player') + .update({'avatar_url': imageUrl}) + .eq('id', player.id) + .select(QuerySupabase.player) + .single(), + queryOption: QueryOption.update, + responseNullable: true, + fromJsonParse: Player.fromJson, + ); + } } diff --git a/lib/src/features/settings/cubit/settings_cubit.dart b/lib/src/features/settings/cubit/settings_cubit.dart new file mode 100644 index 0000000..e7a1b7e --- /dev/null +++ b/lib/src/features/settings/cubit/settings_cubit.dart @@ -0,0 +1,15 @@ +import 'package:bloc/bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:injectable/injectable.dart'; +import 'package:my_app/src/core/exceptions.dart'; +import 'package:my_app/src/core/services/settings_datasource.dart'; + +part 'settings_state.dart'; +part 'settings_cubit.freezed.dart'; + +@injectable +class SettingsCubit extends Cubit { + SettingsCubit(this._settingsDatasource) : super(const SettingsState()); + + final SettingsDatasource _settingsDatasource; +} diff --git a/lib/src/features/settings/cubit/settings_cubit.freezed.dart b/lib/src/features/settings/cubit/settings_cubit.freezed.dart new file mode 100644 index 0000000..411bd58 --- /dev/null +++ b/lib/src/features/settings/cubit/settings_cubit.freezed.dart @@ -0,0 +1,166 @@ +// 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 'settings_cubit.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 _$SettingsState { + SettingsStateStatus get stateStatus => throw _privateConstructorUsedError; + AppException? get error => throw _privateConstructorUsedError; + + /// Create a copy of SettingsState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $SettingsStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $SettingsStateCopyWith<$Res> { + factory $SettingsStateCopyWith( + SettingsState value, $Res Function(SettingsState) then) = + _$SettingsStateCopyWithImpl<$Res, SettingsState>; + @useResult + $Res call({SettingsStateStatus stateStatus, AppException? error}); +} + +/// @nodoc +class _$SettingsStateCopyWithImpl<$Res, $Val extends SettingsState> + implements $SettingsStateCopyWith<$Res> { + _$SettingsStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of SettingsState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? stateStatus = null, + Object? error = freezed, + }) { + return _then(_value.copyWith( + stateStatus: null == stateStatus + ? _value.stateStatus + : stateStatus // ignore: cast_nullable_to_non_nullable + as SettingsStateStatus, + error: freezed == error + ? _value.error + : error // ignore: cast_nullable_to_non_nullable + as AppException?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$SettingsStateImplCopyWith<$Res> + implements $SettingsStateCopyWith<$Res> { + factory _$$SettingsStateImplCopyWith( + _$SettingsStateImpl value, $Res Function(_$SettingsStateImpl) then) = + __$$SettingsStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({SettingsStateStatus stateStatus, AppException? error}); +} + +/// @nodoc +class __$$SettingsStateImplCopyWithImpl<$Res> + extends _$SettingsStateCopyWithImpl<$Res, _$SettingsStateImpl> + implements _$$SettingsStateImplCopyWith<$Res> { + __$$SettingsStateImplCopyWithImpl( + _$SettingsStateImpl _value, $Res Function(_$SettingsStateImpl) _then) + : super(_value, _then); + + /// Create a copy of SettingsState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? stateStatus = null, + Object? error = freezed, + }) { + return _then(_$SettingsStateImpl( + stateStatus: null == stateStatus + ? _value.stateStatus + : stateStatus // ignore: cast_nullable_to_non_nullable + as SettingsStateStatus, + error: freezed == error + ? _value.error + : error // ignore: cast_nullable_to_non_nullable + as AppException?, + )); + } +} + +/// @nodoc + +class _$SettingsStateImpl extends _SettingsState { + const _$SettingsStateImpl( + {this.stateStatus = SettingsStateStatus.initial, this.error}) + : super._(); + + @override + @JsonKey() + final SettingsStateStatus stateStatus; + @override + final AppException? error; + + @override + String toString() { + return 'SettingsState(stateStatus: $stateStatus, error: $error)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$SettingsStateImpl && + (identical(other.stateStatus, stateStatus) || + other.stateStatus == stateStatus) && + (identical(other.error, error) || other.error == error)); + } + + @override + int get hashCode => Object.hash(runtimeType, stateStatus, error); + + /// Create a copy of SettingsState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$SettingsStateImplCopyWith<_$SettingsStateImpl> get copyWith => + __$$SettingsStateImplCopyWithImpl<_$SettingsStateImpl>(this, _$identity); +} + +abstract class _SettingsState extends SettingsState { + const factory _SettingsState( + {final SettingsStateStatus stateStatus, + final AppException? error}) = _$SettingsStateImpl; + const _SettingsState._() : super._(); + + @override + SettingsStateStatus get stateStatus; + @override + AppException? get error; + + /// Create a copy of SettingsState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$SettingsStateImplCopyWith<_$SettingsStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/src/features/settings/cubit/settings_state.dart b/lib/src/features/settings/cubit/settings_state.dart new file mode 100644 index 0000000..1a416c8 --- /dev/null +++ b/lib/src/features/settings/cubit/settings_state.dart @@ -0,0 +1,16 @@ +part of 'settings_cubit.dart'; + +enum SettingsStateStatus { initial, loading, loaded, error } + +@freezed +class SettingsState with _$SettingsState { + const factory SettingsState({ + @Default(SettingsStateStatus.initial) SettingsStateStatus stateStatus, + AppException? error, + }) = _SettingsState; + const SettingsState._(); + + bool get isLoading => stateStatus == SettingsStateStatus.loading; + bool get isLoaded => stateStatus == SettingsStateStatus.loaded; + bool get isError => stateStatus == SettingsStateStatus.error; +} diff --git a/lib/src/features/settings/data/profile_images.dart b/lib/src/features/settings/data/profile_images.dart new file mode 100644 index 0000000..c645a52 --- /dev/null +++ b/lib/src/features/settings/data/profile_images.dart @@ -0,0 +1,13 @@ +import 'package:my_app/src/core/constants.dart'; + +final profileImagesUrl = [ + '${Constants.publicStorageUrl}/avatars/2151021955.jpg?t=2024-10-05T03%3A55%3A11.685Z', + '${Constants.publicStorageUrl}/avatars/3d-cartoon-style-character%20(1).jpg', + '${Constants.publicStorageUrl}/avatars/2151033971.jpg?t=2024-10-05T04%3A03%3A52.520Z', + '${Constants.publicStorageUrl}/avatars/40542.jpg', + '${Constants.publicStorageUrl}/avatars/portrait-beautiful-girl-with-blond-hair-3d-rendering.jpg', + '${Constants.publicStorageUrl}/avatars/51491.jpg', + '${Constants.publicStorageUrl}/avatars/businesswoman-cartoon-character-gray-background-3d-illustration-business-concept.jpg', + '${Constants.publicStorageUrl}/avatars/50955.jpg', + '${Constants.publicStorageUrl}/avatars/portrait-beautiful-young-woman-with-stylish-hairstyle-glasses.jpg?t=2024-10-05T04%3A16%3A07.395Z', +]; diff --git a/lib/src/features/settings/data/settings_repository.dart b/lib/src/features/settings/data/settings_repository.dart new file mode 100644 index 0000000..9d38050 --- /dev/null +++ b/lib/src/features/settings/data/settings_repository.dart @@ -0,0 +1,12 @@ +import 'package:injectable/injectable.dart'; +import 'package:my_app/src/core/interceptor.dart'; +import 'package:my_app/src/core/services/settings_datasource.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; + +@Singleton(as: SettingsDatasource) +class SettingsRepository extends SettingsDatasource { + SettingsRepository(this._supabaseServiceImpl, this._client); + + final SupabaseServiceImpl _supabaseServiceImpl; + final SupabaseClient _client; +} diff --git a/lib/src/features/settings/mocks/settings_mock.dart b/lib/src/features/settings/mocks/settings_mock.dart new file mode 100644 index 0000000..8e15ba6 --- /dev/null +++ b/lib/src/features/settings/mocks/settings_mock.dart @@ -0,0 +1,24 @@ +import 'package:supabase_flutter/supabase_flutter.dart'; + +/// Mocks for the settings feature +final profileImagesMock = List.generate(5, (index) { + return FileObject( + name: 'avatar${index + 1}.png', + id: index.toString(), + updatedAt: '', + createdAt: '', + lastAccessedAt: '', + metadata: { + 'eTag': '"c5e8c553235d9af30ef4f6e280790b92"', + 'size': 32175, + 'mimetype': 'image/png', + 'cacheControl': 'max-age=3600', + 'lastModified': '2024-05-22T23:06:05.574Z', + 'contentLength': 32175, + 'httpStatusCode': 200, + }, + owner: 'owner-id', + buckets: null, + bucketId: index.toString(), + ); +}); diff --git a/lib/src/features/settings/settings_screen.dart b/lib/src/features/settings/settings_screen.dart new file mode 100644 index 0000000..d832e4b --- /dev/null +++ b/lib/src/features/settings/settings_screen.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:my_app/l10n/l10n.dart'; +import 'package:my_app/src/core/ui/colors.dart'; +import 'package:my_app/src/core/ui/typography.dart'; +import 'package:my_app/src/features/auth/cubit/auth_cubit.dart'; +import 'package:my_app/src/features/settings/widgets/change_profile_image_dialog.dart'; + +class SettingsScreen extends StatelessWidget { + const SettingsScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(context.l10n.settings), + ), + body: Column( + children: [ + ListTile( + leading: const Icon(Icons.camera_alt_outlined), + title: Text(context.l10n.changeProfileImage), + onTap: () => showDialog( + context: context, + builder: (context) { + return const ChangeProfileImageDialog(); + }, + ), + ), + ListTile( + onTap: () => _showLogoutConfirmationDialog(context), + leading: const Icon( + Icons.logout, + color: AppColor.fail, + ), + title: Text( + context.l10n.logout, + style: AppTextStyle().body.copyWith(color: AppColor.fail), + ), + ), + ], + ), + ); + } + + void _showLogoutConfirmationDialog(BuildContext context) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text(context.l10n.confirmLogout), + content: Text(context.l10n.logoutConfirmation), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text( + context.l10n.cancel, + style: AppTextStyle().textButton.copyWith(color: Colors.white), + ), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(); + context.read().logout(); + }, + child: Text( + context.l10n.logout, + style: AppTextStyle() + .textButton + .copyWith(color: Theme.of(context).colorScheme.primary), + ), + ), + ], + ); + }, + ); + } +} diff --git a/lib/src/features/settings/widgets/change_profile_image_dialog.dart b/lib/src/features/settings/widgets/change_profile_image_dialog.dart new file mode 100644 index 0000000..f8c9f6c --- /dev/null +++ b/lib/src/features/settings/widgets/change_profile_image_dialog.dart @@ -0,0 +1,119 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:my_app/l10n/l10n.dart'; +import 'package:my_app/src/core/ui/typography.dart'; +import 'package:my_app/src/core/utils/widgets/cache_widget.dart'; +import 'package:my_app/src/core/utils/widgets/generic_button.dart'; +import 'package:my_app/src/features/game/cubit/game_cubit.dart'; +import 'package:my_app/src/features/player/cubit/player_cubit.dart'; +import 'package:my_app/src/features/settings/cubit/settings_cubit.dart'; +import 'package:my_app/src/features/settings/data/profile_images.dart'; +import 'package:sized_context/sized_context.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + +class ChangeProfileImageDialog extends HookWidget { + const ChangeProfileImageDialog({ + super.key, + }); + + @override + Widget build(BuildContext context) { + final indexSelected = useState(-1); + const imageSize = Size(20, 20); + + final borderRadius = BorderRadius.circular(5); + + final colorScheme = Theme.of(context).colorScheme; + + return AlertDialog( + title: Text( + context.l10n.changeProfileImage, + style: + AppTextStyle().dialogTitle.copyWith(color: colorScheme.onSurface), + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + height: context.heightPx * .35, + width: 300, + child: GridView.builder( + itemCount: profileImagesUrl.length, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + mainAxisSpacing: 10, + crossAxisSpacing: 10, + ), + itemBuilder: (context, index) { + final profileImage = profileImagesUrl[index]; + return GestureDetector( + onTap: () => indexSelected.value = index, + child: Container( + decoration: BoxDecoration( + borderRadius: borderRadius, + border: Border.all( + color: indexSelected.value == index + ? colorScheme.primary + : Colors.transparent, + width: 3, + ), + ), + child: CacheWidget( + borderRadius: borderRadius, + imageUrl: profileImage, + size: imageSize, + fit: BoxFit.cover, + ), + ), + ); + }, + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text( + context.l10n.cancel, + style: AppTextStyle() + .textButton + .copyWith(color: colorScheme.onSurface), + ), + ), + BlocConsumer( + listener: (context, state) { + if (state.isError) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + backgroundColor: colorScheme.error, + content: Text(context.l10n.anErrorOccurred), + ), + ); + } + if (state.isSuccess) { + context.read().setUserPlayer(state.player!); + Navigator.of(context).pop(); + } + }, + builder: (context, state) { + return GenericButton( + loading: state.isLoading, + widget: Text( + context.l10n.accept, + style: AppTextStyle().body.copyWith(color: Colors.white), + ), + onPressed: indexSelected.value != -1 + ? () => context + .read() + .setProfileImage(profileImagesUrl[indexSelected.value]) + : null, + ); + }, + ), + ], + ); + } +} diff --git a/lib/src/features/splash/cubit/app_cubit.dart b/lib/src/features/splash/cubit/app_cubit.dart index 7e26411..508c01a 100644 --- a/lib/src/features/splash/cubit/app_cubit.dart +++ b/lib/src/features/splash/cubit/app_cubit.dart @@ -5,7 +5,6 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:injectable/injectable.dart'; import 'package:my_app/src/features/game/data/game_repository.dart'; import 'package:my_app/src/features/game/data/model/game_status.dart'; -import 'package:rxdart/subjects.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; part 'app_state.dart'; diff --git a/lib/src/router/router.dart b/lib/src/router/router.dart index 3afa2b9..51f4fcd 100644 --- a/lib/src/router/router.dart +++ b/lib/src/router/router.dart @@ -1,7 +1,5 @@ // ignore_for_file: use_build_context_synchronously -import 'dart:developer'; - import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -13,7 +11,7 @@ import 'package:my_app/src/features/auth/views/signup_screen.dart'; import 'package:my_app/src/features/game/game_screen.dart'; import 'package:my_app/src/features/game/search_game_screen.dart'; import 'package:my_app/src/features/home/home_screen.dart'; -import 'package:my_app/src/features/splash/loading_profile_data.dart'; +import 'package:my_app/src/features/settings/settings_screen.dart'; import 'package:my_app/src/features/splash/splash_screen.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; @@ -25,6 +23,7 @@ enum AppRoute { home, game, searchGame, + settings, } final rootNavigatorKey = GlobalKey(debugLabel: 'root'); @@ -101,18 +100,18 @@ final class RouterController { ); }, ), + GoRoute( + path: 'settings', + name: AppRoute.settings.name, + pageBuilder: (context, state) { + return adaptivePageRoute( + key: ValueKey(state.pageKey.value), + child: const SettingsScreen(), + ); + }, + ), ], ), - // GoRoute( - // path: '/init-profile-data', - // name: AppRoute.initProfileData.name, - // pageBuilder: (context, state) { - // return adaptivePageRoute( - // key: ValueKey(state.pageKey.value), - // child: const LoadingProfileData(), - // ); - // }, - // ), ], ); } From b4da2a318c0b6c3d4bfd16a61cb93ff66396df12 Mon Sep 17 00:00:00 2001 From: ale24dev Date: Sat, 5 Oct 2024 03:58:56 -0400 Subject: [PATCH 21/30] feat: show rival result on attempt --- lib/src/core/services/game_datasource.dart | 7 +++ lib/src/core/ui/extensions.dart | 16 +++++++ lib/src/core/utils/utils.dart | 7 +++ .../core/utils/widgets/bottom_snackbar.dart | 44 +++++++++++++++++++ lib/src/features/game/cubit/game_cubit.dart | 16 +++++++ .../game/cubit/game_cubit.freezed.dart | 24 +++++++++- lib/src/features/game/cubit/game_state.dart | 1 + .../features/game/data/game_repository.dart | 29 ++++++++++++ lib/src/features/game/data/model/attempt.dart | 7 +++ .../game/domain/mocks/attempt_mock.dart | 2 +- lib/src/features/game/game_screen.dart | 33 +++++++++++++- .../features/home/widgets/game_section.dart | 29 +++++------- .../home/widgets/play_number_card.dart | 26 ++++++++--- pubspec.lock | 8 ++++ pubspec.yaml | 1 + .../functions/create-new-attempt/index.ts | 11 ++--- 16 files changed, 230 insertions(+), 31 deletions(-) create mode 100755 lib/src/core/utils/widgets/bottom_snackbar.dart diff --git a/lib/src/core/services/game_datasource.dart b/lib/src/core/services/game_datasource.dart index 708df11..d422e89 100644 --- a/lib/src/core/services/game_datasource.dart +++ b/lib/src/core/services/game_datasource.dart @@ -1,5 +1,6 @@ import 'package:fpdart/fpdart.dart'; import 'package:my_app/src/core/exceptions.dart'; +import 'package:my_app/src/features/game/data/game_repository.dart'; import 'package:my_app/src/features/game/data/model/attempt.dart'; import 'package:my_app/src/features/game/data/model/game.dart'; import 'package:my_app/src/features/game/data/model/game_status.dart'; @@ -27,4 +28,10 @@ abstract class GameDataSource { Game game, Player player, ); + + void listenAttempts( + Game game, + Player player, + void Function(AttemptCallbackData) callback, + ); } diff --git a/lib/src/core/ui/extensions.dart b/lib/src/core/ui/extensions.dart index 699dd67..13eac99 100644 --- a/lib/src/core/ui/extensions.dart +++ b/lib/src/core/ui/extensions.dart @@ -1,4 +1,9 @@ import 'package:flutter/widgets.dart'; +import 'package:go_router/go_router.dart'; +import 'package:my_app/l10n/l10n.dart'; +import 'package:my_app/src/core/ui/colors.dart'; +import 'package:my_app/src/core/utils/widgets/bottom_snackbar.dart'; +import 'package:top_snackbar_flutter/top_snack_bar.dart'; /// Adds extensions to num to make creating durations more succint: /// @@ -24,3 +29,14 @@ extension TextStyleX on TextStyle { TextStyle color(Color color) => copyWith(color: color); } + +extension BuildContextX on BuildContext { + void genericMessage({String? message, Widget? widget}) { + if (mounted) { + showTopSnackBar( + Overlay.of(this), + BottomSnackbar(message: message, widget: widget), + ); + } + } +} diff --git a/lib/src/core/utils/utils.dart b/lib/src/core/utils/utils.dart index df64a54..a284f2e 100644 --- a/lib/src/core/utils/utils.dart +++ b/lib/src/core/utils/utils.dart @@ -1,3 +1,6 @@ +import 'package:flutter/material.dart'; +import 'package:my_app/l10n/l10n.dart'; + abstract class Utils { static bool isValidPlayerNumber(String value) { final values = value.split(''); @@ -8,4 +11,8 @@ abstract class Utils { return uniqueValues.length == values.length; } + + static String attemptResult(BuildContext context, int cows, int bulls) { + return '${context.l10n.cowPlay}$cows ${context.l10n.bullPlay}$bulls'; + } } diff --git a/lib/src/core/utils/widgets/bottom_snackbar.dart b/lib/src/core/utils/widgets/bottom_snackbar.dart new file mode 100755 index 0000000..73670fe --- /dev/null +++ b/lib/src/core/utils/widgets/bottom_snackbar.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:my_app/src/core/ui/typography.dart'; + +class BottomSnackbar extends StatelessWidget { + const BottomSnackbar({ + this.message, + super.key, + this.backgroundColor, + this.widget, + this.height = 40, + this.textColor = Colors.white, + }); + + final Color? backgroundColor; + final String? message; + final Widget? widget; + final double? height; + final Color? textColor; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return Card( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)), + color: backgroundColor ?? colorScheme.surface, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), + child: Align( + child: SizedBox( + child: widget ?? + Text( + message!, + style: AppTextStyle().body.copyWith( + fontSize: 12, + color: textColor ?? Colors.white, + ), + textAlign: TextAlign.center, + ), + ), + ), + ), + ); + } +} diff --git a/lib/src/features/game/cubit/game_cubit.dart b/lib/src/features/game/cubit/game_cubit.dart index 4a477ed..cefdcc5 100644 --- a/lib/src/features/game/cubit/game_cubit.dart +++ b/lib/src/features/game/cubit/game_cubit.dart @@ -35,6 +35,16 @@ class GameCubit extends Cubit { final GameRepository _gameRepository; final PlayerRepository _playerRepository; + void _listenAttempts(Game game, Player player) { + _gameRepository.listenAttempts(game, player, (callbackData) { + emit(state.copyWith(lastRivalResult: callbackData)); + }); + } + + void removeLastRivalResult() { + emit(state.copyWith(lastRivalResult: null)); + } + void _listenPlayerNumberChanges() { _playerRepository.listenPlayerNumberChanges(state.player!.id, (callbackData) { @@ -125,6 +135,9 @@ class GameCubit extends Cubit { await Future.delayed(Duration.zero, () { if (game!.isInProgress) { + /// Listen to attempts in game + _listenAttempts(game, state.player!); + getAttemptsInGameByPlayer(game, state.player!); } }); @@ -145,6 +158,9 @@ class GameCubit extends Cubit { emit( state.copyWith(stateStatus: GameStateStatus.success, game: game), ); + + /// Listen to attempts in game + _listenAttempts(game!, state.player!); }, ); }); diff --git a/lib/src/features/game/cubit/game_cubit.freezed.dart b/lib/src/features/game/cubit/game_cubit.freezed.dart index 6d7c92a..1df84bf 100644 --- a/lib/src/features/game/cubit/game_cubit.freezed.dart +++ b/lib/src/features/game/cubit/game_cubit.freezed.dart @@ -21,6 +21,7 @@ mixin _$GameState { List get listGameStatus => throw _privateConstructorUsedError; List get listAttempts => throw _privateConstructorUsedError; bool get selectSecretNumberShowed => throw _privateConstructorUsedError; + (int, int)? get lastRivalResult => throw _privateConstructorUsedError; Player? get player => throw _privateConstructorUsedError; DateTime? get serverTime => throw _privateConstructorUsedError; @@ -42,6 +43,7 @@ abstract class $GameStateCopyWith<$Res> { List listGameStatus, List listAttempts, bool selectSecretNumberShowed, + (int, int)? lastRivalResult, Player? player, DateTime? serverTime}); } @@ -66,6 +68,7 @@ class _$GameStateCopyWithImpl<$Res, $Val extends GameState> Object? listGameStatus = null, Object? listAttempts = null, Object? selectSecretNumberShowed = null, + Object? lastRivalResult = freezed, Object? player = freezed, Object? serverTime = freezed, }) { @@ -90,6 +93,10 @@ class _$GameStateCopyWithImpl<$Res, $Val extends GameState> ? _value.selectSecretNumberShowed : selectSecretNumberShowed // ignore: cast_nullable_to_non_nullable as bool, + lastRivalResult: freezed == lastRivalResult + ? _value.lastRivalResult + : lastRivalResult // ignore: cast_nullable_to_non_nullable + as (int, int)?, player: freezed == player ? _value.player : player // ignore: cast_nullable_to_non_nullable @@ -116,6 +123,7 @@ abstract class _$$GameStateImplCopyWith<$Res> List listGameStatus, List listAttempts, bool selectSecretNumberShowed, + (int, int)? lastRivalResult, Player? player, DateTime? serverTime}); } @@ -138,6 +146,7 @@ class __$$GameStateImplCopyWithImpl<$Res> Object? listGameStatus = null, Object? listAttempts = null, Object? selectSecretNumberShowed = null, + Object? lastRivalResult = freezed, Object? player = freezed, Object? serverTime = freezed, }) { @@ -162,6 +171,10 @@ class __$$GameStateImplCopyWithImpl<$Res> ? _value.selectSecretNumberShowed : selectSecretNumberShowed // ignore: cast_nullable_to_non_nullable as bool, + lastRivalResult: freezed == lastRivalResult + ? _value.lastRivalResult + : lastRivalResult // ignore: cast_nullable_to_non_nullable + as (int, int)?, player: freezed == player ? _value.player : player // ignore: cast_nullable_to_non_nullable @@ -183,6 +196,7 @@ class _$GameStateImpl extends _GameState { final List listGameStatus = const [], final List listAttempts = const [], this.selectSecretNumberShowed = false, + this.lastRivalResult, this.player, this.serverTime}) : _listGameStatus = listGameStatus, @@ -216,13 +230,15 @@ class _$GameStateImpl extends _GameState { @JsonKey() final bool selectSecretNumberShowed; @override + final (int, int)? lastRivalResult; + @override final Player? player; @override final DateTime? serverTime; @override String toString() { - return 'GameState(game: $game, stateStatus: $stateStatus, listGameStatus: $listGameStatus, listAttempts: $listAttempts, selectSecretNumberShowed: $selectSecretNumberShowed, player: $player, serverTime: $serverTime)'; + return 'GameState(game: $game, stateStatus: $stateStatus, listGameStatus: $listGameStatus, listAttempts: $listAttempts, selectSecretNumberShowed: $selectSecretNumberShowed, lastRivalResult: $lastRivalResult, player: $player, serverTime: $serverTime)'; } @override @@ -240,6 +256,8 @@ class _$GameStateImpl extends _GameState { (identical( other.selectSecretNumberShowed, selectSecretNumberShowed) || other.selectSecretNumberShowed == selectSecretNumberShowed) && + (identical(other.lastRivalResult, lastRivalResult) || + other.lastRivalResult == lastRivalResult) && (identical(other.player, player) || other.player == player) && (identical(other.serverTime, serverTime) || other.serverTime == serverTime)); @@ -253,6 +271,7 @@ class _$GameStateImpl extends _GameState { const DeepCollectionEquality().hash(_listGameStatus), const DeepCollectionEquality().hash(_listAttempts), selectSecretNumberShowed, + lastRivalResult, player, serverTime); @@ -272,6 +291,7 @@ abstract class _GameState extends GameState { final List listGameStatus, final List listAttempts, final bool selectSecretNumberShowed, + final (int, int)? lastRivalResult, final Player? player, final DateTime? serverTime}) = _$GameStateImpl; const _GameState._() : super._(); @@ -287,6 +307,8 @@ abstract class _GameState extends GameState { @override bool get selectSecretNumberShowed; @override + (int, int)? get lastRivalResult; + @override Player? get player; @override DateTime? get serverTime; diff --git a/lib/src/features/game/cubit/game_state.dart b/lib/src/features/game/cubit/game_state.dart index ca75f1e..d581b40 100644 --- a/lib/src/features/game/cubit/game_state.dart +++ b/lib/src/features/game/cubit/game_state.dart @@ -10,6 +10,7 @@ class GameState with _$GameState { @Default([]) final List listGameStatus, @Default([]) final List listAttempts, @Default(false) final bool selectSecretNumberShowed, + final AttemptCallbackData? lastRivalResult, Player? player, DateTime? serverTime, }) = _GameState; diff --git a/lib/src/features/game/data/game_repository.dart b/lib/src/features/game/data/game_repository.dart index 616c05b..c091d3a 100644 --- a/lib/src/features/game/data/game_repository.dart +++ b/lib/src/features/game/data/game_repository.dart @@ -12,6 +12,8 @@ import 'package:my_app/src/features/game/data/model/game_status.dart'; import 'package:my_app/src/features/player/data/model/player.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; +typedef AttemptCallbackData = (int bull, int cow); + @singleton class GameRepository extends GameDataSource { GameRepository(this._supabaseServiceImpl, this._client); @@ -125,4 +127,31 @@ class GameRepository extends GameDataSource { queryOption: QueryOption.select, ); } + + @override + void listenAttempts( + Game game, + Player player, + void Function(AttemptCallbackData attemptCallback) callback, + ) { + final myChannel = _client.channel('attempt_channel'); + + myChannel + .onPostgresChanges( + event: PostgresChangeEvent.all, + schema: 'public', + table: 'attempt', + callback: (payload) { + + final rival = game.getRivalPlayerNumber(player); + + final newRecord = payload.newRecord['player'] as String; + if(newRecord != rival.player.id) return; + final cows = payload.newRecord['cows'] as int; + final bulls = payload.newRecord['cows'] as int; + return callback((cows, bulls)); + }, + ) + .subscribe(); + } } diff --git a/lib/src/features/game/data/model/attempt.dart b/lib/src/features/game/data/model/attempt.dart index ccedbe4..6874a5a 100644 --- a/lib/src/features/game/data/model/attempt.dart +++ b/lib/src/features/game/data/model/attempt.dart @@ -1,6 +1,9 @@ +import 'package:flutter/widgets.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:my_app/l10n/l10n.dart'; import 'package:my_app/src/core/supabase/table_interface.dart'; import 'package:my_app/src/core/utils/object_extensions.dart'; +import 'package:my_app/src/features/auth/views/auth_screen.dart'; import 'package:my_app/src/features/game/data/model/game.dart'; import 'package:my_app/src/features/player/data/model/player.dart'; @@ -46,6 +49,10 @@ class Attempt with TableInterface { } extension AttemptX on Attempt { + String attemptResult(BuildContext context, {int? cows, int? bulls}) { + return '${context.l10n.cowPlay}${cows ?? this.cows} ${context.l10n.bullPlay}${bulls ?? this.bulls}'; + } + Attempt copyWith({ int? id, Game? game, diff --git a/lib/src/features/game/domain/mocks/attempt_mock.dart b/lib/src/features/game/domain/mocks/attempt_mock.dart index dcbd474..826194c 100644 --- a/lib/src/features/game/domain/mocks/attempt_mock.dart +++ b/lib/src/features/game/domain/mocks/attempt_mock.dart @@ -10,7 +10,7 @@ List getAttemptsMock(int quantity) { game: Game(id: 0, status: GameStatus.empty()), bulls: 0, cows: 0, - number: [1, 2, 3, 4], + number: [8, 8, 8, 8], player: Player.empty(), ); }); diff --git a/lib/src/features/game/game_screen.dart b/lib/src/features/game/game_screen.dart index 7cbfa5a..d2abcc6 100644 --- a/lib/src/features/game/game_screen.dart +++ b/lib/src/features/game/game_screen.dart @@ -6,8 +6,10 @@ import 'package:lottie/lottie.dart'; import 'package:my_app/l10n/l10n.dart'; import 'package:my_app/resources/resources.dart'; import 'package:my_app/src/core/extensions/list.dart'; +import 'package:my_app/src/core/ui/extensions.dart'; import 'package:my_app/src/core/ui/typography.dart'; import 'package:my_app/src/core/utils/object_extensions.dart'; +import 'package:my_app/src/core/utils/utils.dart'; import 'package:my_app/src/features/game/cubit/game_cubit.dart'; import 'package:my_app/src/features/game/data/model/game.dart'; import 'package:my_app/src/features/game/utils/game_utils.dart'; @@ -33,8 +35,37 @@ class _GameScreenState extends State { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; return Scaffold( - body: BlocBuilder( + body: BlocConsumer( + listener: (context, state) { + if (state.lastRivalResult.isNotNull) { + context.genericMessage( + widget: RichText( + text: TextSpan( + style: AppTextStyle() + .body + .copyWith(color: colorScheme.onSurface), + children: [ + const TextSpan(text: 'Tu rival acaba de sumar: '), + TextSpan( + text: Utils.attemptResult( + context, + state.lastRivalResult!.$1, + state.lastRivalResult!.$2, + ), + style: AppTextStyle().body.copyWith( + fontWeight: FontWeight.bold, + color: colorScheme.primary, + ), + ), + ], + ), + ), + ); + context.read().removeLastRivalResult(); + } + }, builder: (context, state) { if (!state.isLoading && state.game.isNotNull) { _gameStatusChanged(state); diff --git a/lib/src/features/home/widgets/game_section.dart b/lib/src/features/home/widgets/game_section.dart index 4d53e6b..aa6808a 100644 --- a/lib/src/features/home/widgets/game_section.dart +++ b/lib/src/features/home/widgets/game_section.dart @@ -1,22 +1,22 @@ import 'dart:async'; -import 'dart:developer'; + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_gutter/flutter_gutter.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:my_app/l10n/l10n.dart'; import 'package:my_app/src/core/extensions/string.dart'; -import 'package:my_app/src/features/game/domain/mocks/attempt_mock.dart'; -import 'package:my_app/src/features/player/cubit/player_cubit.dart'; -import 'package:my_app/src/features/game/data/model/game.dart'; -import 'package:my_app/src/features/game/widgets/game_turn_widget.dart'; -import 'package:my_app/src/core/ui/typography.dart'; import 'package:my_app/src/core/ui/device.dart'; -import 'package:my_app/src/features/home/widgets/play_number_card.dart'; -import 'package:my_app/src/features/home/widgets/otp_fields.dart'; +import 'package:my_app/src/core/ui/typography.dart'; +import 'package:my_app/src/core/utils/utils.dart'; import 'package:my_app/src/features/game/cubit/game_cubit.dart'; import 'package:my_app/src/features/game/data/model/attempt.dart'; -import 'package:flutter_gutter/flutter_gutter.dart'; -import 'package:my_app/src/core/utils/utils.dart'; +import 'package:my_app/src/features/game/data/model/game.dart'; +import 'package:my_app/src/features/game/domain/mocks/attempt_mock.dart'; +import 'package:my_app/src/features/game/widgets/game_turn_widget.dart'; +import 'package:my_app/src/features/home/widgets/otp_fields.dart'; +import 'package:my_app/src/features/home/widgets/play_number_card.dart'; +import 'package:my_app/src/features/player/cubit/player_cubit.dart'; import 'package:skeletonizer/skeletonizer.dart'; class GameSection extends HookWidget { @@ -43,7 +43,7 @@ class GameSection extends HookWidget { useEffect( () { Timer? timer; - if (ownPlayerNumber.isTurn) { + if (gameState.isGameInProgress && ownPlayerNumber.isTurn) { if (isFirstRender.value) { initialTimeLeft.value = calculateInitialTimeLeft( ownPlayerNumber.startedTime, @@ -61,7 +61,7 @@ class GameSection extends HookWidget { } return timer?.cancel; }, - [ownPlayerNumber.isTurn], + [gameState.isGameInProgress, ownPlayerNumber.isTurn], ); final colorScheme = Theme.of(context).colorScheme; @@ -120,14 +120,9 @@ class GameSection extends HookWidget { return 0; // Retorna 0 si el tiempo ha pasado } - log('finishedTime: $finishedTimeUtc'); - log('currentTime: $currentTimeUtc'); - // Calcula la diferencia en segundos final timeLeft = finishedTimeUtc.difference(currentTimeUtc).inSeconds; - log('Initial time left: $timeLeft seconds'); - return timeLeft; } } diff --git a/lib/src/features/home/widgets/play_number_card.dart b/lib/src/features/home/widgets/play_number_card.dart index d1f6bd8..ddb59b2 100644 --- a/lib/src/features/home/widgets/play_number_card.dart +++ b/lib/src/features/home/widgets/play_number_card.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:flutter_gutter/flutter_gutter.dart'; -import 'package:my_app/l10n/l10n.dart'; import 'package:my_app/src/core/ui/theme.dart'; import 'package:my_app/src/core/ui/typography.dart'; import 'package:my_app/src/features/game/data/model/attempt.dart'; @@ -28,7 +27,10 @@ class PlayNumberCard extends StatelessWidget { child: Center( child: Text( index.toString(), - style: AppTextStyle().body.copyWith(color: Colors.black), + style: AppTextStyle().body.copyWith( + fontFamily: AppTextStyle.secondaryFontFamily, + color: Colors.black, + ), ), ), ), @@ -50,9 +52,13 @@ class PlayNumberCard extends StatelessWidget { return _NumberPlay(attempt.number[index].toString()); }), const Gutter(), - Text('${attempt.cows}${context.l10n.cowPlay}'), - const Gutter(), - Text('${attempt.bulls}${context.l10n.bullPlay}'), + Text( + attempt.attemptResult(context), + style: AppTextStyle().body.copyWith( + color: colorScheme.primary, + fontFamily: AppTextStyle.secondaryFontFamily, + ), + ), ], ), ), @@ -80,7 +86,15 @@ class _NumberPlay extends StatelessWidget { border: Border.all(color: Theme.of(context).colorScheme.primary), borderRadius: BorderRadius.circular(5), ), - child: Center(child: Text(number)), + child: Center( + child: Text( + number, + style: AppTextStyle().body.copyWith( + fontFamily: AppTextStyle.secondaryFontFamily, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ), ), ); } diff --git a/pubspec.lock b/pubspec.lock index 843f2eb..f6fbfa3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1151,6 +1151,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + top_snackbar_flutter: + dependency: "direct main" + description: + name: top_snackbar_flutter + sha256: "22d14664a13db6ac714934c3382bd8d4daa57fb888a672f922df71981c5a5cb2" + url: "https://pub.dev" + source: hosted + version: "3.1.0" typed_data: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 1262292..8f21c9c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -28,6 +28,7 @@ dependencies: flutter_hooks: ^0.20.5 skeletonizer: ^1.4.2 + top_snackbar_flutter: ^3.1.0 lottie: ^3.1.2 rxdart: ^0.28.0 diff --git a/supabase/functions/create-new-attempt/index.ts b/supabase/functions/create-new-attempt/index.ts index 2bf4a51..8fc49ab 100644 --- a/supabase/functions/create-new-attempt/index.ts +++ b/supabase/functions/create-new-attempt/index.ts @@ -142,6 +142,12 @@ async function swapTurns(supabase: any, ownPlayerNumber: any, opponent: any, gam // Verificar si el oponente es un bot if (opponent.player.is_bot) { + + // Esperar un tiempo aleatorio entre 10 y 30 segundos + const randomDelay = Math.floor(Math.random() * (30000 - 10000 + 1)) + 10000; // Entre 10 y 30 segundos + console.log(`Waiting for ${randomDelay / 1000} seconds for the bot's turn...`); + await new Promise((resolve) => setTimeout(resolve, randomDelay)); + console.log("Opponent is a bot, waiting for the bot's turn..."); // Insertar intento para el bot @@ -158,11 +164,6 @@ async function swapTurns(supabase: any, ownPlayerNumber: any, opponent: any, gam } console.log("Bot attempt inserted successfully."); - // Esperar un tiempo aleatorio entre 10 y 30 segundos - const randomDelay = Math.floor(Math.random() * (30000 - 10000 + 1)) + 10000; // Entre 10 y 30 segundos - console.log(`Waiting for ${randomDelay / 1000} seconds for the bot's turn...`); - await new Promise((resolve) => setTimeout(resolve, randomDelay)); - console.log("Reverting opponent turn..."); const { error: revertTurnError } = await supabase .from("player_number") From b32f66bd168b5414e06b6f20c06ef75e18a42c3e Mon Sep 17 00:00:00 2001 From: ale24dev Date: Sun, 6 Oct 2024 00:35:50 -0400 Subject: [PATCH 22/30] add: settings preferences --- lib/l10n/arb/app_en.arb | 10 +- lib/l10n/arb/app_es.arb | 10 +- lib/main.dart | 7 +- lib/src/app.dart | 20 +++- .../core/di/dependency_injection.config.dart | 13 +-- lib/src/core/preferences/preferences.dart | 38 ++++++++ .../core/services/settings_datasource.dart | 11 ++- lib/src/core/utils/utils.dart | 11 +++ lib/src/features/ranking/leaderboard.dart | 16 ++- .../settings/cubit/settings_cubit.dart | 31 +++++- .../cubit/settings_cubit.freezed.dart | 57 ++++++++++- .../settings/cubit/settings_state.dart | 15 +++ .../settings/data/settings_repository.dart | 37 +++++-- .../features/settings/settings_screen.dart | 86 +++++++++++----- .../widgets/change_language_dialog.dart | 97 +++++++++++++++++++ lib/src/features/splash/splash_screen.dart | 2 +- 16 files changed, 404 insertions(+), 57 deletions(-) create mode 100644 lib/src/core/preferences/preferences.dart create mode 100644 lib/src/features/settings/widgets/change_language_dialog.dart diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 1c1c3d2..f42210e 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -67,5 +67,13 @@ "settings": "Settings", - "changeProfileImage": "Change profile image" + "changeProfileImage": "Change profile image", + "profileImage": "Profile image", + + "changeLanguage": "Change language", + "language": "Language", + "english": "English", + "spanish": "Spanish", + "theme": "Theme", + "darkMode": "Dark mode" } \ No newline at end of file diff --git a/lib/l10n/arb/app_es.arb b/lib/l10n/arb/app_es.arb index 1f390ae..30460ba 100644 --- a/lib/l10n/arb/app_es.arb +++ b/lib/l10n/arb/app_es.arb @@ -67,5 +67,13 @@ "settings": "Configuración", - "changeProfileImage": "Cambiar imagen de perfil" + "changeProfileImage": "Cambiar imagen de perfil", + "profileImage": "Imagen de perfil", + + "changeLanguage": "Cambiar idioma", + "language": "Idioma", + "english": "Inglés", + "spanish": "Español", + "theme": "Tema", + "darkMode": "Modo oscuro" } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 0180f8c..f835d9c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:my_app/src/app.dart'; import 'package:my_app/src/core/di/dependency_injection.dart'; +import 'package:my_app/src/features/settings/cubit/settings_cubit.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; Future main() async { @@ -15,6 +17,9 @@ Future main() async { configureDependencies(); runApp( - const MyApp(), + BlocProvider( + create: (context) => getIt.get(), + child: const MyApp(), + ), ); } diff --git a/lib/src/app.dart b/lib/src/app.dart index b00c526..2bea960 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -1,3 +1,5 @@ +import 'dart:developer'; + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:my_app/l10n/l10n.dart'; @@ -11,12 +13,20 @@ import 'package:my_app/src/features/settings/cubit/settings_cubit.dart'; import 'package:my_app/src/features/splash/cubit/app_cubit.dart'; import 'package:my_app/src/router/router.dart'; -class MyApp extends StatelessWidget { +class MyApp extends StatefulWidget { const MyApp({super.key}); + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + final router = RouterController(getIt.get()).router; @override Widget build(BuildContext context) { final theme = AppTheme(); + final settings = BlocProvider.of(context, listen: true); + log('Settings: ${settings.state.theme}'); return MultiBlocProvider( providers: [ BlocProvider(create: (_) => getIt.get()), @@ -24,13 +34,15 @@ class MyApp extends StatelessWidget { BlocProvider(create: (_) => getIt.get()), BlocProvider(create: (_) => getIt.get()), BlocProvider(create: (_) => getIt.get()), - BlocProvider(create: (_) => getIt.get()), + // BlocProvider(create: (_) => getIt.get()), ], child: MaterialApp.router( debugShowCheckedModeBanner: false, - theme: theme.dark, + theme: theme.light, darkTheme: theme.dark, - routerConfig: RouterController(getIt.get()).router, + locale: settings.state.locale, + themeMode: settings.state.theme, + routerConfig: router, localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, ), diff --git a/lib/src/core/di/dependency_injection.config.dart b/lib/src/core/di/dependency_injection.config.dart index d9df460..8b6b991 100644 --- a/lib/src/core/di/dependency_injection.config.dart +++ b/lib/src/core/di/dependency_injection.config.dart @@ -12,6 +12,7 @@ import 'package:get_it/get_it.dart' as _i174; import 'package:injectable/injectable.dart' as _i526; import 'package:my_app/src/core/di/modules/modules.dart' as _i560; import 'package:my_app/src/core/interceptor.dart' as _i330; +import 'package:my_app/src/core/preferences/preferences.dart' as _i220; import 'package:my_app/src/core/services/settings_datasource.dart' as _i94; import 'package:my_app/src/core/supabase/client.dart' as _i880; import 'package:my_app/src/features/auth/cubit/auth_cubit.dart' as _i992; @@ -52,10 +53,14 @@ extension GetItInjectableX on _i174.GetIt { ); gh.singleton<_i330.SupabaseServiceImpl>(() => _i330.SupabaseServiceImpl()); gh.lazySingleton<_i454.SupabaseClient>(() => supabaseModule.client); + gh.singleton<_i220.Preferences>( + () => _i220.Preferences(gh<_i460.SharedPreferences>())); gh.singleton<_i427.AuthRepository>( () => _i427.AuthRepository(gh<_i454.SupabaseClient>())); gh.singleton<_i63.RouterController>( () => _i63.RouterController(gh<_i454.SupabaseClient>())); + gh.singleton<_i94.SettingsLocalDatasource>( + () => _i621.SettingsRepository(gh<_i220.Preferences>())); gh.singleton<_i34.RankingRepository>(() => _i34.RankingRepository( gh<_i330.SupabaseServiceImpl>(), gh<_i454.SupabaseClient>(), @@ -68,17 +73,11 @@ extension GetItInjectableX on _i174.GetIt { gh<_i330.SupabaseServiceImpl>(), gh<_i454.SupabaseClient>(), )); - gh.singleton<_i94.SettingsDatasource>(() => _i621.SettingsRepository( - gh<_i330.SupabaseServiceImpl>(), - gh<_i454.SupabaseClient>(), - )); gh.factory<_i584.GameCubit>(() => _i584.GameCubit( gh<_i454.SupabaseClient>(), gh<_i34.GameRepository>(), gh<_i406.PlayerRepository>(), )); - gh.factory<_i303.SettingsCubit>( - () => _i303.SettingsCubit(gh<_i94.SettingsDatasource>())); gh.factory<_i931.RankingCubit>( () => _i931.RankingCubit(gh<_i34.RankingRepository>())); gh.factory<_i126.PlayerCubit>(() => _i126.PlayerCubit( @@ -89,6 +88,8 @@ extension GetItInjectableX on _i174.GetIt { gh<_i427.AuthRepository>(), gh<_i454.SupabaseClient>(), )); + gh.factory<_i303.SettingsCubit>( + () => _i303.SettingsCubit(gh<_i94.SettingsLocalDatasource>())); gh.factory<_i1038.AppCubit>(() => _i1038.AppCubit( gh<_i34.GameRepository>(), gh<_i454.SupabaseClient>(), diff --git a/lib/src/core/preferences/preferences.dart b/lib/src/core/preferences/preferences.dart new file mode 100644 index 0000000..08f92d8 --- /dev/null +++ b/lib/src/core/preferences/preferences.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:injectable/injectable.dart'; +import 'package:my_app/src/core/utils/utils.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +enum PrefsKey { + darkMode, + language, +} + +@singleton +class Preferences { + Preferences(this._prefs); + + final SharedPreferences _prefs; + + ThemeMode getTheme() { + final isDarkMode = _prefs.getBool(PrefsKey.darkMode.name) ?? true; + + return isDarkMode ? ThemeMode.dark : ThemeMode.light; + } + + void changeTheme() { + final theme = _prefs.getBool(PrefsKey.darkMode.name) ?? true; + + _prefs.setBool(PrefsKey.darkMode.name, !theme); + } + + Locale getLanguage() { + final languageString = _prefs.getString(PrefsKey.language.name); + + return Utils.getLocaleByCode(languageString); + } + + void setLanguage(String language) { + _prefs.setString(PrefsKey.language.name, language); + } +} diff --git a/lib/src/core/services/settings_datasource.dart b/lib/src/core/services/settings_datasource.dart index 5296f4e..6941b4c 100644 --- a/lib/src/core/services/settings_datasource.dart +++ b/lib/src/core/services/settings_datasource.dart @@ -1,2 +1,11 @@ +import 'package:flutter/material.dart'; -abstract class SettingsDatasource {} +abstract class SettingsLocalDatasource { + Locale changeLanguage(String language); + + ThemeMode changeTheme(); + + Locale getLanguage(); + + ThemeMode getTheme(); +} diff --git a/lib/src/core/utils/utils.dart b/lib/src/core/utils/utils.dart index a284f2e..f575396 100644 --- a/lib/src/core/utils/utils.dart +++ b/lib/src/core/utils/utils.dart @@ -15,4 +15,15 @@ abstract class Utils { static String attemptResult(BuildContext context, int cows, int bulls) { return '${context.l10n.cowPlay}$cows ${context.l10n.bullPlay}$bulls'; } + + static Locale getLocaleByCode(String? languageString) { + switch (languageString) { + case 'en': + return const Locale('en', 'US'); + case 'es': + return const Locale('es', 'ES'); + default: + return const Locale('en', 'US'); + } + } } diff --git a/lib/src/features/ranking/leaderboard.dart b/lib/src/features/ranking/leaderboard.dart index 7261bfc..fc45629 100644 --- a/lib/src/features/ranking/leaderboard.dart +++ b/lib/src/features/ranking/leaderboard.dart @@ -37,7 +37,6 @@ class _LeaderboardWidgetState extends State { final rankings = state.isLoading ? rankingMock : state.ranking; return Column( children: [ - const GutterLarge(), Text( context.l10n.leaderboard, style: AppTextStyle().bodyLarge.copyWith( @@ -59,13 +58,22 @@ class _LeaderboardWidgetState extends State { Skeletonizer( enabled: state.isLoading, child: ListView.builder( + padding: EdgeInsets.zero, itemCount: rankings.length, itemBuilder: (context, index) { final ranking = rankings[index]; if (index <= 2) return const SizedBox.shrink(); - return RankCard( - colorScheme: colorScheme, - ranking: ranking, + return Column( + children: [ + if (index == 3) + const SizedBox.square(dimension: 40), + RankCard( + colorScheme: colorScheme, + ranking: ranking, + ), + if (index == rankings.length - 1) + const SizedBox.square(dimension: 40), + ], ); }, ), diff --git a/lib/src/features/settings/cubit/settings_cubit.dart b/lib/src/features/settings/cubit/settings_cubit.dart index e7a1b7e..94c30c5 100644 --- a/lib/src/features/settings/cubit/settings_cubit.dart +++ b/lib/src/features/settings/cubit/settings_cubit.dart @@ -1,4 +1,5 @@ import 'package:bloc/bloc.dart'; +import 'package:flutter/material.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:injectable/injectable.dart'; import 'package:my_app/src/core/exceptions.dart'; @@ -9,7 +10,33 @@ part 'settings_cubit.freezed.dart'; @injectable class SettingsCubit extends Cubit { - SettingsCubit(this._settingsDatasource) : super(const SettingsState()); + SettingsCubit(this._settingsDatasource) : super(const SettingsState()) { + _initData(); + } - final SettingsDatasource _settingsDatasource; + final SettingsLocalDatasource _settingsDatasource; + + void changeLanguage(String language) { + final locale = _settingsDatasource.changeLanguage(language); + + emit(state.copyWith(locale: locale)); + } + + void changeTheme() { + final themeMode = _settingsDatasource.changeTheme(); + emit(state.copyWith(theme: themeMode)); + } + + void _initData() { + getTheme(); + getLanguage(); + } + + void getTheme() { + emit(state.copyWith(theme: _settingsDatasource.getTheme())); + } + + void getLanguage() { + emit(state.copyWith(locale: _settingsDatasource.getLanguage())); + } } diff --git a/lib/src/features/settings/cubit/settings_cubit.freezed.dart b/lib/src/features/settings/cubit/settings_cubit.freezed.dart index 411bd58..f3d2285 100644 --- a/lib/src/features/settings/cubit/settings_cubit.freezed.dart +++ b/lib/src/features/settings/cubit/settings_cubit.freezed.dart @@ -17,6 +17,8 @@ final _privateConstructorUsedError = UnsupportedError( /// @nodoc mixin _$SettingsState { SettingsStateStatus get stateStatus => throw _privateConstructorUsedError; + Locale get locale => throw _privateConstructorUsedError; + ThemeMode? get theme => throw _privateConstructorUsedError; AppException? get error => throw _privateConstructorUsedError; /// Create a copy of SettingsState @@ -32,7 +34,11 @@ abstract class $SettingsStateCopyWith<$Res> { SettingsState value, $Res Function(SettingsState) then) = _$SettingsStateCopyWithImpl<$Res, SettingsState>; @useResult - $Res call({SettingsStateStatus stateStatus, AppException? error}); + $Res call( + {SettingsStateStatus stateStatus, + Locale locale, + ThemeMode? theme, + AppException? error}); } /// @nodoc @@ -51,6 +57,8 @@ class _$SettingsStateCopyWithImpl<$Res, $Val extends SettingsState> @override $Res call({ Object? stateStatus = null, + Object? locale = null, + Object? theme = freezed, Object? error = freezed, }) { return _then(_value.copyWith( @@ -58,6 +66,14 @@ class _$SettingsStateCopyWithImpl<$Res, $Val extends SettingsState> ? _value.stateStatus : stateStatus // ignore: cast_nullable_to_non_nullable as SettingsStateStatus, + locale: null == locale + ? _value.locale + : locale // ignore: cast_nullable_to_non_nullable + as Locale, + theme: freezed == theme + ? _value.theme + : theme // ignore: cast_nullable_to_non_nullable + as ThemeMode?, error: freezed == error ? _value.error : error // ignore: cast_nullable_to_non_nullable @@ -74,7 +90,11 @@ abstract class _$$SettingsStateImplCopyWith<$Res> __$$SettingsStateImplCopyWithImpl<$Res>; @override @useResult - $Res call({SettingsStateStatus stateStatus, AppException? error}); + $Res call( + {SettingsStateStatus stateStatus, + Locale locale, + ThemeMode? theme, + AppException? error}); } /// @nodoc @@ -91,6 +111,8 @@ class __$$SettingsStateImplCopyWithImpl<$Res> @override $Res call({ Object? stateStatus = null, + Object? locale = null, + Object? theme = freezed, Object? error = freezed, }) { return _then(_$SettingsStateImpl( @@ -98,6 +120,14 @@ class __$$SettingsStateImplCopyWithImpl<$Res> ? _value.stateStatus : stateStatus // ignore: cast_nullable_to_non_nullable as SettingsStateStatus, + locale: null == locale + ? _value.locale + : locale // ignore: cast_nullable_to_non_nullable + as Locale, + theme: freezed == theme + ? _value.theme + : theme // ignore: cast_nullable_to_non_nullable + as ThemeMode?, error: freezed == error ? _value.error : error // ignore: cast_nullable_to_non_nullable @@ -110,18 +140,26 @@ class __$$SettingsStateImplCopyWithImpl<$Res> class _$SettingsStateImpl extends _SettingsState { const _$SettingsStateImpl( - {this.stateStatus = SettingsStateStatus.initial, this.error}) + {this.stateStatus = SettingsStateStatus.initial, + this.locale = const Locale('en'), + this.theme, + this.error}) : super._(); @override @JsonKey() final SettingsStateStatus stateStatus; @override + @JsonKey() + final Locale locale; + @override + final ThemeMode? theme; + @override final AppException? error; @override String toString() { - return 'SettingsState(stateStatus: $stateStatus, error: $error)'; + return 'SettingsState(stateStatus: $stateStatus, locale: $locale, theme: $theme, error: $error)'; } @override @@ -131,11 +169,14 @@ class _$SettingsStateImpl extends _SettingsState { other is _$SettingsStateImpl && (identical(other.stateStatus, stateStatus) || other.stateStatus == stateStatus) && + (identical(other.locale, locale) || other.locale == locale) && + (identical(other.theme, theme) || other.theme == theme) && (identical(other.error, error) || other.error == error)); } @override - int get hashCode => Object.hash(runtimeType, stateStatus, error); + int get hashCode => + Object.hash(runtimeType, stateStatus, locale, theme, error); /// Create a copy of SettingsState /// with the given fields replaced by the non-null parameter values. @@ -149,12 +190,18 @@ class _$SettingsStateImpl extends _SettingsState { abstract class _SettingsState extends SettingsState { const factory _SettingsState( {final SettingsStateStatus stateStatus, + final Locale locale, + final ThemeMode? theme, final AppException? error}) = _$SettingsStateImpl; const _SettingsState._() : super._(); @override SettingsStateStatus get stateStatus; @override + Locale get locale; + @override + ThemeMode? get theme; + @override AppException? get error; /// Create a copy of SettingsState diff --git a/lib/src/features/settings/cubit/settings_state.dart b/lib/src/features/settings/cubit/settings_state.dart index 1a416c8..9b48469 100644 --- a/lib/src/features/settings/cubit/settings_state.dart +++ b/lib/src/features/settings/cubit/settings_state.dart @@ -2,10 +2,25 @@ part of 'settings_cubit.dart'; enum SettingsStateStatus { initial, loading, loaded, error } +enum Language { english, spanish } + +extension LanguagesX on Language { + Locale get locale { + switch (this) { + case Language.english: + return const Locale('en'); + case Language.spanish: + return const Locale('es'); + } + } +} + @freezed class SettingsState with _$SettingsState { const factory SettingsState({ @Default(SettingsStateStatus.initial) SettingsStateStatus stateStatus, + @Default(Locale('en')) Locale locale, + ThemeMode? theme, AppException? error, }) = _SettingsState; const SettingsState._(); diff --git a/lib/src/features/settings/data/settings_repository.dart b/lib/src/features/settings/data/settings_repository.dart index 9d38050..e1107c0 100644 --- a/lib/src/features/settings/data/settings_repository.dart +++ b/lib/src/features/settings/data/settings_repository.dart @@ -1,12 +1,35 @@ +import 'package:flutter/material.dart'; import 'package:injectable/injectable.dart'; -import 'package:my_app/src/core/interceptor.dart'; +import 'package:my_app/src/core/preferences/preferences.dart'; import 'package:my_app/src/core/services/settings_datasource.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; +import 'package:my_app/src/core/utils/utils.dart'; -@Singleton(as: SettingsDatasource) -class SettingsRepository extends SettingsDatasource { - SettingsRepository(this._supabaseServiceImpl, this._client); +@Singleton(as: SettingsLocalDatasource) +class SettingsRepository extends SettingsLocalDatasource { + SettingsRepository(this._preferences); - final SupabaseServiceImpl _supabaseServiceImpl; - final SupabaseClient _client; + final Preferences _preferences; + + @override + Locale changeLanguage(String language) { + _preferences.setLanguage(language); + return Utils.getLocaleByCode(language); + } + + @override + ThemeMode changeTheme() { + _preferences.changeTheme(); + + return _preferences.getTheme(); + } + + @override + Locale getLanguage() { + return _preferences.getLanguage(); + } + + @override + ThemeMode getTheme() { + return _preferences.getTheme(); + } } diff --git a/lib/src/features/settings/settings_screen.dart b/lib/src/features/settings/settings_screen.dart index d832e4b..6dad902 100644 --- a/lib/src/features/settings/settings_screen.dart +++ b/lib/src/features/settings/settings_screen.dart @@ -4,6 +4,8 @@ import 'package:my_app/l10n/l10n.dart'; import 'package:my_app/src/core/ui/colors.dart'; import 'package:my_app/src/core/ui/typography.dart'; import 'package:my_app/src/features/auth/cubit/auth_cubit.dart'; +import 'package:my_app/src/features/settings/cubit/settings_cubit.dart'; +import 'package:my_app/src/features/settings/widgets/change_language_dialog.dart'; import 'package:my_app/src/features/settings/widgets/change_profile_image_dialog.dart'; class SettingsScreen extends StatelessWidget { @@ -11,34 +13,70 @@ class SettingsScreen extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; return Scaffold( appBar: AppBar( title: Text(context.l10n.settings), ), - body: Column( - children: [ - ListTile( - leading: const Icon(Icons.camera_alt_outlined), - title: Text(context.l10n.changeProfileImage), - onTap: () => showDialog( - context: context, - builder: (context) { - return const ChangeProfileImageDialog(); - }, - ), - ), - ListTile( - onTap: () => _showLogoutConfirmationDialog(context), - leading: const Icon( - Icons.logout, - color: AppColor.fail, - ), - title: Text( - context.l10n.logout, - style: AppTextStyle().body.copyWith(color: AppColor.fail), - ), - ), - ], + body: BlocBuilder( + builder: (context, state) { + return Column( + children: [ + ListTile( + leading: const Icon(Icons.camera_alt_outlined), + title: Text(context.l10n.profileImage, + style: AppTextStyle() + .body + .copyWith(color: colorScheme.onSurface),), + onTap: () => showDialog( + context: context, + builder: (context) { + return const ChangeProfileImageDialog(); + }, + ), + ), + ListTile( + leading: const Icon(Icons.language), + title: Text(context.l10n.language, + style: AppTextStyle() + .body + .copyWith(color: colorScheme.onSurface),), + onTap: () => showDialog( + context: context, + builder: (context) { + return const ChangeLanguageDialog(); + }, + ), + ), + ListTile( + leading: const Icon(Icons.brightness_6), + title: Text(context.l10n.darkMode, + style: AppTextStyle() + .body + .copyWith(color: colorScheme.onSurface),), + trailing: SwitchTheme( + data: Theme.of(context).switchTheme.copyWith(), + child: Switch( + value: state.theme == ThemeMode.dark, + onChanged: (value) => + context.read().changeTheme(), + ), + ), + ), + ListTile( + onTap: () => _showLogoutConfirmationDialog(context), + leading: const Icon( + Icons.logout, + color: AppColor.fail, + ), + title: Text( + context.l10n.logout, + style: AppTextStyle().body.copyWith(color: AppColor.fail), + ), + ), + ], + ); + }, ), ); } diff --git a/lib/src/features/settings/widgets/change_language_dialog.dart b/lib/src/features/settings/widgets/change_language_dialog.dart new file mode 100644 index 0000000..6f93f58 --- /dev/null +++ b/lib/src/features/settings/widgets/change_language_dialog.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:my_app/l10n/l10n.dart'; +import 'package:my_app/src/core/ui/typography.dart'; +import 'package:my_app/src/core/utils/widgets/generic_button.dart'; +import 'package:my_app/src/features/settings/cubit/settings_cubit.dart'; + +class ChangeLanguageDialog extends StatefulWidget { + const ChangeLanguageDialog({super.key}); + + @override + _ChangeLanguageDialogState createState() => _ChangeLanguageDialogState(); +} + +class _ChangeLanguageDialogState extends State { + late String _selectedLanguage; + + @override + void initState() { + super.initState(); + final settingsCubit = context.read(); + _selectedLanguage = settingsCubit.state.locale.languageCode; + } + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final settingsCubit = context.read(); + + return AlertDialog( + title: Text(context.l10n.changeLanguage), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: Text(context.l10n.english), + leading: Radio( + value: 'en', + groupValue: _selectedLanguage, + onChanged: (value) { + setState(() { + _selectedLanguage = value!; + }); + }, + ), + onTap: () { + setState(() { + _selectedLanguage = 'en'; + }); + }, + ), + ListTile( + title: Text(context.l10n.spanish), + leading: Radio( + value: 'es', + groupValue: _selectedLanguage, + onChanged: (value) { + setState(() { + _selectedLanguage = value!; + }); + }, + ), + onTap: () { + setState(() { + _selectedLanguage = 'es'; + }); + }, + ), + // Agrega más idiomas aquí si es necesario + ], + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text( + context.l10n.cancel, + style: AppTextStyle() + .textButton + .copyWith(color: colorScheme.onSurface), + ), + ), + GenericButton( + widget: Text( + context.l10n.accept, + style: AppTextStyle().body.copyWith(color: Colors.white), + ), + onPressed: () { + settingsCubit.changeLanguage(_selectedLanguage); + Navigator.of(context).pop(); + }, + ), + ], + ); + } +} diff --git a/lib/src/features/splash/splash_screen.dart b/lib/src/features/splash/splash_screen.dart index b6cdfc7..4940145 100644 --- a/lib/src/features/splash/splash_screen.dart +++ b/lib/src/features/splash/splash_screen.dart @@ -113,7 +113,7 @@ class _SplashScreenState extends State { ), const GutterSmall(), Text( - 'Loading...', + context.l10n.loading, style: AppTextStyle().body.copyWith(color: Colors.black), ), ], From 9f2ec95df8069d0e5c7bf50040b72f2b20081f6f Mon Sep 17 00:00:00 2001 From: ale24dev Date: Sun, 6 Oct 2024 01:43:32 -0400 Subject: [PATCH 23/30] add: ui improvements --- lib/l10n/arb/app_en.arb | 4 +- lib/l10n/arb/app_es.arb | 4 +- .../features/game/data/game_repository.dart | 2 +- lib/src/features/game/game_screen.dart | 2 +- .../features/home/widgets/game_section.dart | 69 ++++++++++--------- lib/src/features/home/widgets/otp_fields.dart | 12 ++-- .../home/widgets/play_number_card.dart | 2 +- .../home/widgets/search_game_section.dart | 2 +- .../home/widgets/user_header_info.dart | 2 +- lib/src/features/ranking/leaderboard.dart | 2 +- 10 files changed, 58 insertions(+), 43 deletions(-) diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index f42210e..5c1eaca 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -75,5 +75,7 @@ "english": "English", "spanish": "Spanish", "theme": "Theme", - "darkMode": "Dark mode" + "darkMode": "Dark mode", + + "yourOpponentHasScored": "Your opponent has scored: " } \ No newline at end of file diff --git a/lib/l10n/arb/app_es.arb b/lib/l10n/arb/app_es.arb index 30460ba..e1ebb2b 100644 --- a/lib/l10n/arb/app_es.arb +++ b/lib/l10n/arb/app_es.arb @@ -75,5 +75,7 @@ "english": "Inglés", "spanish": "Español", "theme": "Tema", - "darkMode": "Modo oscuro" + "darkMode": "Modo oscuro", + + "yourOpponentHasScored": "Tu rival acaba de sumar: " } \ No newline at end of file diff --git a/lib/src/features/game/data/game_repository.dart b/lib/src/features/game/data/game_repository.dart index c091d3a..4fce6b4 100644 --- a/lib/src/features/game/data/game_repository.dart +++ b/lib/src/features/game/data/game_repository.dart @@ -148,7 +148,7 @@ class GameRepository extends GameDataSource { final newRecord = payload.newRecord['player'] as String; if(newRecord != rival.player.id) return; final cows = payload.newRecord['cows'] as int; - final bulls = payload.newRecord['cows'] as int; + final bulls = payload.newRecord['bulls'] as int; return callback((cows, bulls)); }, ) diff --git a/lib/src/features/game/game_screen.dart b/lib/src/features/game/game_screen.dart index d2abcc6..ad087c6 100644 --- a/lib/src/features/game/game_screen.dart +++ b/lib/src/features/game/game_screen.dart @@ -47,7 +47,7 @@ class _GameScreenState extends State { .body .copyWith(color: colorScheme.onSurface), children: [ - const TextSpan(text: 'Tu rival acaba de sumar: '), + TextSpan(text: context.l10n.yourOpponentHasScored), TextSpan( text: Utils.attemptResult( context, diff --git a/lib/src/features/home/widgets/game_section.dart b/lib/src/features/home/widgets/game_section.dart index aa6808a..bf36125 100644 --- a/lib/src/features/home/widgets/game_section.dart +++ b/lib/src/features/home/widgets/game_section.dart @@ -207,40 +207,47 @@ class __SendNumberSectionState extends State<_SendNumberSection> { return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - const IconButton( - onPressed: null, - icon: Icon(Icons.send, color: Colors.transparent), + const Expanded( + child: IconButton( + onPressed: null, + icon: Icon(Icons.send, color: Colors.transparent), + ), ), - OTPFields( - key: otpFieldsKey, - allowRepetitions: false, - onChanged: _onChanged, + Expanded( + flex: 3, + child: OTPFields( + key: otpFieldsKey, + allowRepetitions: false, + onChanged: _onChanged, + ), ), - IconButton( - onPressed: !widget.canSendNumber - ? null - : () { - final isValidNumber = Utils.isValidPlayerNumber(otpValue); - if (!isValidNumber) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.introduceValidNumber), - backgroundColor: Colors.red, - ), - ); - return; - } - context.read().insertAttempt( - Attempt.empty() - .copyWith(number: otpValue.parseOptToNumberValue), + Expanded( + child: IconButton( + onPressed: !widget.canSendNumber + ? null + : () { + final isValidNumber = Utils.isValidPlayerNumber(otpValue); + if (!isValidNumber) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.l10n.introduceValidNumber), + backgroundColor: Colors.red, + ), ); - otpFieldsKey.currentState?.clearFields(); - }, - icon: Icon( - Icons.send, - color: widget.canSendNumber - ? colorScheme.primary - : colorScheme.onSurface.withOpacity(.4), + return; + } + context.read().insertAttempt( + Attempt.empty() + .copyWith(number: otpValue.parseOptToNumberValue), + ); + otpFieldsKey.currentState?.clearFields(); + }, + icon: Icon( + Icons.send, + color: widget.canSendNumber + ? colorScheme.primary + : colorScheme.onSurface.withOpacity(.4), + ), ), ), ], diff --git a/lib/src/features/home/widgets/otp_fields.dart b/lib/src/features/home/widgets/otp_fields.dart index 322f19c..5528a6d 100644 --- a/lib/src/features/home/widgets/otp_fields.dart +++ b/lib/src/features/home/widgets/otp_fields.dart @@ -6,14 +6,14 @@ class OTPFields extends StatefulWidget { required this.onChanged, super.key, this.length = 4, - this.fieldWidth = 50, + this.fieldWidth = 40, this.fieldHeight, - this.margin = const EdgeInsets.symmetric(horizontal: 5), + this.margin = const EdgeInsets.symmetric(horizontal: 2), this.allowRepetitions = true, }); final int length; - final Function(String) onChanged; + final void Function(String) onChanged; final double fieldWidth; final double? fieldHeight; final EdgeInsets margin; @@ -69,6 +69,9 @@ class OTPFieldsState extends State { } void clearFields() { + // Ocultar el teclado eliminando el foco de cualquier campo de texto activo + FocusManager.instance.primaryFocus?.unfocus(); + setState(() { for (var i = 0; i < widget.length; i++) { controllers[i].clear(); @@ -76,8 +79,9 @@ class OTPFieldsState extends State { isFilled[i] = false; } }); + // Llevar el foco al primer campo de texto - FocusScope.of(context).requestFocus(focusNodes[0]); + // FocusScope.of(context).requestFocus(focusNodes[0]); } @override diff --git a/lib/src/features/home/widgets/play_number_card.dart b/lib/src/features/home/widgets/play_number_card.dart index ddb59b2..e55186b 100644 --- a/lib/src/features/home/widgets/play_number_card.dart +++ b/lib/src/features/home/widgets/play_number_card.dart @@ -34,7 +34,7 @@ class PlayNumberCard extends StatelessWidget { ), ), ), - const GutterLarge(), + // const GutterLarge(), Expanded( child: Container( decoration: BoxDecoration( diff --git a/lib/src/features/home/widgets/search_game_section.dart b/lib/src/features/home/widgets/search_game_section.dart index ab3ae20..40217bc 100644 --- a/lib/src/features/home/widgets/search_game_section.dart +++ b/lib/src/features/home/widgets/search_game_section.dart @@ -26,7 +26,7 @@ class _SearchGameSectionState extends State { Widget build(BuildContext context) { return Padding( padding: - EdgeInsets.symmetric(vertical: 40, horizontal: context.widthPx * .05), + EdgeInsets.symmetric(vertical: 15, horizontal: context.widthPx * .05), child: BlocBuilder( builder: (context, state) { if (state.isGameStarted && !_hasNavigated) { diff --git a/lib/src/features/home/widgets/user_header_info.dart b/lib/src/features/home/widgets/user_header_info.dart index 54f4f78..274bb6a 100644 --- a/lib/src/features/home/widgets/user_header_info.dart +++ b/lib/src/features/home/widgets/user_header_info.dart @@ -42,7 +42,7 @@ class _UserHeaderInfoState extends State { Row( children: [ CacheWidget( - size: const Size(60, 60), + size: const Size(40, 40), imageUrl: player.avatarUrl, fit: BoxFit.cover, ), diff --git a/lib/src/features/ranking/leaderboard.dart b/lib/src/features/ranking/leaderboard.dart index fc45629..cb9bfeb 100644 --- a/lib/src/features/ranking/leaderboard.dart +++ b/lib/src/features/ranking/leaderboard.dart @@ -52,7 +52,7 @@ class _LeaderboardWidgetState extends State { ), Expanded( child: Padding( - padding: EdgeInsets.symmetric(horizontal: context.widthPx * .1), + padding: EdgeInsets.symmetric(horizontal: context.widthPx * .1, vertical: 10), child: Stack( children: [ Skeletonizer( From e3333c3541e80519496867b4601c1c8739580a51 Mon Sep 17 00:00:00 2001 From: ale24dev Date: Sun, 6 Oct 2024 03:13:53 -0400 Subject: [PATCH 24/30] fix: use hooks --- .../home/widgets/play_number_card.dart | 4 +- .../home/widgets/select_secret_number.dart | 91 ++++++++----------- 2 files changed, 39 insertions(+), 56 deletions(-) diff --git a/lib/src/features/home/widgets/play_number_card.dart b/lib/src/features/home/widgets/play_number_card.dart index e55186b..25714cf 100644 --- a/lib/src/features/home/widgets/play_number_card.dart +++ b/lib/src/features/home/widgets/play_number_card.dart @@ -34,7 +34,7 @@ class PlayNumberCard extends StatelessWidget { ), ), ), - // const GutterLarge(), + const GutterSmall(), Expanded( child: Container( decoration: BoxDecoration( @@ -44,7 +44,7 @@ class PlayNumberCard extends StatelessWidget { ), child: Padding( padding: - const EdgeInsets.symmetric(vertical: 10, horizontal: 20), + const EdgeInsets.symmetric(vertical: 10, horizontal: 5), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ diff --git a/lib/src/features/home/widgets/select_secret_number.dart b/lib/src/features/home/widgets/select_secret_number.dart index 613d3be..6b11822 100644 --- a/lib/src/features/home/widgets/select_secret_number.dart +++ b/lib/src/features/home/widgets/select_secret_number.dart @@ -1,7 +1,9 @@ import 'dart:async'; +import 'dart:developer'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gutter/flutter_gutter.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:my_app/l10n/l10n.dart'; import 'package:my_app/src/core/extensions/string.dart'; import 'package:my_app/src/core/ui/typography.dart'; @@ -11,7 +13,7 @@ import 'package:my_app/src/features/home/widgets/otp_fields.dart'; import 'package:my_app/src/features/player/cubit/player_cubit.dart'; import 'package:my_app/src/features/player/data/model/player_number.dart'; -class SelectSecretNumber extends StatefulWidget { +class SelectSecretNumber extends HookWidget { const SelectSecretNumber({ required this.onSelect, required this.playerNumber, @@ -22,48 +24,40 @@ class SelectSecretNumber extends StatefulWidget { final PlayerNumber playerNumber; @override - State createState() => _SelectSecretNumberState(); -} + Widget build(BuildContext context) { + final secretNumber = useState(''); + final start = useState(60); + final timer = useRef(null); -class _SelectSecretNumberState extends State { - String secretNumber = ''; - Timer? _timer; - int _start = 60; + final isValidNumber = useState(false); - void onChangedNumber(String secretNumber) { - setState(() { - this.secretNumber = secretNumber; - }); - } + void onChangedNumber(String newSecretNumber) { + secretNumber.value = newSecretNumber; - void startTimer() { - _timer = Timer.periodic(const Duration(seconds: 1), (timer) { - if (_start == 0) { - setState(() { - timer.cancel(); - }); - } else { - setState(() { - _start--; - }); - } - }); - } + isValidNumber.value = Utils.isValidPlayerNumber(newSecretNumber); + log(isValidNumber.value.toString()); + } - @override - void initState() { - super.initState(); - startTimer(); - } + void startTimer() { + timer.value = Timer.periodic(const Duration(seconds: 1), (timer) { + if (start.value == 0) { + timer.cancel(); + } else { + start.value--; + } + }); + } - @override - void dispose() { - _timer?.cancel(); - super.dispose(); - } + useEffect( + () { + startTimer(); + return () { + timer.value?.cancel(); + }; + }, + [], + ); - @override - Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return AlertDialog( title: Text( @@ -79,10 +73,10 @@ class _SelectSecretNumberState extends State { ), const SizedBox(height: 16), Text( - context.l10n.timeRemaining(_start), + context.l10n.timeRemaining(start.value), style: AppTextStyle().body.copyWith( fontWeight: FontWeight.w600, - color: _start <= 10 ? Colors.red : colorScheme.onSurface, + color: start.value <= 10 ? Colors.red : colorScheme.onSurface, ), ), const GutterSmall(), @@ -100,26 +94,15 @@ class _SelectSecretNumberState extends State { context.l10n.send, style: AppTextStyle().body.copyWith(color: Colors.white), ), - onPressed: secretNumber.length != 4 + onPressed: !isValidNumber.value ? null : () async { - final isValidNumber = - Utils.isValidPlayerNumber(secretNumber); - if (!isValidNumber) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(context.l10n.introduceValidNumber), - backgroundColor: Colors.red, - ), - ); - return; - } await context.read().updatePlayerNumber( - widget.playerNumber.copyWith( - number: secretNumber.parseOptToNumberValue, + playerNumber.copyWith( + number: secretNumber.value.parseOptToNumberValue, ), ); - widget.onSelect(); + onSelect(); }, ), ], From 61bbf8a030b72abfa329a0ea3a9ed17343fd5e47 Mon Sep 17 00:00:00 2001 From: ale24dev Date: Sun, 6 Oct 2024 04:34:27 -0400 Subject: [PATCH 25/30] feat: add firebase crashlytics --- android/app/build.gradle | 4 + android/app/google-services.json | 29 ++++ android/settings.gradle | 4 + firebase.json | 1 + ios/Podfile | 14 +- ios/Podfile.lock | 105 +++++++++++- ios/Runner.xcodeproj/project.pbxproj | 23 +++ ios/Runner/GoogleService-Info.plist | 30 ++++ lib/firebase_options.dart | 68 ++++++++ lib/main.dart | 31 +++- lib/src/app.dart | 26 +-- .../home/widgets/search_game_section.dart | 3 +- macos/Flutter/GeneratedPluginRegistrant.swift | 4 + pubspec.lock | 152 ++++++++++++------ pubspec.yaml | 3 + .../functions/create-new-attempt/index.ts | 29 +++- .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 18 files changed, 448 insertions(+), 82 deletions(-) create mode 100644 android/app/google-services.json create mode 100644 firebase.json mode change 100644 => 100755 ios/Podfile.lock create mode 100644 ios/Runner/GoogleService-Info.plist create mode 100644 lib/firebase_options.dart diff --git a/android/app/build.gradle b/android/app/build.gradle index 3f42e10..a36e923 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,5 +1,9 @@ plugins { id "com.android.application" + // START: FlutterFire Configuration + id 'com.google.gms.google-services' + id 'com.google.firebase.crashlytics' + // END: FlutterFire Configuration id "kotlin-android" // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. id "dev.flutter.flutter-gradle-plugin" diff --git a/android/app/google-services.json b/android/app/google-services.json new file mode 100644 index 0000000..cbe8efb --- /dev/null +++ b/android/app/google-services.json @@ -0,0 +1,29 @@ +{ + "project_info": { + "project_number": "43890310910", + "project_id": "mindcows-e6b33", + "storage_bucket": "mindcows-e6b33.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:43890310910:android:a2ac88ac0ab8108b0defaf", + "android_client_info": { + "package_name": "com.quasar.easehouse" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyCwT6qBwOxItq7Mh3Xp9w-xNECUv7ZTiP0" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/android/settings.gradle b/android/settings.gradle index c61a818..07851cc 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -19,6 +19,10 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "com.android.application" version "7.3.0" apply false + // START: FlutterFire Configuration + id "com.google.gms.google-services" version "4.3.15" apply false + id "com.google.firebase.crashlytics" version "2.8.1" apply false + // END: FlutterFire Configuration id "org.jetbrains.kotlin.android" version "1.9.0" apply false } diff --git a/firebase.json b/firebase.json new file mode 100644 index 0000000..3145714 --- /dev/null +++ b/firebase.json @@ -0,0 +1 @@ +{"flutter":{"platforms":{"android":{"default":{"projectId":"mindcows-e6b33","appId":"1:43890310910:android:a2ac88ac0ab8108b0defaf","fileOutput":"android/app/google-services.json"}},"ios":{"default":{"projectId":"mindcows-e6b33","appId":"1:43890310910:ios:3d504774e0667cee0defaf","uploadDebugSymbols":true,"fileOutput":"ios/Runner/GoogleService-Info.plist"}},"dart":{"lib/firebase_options.dart":{"projectId":"mindcows-e6b33","configurations":{"android":"1:43890310910:android:a2ac88ac0ab8108b0defaf","ios":"1:43890310910:ios:3d504774e0667cee0defaf"}}}}}} \ No newline at end of file diff --git a/ios/Podfile b/ios/Podfile index d97f17e..8de2c37 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '12.0' +platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' @@ -38,7 +38,19 @@ target 'Runner' do end post_install do |installer| + installer.generated_projects.each do |project| + project.targets.each do |target| + target.build_configurations.each do |config| + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0' + end + end +end installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) end end +# post_install do |installer| +# installer.pods_project.targets.each do |target| +# flutter_additional_ios_build_settings(target) +# end +# end diff --git a/ios/Podfile.lock b/ios/Podfile.lock old mode 100644 new mode 100755 index 47472a5..595cb4f --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,10 +1,76 @@ PODS: - app_links (0.0.2): - Flutter + - Firebase/CoreOnly (11.2.0): + - FirebaseCore (= 11.2.0) + - Firebase/Crashlytics (11.2.0): + - Firebase/CoreOnly + - FirebaseCrashlytics (~> 11.2.0) + - firebase_core (3.6.0): + - Firebase/CoreOnly (= 11.2.0) + - Flutter + - firebase_crashlytics (4.1.3): + - Firebase/Crashlytics (= 11.2.0) + - firebase_core + - Flutter + - FirebaseCore (11.2.0): + - FirebaseCoreInternal (~> 11.0) + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/Logger (~> 8.0) + - FirebaseCoreExtension (11.3.0): + - FirebaseCore (~> 11.0) + - FirebaseCoreInternal (11.3.0): + - "GoogleUtilities/NSData+zlib (~> 8.0)" + - FirebaseCrashlytics (11.2.0): + - FirebaseCore (~> 11.0) + - FirebaseInstallations (~> 11.0) + - FirebaseRemoteConfigInterop (~> 11.0) + - FirebaseSessions (~> 11.0) + - GoogleDataTransport (~> 10.0) + - GoogleUtilities/Environment (~> 8.0) + - nanopb (~> 3.30910.0) + - PromisesObjC (~> 2.4) + - FirebaseInstallations (11.3.0): + - FirebaseCore (~> 11.0) + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/UserDefaults (~> 8.0) + - PromisesObjC (~> 2.4) + - FirebaseRemoteConfigInterop (11.3.0) + - FirebaseSessions (11.3.0): + - FirebaseCore (~> 11.0) + - FirebaseCoreExtension (~> 11.0) + - FirebaseInstallations (~> 11.0) + - GoogleDataTransport (~> 10.0) + - GoogleUtilities/Environment (~> 8.0) + - GoogleUtilities/UserDefaults (~> 8.0) + - nanopb (~> 3.30910.0) + - PromisesSwift (~> 2.1) - Flutter (1.0.0) + - GoogleDataTransport (10.1.0): + - nanopb (~> 3.30910.0) + - PromisesObjC (~> 2.4) + - GoogleUtilities/Environment (8.0.2): + - GoogleUtilities/Privacy + - GoogleUtilities/Logger (8.0.2): + - GoogleUtilities/Environment + - GoogleUtilities/Privacy + - "GoogleUtilities/NSData+zlib (8.0.2)": + - GoogleUtilities/Privacy + - GoogleUtilities/Privacy (8.0.2) + - GoogleUtilities/UserDefaults (8.0.2): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - nanopb (3.30910.0): + - nanopb/decode (= 3.30910.0) + - nanopb/encode (= 3.30910.0) + - nanopb/decode (3.30910.0) + - nanopb/encode (3.30910.0) - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS + - PromisesObjC (2.4.0) + - PromisesSwift (2.4.0): + - PromisesObjC (= 2.4.0) - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS @@ -16,15 +82,37 @@ PODS: DEPENDENCIES: - app_links (from `.symlinks/plugins/app_links/ios`) + - firebase_core (from `.symlinks/plugins/firebase_core/ios`) + - firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`) - Flutter (from `Flutter`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - sqflite (from `.symlinks/plugins/sqflite/darwin`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) +SPEC REPOS: + trunk: + - Firebase + - FirebaseCore + - FirebaseCoreExtension + - FirebaseCoreInternal + - FirebaseCrashlytics + - FirebaseInstallations + - FirebaseRemoteConfigInterop + - FirebaseSessions + - GoogleDataTransport + - GoogleUtilities + - nanopb + - PromisesObjC + - PromisesSwift + EXTERNAL SOURCES: app_links: :path: ".symlinks/plugins/app_links/ios" + firebase_core: + :path: ".symlinks/plugins/firebase_core/ios" + firebase_crashlytics: + :path: ".symlinks/plugins/firebase_crashlytics/ios" Flutter: :path: Flutter path_provider_foundation: @@ -38,12 +126,27 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: app_links: e7a6750a915a9e161c58d91bc610e8cd1d4d0ad0 + Firebase: 98e6bf5278170668a7983e12971a66b2cd57fc8c + firebase_core: 2bedc3136ec7c7b8561c6123ed0239387b53f2af + firebase_crashlytics: 37d104d457b51760b48504a93a12b3bf70995d77 + FirebaseCore: a282032ae9295c795714ded2ec9c522fc237f8da + FirebaseCoreExtension: 30bb063476ef66cd46925243d64ad8b2c8ac3264 + FirebaseCoreInternal: ac26d09a70c730e497936430af4e60fb0c68ec4e + FirebaseCrashlytics: cfc69af5b53565dc6a5e563788809b5778ac4eac + FirebaseInstallations: 58cf94dabf1e2bb2fa87725a9be5c2249171cda0 + FirebaseRemoteConfigInterop: c3a5c31b3c22079f41ba1dc645df889d9ce38cb9 + FirebaseSessions: 655ff17f3cc1a635cbdc2d69b953878001f9e25b Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 + GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d + nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 + PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe -PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796 +PODFILE CHECKSUM: 6fec2c3a7aadf61c5a1a205c75037a6dfbe8e280 COCOAPODS: 1.15.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index a0a6d5b..9e6b865 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -16,6 +16,7 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + D997336F5971113A0F6592A9 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = F8E72E0BE2775CBC84077B36 /* GoogleService-Info.plist */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -74,6 +75,7 @@ D3778439BC1270FFDDA09694 /* Pods-RunnerTests.release-development.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release-development.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release-development.xcconfig"; sourceTree = ""; }; EB584FB092DE42D14C8F5FDC /* Pods-Runner.debug-staging.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug-staging.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug-staging.xcconfig"; sourceTree = ""; }; ED0B98307A2E199B4D22E3F6 /* Pods-RunnerTests.profile-production.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile-production.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile-production.xcconfig"; sourceTree = ""; }; + F8E72E0BE2775CBC84077B36 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; }; FB5A7593C1801CA1BDBEA892 /* Pods-Runner.profile-development.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile-development.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile-development.xcconfig"; sourceTree = ""; }; FBB3FE19EA732384351A848B /* Pods-Runner.release-staging.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release-staging.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release-staging.xcconfig"; sourceTree = ""; }; FF16E922D86F8DAC351E32B2 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -153,6 +155,7 @@ 331C8082294A63A400263BE5 /* RunnerTests */, 36C3F4B2E1EAE773443BD4BC /* Pods */, E79239A656664099DB10A9F6 /* Frameworks */, + F8E72E0BE2775CBC84077B36 /* GoogleService-Info.plist */, ); sourceTree = ""; }; @@ -223,6 +226,7 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 38BB4DA76D6E1DBAF978421D /* [CP] Embed Pods Frameworks */, + 2FD113A83742DFDE7F9CEF18 /* FlutterFire: "flutterfire upload-crashlytics-symbols" */, ); buildRules = ( ); @@ -288,6 +292,7 @@ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + D997336F5971113A0F6592A9 /* GoogleService-Info.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -316,6 +321,24 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; + 2FD113A83742DFDE7F9CEF18 /* FlutterFire: "flutterfire upload-crashlytics-symbols" */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "FlutterFire: \"flutterfire upload-crashlytics-symbols\""; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\n#!/bin/bash\nPATH=${PATH}:$FLUTTER_ROOT/bin:$HOME/.pub-cache/bin\nflutterfire upload-crashlytics-symbols --upload-symbols-script-path=$PODS_ROOT/FirebaseCrashlytics/upload-symbols --platform=ios --apple-project-path=${SRCROOT} --env-platform-name=${PLATFORM_NAME} --env-configuration=${CONFIGURATION} --env-project-dir=${PROJECT_DIR} --env-built-products-dir=${BUILT_PRODUCTS_DIR} --env-dwarf-dsym-folder-path=${DWARF_DSYM_FOLDER_PATH} --env-dwarf-dsym-file-name=${DWARF_DSYM_FILE_NAME} --env-infoplist-path=${INFOPLIST_PATH} --default-config=default\n"; + }; 3846ADF370841B82F3E448CE /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; diff --git a/ios/Runner/GoogleService-Info.plist b/ios/Runner/GoogleService-Info.plist new file mode 100644 index 0000000..7dca297 --- /dev/null +++ b/ios/Runner/GoogleService-Info.plist @@ -0,0 +1,30 @@ + + + + + API_KEY + AIzaSyCEULtTU1gnEmv27AV3IajcJ_iEZDbrlfs + GCM_SENDER_ID + 43890310910 + PLIST_VERSION + 1 + BUNDLE_ID + com.mindcows.app + PROJECT_ID + mindcows-e6b33 + STORAGE_BUCKET + mindcows-e6b33.appspot.com + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:43890310910:ios:3d504774e0667cee0defaf + + \ No newline at end of file diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart new file mode 100644 index 0000000..d47e177 --- /dev/null +++ b/lib/firebase_options.dart @@ -0,0 +1,68 @@ +// File generated by FlutterFire CLI. +// ignore_for_file: type=lint +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +/// Default [FirebaseOptions] for use with your Firebase apps. +/// +/// Example: +/// ```dart +/// import 'firebase_options.dart'; +/// // ... +/// await Firebase.initializeApp( +/// options: DefaultFirebaseOptions.currentPlatform, +/// ); +/// ``` +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for web - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + case TargetPlatform.iOS: + return ios; + case TargetPlatform.macOS: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for macos - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.windows: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for windows - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.linux: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for linux - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'AIzaSyCwT6qBwOxItq7Mh3Xp9w-xNECUv7ZTiP0', + appId: '1:43890310910:android:a2ac88ac0ab8108b0defaf', + messagingSenderId: '43890310910', + projectId: 'mindcows-e6b33', + storageBucket: 'mindcows-e6b33.appspot.com', + ); + + static const FirebaseOptions ios = FirebaseOptions( + apiKey: 'AIzaSyCEULtTU1gnEmv27AV3IajcJ_iEZDbrlfs', + appId: '1:43890310910:ios:3d504774e0667cee0defaf', + messagingSenderId: '43890310910', + projectId: 'mindcows-e6b33', + storageBucket: 'mindcows-e6b33.appspot.com', + iosBundleId: 'com.mindcows.app', + ); +} diff --git a/lib/main.dart b/lib/main.dart index f835d9c..458572d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,9 +1,13 @@ +import 'dart:developer'; + +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_crashlytics/firebase_crashlytics.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:my_app/firebase_options.dart'; import 'package:my_app/src/app.dart'; import 'package:my_app/src/core/di/dependency_injection.dart'; -import 'package:my_app/src/features/settings/cubit/settings_cubit.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; Future main() async { @@ -15,11 +19,22 @@ Future main() async { anonKey: dotenv.get('ANON_KEY'), ); - configureDependencies(); - runApp( - BlocProvider( - create: (context) => getIt.get(), - child: const MyApp(), - ), + await configureDependencies(); + + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, ); + + FlutterError.onError = (details) { + FirebaseCrashlytics.instance.recordFlutterError(details); + log('FlutterError.onError: $details'); + }; + + PlatformDispatcher.instance.onError = (error, stack) { + FirebaseCrashlytics.instance.recordError(error, stack); + log('PlatformDispatcher.instance.onError: $error'); + return true; + }; + + runApp(const MyApp()); } diff --git a/lib/src/app.dart b/lib/src/app.dart index 2bea960..f144c24 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -25,26 +25,28 @@ class _MyAppState extends State { @override Widget build(BuildContext context) { final theme = AppTheme(); - final settings = BlocProvider.of(context, listen: true); - log('Settings: ${settings.state.theme}'); return MultiBlocProvider( providers: [ + BlocProvider(create: (_) => getIt.get()), BlocProvider(create: (_) => getIt.get()), BlocProvider(create: (_) => getIt.get()), BlocProvider(create: (_) => getIt.get()), BlocProvider(create: (_) => getIt.get()), BlocProvider(create: (_) => getIt.get()), - // BlocProvider(create: (_) => getIt.get()), ], - child: MaterialApp.router( - debugShowCheckedModeBanner: false, - theme: theme.light, - darkTheme: theme.dark, - locale: settings.state.locale, - themeMode: settings.state.theme, - routerConfig: router, - localizationsDelegates: AppLocalizations.localizationsDelegates, - supportedLocales: AppLocalizations.supportedLocales, + child: BlocBuilder( + builder: (context, state) { + return MaterialApp.router( + debugShowCheckedModeBanner: false, + theme: theme.light, + darkTheme: theme.dark, + locale: state.locale, + themeMode: state.theme, + routerConfig: router, + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + ); + }, ), ); } diff --git a/lib/src/features/home/widgets/search_game_section.dart b/lib/src/features/home/widgets/search_game_section.dart index 40217bc..d95ebdb 100644 --- a/lib/src/features/home/widgets/search_game_section.dart +++ b/lib/src/features/home/widgets/search_game_section.dart @@ -1,5 +1,6 @@ // ignore_for_file: inference_failure_on_function_invocation +import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gutter/flutter_gutter.dart'; @@ -26,7 +27,7 @@ class _SearchGameSectionState extends State { Widget build(BuildContext context) { return Padding( padding: - EdgeInsets.symmetric(vertical: 15, horizontal: context.widthPx * .05), + EdgeInsets.symmetric(vertical: 35, horizontal: context.widthPx * .05), child: BlocBuilder( builder: (context, state) { if (state.isGameStarted && !_hasNavigated) { diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index a7773d2..0fa7703 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,6 +6,8 @@ import FlutterMacOS import Foundation import app_links +import firebase_core +import firebase_crashlytics import path_provider_foundation import shared_preferences_foundation import sqflite @@ -13,6 +15,8 @@ import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) + FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) + FLTFirebaseCrashlyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCrashlyticsPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) diff --git a/pubspec.lock b/pubspec.lock index f6fbfa3..ad08a3c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -9,6 +9,14 @@ packages: url: "https://pub.dev" source: hosted version: "72.0.0" + _flutterfire_internals: + dependency: transitive + description: + name: _flutterfire_internals + sha256: "5534e701a2c505fed1f0799e652dd6ae23bd4d2c4cf797220e5ced5764a7c1c2" + url: "https://pub.dev" + source: hosted + version: "1.3.44" _macros: dependency: transitive description: dart @@ -146,10 +154,10 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: dd09dd4e2b078992f42aac7f1a622f01882a8492fef08486b27ddde929c19f04 + sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" url: "https://pub.dev" source: hosted - version: "2.4.12" + version: "2.4.13" build_runner_core: dependency: transitive description: @@ -274,10 +282,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" + sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" url: "https://pub.dev" source: hosted - version: "2.3.6" + version: "2.3.7" diff_match_patch: dependency: transitive description: @@ -318,6 +326,46 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + firebase_core: + dependency: "direct main" + description: + name: firebase_core + sha256: "51dfe2fbf3a984787a2e7b8592f2f05c986bfedd6fdacea3f9e0a7beb334de96" + url: "https://pub.dev" + source: hosted + version: "3.6.0" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + sha256: e30da58198a6d4b49d5bce4e852f985c32cb10db329ebef9473db2b9f09ce810 + url: "https://pub.dev" + source: hosted + version: "5.3.0" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + sha256: f967a7138f5d2ffb1ce15950e2a382924239eaa521150a8f144af34e68b3b3e5 + url: "https://pub.dev" + source: hosted + version: "2.18.1" + firebase_crashlytics: + dependency: "direct main" + description: + name: firebase_crashlytics + sha256: "6899800fff1af819955aef740f18c4c8600f8b952a2a1ea97bc0872ebb257387" + url: "https://pub.dev" + source: hosted + version: "4.1.3" + firebase_crashlytics_platform_interface: + dependency: transitive + description: + name: firebase_crashlytics_platform_interface + sha256: "97c47b0a1779a3d4118416a3f0c6c564cc59ad89095e899893204d4b2ad08f4c" + url: "https://pub.dev" + source: hosted + version: "3.6.44" fixnum: dependency: transitive description: @@ -351,10 +399,10 @@ packages: dependency: transitive description: name: flutter_adaptive_scaffold - sha256: "6b587d439c7da037432bbfc78d9676e1d08f2d7490f08e8d689a20f08e049802" + sha256: "8c515a2cb8abb3a567f8e77f10b33f47bb6fcadfe31f62364e0aca36280cdf93" url: "https://pub.dev" source: hosted - version: "0.2.6" + version: "0.3.1" flutter_bloc: dependency: "direct main" description: @@ -383,10 +431,10 @@ packages: dependency: "direct main" description: name: flutter_gutter - sha256: "22d898d2ffafd686a1df82725ddcc1012c0418afbf0804347e656b0be03cc91c" + sha256: "2aa99181796d6f7d2de66da962b71b0feb996ec69b7a1ad2ac1c2119f25b041b" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.2.0" flutter_hooks: dependency: "direct main" description: @@ -446,10 +494,10 @@ packages: dependency: transitive description: name: functions_client - sha256: e63f49cd3b41727f47b3bde284a11a4ac62839e0604f64077d4257487510e484 + sha256: b09f01be55ceec6a7a0916a0934e0c58d551292790901f15a24ae9d5654ea1f9 url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.3" get_it: dependency: "direct main" description: @@ -470,18 +518,18 @@ packages: dependency: "direct main" description: name: go_router - sha256: "2ddb88e9ad56ae15ee144ed10e33886777eb5ca2509a914850a5faa7b52ff459" + sha256: "6f1b756f6e863259a99135ff3c95026c3cdca17d10ebef2bba2261a25ddc8bbc" url: "https://pub.dev" source: hosted - version: "14.2.7" + version: "14.3.0" gotrue: dependency: transitive description: name: gotrue - sha256: "8703db795511f69194fe77125a0c838bbb6befc2f95717b6e40331784a8bdecb" + sha256: f3d4b70b8df6d05be2c6d558e8887576766685918be4ef87dbb8d7e547aeb049 url: "https://pub.dev" source: hosted - version: "2.8.4" + version: "2.9.0" graphs: dependency: transitive description: @@ -534,10 +582,10 @@ packages: dependency: "direct main" description: name: injectable - sha256: "69874ba3ec10e3a0de3f519a184442878291d928f3299d718813f24642585198" + sha256: "5e1556ea1d374fe44cbe846414d9bab346285d3d8a1da5877c01ad0774006068" url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "2.5.0" injectable_generator: dependency: "direct dev" description: @@ -742,10 +790,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7" + sha256: f7544c346a0742aee1450f9e5c0f5269d7c602b9c95fdbcd9fb8f5b1df13b1cc url: "https://pub.dev" source: hosted - version: "2.2.10" + version: "2.2.11" path_provider_foundation: dependency: transitive description: @@ -806,10 +854,10 @@ packages: dependency: transitive description: name: postgrest - sha256: c4197238601c7c3103b03a4bb77f2050b17d0064bf8b968309421abdebbb7f0e + sha256: "3c6aea8d41cbb7cc33190a67fd8b0f21735a74d5871e52ada4c274aa3aa50dbb" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" provider: dependency: transitive description: @@ -838,10 +886,10 @@ packages: dependency: transitive description: name: realtime_client - sha256: d897a65ee3b1b5ddc1cf606f0b83792262d38fd5679c2df7e38da29c977513da + sha256: "5d449be6dd49a413847b6da5cf48b44f7379047bc0ddc6f301d387d2ce4633b9" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.0" recase: dependency: transitive description: @@ -878,18 +926,18 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: "480ba4345773f56acda9abf5f50bd966f581dac5d514e5fc4a18c62976bbba7e" + sha256: "3b9febd815c9ca29c9e3520d50ec32f49157711e143b7a4ca039eb87e8ade5ab" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.3" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: c4b35f6cb8f63c147312c054ce7c2254c8066745125264f0c88739c417fc9d9f + sha256: "07e050c7cd39bad516f8d64c455f04508d09df104be326d8c02551590a0d513d" url: "https://pub.dev" source: hosted - version: "2.5.2" + version: "2.5.3" shared_preferences_linux: dependency: transitive description: @@ -942,10 +990,10 @@ packages: dependency: transitive description: name: shelf_static - sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.3" shelf_web_socket: dependency: transitive description: @@ -1035,18 +1083,18 @@ packages: dependency: transitive description: name: sqflite - sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d + sha256: ff5a2436ef8ebdfda748fbfe957f9981524cb5ff11e7bafa8c42771840e8a788 url: "https://pub.dev" source: hosted - version: "2.3.3+1" + version: "2.3.3+2" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: "4058172e418eb7e7f2058dcb7657d451a8fc264afa0dea4dbd0f304a57131611" + sha256: "2d8e607db72e9cb7748c9c6e739e2c9618320a5517de693d5a24609c4671b1a4" url: "https://pub.dev" source: hosted - version: "2.5.4+3" + version: "2.5.4+4" stack_trace: dependency: transitive description: @@ -1059,10 +1107,10 @@ packages: dependency: transitive description: name: storage_client - sha256: "28c147c805304dbc2b762becd1fc26ee0cb621ace3732b9ae61ef979aab8b367" + sha256: "060aa8651ad2b30ce6fc30f4652317f45d21f8c6f6cc222b2430fc7f69f7d8ba" url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "2.1.0" stream_channel: dependency: transitive description: @@ -1091,26 +1139,26 @@ packages: dependency: transitive description: name: supabase - sha256: "4ed1cf3298f39865c05b2d8557f92eb131a9b9af70e32e218672a0afce01a6bc" + sha256: c3a03d656110e13dd80ccabba7a5c591fece4c56ec3a21108e51b1e18d9c9a94 url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.4.0" supabase_flutter: dependency: "direct main" description: name: supabase_flutter - sha256: ff6ba3048fd47d831fdc0027d3efb99346d99b95becfcb406562454bd9b229c5 + sha256: "79e5067f08572c7900bc77251e6c1e0cac80c3059b5820ba2b86469b22822b75" url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "2.7.0" synchronized: dependency: transitive description: name: synchronized - sha256: "51b08572b9f091f8c3eb4d9d4be253f196ff0075d5ec9b10a884026d5b55d7bc" + sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225" url: "https://pub.dev" source: hosted - version: "3.3.0+2" + version: "3.3.0+3" term_glyph: dependency: transitive description: @@ -1187,10 +1235,10 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: e35a698ac302dd68e41f73250bd9517fe3ab5fa4f18fe4647a0872db61bacbab + sha256: "8fc3bae0b68c02c47c5c86fa8bfa74471d42687b0eded01b78de87872db745e2" url: "https://pub.dev" source: hosted - version: "6.3.10" + version: "6.3.12" url_launcher_ios: dependency: transitive description: @@ -1211,10 +1259,10 @@ packages: dependency: transitive description: name: url_launcher_macos - sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de" + sha256: "769549c999acdb42b8bcfa7c43d72bf79a382ca7441ab18a808e101149daf672" url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "3.2.1" url_launcher_platform_interface: dependency: transitive description: @@ -1243,10 +1291,10 @@ packages: dependency: "direct main" description: name: uuid - sha256: f33d6bb662f0e4f79dcd7ada2e6170f3b3a2530c28fc41f49a411ddedd576a77 + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff url: "https://pub.dev" source: hosted - version: "4.5.0" + version: "4.5.1" vector_math: dependency: transitive description: @@ -1283,10 +1331,10 @@ packages: dependency: transitive description: name: web - sha256: d43c1d6b787bf0afad444700ae7f4db8827f701bc61c255ac8d328c6f4d52062 + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.0" web_socket: dependency: transitive description: @@ -1315,10 +1363,10 @@ packages: dependency: transitive description: name: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.1.0" yaml: dependency: transitive description: @@ -1331,10 +1379,10 @@ packages: dependency: transitive description: name: yet_another_json_isolate - sha256: "47ed3900e6b0e4dfe378811a4402e85b7fc126a7daa94f840fef65ea9c8e46f4" + sha256: "56155e9e0002cc51ea7112857bbcdc714d4c35e176d43e4d3ee233009ff410c9" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.0.3" sdks: dart: ">=3.5.0 <4.0.0" flutter: ">=3.24.0" diff --git a/pubspec.yaml b/pubspec.yaml index 8f21c9c..d70b7c0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -43,6 +43,9 @@ dependencies: equatable: ^2.0.0 + firebase_crashlytics: ^4.1.3 + firebase_core: ^3.6.0 + dev_dependencies: bloc_test: ^9.1.7 build_runner: ^2.4.12 diff --git a/supabase/functions/create-new-attempt/index.ts b/supabase/functions/create-new-attempt/index.ts index 8fc49ab..e627b4b 100644 --- a/supabase/functions/create-new-attempt/index.ts +++ b/supabase/functions/create-new-attempt/index.ts @@ -111,7 +111,6 @@ async function swapTurns(supabase: any, ownPlayerNumber: any, opponent: any, gam .update({ is_turn: false, started_time: new Date().toISOString(), - // time_left: newTimeLeft, // Asegurarse de que no sea null }) .eq("id", ownPlayerNumber.id); @@ -130,7 +129,6 @@ async function swapTurns(supabase: any, ownPlayerNumber: any, opponent: any, gam .from("player_number") .update({ is_turn: true, - // started_time: new Date().toISOString(), }) .eq("id", opponent.id); @@ -150,19 +148,25 @@ async function swapTurns(supabase: any, ownPlayerNumber: any, opponent: any, gam console.log("Opponent is a bot, waiting for the bot's turn..."); + // Generar un número aleatorio sin repeticiones entre 0 y 9 + const botNumber = generateUniqueRandomNumbers(4); + + // Contar "Toros" y "Vacas" con el número generado para el bot + const { bulls, cows } = countBullsAndCows(botNumber, ownPlayerNumber.number); + // Insertar intento para el bot const { error: botAttemptError } = await supabase.from("attempt").insert({ player: opponent.player.id, game: gameId, - number: [], // Número vacío - bulls: 0, // Bulls para el bot - cows: 0, // Cows para el bot + number: botNumber, + bulls, + cows, }); if (botAttemptError) { throw new Error(`Error inserting bot attempt: ${botAttemptError.message}`); } - console.log("Bot attempt inserted successfully."); + console.log("Bot attempt inserted successfully with generated number:", botNumber); console.log("Reverting opponent turn..."); const { error: revertTurnError } = await supabase @@ -185,7 +189,6 @@ async function swapTurns(supabase: any, ownPlayerNumber: any, opponent: any, gam .from("player_number") .update({ is_turn: true, - // started_time: new Date().toISOString(), }) .eq("id", ownPlayerNumber.id); @@ -196,6 +199,18 @@ async function swapTurns(supabase: any, ownPlayerNumber: any, opponent: any, gam } } +// Función para generar números aleatorios únicos entre 0 y 9 +function generateUniqueRandomNumbers(count) { + const numbers = new Set(); + + while (numbers.size < count) { + const randomNum = Math.floor(Math.random() * 10); // Generar un número entre 0 y 9 + numbers.add(randomNum); + } + + return Array.from(numbers); +} + // Servidor Deno.serve(async (req) => { try { diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 785a046..c87e8b8 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,11 +7,14 @@ #include "generated_plugin_registrant.h" #include +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { AppLinksPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("AppLinksPluginCApi")); + FirebaseCorePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 8f8ee4f..19833e4 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST app_links + firebase_core url_launcher_windows ) From 83bad7a7d1cfb7b1eee306c41df5cfc34da9cf06 Mon Sep 17 00:00:00 2001 From: ale24dev Date: Sun, 6 Oct 2024 16:42:29 -0400 Subject: [PATCH 26/30] chroe: ui improvements --- .../app/src/main/ic_launcher-playstore.png | Bin 32868 -> 27524 bytes .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 4720 bytes .../main/res/mipmap-hdpi/launcher_icon.png | Bin 7492 -> 0 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2880 bytes .../main/res/mipmap-mdpi/launcher_icon.png | Bin 4545 -> 0 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 10748 -> 6570 bytes .../main/res/mipmap-xhdpi/launcher_icon.png | Bin 10748 -> 0 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 10449 bytes .../main/res/mipmap-xxhdpi/launcher_icon.png | Bin 16980 -> 0 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 14943 bytes .../main/res/mipmap-xxxhdpi/launcher_icon.png | Bin 22994 -> 0 bytes lib/src/features/game/cubit/game_cubit.dart | 17 ++-- .../game/cubit/game_cubit.freezed.dart | 35 ++++++-- lib/src/features/game/cubit/game_state.dart | 1 + .../game/widgets/game_turn_widget.dart | 2 +- .../features/home/widgets/game_section.dart | 80 +++++++++--------- 16 files changed, 76 insertions(+), 59 deletions(-) mode change 100644 => 100755 android/app/src/main/ic_launcher-playstore.png create mode 100755 android/app/src/main/res/mipmap-hdpi/ic_launcher.png delete mode 100755 android/app/src/main/res/mipmap-hdpi/launcher_icon.png create mode 100755 android/app/src/main/res/mipmap-mdpi/ic_launcher.png delete mode 100755 android/app/src/main/res/mipmap-mdpi/launcher_icon.png delete mode 100755 android/app/src/main/res/mipmap-xhdpi/launcher_icon.png create mode 100755 android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png delete mode 100755 android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png create mode 100755 android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png delete mode 100755 android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png diff --git a/android/app/src/main/ic_launcher-playstore.png b/android/app/src/main/ic_launcher-playstore.png old mode 100644 new mode 100755 index d920815d38d2392bda953fb2c019604b20b12ce9..d773eada6b3e15dbd355b99cedd720dde1b46def GIT binary patch literal 27524 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 Q=#!IG7ZeAP@*gNfDt90)c^F!60;0;K!-Q)HMj? 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 diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100755 index 0000000000000000000000000000000000000000..ebf9e80a3c0a305b684d54f4d8c71bfa5ef223d6 GIT binary patch literal 4720 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 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-hdpi/launcher_icon.png b/android/app/src/main/res/mipmap-hdpi/launcher_icon.png deleted file mode 100755 index 1374c24edaa64aef58a711c339c112a5a910548e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7492 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 O0000= diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100755 index 0000000000000000000000000000000000000000..09e49e4d684c76781d911f6f4470696ac26d667d GIT binary patch literal 2880 zcmV-G3%~SPx<`bk7VRA@u(nR$%eRTalS_qV=n-Yngr(3Wm6#nL5HqylAY5DG@opoE0rl1Sn| zm>B&7BLNd78ly%M5(!2U3?PuyLRi|AEyaR>EJB%Xw1sv$ozB)bZ~5)l-1~dO%(OFa zrnHSQdXqOV?>E2qyXSk(_k7Pe_lZv*Me<*~#Qy>Ohokqu76AdMmr9#6nL0*2u9NRG z#Rw>)mCZ7>dl<)=qR+718i)FmOt{k&0MtD$LD=UYGM6FaZe;aE5W~?pjYx!zQMd)7 zLnQv25SJS6GYJ5Zdfg~uG<_2zvle8Hc;Hva@IV@g(@Lc@(jvPav-n+z50g}$C8=(n z5JUq2b>q$Hm@Pj<*z+JM6A%0n85{uP40t~^9})S-W>lZ#e0F-dKbbv2aGJq&|@T(XF!b2i3tE0AQOz- z)fi2;gDgW*CF=hf$m%GC7$1X_VkFl^s#faSFyXWY)US{KgmizA>A-CML48j_)PFZb zJ!9xJ0f1Cy%+{YGtaCwFNbm2&$NqSN2ef(mPo2L)Vp(9CkR*`Ab>R}}HFZ}MLllku zjsiFWs;+uQ9;@x=ATl8GB;_YaDo>BGVH^N;?pxEaTJO=uBOGL~i+JF^VLP?s3}fV@ zGEU(LL&4F$W)61Eptr9X&$meu1IvoZ=R9W5>}T#-y)+lA`ul3daq3j{o~|?kSaY!2 z?oMS?WRd=R#Dl+^ECMM2C7CJ$pA{|Pl2kU5R5l}}r@dJ&JG{7Uwcaw9^@wAWV?%lR z2bu^&3t`AqX%#d@t2(vRz0=xC3iPaL(7trvZd} zhzEW#hINf2&~^`Ck4U9(k?|2^T*7rN_U<{xQ;&U&Z5t$s03k69iDkz6T`BPKOBoj@ z22pGv6GP`@9LKb_y4-sE5-iKm-xKCcFbW|3bbpaU`tMF90wdfsTYiFYmZlyUu86|X z#%8k)yE;E%)8nt;W^CjrYljnd`~*#}4)a}8CrZdS<+$~G*JGJx{aGqW)JyS9g{7IK z{4hyn%Vaq)!a?CK$87l#$cpxYl*&5^rfFbW1{)uLjeUE%ah$OL8W}#`|AY_(LC96t zuAt+RrId$i2%R7*v6b;TWZXmCfA0w@IB6jh`5O@VZ(#@@(}=Mw6VD5He$$)m?mUF+ zo|^Ha0mX5`<<~5yi@$s&^y^4M>Y;ocJ}a>G_adE5{Qnc@l}`>PC;iqxt# z-rBy4y*sDRT3pF6M>1Z{Y42%NoSQaJEW82Q1%%47;^XJSV zP{lwwNP|+2k(4SKRxLZoH@^HKK{!&i8vt;5=&v1=${8|llxn&ZG-omxhRO}0J@oee z{TM+&`@DI~XlWsgV=a8PWsM6)#mI{y?R5qCz>y>DI((SfGiGqn*=K7RRjU?-RDbC2h7c@>{8;$n;&N+Ozdp1Y=^Hgg#Ex8;7RTg%1@SXea!>`qN zbt}`Gn{{$er1@bO;yVsYuDF8P3c$@bYw*9h?KT~?Auq;{J&J{ZxwDV4 zvb~Fi=ME4?Q%vIFjrA}lK;@O{9*+$<)O?p6o%8wNqXlGhlJEci56EnmyY9S`OWND{ zV9OR-8vxJ=8plY-VbZk3v4yjPQfB zRXJWJrZ5vL)M?VF;+V_A-u-QCeC{%YtdehTWzB{SIJq2~9)4KqE`?lf!U*U(Zy5CS z_p^BAN-kNyo&z6!#J^sC5s6DnbA?-OdJ)S+dpuTqH<%x$1Yjg0LW3w*T%Oppjwo_S z;*g+PB}x)%hCypSk8KzYYlBjsau86fR>>Qxp)<%9a`=J6qWK58e*Ienf!b^Gqnncg zFdE2{PhU>oKr^36l5Be6Vh(&d8^;NC$EXadh9Rxltd`hDO94=T%f7D*qbh`o-=6Pq z<(J-J=oKv_ZNeHwb|+1VI?-0;cMUtq70A4{WZz z@=Y#S(uMDj9x*3&$4OI2MZk3>n_pVNj-B&y-9Q6S-mLmT8Ua=4RO3~jPWARmL{wc> zA}$496f)%dwC1unbpSzNbN%|)S-jwgR-ML!|0z&N0od~LMSRe?fQ$kl)1=H59Huqr z3A~6(*(ZvIN6NYfrNnV93dM|?F6iznVMKzAW9bMe02|hCXVHS5GYr6yyzuf0KB`AR zMP=rkCW_73&)NsykEjg!cwU4|q#l5>Ih#Vk#k38C6sSh7r+bK@!5XFozHf8gHE*zF zVfUG%P$C7{jO4X9mhz8(wqx5hTG|TCnAJoSC7L>_nyNg|XHbeUs`S_MT2n~tdU~H) z2lo#U1nKyA(}q`>HM68oH_jM^Y+G{VXptv3t<(L{S?9En$*Gzk3H$)Zwd)3s>fP%H z+e&c4P_@K~9z_-4{-Y(j4~LvJXONpVY&*G*_VfJ2q$s15=+*7*y!p-rEN#!=x(?N< z&%V7~%wI5@Tt1T~?5L$2u1+1g4)s#4c$~NBY+bC5_IMmUT;XdsZ0Fp$eY%KE#rryv zp9era|8-SXAbLwHX=%0fVBXhTqHS6c+g4rWSouq54MFKxMN?+_j3S9N8R+e2Rr_|% zUvgy1+#i1ODF8^dQAJ|!CyUte@s-$CfDr2BLTOC>OjOIJItdw(Rh=I~9Wfd>R+Wxr z&(KmVPg!|JyX6!Cq=&YSE%{(~2m8C0W80y=76q|>$vB;vmC;IxEXTyK(lbS+POIBD zGjm$EHhwC-&+#Sgv;atxdK%l_xDLmSh*jUNW>9^uGqUBdI+>Ol-YxRkey+asS<)tX z^2Tl4`Qr2Ez{um2k$&K^^_2|-p`)j_6Fc{j`wbD}`B~Px`dr3q=RA@uBS_gC#Roi~%&dly6B%~9NgwQ*PNQo4sD=3Hxf`FiO5ClY2&`-hg z2_GOmA~uS2ERiFkG^GZJ6lqc=Gy^1r)Gc%Go&UU(oe&@i<@n!o_UtBeckcVV^?Bb} z1tl*9fC<0>AOgTYci8`LT`mV89e@vjEklv_ri;Zb20#L62VfR}vH;cs7z)7a{}O@w zHjOwB$+Y)i8QVSLn4%mC4sr&i?*0%d=h9{EPLr8wVxMW6jf#tf zq5#-I69wRO)wsyuBkSHW9Z|u`qz}Fm6X*P*>YBR2;G%=yjVMm;Hx}qD0w8ov{ng;2 z{iKmkh~K zwCFlbBp!E{eV0DlfGAFAk$=;o^XO&orO!fD**qcS@J4lF#P%P? zs;V@h1v=5b$1H5zd4ld$7r@b?EM6=CT}K1M!2Zr{>X?a3UQzh@eiWkUFl?_Sg5Vk2PWc%X#_ z;^`OXVeRH4&~-dy7&vfG002}6aO}~>RoL&}jxkNQPm$7u8XSNlM=zj5@45Wa)hvxM zwpvO_nJEuI9Ds|31by8PWV#=u4uEp}IRJr-v4v8~?hizj=D#i-q1CA!1Ci|pV+xc2 zhxyoZGnB1+Pm>aA0XTWL0PGkDrWhz69TIhF<2$A+Kvzs@LJA2r$c0mB|PcC zSq+ME&@|0-(=?k0l@8t3zGKhbA)%2~zh61O2MPX6kD)6wGty6QSih`kPF7|MMNxM6 z3*&>2G^ofI&3I9(S|u7H#{;G^C`^IJZJ1TteW+Z#oO#6`137ma19YZkVntClo2J=% z=c>^nzFBou=6Il(CYVD5f?W9E<3zkSbDc2rYXYLnRY2v&U zsK^`+C$?=##O7aCA??Z~C@{&##H3Lj)py?N0_1ogeH^BufCagbwBxvF)?=3Hm!WNO z8LzlBO4LEuf z>AJ&oyK@OVbcd5-0fx`ZP1A(a6~HuAS5|#9QzmWy+0H;{6JH$*XY@gAY$Uwba^Nf* ziUGsF6kn~`t?H^e&=6vwKUom9xM>vyfV61`-$Rz?7nZFctrbROZ-`}#jh9mP z0zk2##5bs@BuwpvE`1Vk;qrAfZPpIY^n4YXuEUd;YYCYzxJh*5Cc*6kYyeBYo{YV_ zHe$_pYtg0ilNdPQWh`F22(e|ukho+RhQGcHNe9o`={TD)woFPn*&qBjfhcSN`@)Ng zqRcQ&Ga|55Fdk{y31?3q#p#oW5a4jY?en2g(>CbU|4n#3ZV17_m}+U;G$mA3vtIPK z;0B~=g@Lt#fV ze02=|H}w;wrKLgFbvPUjJl?()_U_$-au38|$ml5ukBG+A%NNWo8`g;JTN15!^2;~L z=0*g-*8FGyGn}qK-s`1ttU;qknL97X(l9p+%$hO^nb*_s%Gl|sP^kty?%V>+YnpCl zoz1_j#J)XStbL@Lm_;)5L(`lhoREnWgTt_8!)he1{tOM9w84u5$0Ii<(-NMXo&26aelhP|%hgDe_3J-~>({U2)G4Yx zH&(4$wefQQK^Qn_2sq~uf-GnUI7dW81bX*+9^ZYt8eN|sidOBqAv-(6x+NC2ZR<4a18_qDkWiaXIxe-uY+|!othI z;~|!kpO#6cxzoqWGbax-RaKuO07*iKhMl^;jQHjqRUx@`Bg5yTylx)+W2>@$?GpBI zlQtMQ;sfO7W?2G}R#Z*Hx65W@>*hoZ8axd15)zP_dew3xT4ce(wSYLzjl6Li2FnVe zp<(FR<5{fz@dtG5(pT1R*wS=3Tnc%D6aq|Dk+l71vF?YjNzOkkI-BoCiltGfE6}ol zVR%77Oajo~u7FZ#6yFxrYBT_)*X*ne?Ax;$hYsvQSXelI+qx4mF|n2g3EcfGvQAV~ z1m2!79@C~$+Rt~hz`$VW4u>TRS;Ob`*ou(^;1Pcl-4H+wfDk|La0586C`uXAG;8}= zuV@8iqbX`Ac7zUBK zSO5Y3J|Yvq1pxbK$WRoOppCX=yo|Arq?8jro;;O@M?`4bwr)jEPBtPVqYxk83>_YS z0?}oozzu2nxM3JK1?R28TlFD02yV9*9*_IBKqhN0U$%q~=-*e9oBl6onl_DdK4#I9<-Bj7o?5O)lD}D;T?lCf<$(kH@JI4qJk|MW zgoj5I34%0F1?$?ibgca`5lx%MqfWhgaOV;K&v!Yp3;|fRav6WAUvC01gL6(jg{@Fq zaQcG9TZ-$6X(OcyL({bPIp_bGH9LV17&u5vPrqilN#U%~qIqLnIDa0UyL82}<*ThU zNr~V_XGvfwKts{1XE&@}n~0d$SnSxDjDUcEf_gxE!o$O{XyI3U=#T*f-~-P2y8x&W zvb(vP`tRcCv<-mkx<1-4j2ROreaOd8cw4)AHNBulP=HasZ_}zd{{H(6dcM#bi-FSc*!ODr56+JK%D;{8S`A*Ha99GJP7KFn*loKbwYX zHr2T;H{20`s;b?E5UU0a9wz3@O;E2~xoT;bQl-=BL~tn`Cr_Wl&K=vWm>@!TJ>xn& zUJnc-pT6t511_fvp<$sGcS=gyiDu1Ppjwp*$j&va%;od>EK5X3M`6t9Q6gd9Y*kg& z=Y4{G0)2AOEdbp9TGy(N2V*NCE<6$urGnscI003oQcYDT z|3K<+qyz+q$LB?QZVpnerQzWDzp>@mpV)9P8P`4e+3~T*T8pHlovPRC-45W2gpKFB0VD=iTihB%8x5?;_@ZV6h)J!`7wYAcbdVL1i;pQIhC<(Ldr@* zTX)3N-h-8(;L?EGZFOAKtPs7U+F%Lmbma?oi<=-Y$O374J=Q)+9i0EW#HkPvh@9*! zj9a?EoWJE~Qo!FrO0vMElB`e?0ISha8Cx!->^bn!_Tuvq6IA5pSj>_%T$m1%K+NI9 zj$;RL{GW^H*Rq}Ef0nQS7H!*zn&o29tX4fppRWLLzK5BXg{Uz?sZtm;_ha$J)(xu4 z*a{)#3x!+ATm7gw0QTsQg6OZ9un2a1%4dqp;Z(q>2fm%GaVek$hoJR)qww2bhjD4< zB9tQ+qsKZK>3CptKeVb@4_hY91kcWdMh(#|i!vNEjpD?t@#YZoLPk1auSo;8dz}ke zg+o`>L52_uUVq{lKDGZyjb~;+qW~>pS*%sp@z?pYNJ+nn4)q=e?1o-p`1Q~qh>M6q z&De@COW;dfaA+Z6828m|K5hLEny#vYX*PAQ&j;)W9MiazV^<6v$DeQYxF&M4p_N3H zDQYPTkx#qxn~;JP5J+7O;Pc+dPDLn!Q?LgGW7TiJ@SgLhY8qqHxs=3*imb-PxsbJ4 zW$aTS<w!Vzts6my1*RZEJ2dz7=-?RE@|{A{|IyXhz5LoW#-7TqfVI}61sz{$wq z5dFmDPnD415JmdDQ2(E-Af&KTab|X=S^ceHW?FWZVZyBH@4$+D)Ot5{;4>lR;BJkY z8LMBLs8ccGUXL{wx|2WOtyL&)Xhfs=i(vOpCK_v!cj_u*3xt$I?xzk|Mb-Zb)fh|S zQdWDmVZ1T7|46M|nR3(!KpH-4@?Z^i6?@>4&mC^njKwPa+`_8CW_|&)dSl%IxLioR zl4`#4)olLVz9d~^>?D^m9srTL!q44q(#xH=kZrcgjIl&1W!>n~AsErV3x6U0F;=Bq zc?R8CK-4TwM8(LT|8E_yLt*(mh&Yf<_zW*jr(TrHcWsq(HzaD8vuNta4ofMYCKi0B zN4a+bKxl0crB%j05>gJLCflVu(6mYow5VR2H;k)-s!?T?GNnV5kbpqAG+j|hND|o6 zyo4zPJlrrd-Fdi@m1&;3nriM#IfLydj;Om&A6Gp2&5u`Q>~kUII{;|>6YSHKiBDnx)TKkX2 z*cnAp4nv5225`904@ih4-SxxsyR|@JkoHKUxZg>*{6i>uw}}(&B_2(x&*#&(FWO6j f7ar~0QMvyI0YBaYnDV?h00000NkvXXu0mjf>#vbu diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index fac36de3ed1a34f261661a6dc2255206d9e7abfb..075e024f3bbb92d1cad7c2c8f78f6b6ee9713efc 100755 GIT binary patch literal 6570 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 Hu0mjf3zA!p literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png deleted file mode 100755 index 971b92b47f914f582d2b5748d2b91ff0accfa788..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16980 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 diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100755 index 0000000000000000000000000000000000000000..180802b93485263e329a637d314bfeb8db328c27 GIT binary patch literal 14943 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 Z@P8 { } void _listenGame() { - final game = Game.empty(); final myChannel = _client.channel('games_channel'); + log('GameCubit: Database listen on'); + myChannel .onPostgresChanges( event: PostgresChangeEvent.all, schema: 'public', - table: game.tableName(), + table: 'game', callback: (payload) { log('GameCubit: Database change detected'); @@ -111,21 +113,16 @@ class GameCubit extends Cubit { final game = result.fold( (error) { - emit(state.copyWith(stateStatus: GameStateStatus.error)); + emit(state.copyWith(stateStatus: GameStateStatus.error, error: error)); return null; }, (response) => response, ); - if (state.isError) { - emit(state.copyWith(stateStatus: GameStateStatus.error)); - return; - } + if (state.isError) return; if (game.isNull) { - emit( - state.copyWith(stateStatus: GameStateStatus.success), - ); + emit(state.copyWith(stateStatus: GameStateStatus.success)); return; } diff --git a/lib/src/features/game/cubit/game_cubit.freezed.dart b/lib/src/features/game/cubit/game_cubit.freezed.dart index 1df84bf..75972cc 100644 --- a/lib/src/features/game/cubit/game_cubit.freezed.dart +++ b/lib/src/features/game/cubit/game_cubit.freezed.dart @@ -24,6 +24,7 @@ mixin _$GameState { (int, int)? get lastRivalResult => throw _privateConstructorUsedError; Player? get player => throw _privateConstructorUsedError; DateTime? get serverTime => throw _privateConstructorUsedError; + AppException? get error => throw _privateConstructorUsedError; /// Create a copy of GameState /// with the given fields replaced by the non-null parameter values. @@ -45,7 +46,8 @@ abstract class $GameStateCopyWith<$Res> { bool selectSecretNumberShowed, (int, int)? lastRivalResult, Player? player, - DateTime? serverTime}); + DateTime? serverTime, + AppException? error}); } /// @nodoc @@ -71,6 +73,7 @@ class _$GameStateCopyWithImpl<$Res, $Val extends GameState> Object? lastRivalResult = freezed, Object? player = freezed, Object? serverTime = freezed, + Object? error = freezed, }) { return _then(_value.copyWith( game: freezed == game @@ -105,6 +108,10 @@ class _$GameStateCopyWithImpl<$Res, $Val extends GameState> ? _value.serverTime : serverTime // ignore: cast_nullable_to_non_nullable as DateTime?, + error: freezed == error + ? _value.error + : error // ignore: cast_nullable_to_non_nullable + as AppException?, ) as $Val); } } @@ -125,7 +132,8 @@ abstract class _$$GameStateImplCopyWith<$Res> bool selectSecretNumberShowed, (int, int)? lastRivalResult, Player? player, - DateTime? serverTime}); + DateTime? serverTime, + AppException? error}); } /// @nodoc @@ -149,6 +157,7 @@ class __$$GameStateImplCopyWithImpl<$Res> Object? lastRivalResult = freezed, Object? player = freezed, Object? serverTime = freezed, + Object? error = freezed, }) { return _then(_$GameStateImpl( game: freezed == game @@ -183,6 +192,10 @@ class __$$GameStateImplCopyWithImpl<$Res> ? _value.serverTime : serverTime // ignore: cast_nullable_to_non_nullable as DateTime?, + error: freezed == error + ? _value.error + : error // ignore: cast_nullable_to_non_nullable + as AppException?, )); } } @@ -198,7 +211,8 @@ class _$GameStateImpl extends _GameState { this.selectSecretNumberShowed = false, this.lastRivalResult, this.player, - this.serverTime}) + this.serverTime, + this.error}) : _listGameStatus = listGameStatus, _listAttempts = listAttempts, super._(); @@ -235,10 +249,12 @@ class _$GameStateImpl extends _GameState { final Player? player; @override final DateTime? serverTime; + @override + final AppException? error; @override String toString() { - return 'GameState(game: $game, stateStatus: $stateStatus, listGameStatus: $listGameStatus, listAttempts: $listAttempts, selectSecretNumberShowed: $selectSecretNumberShowed, lastRivalResult: $lastRivalResult, player: $player, serverTime: $serverTime)'; + return 'GameState(game: $game, stateStatus: $stateStatus, listGameStatus: $listGameStatus, listAttempts: $listAttempts, selectSecretNumberShowed: $selectSecretNumberShowed, lastRivalResult: $lastRivalResult, player: $player, serverTime: $serverTime, error: $error)'; } @override @@ -260,7 +276,8 @@ class _$GameStateImpl extends _GameState { other.lastRivalResult == lastRivalResult) && (identical(other.player, player) || other.player == player) && (identical(other.serverTime, serverTime) || - other.serverTime == serverTime)); + other.serverTime == serverTime) && + (identical(other.error, error) || other.error == error)); } @override @@ -273,7 +290,8 @@ class _$GameStateImpl extends _GameState { selectSecretNumberShowed, lastRivalResult, player, - serverTime); + serverTime, + error); /// Create a copy of GameState /// with the given fields replaced by the non-null parameter values. @@ -293,7 +311,8 @@ abstract class _GameState extends GameState { final bool selectSecretNumberShowed, final (int, int)? lastRivalResult, final Player? player, - final DateTime? serverTime}) = _$GameStateImpl; + final DateTime? serverTime, + final AppException? error}) = _$GameStateImpl; const _GameState._() : super._(); @override @@ -312,6 +331,8 @@ abstract class _GameState extends GameState { Player? get player; @override DateTime? get serverTime; + @override + AppException? get error; /// Create a copy of GameState /// with the given fields replaced by the non-null parameter values. diff --git a/lib/src/features/game/cubit/game_state.dart b/lib/src/features/game/cubit/game_state.dart index d581b40..52aadf5 100644 --- a/lib/src/features/game/cubit/game_state.dart +++ b/lib/src/features/game/cubit/game_state.dart @@ -13,6 +13,7 @@ class GameState with _$GameState { final AttemptCallbackData? lastRivalResult, Player? player, DateTime? serverTime, + AppException? error, }) = _GameState; const GameState._(); diff --git a/lib/src/features/game/widgets/game_turn_widget.dart b/lib/src/features/game/widgets/game_turn_widget.dart index 36fc634..7c853f6 100644 --- a/lib/src/features/game/widgets/game_turn_widget.dart +++ b/lib/src/features/game/widgets/game_turn_widget.dart @@ -19,7 +19,7 @@ class GameTurnWidget extends StatelessWidget { borderRadius: BorderRadius.circular(5), ), child: Padding( - padding: const EdgeInsets.all(5), + padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 15), child: Text( ownPlayerNumber.isTurn ? context.l10n.yourTurn diff --git a/lib/src/features/home/widgets/game_section.dart b/lib/src/features/home/widgets/game_section.dart index bf36125..6cc88b7 100644 --- a/lib/src/features/home/widgets/game_section.dart +++ b/lib/src/features/home/widgets/game_section.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gutter/flutter_gutter.dart'; @@ -71,10 +72,11 @@ class GameSection extends HookWidget { child: Column( children: [ const Gutter(), - GameTurnWidget(ownPlayerNumber: ownPlayerNumber), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ + GameTurnWidget(ownPlayerNumber: ownPlayerNumber), + const Spacer(), Text( context.l10n.timeLeft, style: AppTextStyle().body.copyWith( @@ -133,50 +135,46 @@ class _PlayList extends StatelessWidget { @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; - return ListView( - padding: EdgeInsets.zero, - children: [ - Text( - context.l10n.previousAttempts.toUpperCase(), - style: AppTextStyle().body.copyWith( - fontWeight: FontWeight.w600, - color: colorScheme.onSurface, - ), - ), - const GutterSmall(), - BlocBuilder( - builder: (context, state) { - if (state.isError) { - // log('Error'); - } + return BlocBuilder( + builder: (context, state) { + if (state.isError) { + FirebaseCrashlytics.instance.recordFlutterError( + FlutterErrorDetails(exception: state.error!), + ); + } - final attempts = state.isLoading - ? getAttemptsMock(state.listAttempts.length + 1) - : state.listAttempts; - return Skeletonizer( - enabled: state.isLoading, - child: Column( - children: attempts.isEmpty - ? [ - Center( - child: Text( - context.l10n.noAttempts, - style: AppTextStyle().body.copyWith( - color: colorScheme.onSurface, - ), - ), - ), - ] - : attempts.asMap().entries.map((entry) { + final attempts = state.isLoading + ? getAttemptsMock(state.listAttempts.length + 1) + : state.listAttempts; + return attempts.isEmpty + ? Center( + child: Text( + context.l10n.noAttempts, + style: AppTextStyle() + .body + .copyWith(color: colorScheme.onSurface), + ), + ) + : ListView( + padding: EdgeInsets.zero, + children: [ + const GutterSmall(), + Skeletonizer( + enabled: state.isLoading, + child: Column( + children: attempts.asMap().entries.map((entry) { final index = attempts.length - entry.key; final value = entry.value; - return PlayNumberCard(attempt: value, index: index); + return PlayNumberCard( + attempt: value, + index: index, + ); }).toList(), - ), - ); - }, - ), - ], + ), + ), + ], + ); + }, ); } } From 78269085feb04c7ff0021bd7724a82b6687e4ced Mon Sep 17 00:00:00 2001 From: ale24dev Date: Sun, 6 Oct 2024 18:20:09 -0400 Subject: [PATCH 27/30] fix: logout error --- android/app/google-services.json | 2 +- .../app/src/main/ic_launcher-playstore.png | Bin 27524 -> 28175 bytes .../{ic_launcher.png => launcher_icon.png} | Bin .../{ic_launcher.png => launcher_icon.png} | Bin .../{ic_launcher.png => launcher_icon.png} | Bin .../{ic_launcher.png => launcher_icon.png} | Bin .../{ic_launcher.png => launcher_icon.png} | Bin .../home/widgets/user_header_info.dart | 118 +++++++++--------- .../features/settings/settings_screen.dart | 41 +++--- 9 files changed, 87 insertions(+), 74 deletions(-) rename android/app/src/main/res/mipmap-hdpi/{ic_launcher.png => launcher_icon.png} (100%) rename android/app/src/main/res/mipmap-mdpi/{ic_launcher.png => launcher_icon.png} (100%) rename android/app/src/main/res/mipmap-xhdpi/{ic_launcher.png => launcher_icon.png} (100%) rename android/app/src/main/res/mipmap-xxhdpi/{ic_launcher.png => launcher_icon.png} (100%) rename android/app/src/main/res/mipmap-xxxhdpi/{ic_launcher.png => launcher_icon.png} (100%) diff --git a/android/app/google-services.json b/android/app/google-services.json index cbe8efb..9a654f8 100644 --- a/android/app/google-services.json +++ b/android/app/google-services.json @@ -9,7 +9,7 @@ "client_info": { "mobilesdk_app_id": "1:43890310910:android:a2ac88ac0ab8108b0defaf", "android_client_info": { - "package_name": "com.quasar.easehouse" + "package_name": "com.mindcows.app" } }, "oauth_client": [], diff --git a/android/app/src/main/ic_launcher-playstore.png b/android/app/src/main/ic_launcher-playstore.png index d773eada6b3e15dbd355b99cedd720dde1b46def..5585017865cb018e902518dfa80bda02c894bcfe 100755 GIT binary patch literal 28175 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%0A_E5AQ2BUziI@ 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 Lv{WjTEW-Z>PH_kx literal 27524 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 Q=#! { - late Player player; + Player? player; @override Widget build(BuildContext context) { - player = context.watch().state.player!; - return Container( - width: context.widthPx, - color: Theme.of(context).colorScheme.primary, - child: Padding( - padding: context.responsiveContentPadding, - child: Column( - children: [ - SizedBox.square(dimension: context.heightPx * .1), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - CacheWidget( - size: const Size(40, 40), - imageUrl: player.avatarUrl, - fit: BoxFit.cover, - ), - const GutterSmall(), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - player.username, - style: AppTextStyle() - .body - .copyWith(fontWeight: FontWeight.bold), - ), - BlocBuilder( - builder: (context, state) { - if (state.isLoading) { - return const CircularProgressIndicator.adaptive(); - } + player = context.watch().state.player; + return player.isNull + ? const CircularProgressIndicator.adaptive() + : Container( + width: context.widthPx, + color: Theme.of(context).colorScheme.primary, + child: Padding( + padding: context.responsiveContentPadding, + child: Column( + children: [ + SizedBox.square(dimension: context.heightPx * .1), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + CacheWidget( + size: const Size(40, 40), + imageUrl: player!.avatarUrl, + fit: BoxFit.cover, + ), + const GutterSmall(), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + player!.username, + style: AppTextStyle() + .body + .copyWith(fontWeight: FontWeight.bold), + ), + BlocBuilder( + builder: (context, state) { + if (state.isLoading) { + return const CircularProgressIndicator + .adaptive(); + } - final ranking = RankingUtils.getRankingByPlayerId( - state.ranking, - player.id, - ); - return Text( - '${context.l10n.rank}: ${ranking.position}', - style: AppTextStyle().body.copyWith(fontSize: 12), - ); - }, - ), - ], - ), - ], - ), - UserPoints(player: player), - ], + final ranking = + RankingUtils.getRankingByPlayerId( + state.ranking, + player!.id, + ); + return Text( + '${context.l10n.rank}: ${ranking.position}', + style: AppTextStyle() + .body + .copyWith(fontSize: 12), + ); + }, + ), + ], + ), + ], + ), + UserPoints(player: player!), + ], + ), + ], + ), ), - ], - ), - ), - ); + ); } } diff --git a/lib/src/features/settings/settings_screen.dart b/lib/src/features/settings/settings_screen.dart index 6dad902..b7d5695 100644 --- a/lib/src/features/settings/settings_screen.dart +++ b/lib/src/features/settings/settings_screen.dart @@ -1,3 +1,5 @@ +// ignore_for_file: use_build_context_synchronously + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:my_app/l10n/l10n.dart'; @@ -24,10 +26,12 @@ class SettingsScreen extends StatelessWidget { children: [ ListTile( leading: const Icon(Icons.camera_alt_outlined), - title: Text(context.l10n.profileImage, - style: AppTextStyle() - .body - .copyWith(color: colorScheme.onSurface),), + title: Text( + context.l10n.profileImage, + style: AppTextStyle() + .body + .copyWith(color: colorScheme.onSurface), + ), onTap: () => showDialog( context: context, builder: (context) { @@ -37,10 +41,12 @@ class SettingsScreen extends StatelessWidget { ), ListTile( leading: const Icon(Icons.language), - title: Text(context.l10n.language, - style: AppTextStyle() - .body - .copyWith(color: colorScheme.onSurface),), + title: Text( + context.l10n.language, + style: AppTextStyle() + .body + .copyWith(color: colorScheme.onSurface), + ), onTap: () => showDialog( context: context, builder: (context) { @@ -50,10 +56,12 @@ class SettingsScreen extends StatelessWidget { ), ListTile( leading: const Icon(Icons.brightness_6), - title: Text(context.l10n.darkMode, - style: AppTextStyle() - .body - .copyWith(color: colorScheme.onSurface),), + title: Text( + context.l10n.darkMode, + style: AppTextStyle() + .body + .copyWith(color: colorScheme.onSurface), + ), trailing: SwitchTheme( data: Theme.of(context).switchTheme.copyWith(), child: Switch( @@ -81,8 +89,8 @@ class SettingsScreen extends StatelessWidget { ); } - void _showLogoutConfirmationDialog(BuildContext context) { - showDialog( + Future _showLogoutConfirmationDialog(BuildContext context) async { + await showDialog( context: context, builder: (BuildContext context) { return AlertDialog( @@ -99,9 +107,8 @@ class SettingsScreen extends StatelessWidget { ), ), TextButton( - onPressed: () { - Navigator.of(context).pop(); - context.read().logout(); + onPressed: () async { + await context.read().logout(); }, child: Text( context.l10n.logout, From 8de9723ec3d5ebb04c7e80eac21742e7e5b22e96 Mon Sep 17 00:00:00 2001 From: ale24dev Date: Tue, 8 Oct 2024 03:16:26 -0400 Subject: [PATCH 28/30] feat: game rules implementation --- lib/l10n/arb/app_en.arb | 3 +- lib/l10n/arb/app_es.arb | 3 +- .../core/di/dependency_injection.config.dart | 11 +- .../core/services/settings_datasource.dart | 7 +- lib/src/core/supabase/query_supabase.dart | 2 + lib/src/core/utils/utils.dart | 11 + .../settings/cubit/settings_cubit.dart | 25 +- .../cubit/settings_cubit.freezed.dart | 37 ++- .../settings/cubit/settings_state.dart | 1 + .../features/settings/data/model/rules.dart | 28 +++ .../settings/data/model/rules.freezed.dart | 230 ++++++++++++++++++ .../features/settings/data/model/rules.g.dart | 27 ++ .../settings/data/profile_images.dart | 2 +- .../settings/data/settings_repository.dart | 24 +- .../settings/pages/how_to_play_screen.dart | 30 +++ .../settings/{ => pages}/settings_screen.dart | 19 ++ lib/src/router/router.dart | 28 ++- 17 files changed, 471 insertions(+), 17 deletions(-) create mode 100644 lib/src/features/settings/data/model/rules.dart create mode 100644 lib/src/features/settings/data/model/rules.freezed.dart create mode 100644 lib/src/features/settings/data/model/rules.g.dart create mode 100644 lib/src/features/settings/pages/how_to_play_screen.dart rename lib/src/features/settings/{ => pages}/settings_screen.dart (85%) diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 5c1eaca..e74fef2 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -77,5 +77,6 @@ "theme": "Theme", "darkMode": "Dark mode", - "yourOpponentHasScored": "Your opponent has scored: " + "yourOpponentHasScored": "Your opponent has scored: ", + "howToPlay": "How to play?" } \ No newline at end of file diff --git a/lib/l10n/arb/app_es.arb b/lib/l10n/arb/app_es.arb index e1ebb2b..b76c37c 100644 --- a/lib/l10n/arb/app_es.arb +++ b/lib/l10n/arb/app_es.arb @@ -77,5 +77,6 @@ "theme": "Tema", "darkMode": "Modo oscuro", - "yourOpponentHasScored": "Tu rival acaba de sumar: " + "yourOpponentHasScored": "Tu rival acaba de sumar: ", + "howToPlay": "¿Cómo jugar?" } \ No newline at end of file diff --git a/lib/src/core/di/dependency_injection.config.dart b/lib/src/core/di/dependency_injection.config.dart index 8b6b991..afb41a4 100644 --- a/lib/src/core/di/dependency_injection.config.dart +++ b/lib/src/core/di/dependency_injection.config.dart @@ -55,12 +55,15 @@ extension GetItInjectableX on _i174.GetIt { gh.lazySingleton<_i454.SupabaseClient>(() => supabaseModule.client); gh.singleton<_i220.Preferences>( () => _i220.Preferences(gh<_i460.SharedPreferences>())); + gh.singleton<_i94.SettingsDatasource>(() => _i621.SettingsRepository( + gh<_i220.Preferences>(), + gh<_i454.SupabaseClient>(), + gh<_i330.SupabaseServiceImpl>(), + )); gh.singleton<_i427.AuthRepository>( () => _i427.AuthRepository(gh<_i454.SupabaseClient>())); gh.singleton<_i63.RouterController>( () => _i63.RouterController(gh<_i454.SupabaseClient>())); - gh.singleton<_i94.SettingsLocalDatasource>( - () => _i621.SettingsRepository(gh<_i220.Preferences>())); gh.singleton<_i34.RankingRepository>(() => _i34.RankingRepository( gh<_i330.SupabaseServiceImpl>(), gh<_i454.SupabaseClient>(), @@ -78,6 +81,8 @@ extension GetItInjectableX on _i174.GetIt { gh<_i34.GameRepository>(), gh<_i406.PlayerRepository>(), )); + gh.factory<_i303.SettingsCubit>( + () => _i303.SettingsCubit(gh<_i94.SettingsDatasource>())); gh.factory<_i931.RankingCubit>( () => _i931.RankingCubit(gh<_i34.RankingRepository>())); gh.factory<_i126.PlayerCubit>(() => _i126.PlayerCubit( @@ -88,8 +93,6 @@ extension GetItInjectableX on _i174.GetIt { gh<_i427.AuthRepository>(), gh<_i454.SupabaseClient>(), )); - gh.factory<_i303.SettingsCubit>( - () => _i303.SettingsCubit(gh<_i94.SettingsLocalDatasource>())); gh.factory<_i1038.AppCubit>(() => _i1038.AppCubit( gh<_i34.GameRepository>(), gh<_i454.SupabaseClient>(), diff --git a/lib/src/core/services/settings_datasource.dart b/lib/src/core/services/settings_datasource.dart index 6941b4c..0daf69a 100644 --- a/lib/src/core/services/settings_datasource.dart +++ b/lib/src/core/services/settings_datasource.dart @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:my_app/src/core/exceptions.dart'; +import 'package:my_app/src/features/settings/data/model/rules.dart'; -abstract class SettingsLocalDatasource { +abstract class SettingsDatasource { Locale changeLanguage(String language); ThemeMode changeTheme(); @@ -8,4 +11,6 @@ abstract class SettingsLocalDatasource { Locale getLanguage(); ThemeMode getTheme(); + + Future?>> getRules(); } diff --git a/lib/src/core/supabase/query_supabase.dart b/lib/src/core/supabase/query_supabase.dart index a959b60..b13517e 100644 --- a/lib/src/core/supabase/query_supabase.dart +++ b/lib/src/core/supabase/query_supabase.dart @@ -1,5 +1,7 @@ ///This class contains all tables inside of supabase database abstract class QuerySupabase { + static String get rules => 'id, rules, language, updated_at'; + static String get player => 'id, username, avatar_url'; static String get playerNumber => diff --git a/lib/src/core/utils/utils.dart b/lib/src/core/utils/utils.dart index f575396..635fe7e 100644 --- a/lib/src/core/utils/utils.dart +++ b/lib/src/core/utils/utils.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:my_app/l10n/l10n.dart'; +import 'package:my_app/src/features/settings/data/model/rules.dart'; abstract class Utils { static bool isValidPlayerNumber(String value) { @@ -26,4 +27,14 @@ abstract class Utils { return const Locale('en', 'US'); } } + + static Rules getRulesByLanguage( + String language, { + required List rules, + }) { + final rule = + rules.firstWhere((element) => element.language.name == language); + + return rule; + } } diff --git a/lib/src/features/settings/cubit/settings_cubit.dart b/lib/src/features/settings/cubit/settings_cubit.dart index 94c30c5..8e76518 100644 --- a/lib/src/features/settings/cubit/settings_cubit.dart +++ b/lib/src/features/settings/cubit/settings_cubit.dart @@ -4,6 +4,7 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:injectable/injectable.dart'; import 'package:my_app/src/core/exceptions.dart'; import 'package:my_app/src/core/services/settings_datasource.dart'; +import 'package:my_app/src/features/settings/data/model/rules.dart'; part 'settings_state.dart'; part 'settings_cubit.freezed.dart'; @@ -14,7 +15,7 @@ class SettingsCubit extends Cubit { _initData(); } - final SettingsLocalDatasource _settingsDatasource; + final SettingsDatasource _settingsDatasource; void changeLanguage(String language) { final locale = _settingsDatasource.changeLanguage(language); @@ -30,6 +31,7 @@ class SettingsCubit extends Cubit { void _initData() { getTheme(); getLanguage(); + getRules(); } void getTheme() { @@ -39,4 +41,25 @@ class SettingsCubit extends Cubit { void getLanguage() { emit(state.copyWith(locale: _settingsDatasource.getLanguage())); } + + void getRules() { + emit(state.copyWith(stateStatus: SettingsStateStatus.loading)); + + _settingsDatasource.getRules().then((result) { + result.fold( + (error) => emit( + state.copyWith( + stateStatus: SettingsStateStatus.error, + error: error, + ), + ), + (rules) => emit( + state.copyWith( + rules: rules ?? [], + stateStatus: SettingsStateStatus.loaded, + ), + ), + ); + }); + } } diff --git a/lib/src/features/settings/cubit/settings_cubit.freezed.dart b/lib/src/features/settings/cubit/settings_cubit.freezed.dart index f3d2285..049f2ef 100644 --- a/lib/src/features/settings/cubit/settings_cubit.freezed.dart +++ b/lib/src/features/settings/cubit/settings_cubit.freezed.dart @@ -18,6 +18,7 @@ final _privateConstructorUsedError = UnsupportedError( mixin _$SettingsState { SettingsStateStatus get stateStatus => throw _privateConstructorUsedError; Locale get locale => throw _privateConstructorUsedError; + List? get rules => throw _privateConstructorUsedError; ThemeMode? get theme => throw _privateConstructorUsedError; AppException? get error => throw _privateConstructorUsedError; @@ -37,6 +38,7 @@ abstract class $SettingsStateCopyWith<$Res> { $Res call( {SettingsStateStatus stateStatus, Locale locale, + List? rules, ThemeMode? theme, AppException? error}); } @@ -58,6 +60,7 @@ class _$SettingsStateCopyWithImpl<$Res, $Val extends SettingsState> $Res call({ Object? stateStatus = null, Object? locale = null, + Object? rules = freezed, Object? theme = freezed, Object? error = freezed, }) { @@ -70,6 +73,10 @@ class _$SettingsStateCopyWithImpl<$Res, $Val extends SettingsState> ? _value.locale : locale // ignore: cast_nullable_to_non_nullable as Locale, + rules: freezed == rules + ? _value.rules + : rules // ignore: cast_nullable_to_non_nullable + as List?, theme: freezed == theme ? _value.theme : theme // ignore: cast_nullable_to_non_nullable @@ -93,6 +100,7 @@ abstract class _$$SettingsStateImplCopyWith<$Res> $Res call( {SettingsStateStatus stateStatus, Locale locale, + List? rules, ThemeMode? theme, AppException? error}); } @@ -112,6 +120,7 @@ class __$$SettingsStateImplCopyWithImpl<$Res> $Res call({ Object? stateStatus = null, Object? locale = null, + Object? rules = freezed, Object? theme = freezed, Object? error = freezed, }) { @@ -124,6 +133,10 @@ class __$$SettingsStateImplCopyWithImpl<$Res> ? _value.locale : locale // ignore: cast_nullable_to_non_nullable as Locale, + rules: freezed == rules + ? _value._rules + : rules // ignore: cast_nullable_to_non_nullable + as List?, theme: freezed == theme ? _value.theme : theme // ignore: cast_nullable_to_non_nullable @@ -142,9 +155,11 @@ class _$SettingsStateImpl extends _SettingsState { const _$SettingsStateImpl( {this.stateStatus = SettingsStateStatus.initial, this.locale = const Locale('en'), + final List? rules, this.theme, this.error}) - : super._(); + : _rules = rules, + super._(); @override @JsonKey() @@ -152,6 +167,16 @@ class _$SettingsStateImpl extends _SettingsState { @override @JsonKey() final Locale locale; + final List? _rules; + @override + List? get rules { + final value = _rules; + if (value == null) return null; + if (_rules is EqualUnmodifiableListView) return _rules; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); + } + @override final ThemeMode? theme; @override @@ -159,7 +184,7 @@ class _$SettingsStateImpl extends _SettingsState { @override String toString() { - return 'SettingsState(stateStatus: $stateStatus, locale: $locale, theme: $theme, error: $error)'; + return 'SettingsState(stateStatus: $stateStatus, locale: $locale, rules: $rules, theme: $theme, error: $error)'; } @override @@ -170,13 +195,14 @@ class _$SettingsStateImpl extends _SettingsState { (identical(other.stateStatus, stateStatus) || other.stateStatus == stateStatus) && (identical(other.locale, locale) || other.locale == locale) && + const DeepCollectionEquality().equals(other._rules, _rules) && (identical(other.theme, theme) || other.theme == theme) && (identical(other.error, error) || other.error == error)); } @override - int get hashCode => - Object.hash(runtimeType, stateStatus, locale, theme, error); + int get hashCode => Object.hash(runtimeType, stateStatus, locale, + const DeepCollectionEquality().hash(_rules), theme, error); /// Create a copy of SettingsState /// with the given fields replaced by the non-null parameter values. @@ -191,6 +217,7 @@ abstract class _SettingsState extends SettingsState { const factory _SettingsState( {final SettingsStateStatus stateStatus, final Locale locale, + final List? rules, final ThemeMode? theme, final AppException? error}) = _$SettingsStateImpl; const _SettingsState._() : super._(); @@ -200,6 +227,8 @@ abstract class _SettingsState extends SettingsState { @override Locale get locale; @override + List? get rules; + @override ThemeMode? get theme; @override AppException? get error; diff --git a/lib/src/features/settings/cubit/settings_state.dart b/lib/src/features/settings/cubit/settings_state.dart index 9b48469..77d4268 100644 --- a/lib/src/features/settings/cubit/settings_state.dart +++ b/lib/src/features/settings/cubit/settings_state.dart @@ -20,6 +20,7 @@ class SettingsState with _$SettingsState { const factory SettingsState({ @Default(SettingsStateStatus.initial) SettingsStateStatus stateStatus, @Default(Locale('en')) Locale locale, + List? rules, ThemeMode? theme, AppException? error, }) = _SettingsState; diff --git a/lib/src/features/settings/data/model/rules.dart b/lib/src/features/settings/data/model/rules.dart new file mode 100644 index 0000000..cebed4b --- /dev/null +++ b/lib/src/features/settings/data/model/rules.dart @@ -0,0 +1,28 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:my_app/src/core/utils/object_extensions.dart'; +import 'package:my_app/src/features/settings/cubit/settings_cubit.dart'; + +part 'rules.freezed.dart'; +part 'rules.g.dart'; + +List rulesFromJson(List str) => + str.map((x) => Rules.fromJson(x as Json)).toList(); + +@freezed +class Rules with _$Rules { + const factory Rules({ + required int id, + required String rules, + required LanguageEnum language, + @JsonKey(name: 'updated_at') required DateTime updatedAt, + }) = _Rules; + + factory Rules.fromJson(Map json) => _$RulesFromJson(json); +} + +enum LanguageEnum { + @JsonValue('en') + en, + @JsonValue('es') + es, +} diff --git a/lib/src/features/settings/data/model/rules.freezed.dart b/lib/src/features/settings/data/model/rules.freezed.dart new file mode 100644 index 0000000..c2e631b --- /dev/null +++ b/lib/src/features/settings/data/model/rules.freezed.dart @@ -0,0 +1,230 @@ +// 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 'rules.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'); + +Rules _$RulesFromJson(Map json) { + return _Rules.fromJson(json); +} + +/// @nodoc +mixin _$Rules { + int get id => throw _privateConstructorUsedError; + String get rules => throw _privateConstructorUsedError; + LanguageEnum get language => throw _privateConstructorUsedError; + @JsonKey(name: 'updated_at') + DateTime get updatedAt => throw _privateConstructorUsedError; + + /// Serializes this Rules to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of Rules + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $RulesCopyWith get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $RulesCopyWith<$Res> { + factory $RulesCopyWith(Rules value, $Res Function(Rules) then) = + _$RulesCopyWithImpl<$Res, Rules>; + @useResult + $Res call( + {int id, + String rules, + LanguageEnum language, + @JsonKey(name: 'updated_at') DateTime updatedAt}); +} + +/// @nodoc +class _$RulesCopyWithImpl<$Res, $Val extends Rules> + implements $RulesCopyWith<$Res> { + _$RulesCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of Rules + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? rules = null, + Object? language = null, + Object? updatedAt = null, + }) { + return _then(_value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int, + rules: null == rules + ? _value.rules + : rules // ignore: cast_nullable_to_non_nullable + as String, + language: null == language + ? _value.language + : language // ignore: cast_nullable_to_non_nullable + as LanguageEnum, + updatedAt: null == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$RulesImplCopyWith<$Res> implements $RulesCopyWith<$Res> { + factory _$$RulesImplCopyWith( + _$RulesImpl value, $Res Function(_$RulesImpl) then) = + __$$RulesImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {int id, + String rules, + LanguageEnum language, + @JsonKey(name: 'updated_at') DateTime updatedAt}); +} + +/// @nodoc +class __$$RulesImplCopyWithImpl<$Res> + extends _$RulesCopyWithImpl<$Res, _$RulesImpl> + implements _$$RulesImplCopyWith<$Res> { + __$$RulesImplCopyWithImpl( + _$RulesImpl _value, $Res Function(_$RulesImpl) _then) + : super(_value, _then); + + /// Create a copy of Rules + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? rules = null, + Object? language = null, + Object? updatedAt = null, + }) { + return _then(_$RulesImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int, + rules: null == rules + ? _value.rules + : rules // ignore: cast_nullable_to_non_nullable + as String, + language: null == language + ? _value.language + : language // ignore: cast_nullable_to_non_nullable + as LanguageEnum, + updatedAt: null == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$RulesImpl implements _Rules { + const _$RulesImpl( + {required this.id, + required this.rules, + required this.language, + @JsonKey(name: 'updated_at') required this.updatedAt}); + + factory _$RulesImpl.fromJson(Map json) => + _$$RulesImplFromJson(json); + + @override + final int id; + @override + final String rules; + @override + final LanguageEnum language; + @override + @JsonKey(name: 'updated_at') + final DateTime updatedAt; + + @override + String toString() { + return 'Rules(id: $id, rules: $rules, language: $language, updatedAt: $updatedAt)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$RulesImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.rules, rules) || other.rules == rules) && + (identical(other.language, language) || + other.language == language) && + (identical(other.updatedAt, updatedAt) || + other.updatedAt == updatedAt)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, id, rules, language, updatedAt); + + /// Create a copy of Rules + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$RulesImplCopyWith<_$RulesImpl> get copyWith => + __$$RulesImplCopyWithImpl<_$RulesImpl>(this, _$identity); + + @override + Map toJson() { + return _$$RulesImplToJson( + this, + ); + } +} + +abstract class _Rules implements Rules { + const factory _Rules( + {required final int id, + required final String rules, + required final LanguageEnum language, + @JsonKey(name: 'updated_at') required final DateTime updatedAt}) = + _$RulesImpl; + + factory _Rules.fromJson(Map json) = _$RulesImpl.fromJson; + + @override + int get id; + @override + String get rules; + @override + LanguageEnum get language; + @override + @JsonKey(name: 'updated_at') + DateTime get updatedAt; + + /// Create a copy of Rules + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$RulesImplCopyWith<_$RulesImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/src/features/settings/data/model/rules.g.dart b/lib/src/features/settings/data/model/rules.g.dart new file mode 100644 index 0000000..bd45c53 --- /dev/null +++ b/lib/src/features/settings/data/model/rules.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'rules.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$RulesImpl _$$RulesImplFromJson(Map json) => _$RulesImpl( + id: (json['id'] as num).toInt(), + rules: json['rules'] as String, + language: $enumDecode(_$LanguageEnumEnumMap, json['language']), + updatedAt: DateTime.parse(json['updated_at'] as String), + ); + +Map _$$RulesImplToJson(_$RulesImpl instance) => + { + 'id': instance.id, + 'rules': instance.rules, + 'language': _$LanguageEnumEnumMap[instance.language]!, + 'updated_at': instance.updatedAt.toIso8601String(), + }; + +const _$LanguageEnumEnumMap = { + LanguageEnum.en: 'en', + LanguageEnum.es: 'es', +}; diff --git a/lib/src/features/settings/data/profile_images.dart b/lib/src/features/settings/data/profile_images.dart index c645a52..2e6c259 100644 --- a/lib/src/features/settings/data/profile_images.dart +++ b/lib/src/features/settings/data/profile_images.dart @@ -9,5 +9,5 @@ final profileImagesUrl = [ '${Constants.publicStorageUrl}/avatars/51491.jpg', '${Constants.publicStorageUrl}/avatars/businesswoman-cartoon-character-gray-background-3d-illustration-business-concept.jpg', '${Constants.publicStorageUrl}/avatars/50955.jpg', - '${Constants.publicStorageUrl}/avatars/portrait-beautiful-young-woman-with-stylish-hairstyle-glasses.jpg?t=2024-10-05T04%3A16%3A07.395Z', + '${Constants.publicStorageUrl}/avatars/portrait-beautiful-young-woman-with-stylish-hairstyle-glasses.jpg', ]; diff --git a/lib/src/features/settings/data/settings_repository.dart b/lib/src/features/settings/data/settings_repository.dart index e1107c0..cff6a07 100644 --- a/lib/src/features/settings/data/settings_repository.dart +++ b/lib/src/features/settings/data/settings_repository.dart @@ -1,14 +1,22 @@ import 'package:flutter/material.dart'; +import 'package:fpdart/src/either.dart'; import 'package:injectable/injectable.dart'; +import 'package:my_app/src/core/exceptions.dart'; +import 'package:my_app/src/core/interceptor.dart'; import 'package:my_app/src/core/preferences/preferences.dart'; import 'package:my_app/src/core/services/settings_datasource.dart'; +import 'package:my_app/src/core/supabase/query_supabase.dart'; import 'package:my_app/src/core/utils/utils.dart'; +import 'package:my_app/src/features/settings/data/model/rules.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; -@Singleton(as: SettingsLocalDatasource) -class SettingsRepository extends SettingsLocalDatasource { - SettingsRepository(this._preferences); +@Singleton(as: SettingsDatasource) +class SettingsRepository extends SettingsDatasource { + SettingsRepository(this._preferences, this._client, this._supabaseService); final Preferences _preferences; + final SupabaseClient _client; + final SupabaseServiceImpl _supabaseService; @override Locale changeLanguage(String language) { @@ -32,4 +40,14 @@ class SettingsRepository extends SettingsLocalDatasource { ThemeMode getTheme() { return _preferences.getTheme(); } + + @override + Future?>> getRules() async { + return _supabaseService.query>( + table: 'getRules', + request: () => _client.from('rules').select(QuerySupabase.rules), + queryOption: QueryOption.select, + fromJsonParse: rulesFromJson, + ); + } } diff --git a/lib/src/features/settings/pages/how_to_play_screen.dart b/lib/src/features/settings/pages/how_to_play_screen.dart new file mode 100644 index 0000000..1045cd5 --- /dev/null +++ b/lib/src/features/settings/pages/how_to_play_screen.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gutter/flutter_gutter.dart'; +import 'package:my_app/l10n/l10n.dart'; +import 'package:my_app/src/core/ui/device.dart'; +import 'package:my_app/src/features/settings/data/model/rules.dart'; + +class HowToPlayScreen extends StatelessWidget { + const HowToPlayScreen({required this.rules, super.key}); + + final Rules rules; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text(context.l10n.howToPlay)), + body: Padding( + padding: context.responsiveContentPadding, + child: SingleChildScrollView( + child: Column( + children: [ + const Gutter(), + Text(rules.rules), + const Gutter(), + ], + ), + ), + ), + ); + } +} diff --git a/lib/src/features/settings/settings_screen.dart b/lib/src/features/settings/pages/settings_screen.dart similarity index 85% rename from lib/src/features/settings/settings_screen.dart rename to lib/src/features/settings/pages/settings_screen.dart index b7d5695..017059d 100644 --- a/lib/src/features/settings/settings_screen.dart +++ b/lib/src/features/settings/pages/settings_screen.dart @@ -2,13 +2,16 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; import 'package:my_app/l10n/l10n.dart'; import 'package:my_app/src/core/ui/colors.dart'; import 'package:my_app/src/core/ui/typography.dart'; +import 'package:my_app/src/core/utils/utils.dart'; import 'package:my_app/src/features/auth/cubit/auth_cubit.dart'; import 'package:my_app/src/features/settings/cubit/settings_cubit.dart'; import 'package:my_app/src/features/settings/widgets/change_language_dialog.dart'; import 'package:my_app/src/features/settings/widgets/change_profile_image_dialog.dart'; +import 'package:my_app/src/router/router.dart'; class SettingsScreen extends StatelessWidget { const SettingsScreen({super.key}); @@ -39,6 +42,22 @@ class SettingsScreen extends StatelessWidget { }, ), ), + ListTile( + leading: const Icon(Icons.question_mark), + title: Text( + context.l10n.howToPlay, + style: AppTextStyle() + .body + .copyWith(color: colorScheme.onSurface), + ), + onTap: () => context.goNamed( + AppRoute.howToPlay.name, + extra: Utils.getRulesByLanguage( + context.l10n.localeName, + rules: state.rules!, + ), + ), + ), ListTile( leading: const Icon(Icons.language), title: Text( diff --git a/lib/src/router/router.dart b/lib/src/router/router.dart index 51f4fcd..2175c72 100644 --- a/lib/src/router/router.dart +++ b/lib/src/router/router.dart @@ -5,13 +5,16 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:injectable/injectable.dart'; +import 'package:my_app/src/core/exceptions.dart'; import 'package:my_app/src/core/utils/object_extensions.dart'; import 'package:my_app/src/features/auth/views/auth_screen.dart'; import 'package:my_app/src/features/auth/views/signup_screen.dart'; import 'package:my_app/src/features/game/game_screen.dart'; import 'package:my_app/src/features/game/search_game_screen.dart'; import 'package:my_app/src/features/home/home_screen.dart'; -import 'package:my_app/src/features/settings/settings_screen.dart'; +import 'package:my_app/src/features/settings/data/model/rules.dart'; +import 'package:my_app/src/features/settings/pages/how_to_play_screen.dart'; +import 'package:my_app/src/features/settings/pages/settings_screen.dart'; import 'package:my_app/src/features/splash/splash_screen.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; @@ -24,6 +27,7 @@ enum AppRoute { game, searchGame, settings, + howToPlay, } final rootNavigatorKey = GlobalKey(debugLabel: 'root'); @@ -109,6 +113,19 @@ final class RouterController { child: const SettingsScreen(), ); }, + routes: [ + GoRoute( + path: 'how-to-play', + name: AppRoute.howToPlay.name, + pageBuilder: (context, state) { + final rules = extraFromState(state); + return adaptivePageRoute( + key: ValueKey(state.pageKey.value), + child: HowToPlayScreen(rules: rules), + ); + }, + ), + ], ), ], ), @@ -158,3 +175,12 @@ Page adaptivePageRoute({ restorationId: restorationId, ); } + +T extraFromState(GoRouterState state, [T? orElse]) { + if (state.extra is T) { + return state.extra as T; + } else { + if (orElse != null) return orElse; + throw Exception(state.uri.path); + } +} From ff56936a1ee7a26583ad392c43299147b7cb6009 Mon Sep 17 00:00:00 2001 From: ale24dev Date: Tue, 8 Oct 2024 03:17:29 -0400 Subject: [PATCH 29/30] add: find or create game function --- lib/src/core/interceptor.dart | 18 +- .../features/game/data/game_repository.dart | 18 +- .../functions/find-or-create-game/index.ts | 192 ++++++++++++++++++ 3 files changed, 218 insertions(+), 10 deletions(-) create mode 100644 supabase/functions/find-or-create-game/index.ts diff --git a/lib/src/core/interceptor.dart b/lib/src/core/interceptor.dart index f4f74be..b921786 100644 --- a/lib/src/core/interceptor.dart +++ b/lib/src/core/interceptor.dart @@ -1,8 +1,13 @@ +// ignore_for_file: avoid_dynamic_calls + +import 'dart:convert'; + import 'package:fpdart/fpdart.dart'; import 'package:injectable/injectable.dart'; import 'package:logger/logger.dart'; import 'package:my_app/src/core/di/dependency_injection.dart'; import 'package:my_app/src/core/exceptions.dart'; +import 'package:my_app/src/core/utils/object_extensions.dart'; import 'package:my_app/src/features/auth/data/auth_repository.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; @@ -11,6 +16,7 @@ enum QueryOption { select, insert, uploadStorage, update, delete } @singleton class SupabaseServiceImpl { SupabaseServiceImpl(); + /// Executes a query to the database. /// /// [table] The name of the table. @@ -48,9 +54,15 @@ class SupabaseServiceImpl { return right(response as T); } - final parsedData = fromJsonParse != null - ? fromJsonParse(table == 'result' ? response[0] : response) as T - : response as T; + T parsedData; + if (response is FunctionResponse && fromJsonParse.isNotNull) { + final responseString = json.decode(response.data.toString()); + parsedData = fromJsonParse!(responseString['data']) as T; + } else { + parsedData = fromJsonParse != null + ? fromJsonParse(response) as T + : response as T; + } logger.d('✅ Request to -> ${queryOption.name} $table'); return right(parsedData); diff --git a/lib/src/features/game/data/game_repository.dart b/lib/src/features/game/data/game_repository.dart index 4fce6b4..e303d9a 100644 --- a/lib/src/features/game/data/game_repository.dart +++ b/lib/src/features/game/data/game_repository.dart @@ -1,5 +1,7 @@ // ignore_for_file: prefer_interpolation_to_compose_strings +import 'dart:developer'; + import 'package:fpdart/fpdart.dart'; import 'package:injectable/injectable.dart'; import 'package:my_app/src/core/exceptions.dart'; @@ -22,14 +24,17 @@ class GameRepository extends GameDataSource { final SupabaseClient _client; final gameStatus = GameStatus.empty(); + @override - Future> findOrCreateGame(Player player) { + Future> findOrCreateGame(Player player) async{ return _supabaseServiceImpl.query( table: 'RPC create_game', - request: () => - // _client.rpc('create_game', params: {'player_id': player.id}), - _client - .rpc('find_or_create_game', params: {'p_player_id': player.id}), + request: () => _client.functions.invoke( + 'find-or-create-game', + body: { + 'playerId': player.id, + }, + ), queryOption: QueryOption.insert, fromJsonParse: Game.fromJson, ); @@ -142,11 +147,10 @@ class GameRepository extends GameDataSource { schema: 'public', table: 'attempt', callback: (payload) { - final rival = game.getRivalPlayerNumber(player); final newRecord = payload.newRecord['player'] as String; - if(newRecord != rival.player.id) return; + if (newRecord != rival.player.id) return; final cows = payload.newRecord['cows'] as int; final bulls = payload.newRecord['bulls'] as int; return callback((cows, bulls)); diff --git a/supabase/functions/find-or-create-game/index.ts b/supabase/functions/find-or-create-game/index.ts new file mode 100644 index 0000000..05af79d --- /dev/null +++ b/supabase/functions/find-or-create-game/index.ts @@ -0,0 +1,192 @@ +import { createClient } from "jsr:@supabase/supabase-js@2"; + +const player = "id, username, avatar_url, is_bot"; +const playerNumber = `id, player(${player}), number, time_left, is_turn, started_time, attempts_to_win`; +const gameStatus = "id, status"; +const gameColumns = ` + id, status!inner(${gameStatus}), player_number1!inner(${playerNumber}), player_number2!inner(${playerNumber}), winner(${player}) + `; + +// Servidor +Deno.serve(async (req) => { + const supabase = createSupabaseClient(req); + const { playerId } = await req.json(); // Suponemos que el ID del jugador se envía en el cuerpo de la solicitud + console.log("Player ID received:", playerId); + + try { + const game = await findOrCreateGame(supabase, playerId); + console.log("Game found or created:", game); + return new Response(JSON.stringify(game), { status: 200 }); + } catch (error) { + console.error("Error in handler:", error); + return new Response(JSON.stringify({ error: error.message }), { status: 500 }); + } +}); + +// Función para esperar un tiempo específico en milisegundos +function wait(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +function getRandomNumberToAttemptsWin() { + return Math.floor(Math.random() * (10 - 6 + 1)) + 6; +} + +// Función para crear el cliente de Supabase +function createSupabaseClient(req: Request) { + const supabaseUrl = Deno.env.get("SUPABASE_URL") ?? ""; + const supabaseKey = Deno.env.get("SUPABASE_ANON_KEY") ?? ""; + + console.log("Creating Supabase client with URL:", supabaseUrl); + + return createClient(supabaseUrl, supabaseKey, { + global: { headers: { Authorization: req.headers.get("Authorization") ?? "" } }, + }); +} + +async function pairPlayerWithBot(supabase: any, gameId: number) { + console.log("Pairing player with bot for game ID:", gameId); + const { data: success, error } = await supabase.rpc("pair_player_with_bot", { game_id: gameId }); + + if (error || !success) { + console.error("Error pairing player with bot:", error ?? "No success response"); + throw new Error(`Error pairing player with bot: ${error?.message ?? "No success response"}`); + } + console.log("Player paired with bot: ", success); +} + +// Función para encontrar o crear un juego +async function findOrCreateGame(supabase: any, playerId: string) { + console.log("Finding or creating game for player ID:", playerId); + + const { data: searchingStatus } = await supabase.from("game_status").select("id").eq("status", "searching").single(); + + if (!searchingStatus) { + console.error('No status with value "searching" found'); + throw new Error('No status with value "searching" found'); + } + + const { data: selectingSecretNumberStatus } = await supabase.from("game_status").select("id").eq("status", "selecting_secret_numbers").single(); + + if (!selectingSecretNumberStatus) { + console.error('No status with value "selecting_secret_numbers" found'); + throw new Error('No status with value "selecting_secret_numbers" found'); + } + console.log("Searching game...", searchingStatus.id); + + // Buscar un juego existente + const { data: foundGame } = await supabase.from("game").select("id, player_number1, player_number2").eq("status", searchingStatus.id).single(); + + console.log("Found game:", foundGame); + + const playerNumberId = await createPlayerNumber(supabase, playerId, null, null); + + let newGameId: number; + + if (foundGame) { + console.log("Updating existing game..."); + await updateGame(supabase, foundGame.id, playerNumberId, selectingSecretNumberStatus.id); + } else { + console.log("Creating new game..."); + newGameId = await createNewGame(supabase, playerNumberId, searchingStatus.id); + + await wait(5000); // Esperar 10 segundos para que otro jugador se una al juego + + const isSearching = await verifyIfGameIsSearching(supabase, newGameId, searchingStatus.id); + console.log("Is searching yet?", isSearching); + + if (isSearching) { + console.log("Updating new game with bot player..."); + + await pairPlayerWithBot(supabase, newGameId); + } + } + return supabase + .from("game") + .select(gameColumns) + .eq("id", foundGame ? foundGame.id : newGameId!) + .single(); +} + +// Función para verificar si el juego está en progreso +async function verifyIfGameIsSearching(supabase: any, gameId: number, searchingStatusId: number) { + console.log("Verifying if game is in progress for game ID:", gameId); + + const { status, error } = await supabase.from("game").select("status").eq("id", gameId).single(); + + if (error) { + console.error("Error searching game:", error); + throw new Error(`Error searching game: ${error.message}`); + } + + console.log("Game status:", status); + if (status.id === searchingStatusId) return true; + + return false; +} + +// Función para crear un nuevo juego con el jugador utilizando RPC +async function createNewGame(supabase: any, playerNumberId: number, searchingStatusId: number) { + console.log("Creating new game..."); + + const { data, error } = await supabase.from("game").insert({ player_number1: playerNumberId, status: searchingStatusId }).select("id").single(); + + if (error) { + console.error("Error creating game:", error); + throw new Error(`Error creating game: ${error.message}`); + } + + console.log("New game created with ID:", data); + return data.id; // Devuelve el resultado de la función RPC +} + +// Función para actualizar el juego con el jugador encontrado +async function updateGame(supabase: any, gameId: number, playerNumberId: number, selectingSecretNumberStatusId: number) { + console.log("Updating game ID:", gameId, "with player number ID:", playerNumberId); + + // Actualizar el juego + const { game, error } = await supabase + .from("game") + .update({ player_number2: playerNumberId, status: selectingSecretNumberStatusId }) + .eq("id", gameId) + .select(gameColumns) + .single(); + + if (error) { + throw new Error("Error inserting player number"); + } + + console.log("Game updated:", game); +} + +// Función para crear un nuevo player_number +async function createPlayerNumber(supabase: any, playerId: string | null, botNumbers: number[] | null, attemptsToWin: number | null) { + console.log("Creating player number for player ID:", playerId); + + // Insertar player_number para el nuevo jugador + const { data: playerNumber } = await supabase + .from("player_number") + .insert({ player: playerId, number: botNumbers, attempts_to_win: attemptsToWin }) + .select("id") + .single(); + + if (!playerNumber) { + console.error("Error inserting player number"); + throw new Error("Error inserting player number"); + } + + console.log("Player number created with ID:", playerNumber.id); + return playerNumber.id; +} + +// Función para obtener números únicos aleatorios +function getUniqueRandomDigits() { + const uniqueNumbers = new Set(); + + while (uniqueNumbers.size < 4) { + const randomNumber = Math.floor(Math.random() * 10); + uniqueNumbers.add(randomNumber); + } + + return Array.from(uniqueNumbers); +} From 07c971af75aae6a39781fa6e3918e24cb9e33ee6 Mon Sep 17 00:00:00 2001 From: ale24dev Date: Tue, 8 Oct 2024 03:22:52 -0400 Subject: [PATCH 30/30] fix: comment --- supabase/functions/find-or-create-game/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/supabase/functions/find-or-create-game/index.ts b/supabase/functions/find-or-create-game/index.ts index 05af79d..762dfe7 100644 --- a/supabase/functions/find-or-create-game/index.ts +++ b/supabase/functions/find-or-create-game/index.ts @@ -90,7 +90,7 @@ async function findOrCreateGame(supabase: any, playerId: string) { console.log("Creating new game..."); newGameId = await createNewGame(supabase, playerNumberId, searchingStatus.id); - await wait(5000); // Esperar 10 segundos para que otro jugador se una al juego + await wait(5000); // Esperar 5 segundos para que otro jugador se una al juego const isSearching = await verifyIfGameIsSearching(supabase, newGameId, searchingStatus.id); console.log("Is searching yet?", isSearching);