From b726f3c8f35a58f945eab18eb6bd7530a60dffe7 Mon Sep 17 00:00:00 2001 From: Sandip Date: Mon, 5 Aug 2024 02:18:48 +0530 Subject: [PATCH] feat: add player stats with cloud firestore --- ios/Podfile.lock | 1091 +++++++++++++++++++ lib/app/view/app.dart | 8 +- lib/bootstrap.dart | 9 +- lib/colors/colors.dart | 4 +- lib/home/bloc/home_bloc.dart | 39 +- lib/home/bloc/home_event.dart | 13 + lib/home/bloc/home_state.dart | 5 + lib/home/home.dart | 1 + lib/home/view/home_page.dart | 56 +- lib/home/widgets/player_info.dart | 255 +++++ lib/home/widgets/widgets.dart | 1 + lib/main_development.dart | 8 +- lib/main_production.dart | 8 +- lib/main_staging.dart | 8 +- lib/models/models.dart | 1 + lib/models/player.dart | 191 ++++ lib/models/player.g.dart | 36 + lib/puzzle/bloc/puzzle_bloc.dart | 26 +- lib/puzzle/view/puzzle_page.dart | 4 +- lib/repository/player_repository.dart | 51 + lib/repository/repository.dart | 1 + pubspec.lock | 108 +- pubspec.yaml | 4 +- test/app/view/app_test.dart | 13 + test/helpers/mocks.dart | 6 + test/helpers/pump_app.dart | 4 + test/home/bloc/home_bloc_test.dart | 80 ++ test/home/bloc/home_event_test.dart | 32 + test/home/bloc/home_state_test.dart | 6 + test/home/view/home_page_test.dart | 77 +- test/models/player_test.dart | 221 ++++ test/puzzle/bloc/puzzle_bloc_test.dart | 17 + test/repository/player_repository_test.dart | 155 +++ 33 files changed, 2453 insertions(+), 86 deletions(-) create mode 100644 lib/home/widgets/player_info.dart create mode 100644 lib/home/widgets/widgets.dart create mode 100644 lib/models/player.dart create mode 100644 lib/models/player.g.dart create mode 100644 lib/repository/player_repository.dart create mode 100644 test/models/player_test.dart create mode 100644 test/repository/player_repository_test.dart diff --git a/ios/Podfile.lock b/ios/Podfile.lock index f45b0fd..d8bacda 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,9 +1,969 @@ PODS: + - abseil/algorithm (1.20240116.2): + - abseil/algorithm/algorithm (= 1.20240116.2) + - abseil/algorithm/container (= 1.20240116.2) + - abseil/algorithm/algorithm (1.20240116.2): + - abseil/base/config + - abseil/xcprivacy + - abseil/algorithm/container (1.20240116.2): + - abseil/algorithm/algorithm + - abseil/base/core_headers + - abseil/base/nullability + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/base (1.20240116.2): + - abseil/base/atomic_hook (= 1.20240116.2) + - abseil/base/base (= 1.20240116.2) + - abseil/base/base_internal (= 1.20240116.2) + - abseil/base/config (= 1.20240116.2) + - abseil/base/core_headers (= 1.20240116.2) + - abseil/base/cycleclock_internal (= 1.20240116.2) + - abseil/base/dynamic_annotations (= 1.20240116.2) + - abseil/base/endian (= 1.20240116.2) + - abseil/base/errno_saver (= 1.20240116.2) + - abseil/base/fast_type_id (= 1.20240116.2) + - abseil/base/log_severity (= 1.20240116.2) + - abseil/base/malloc_internal (= 1.20240116.2) + - abseil/base/no_destructor (= 1.20240116.2) + - abseil/base/nullability (= 1.20240116.2) + - abseil/base/prefetch (= 1.20240116.2) + - abseil/base/pretty_function (= 1.20240116.2) + - abseil/base/raw_logging_internal (= 1.20240116.2) + - abseil/base/spinlock_wait (= 1.20240116.2) + - abseil/base/strerror (= 1.20240116.2) + - abseil/base/throw_delegate (= 1.20240116.2) + - abseil/base/atomic_hook (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/xcprivacy + - abseil/base/base (1.20240116.2): + - abseil/base/atomic_hook + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/base/cycleclock_internal + - abseil/base/dynamic_annotations + - abseil/base/log_severity + - abseil/base/nullability + - abseil/base/raw_logging_internal + - abseil/base/spinlock_wait + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/base/base_internal (1.20240116.2): + - abseil/base/config + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/base/config (1.20240116.2): + - abseil/xcprivacy + - abseil/base/core_headers (1.20240116.2): + - abseil/base/config + - abseil/xcprivacy + - abseil/base/cycleclock_internal (1.20240116.2): + - abseil/base/base_internal + - abseil/base/config + - abseil/xcprivacy + - abseil/base/dynamic_annotations (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/xcprivacy + - abseil/base/endian (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/nullability + - abseil/xcprivacy + - abseil/base/errno_saver (1.20240116.2): + - abseil/base/config + - abseil/xcprivacy + - abseil/base/fast_type_id (1.20240116.2): + - abseil/base/config + - abseil/xcprivacy + - abseil/base/log_severity (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/xcprivacy + - abseil/base/malloc_internal (1.20240116.2): + - abseil/base/base + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/raw_logging_internal + - abseil/xcprivacy + - abseil/base/no_destructor (1.20240116.2): + - abseil/base/config + - abseil/xcprivacy + - abseil/base/nullability (1.20240116.2): + - abseil/base/core_headers + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/base/prefetch (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/xcprivacy + - abseil/base/pretty_function (1.20240116.2): + - abseil/xcprivacy + - abseil/base/raw_logging_internal (1.20240116.2): + - abseil/base/atomic_hook + - abseil/base/config + - abseil/base/core_headers + - abseil/base/errno_saver + - abseil/base/log_severity + - abseil/xcprivacy + - abseil/base/spinlock_wait (1.20240116.2): + - abseil/base/base_internal + - abseil/base/core_headers + - abseil/base/errno_saver + - abseil/xcprivacy + - abseil/base/strerror (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/errno_saver + - abseil/xcprivacy + - abseil/base/throw_delegate (1.20240116.2): + - abseil/base/config + - abseil/base/raw_logging_internal + - abseil/xcprivacy + - abseil/cleanup/cleanup (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/cleanup/cleanup_internal + - abseil/xcprivacy + - abseil/cleanup/cleanup_internal (1.20240116.2): + - abseil/base/base_internal + - abseil/base/core_headers + - abseil/utility/utility + - abseil/xcprivacy + - abseil/container/common (1.20240116.2): + - abseil/meta/type_traits + - abseil/types/optional + - abseil/xcprivacy + - abseil/container/common_policy_traits (1.20240116.2): + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/container/compressed_tuple (1.20240116.2): + - abseil/utility/utility + - abseil/xcprivacy + - abseil/container/container_memory (1.20240116.2): + - abseil/base/config + - abseil/memory/memory + - abseil/meta/type_traits + - abseil/utility/utility + - abseil/xcprivacy + - abseil/container/fixed_array (1.20240116.2): + - abseil/algorithm/algorithm + - abseil/base/config + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/throw_delegate + - abseil/container/compressed_tuple + - abseil/memory/memory + - abseil/xcprivacy + - abseil/container/flat_hash_map (1.20240116.2): + - abseil/algorithm/container + - abseil/base/core_headers + - abseil/container/container_memory + - abseil/container/hash_function_defaults + - abseil/container/raw_hash_map + - abseil/memory/memory + - abseil/xcprivacy + - abseil/container/flat_hash_set (1.20240116.2): + - abseil/algorithm/container + - abseil/base/core_headers + - abseil/container/container_memory + - abseil/container/hash_function_defaults + - abseil/container/raw_hash_set + - abseil/memory/memory + - abseil/xcprivacy + - abseil/container/hash_function_defaults (1.20240116.2): + - abseil/base/config + - abseil/hash/hash + - abseil/strings/cord + - abseil/strings/strings + - abseil/xcprivacy + - abseil/container/hash_policy_traits (1.20240116.2): + - abseil/container/common_policy_traits + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/container/hashtable_debug_hooks (1.20240116.2): + - abseil/base/config + - abseil/xcprivacy + - abseil/container/hashtablez_sampler (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/raw_logging_internal + - abseil/debugging/stacktrace + - abseil/memory/memory + - abseil/profiling/exponential_biased + - abseil/profiling/sample_recorder + - abseil/synchronization/synchronization + - abseil/time/time + - abseil/utility/utility + - abseil/xcprivacy + - abseil/container/inlined_vector (1.20240116.2): + - abseil/algorithm/algorithm + - abseil/base/core_headers + - abseil/base/throw_delegate + - abseil/container/inlined_vector_internal + - abseil/memory/memory + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/container/inlined_vector_internal (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/container/compressed_tuple + - abseil/memory/memory + - abseil/meta/type_traits + - abseil/types/span + - abseil/xcprivacy + - abseil/container/layout (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/debugging/demangle_internal + - abseil/meta/type_traits + - abseil/strings/strings + - abseil/types/span + - abseil/utility/utility + - abseil/xcprivacy + - abseil/container/raw_hash_map (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/throw_delegate + - abseil/container/container_memory + - abseil/container/raw_hash_set + - abseil/xcprivacy + - abseil/container/raw_hash_set (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/endian + - abseil/base/prefetch + - abseil/base/raw_logging_internal + - abseil/container/common + - abseil/container/compressed_tuple + - abseil/container/container_memory + - abseil/container/hash_policy_traits + - abseil/container/hashtable_debug_hooks + - abseil/container/hashtablez_sampler + - abseil/hash/hash + - abseil/memory/memory + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/utility/utility + - abseil/xcprivacy + - abseil/crc/cpu_detect (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/xcprivacy + - abseil/crc/crc32c (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/base/prefetch + - abseil/crc/cpu_detect + - abseil/crc/crc_internal + - abseil/crc/non_temporal_memcpy + - abseil/strings/str_format + - abseil/strings/strings + - abseil/xcprivacy + - abseil/crc/crc_cord_state (1.20240116.2): + - abseil/base/config + - abseil/crc/crc32c + - abseil/numeric/bits + - abseil/strings/strings + - abseil/xcprivacy + - abseil/crc/crc_internal (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/base/prefetch + - abseil/base/raw_logging_internal + - abseil/crc/cpu_detect + - abseil/memory/memory + - abseil/numeric/bits + - abseil/xcprivacy + - abseil/crc/non_temporal_arm_intrinsics (1.20240116.2): + - abseil/base/config + - abseil/xcprivacy + - abseil/crc/non_temporal_memcpy (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/crc/non_temporal_arm_intrinsics + - abseil/xcprivacy + - abseil/debugging/debugging_internal (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/errno_saver + - abseil/base/raw_logging_internal + - abseil/xcprivacy + - abseil/debugging/demangle_internal (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/xcprivacy + - abseil/debugging/stacktrace (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/raw_logging_internal + - abseil/debugging/debugging_internal + - abseil/xcprivacy + - abseil/debugging/symbolize (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/malloc_internal + - abseil/base/raw_logging_internal + - abseil/debugging/debugging_internal + - abseil/debugging/demangle_internal + - abseil/strings/strings + - abseil/xcprivacy + - abseil/flags/commandlineflag (1.20240116.2): + - abseil/base/config + - abseil/base/fast_type_id + - abseil/flags/commandlineflag_internal + - abseil/strings/strings + - abseil/types/optional + - abseil/xcprivacy + - abseil/flags/commandlineflag_internal (1.20240116.2): + - abseil/base/config + - abseil/base/fast_type_id + - abseil/xcprivacy + - abseil/flags/config (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/flags/path_util + - abseil/flags/program_name + - abseil/strings/strings + - abseil/synchronization/synchronization + - abseil/xcprivacy + - abseil/flags/flag (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/flags/config + - abseil/flags/flag_internal + - abseil/flags/reflection + - abseil/strings/strings + - abseil/xcprivacy + - abseil/flags/flag_internal (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/flags/commandlineflag + - abseil/flags/commandlineflag_internal + - abseil/flags/config + - abseil/flags/marshalling + - abseil/flags/reflection + - abseil/memory/memory + - abseil/meta/type_traits + - abseil/strings/strings + - abseil/synchronization/synchronization + - abseil/utility/utility + - abseil/xcprivacy + - abseil/flags/marshalling (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/log_severity + - abseil/numeric/int128 + - abseil/strings/str_format + - abseil/strings/strings + - abseil/types/optional + - abseil/xcprivacy + - abseil/flags/path_util (1.20240116.2): + - abseil/base/config + - abseil/strings/strings + - abseil/xcprivacy + - abseil/flags/private_handle_accessor (1.20240116.2): + - abseil/base/config + - abseil/flags/commandlineflag + - abseil/flags/commandlineflag_internal + - abseil/strings/strings + - abseil/xcprivacy + - abseil/flags/program_name (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/flags/path_util + - abseil/strings/strings + - abseil/synchronization/synchronization + - abseil/xcprivacy + - abseil/flags/reflection (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/no_destructor + - abseil/container/flat_hash_map + - abseil/flags/commandlineflag + - abseil/flags/commandlineflag_internal + - abseil/flags/config + - abseil/flags/private_handle_accessor + - abseil/strings/strings + - abseil/synchronization/synchronization + - abseil/xcprivacy + - abseil/functional/any_invocable (1.20240116.2): + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/meta/type_traits + - abseil/utility/utility + - abseil/xcprivacy + - abseil/functional/bind_front (1.20240116.2): + - abseil/base/base_internal + - abseil/container/compressed_tuple + - abseil/meta/type_traits + - abseil/utility/utility + - abseil/xcprivacy + - abseil/functional/function_ref (1.20240116.2): + - abseil/base/base_internal + - abseil/base/core_headers + - abseil/functional/any_invocable + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/hash/city (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/xcprivacy + - abseil/hash/hash (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/container/fixed_array + - abseil/functional/function_ref + - abseil/hash/city + - abseil/hash/low_level_hash + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/numeric/int128 + - abseil/strings/strings + - abseil/types/optional + - abseil/types/variant + - abseil/utility/utility + - abseil/xcprivacy + - abseil/hash/low_level_hash (1.20240116.2): + - abseil/base/config + - abseil/base/endian + - abseil/base/prefetch + - abseil/numeric/int128 + - abseil/xcprivacy + - abseil/memory (1.20240116.2): + - abseil/memory/memory (= 1.20240116.2) + - abseil/memory/memory (1.20240116.2): + - abseil/base/core_headers + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/meta (1.20240116.2): + - abseil/meta/type_traits (= 1.20240116.2) + - abseil/meta/type_traits (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/xcprivacy + - abseil/numeric/bits (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/xcprivacy + - abseil/numeric/int128 (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/numeric/bits + - abseil/xcprivacy + - abseil/numeric/representation (1.20240116.2): + - abseil/base/config + - abseil/xcprivacy + - abseil/profiling/exponential_biased (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/xcprivacy + - abseil/profiling/sample_recorder (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/synchronization/synchronization + - abseil/time/time + - abseil/xcprivacy + - abseil/random/bit_gen_ref (1.20240116.2): + - abseil/base/core_headers + - abseil/base/fast_type_id + - abseil/meta/type_traits + - abseil/random/internal/distribution_caller + - abseil/random/internal/fast_uniform_bits + - abseil/random/random + - abseil/xcprivacy + - abseil/random/distributions (1.20240116.2): + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/random/internal/distribution_caller + - abseil/random/internal/fast_uniform_bits + - abseil/random/internal/fastmath + - abseil/random/internal/generate_real + - abseil/random/internal/iostream_state_saver + - abseil/random/internal/traits + - abseil/random/internal/uniform_helper + - abseil/random/internal/wide_multiply + - abseil/strings/strings + - abseil/xcprivacy + - abseil/random/internal/distribution_caller (1.20240116.2): + - abseil/base/config + - abseil/base/fast_type_id + - abseil/utility/utility + - abseil/xcprivacy + - abseil/random/internal/fast_uniform_bits (1.20240116.2): + - abseil/base/config + - abseil/meta/type_traits + - abseil/random/internal/traits + - abseil/xcprivacy + - abseil/random/internal/fastmath (1.20240116.2): + - abseil/numeric/bits + - abseil/xcprivacy + - abseil/random/internal/generate_real (1.20240116.2): + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/random/internal/fastmath + - abseil/random/internal/traits + - abseil/xcprivacy + - abseil/random/internal/iostream_state_saver (1.20240116.2): + - abseil/meta/type_traits + - abseil/numeric/int128 + - abseil/xcprivacy + - abseil/random/internal/nonsecure_base (1.20240116.2): + - abseil/base/core_headers + - abseil/container/inlined_vector + - abseil/meta/type_traits + - abseil/random/internal/pool_urbg + - abseil/random/internal/salted_seed_seq + - abseil/random/internal/seed_material + - abseil/types/span + - abseil/xcprivacy + - abseil/random/internal/pcg_engine (1.20240116.2): + - abseil/base/config + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/numeric/int128 + - abseil/random/internal/fastmath + - abseil/random/internal/iostream_state_saver + - abseil/xcprivacy + - abseil/random/internal/platform (1.20240116.2): + - abseil/base/config + - abseil/xcprivacy + - abseil/random/internal/pool_urbg (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/base/raw_logging_internal + - abseil/random/internal/randen + - abseil/random/internal/seed_material + - abseil/random/internal/traits + - abseil/random/seed_gen_exception + - abseil/types/span + - abseil/xcprivacy + - abseil/random/internal/randen (1.20240116.2): + - abseil/base/raw_logging_internal + - abseil/random/internal/platform + - abseil/random/internal/randen_hwaes + - abseil/random/internal/randen_slow + - abseil/xcprivacy + - abseil/random/internal/randen_engine (1.20240116.2): + - abseil/base/endian + - abseil/meta/type_traits + - abseil/random/internal/iostream_state_saver + - abseil/random/internal/randen + - abseil/xcprivacy + - abseil/random/internal/randen_hwaes (1.20240116.2): + - abseil/base/config + - abseil/random/internal/platform + - abseil/random/internal/randen_hwaes_impl + - abseil/xcprivacy + - abseil/random/internal/randen_hwaes_impl (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/numeric/int128 + - abseil/random/internal/platform + - abseil/xcprivacy + - abseil/random/internal/randen_slow (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/numeric/int128 + - abseil/random/internal/platform + - abseil/xcprivacy + - abseil/random/internal/salted_seed_seq (1.20240116.2): + - abseil/container/inlined_vector + - abseil/meta/type_traits + - abseil/random/internal/seed_material + - abseil/types/optional + - abseil/types/span + - abseil/xcprivacy + - abseil/random/internal/seed_material (1.20240116.2): + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/raw_logging_internal + - abseil/random/internal/fast_uniform_bits + - abseil/strings/strings + - abseil/types/optional + - abseil/types/span + - abseil/xcprivacy + - abseil/random/internal/traits (1.20240116.2): + - abseil/base/config + - abseil/numeric/bits + - abseil/numeric/int128 + - abseil/xcprivacy + - abseil/random/internal/uniform_helper (1.20240116.2): + - abseil/base/config + - abseil/meta/type_traits + - abseil/numeric/int128 + - abseil/random/internal/traits + - abseil/xcprivacy + - abseil/random/internal/wide_multiply (1.20240116.2): + - abseil/base/config + - abseil/numeric/bits + - abseil/numeric/int128 + - abseil/random/internal/traits + - abseil/xcprivacy + - abseil/random/random (1.20240116.2): + - abseil/random/distributions + - abseil/random/internal/nonsecure_base + - abseil/random/internal/pcg_engine + - abseil/random/internal/pool_urbg + - abseil/random/internal/randen_engine + - abseil/random/seed_sequences + - abseil/xcprivacy + - abseil/random/seed_gen_exception (1.20240116.2): + - abseil/base/config + - abseil/xcprivacy + - abseil/random/seed_sequences (1.20240116.2): + - abseil/base/config + - abseil/random/internal/pool_urbg + - abseil/random/internal/salted_seed_seq + - abseil/random/internal/seed_material + - abseil/random/seed_gen_exception + - abseil/types/span + - abseil/xcprivacy + - abseil/status/status (1.20240116.2): + - abseil/base/atomic_hook + - abseil/base/config + - abseil/base/core_headers + - abseil/base/no_destructor + - abseil/base/nullability + - abseil/base/raw_logging_internal + - abseil/base/strerror + - abseil/container/inlined_vector + - abseil/debugging/stacktrace + - abseil/debugging/symbolize + - abseil/functional/function_ref + - abseil/memory/memory + - abseil/strings/cord + - abseil/strings/str_format + - abseil/strings/strings + - abseil/types/optional + - abseil/types/span + - abseil/xcprivacy + - abseil/status/statusor (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/nullability + - abseil/base/raw_logging_internal + - abseil/meta/type_traits + - abseil/status/status + - abseil/strings/has_ostream_operator + - abseil/strings/str_format + - abseil/strings/strings + - abseil/types/variant + - abseil/utility/utility + - abseil/xcprivacy + - abseil/strings/charset (1.20240116.2): + - abseil/base/core_headers + - abseil/strings/string_view + - abseil/xcprivacy + - abseil/strings/cord (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/base/nullability + - abseil/base/raw_logging_internal + - abseil/container/inlined_vector + - abseil/crc/crc32c + - abseil/crc/crc_cord_state + - abseil/functional/function_ref + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/strings/cord_internal + - abseil/strings/cordz_functions + - abseil/strings/cordz_info + - abseil/strings/cordz_statistics + - abseil/strings/cordz_update_scope + - abseil/strings/cordz_update_tracker + - abseil/strings/internal + - abseil/strings/strings + - abseil/types/optional + - abseil/types/span + - abseil/xcprivacy + - abseil/strings/cord_internal (1.20240116.2): + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/base/raw_logging_internal + - abseil/base/throw_delegate + - abseil/container/compressed_tuple + - abseil/container/container_memory + - abseil/container/inlined_vector + - abseil/container/layout + - abseil/crc/crc_cord_state + - abseil/functional/function_ref + - abseil/meta/type_traits + - abseil/strings/strings + - abseil/types/span + - abseil/xcprivacy + - abseil/strings/cordz_functions (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/raw_logging_internal + - abseil/profiling/exponential_biased + - abseil/xcprivacy + - abseil/strings/cordz_handle (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/base/raw_logging_internal + - abseil/synchronization/synchronization + - abseil/xcprivacy + - abseil/strings/cordz_info (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/raw_logging_internal + - abseil/container/inlined_vector + - abseil/debugging/stacktrace + - abseil/strings/cord_internal + - abseil/strings/cordz_functions + - abseil/strings/cordz_handle + - abseil/strings/cordz_statistics + - abseil/strings/cordz_update_tracker + - abseil/synchronization/synchronization + - abseil/time/time + - abseil/types/span + - abseil/xcprivacy + - abseil/strings/cordz_statistics (1.20240116.2): + - abseil/base/config + - abseil/strings/cordz_update_tracker + - abseil/xcprivacy + - abseil/strings/cordz_update_scope (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/strings/cord_internal + - abseil/strings/cordz_info + - abseil/strings/cordz_update_tracker + - abseil/xcprivacy + - abseil/strings/cordz_update_tracker (1.20240116.2): + - abseil/base/config + - abseil/xcprivacy + - abseil/strings/has_ostream_operator (1.20240116.2): + - abseil/base/config + - abseil/xcprivacy + - abseil/strings/internal (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/base/raw_logging_internal + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/strings/str_format (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/nullability + - abseil/strings/str_format_internal + - abseil/strings/string_view + - abseil/types/span + - abseil/xcprivacy + - abseil/strings/str_format_internal (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/container/fixed_array + - abseil/container/inlined_vector + - abseil/functional/function_ref + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/numeric/int128 + - abseil/numeric/representation + - abseil/strings/strings + - abseil/types/optional + - abseil/types/span + - abseil/utility/utility + - abseil/xcprivacy + - abseil/strings/string_view (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/nullability + - abseil/base/throw_delegate + - abseil/xcprivacy + - abseil/strings/strings (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/base/nullability + - abseil/base/raw_logging_internal + - abseil/base/throw_delegate + - abseil/memory/memory + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/numeric/int128 + - abseil/strings/charset + - abseil/strings/internal + - abseil/strings/string_view + - abseil/xcprivacy + - abseil/synchronization/graphcycles_internal (1.20240116.2): + - abseil/base/base + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/base/malloc_internal + - abseil/base/raw_logging_internal + - abseil/xcprivacy + - abseil/synchronization/kernel_timeout_internal (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/raw_logging_internal + - abseil/time/time + - abseil/xcprivacy + - abseil/synchronization/synchronization (1.20240116.2): + - abseil/base/atomic_hook + - abseil/base/base + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/malloc_internal + - abseil/base/raw_logging_internal + - abseil/debugging/stacktrace + - abseil/debugging/symbolize + - abseil/synchronization/graphcycles_internal + - abseil/synchronization/kernel_timeout_internal + - abseil/time/time + - abseil/xcprivacy + - abseil/time (1.20240116.2): + - abseil/time/internal (= 1.20240116.2) + - abseil/time/time (= 1.20240116.2) + - abseil/time/internal (1.20240116.2): + - abseil/time/internal/cctz (= 1.20240116.2) + - abseil/time/internal/cctz (1.20240116.2): + - abseil/time/internal/cctz/civil_time (= 1.20240116.2) + - abseil/time/internal/cctz/time_zone (= 1.20240116.2) + - abseil/time/internal/cctz/civil_time (1.20240116.2): + - abseil/base/config + - abseil/xcprivacy + - abseil/time/internal/cctz/time_zone (1.20240116.2): + - abseil/base/config + - abseil/time/internal/cctz/civil_time + - abseil/xcprivacy + - abseil/time/time (1.20240116.2): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/raw_logging_internal + - abseil/numeric/int128 + - abseil/strings/strings + - abseil/time/internal/cctz/civil_time + - abseil/time/internal/cctz/time_zone + - abseil/types/optional + - abseil/xcprivacy + - abseil/types (1.20240116.2): + - abseil/types/any (= 1.20240116.2) + - abseil/types/bad_any_cast (= 1.20240116.2) + - abseil/types/bad_any_cast_impl (= 1.20240116.2) + - abseil/types/bad_optional_access (= 1.20240116.2) + - abseil/types/bad_variant_access (= 1.20240116.2) + - abseil/types/compare (= 1.20240116.2) + - abseil/types/optional (= 1.20240116.2) + - abseil/types/span (= 1.20240116.2) + - abseil/types/variant (= 1.20240116.2) + - abseil/types/any (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/fast_type_id + - abseil/meta/type_traits + - abseil/types/bad_any_cast + - abseil/utility/utility + - abseil/xcprivacy + - abseil/types/bad_any_cast (1.20240116.2): + - abseil/base/config + - abseil/types/bad_any_cast_impl + - abseil/xcprivacy + - abseil/types/bad_any_cast_impl (1.20240116.2): + - abseil/base/config + - abseil/base/raw_logging_internal + - abseil/xcprivacy + - abseil/types/bad_optional_access (1.20240116.2): + - abseil/base/config + - abseil/base/raw_logging_internal + - abseil/xcprivacy + - abseil/types/bad_variant_access (1.20240116.2): + - abseil/base/config + - abseil/base/raw_logging_internal + - abseil/xcprivacy + - abseil/types/compare (1.20240116.2): + - abseil/base/config + - abseil/base/core_headers + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/types/optional (1.20240116.2): + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/base/nullability + - abseil/memory/memory + - abseil/meta/type_traits + - abseil/types/bad_optional_access + - abseil/utility/utility + - abseil/xcprivacy + - abseil/types/span (1.20240116.2): + - abseil/algorithm/algorithm + - abseil/base/core_headers + - abseil/base/nullability + - abseil/base/throw_delegate + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/types/variant (1.20240116.2): + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/meta/type_traits + - abseil/types/bad_variant_access + - abseil/utility/utility + - abseil/xcprivacy + - abseil/utility/utility (1.20240116.2): + - abseil/base/base_internal + - abseil/base/config + - abseil/meta/type_traits + - abseil/xcprivacy + - abseil/xcprivacy (1.20240116.2) + - BoringSSL-GRPC (0.0.32): + - BoringSSL-GRPC/Implementation (= 0.0.32) + - BoringSSL-GRPC/Interface (= 0.0.32) + - BoringSSL-GRPC/Implementation (0.0.32): + - BoringSSL-GRPC/Interface (= 0.0.32) + - BoringSSL-GRPC/Interface (0.0.32) + - cloud_firestore (5.2.0): + - Firebase/Firestore (= 10.29.0) + - firebase_core + - Flutter - Firebase/Auth (10.29.0): - Firebase/CoreOnly - FirebaseAuth (~> 10.29.0) - Firebase/CoreOnly (10.29.0): - FirebaseCore (= 10.29.0) + - Firebase/Firestore (10.29.0): + - Firebase/CoreOnly + - FirebaseFirestore (~> 10.29.0) - firebase_auth (5.1.3): - Firebase/Auth (= 10.29.0) - firebase_core @@ -23,8 +983,31 @@ PODS: - FirebaseCoreInternal (~> 10.0) - GoogleUtilities/Environment (~> 7.12) - GoogleUtilities/Logger (~> 7.12) + - FirebaseCoreExtension (10.29.0): + - FirebaseCore (~> 10.0) - FirebaseCoreInternal (10.29.0): - "GoogleUtilities/NSData+zlib (~> 7.8)" + - FirebaseFirestore (10.29.0): + - FirebaseCore (~> 10.0) + - FirebaseCoreExtension (~> 10.0) + - FirebaseFirestoreInternal (= 10.29.0) + - FirebaseSharedSwift (~> 10.0) + - FirebaseFirestoreInternal (10.29.0): + - abseil/algorithm (~> 1.20240116.1) + - abseil/base (~> 1.20240116.1) + - abseil/container/flat_hash_map (~> 1.20240116.1) + - abseil/memory (~> 1.20240116.1) + - abseil/meta (~> 1.20240116.1) + - abseil/strings/strings (~> 1.20240116.1) + - abseil/time (~> 1.20240116.1) + - abseil/types (~> 1.20240116.1) + - FirebaseAppCheckInterop (~> 10.17) + - FirebaseCore (~> 10.0) + - "gRPC-C++ (~> 1.62.0)" + - gRPC-Core (~> 1.62.0) + - leveldb-library (~> 1.22) + - nanopb (< 2.30911.0, >= 2.30908.0) + - FirebaseSharedSwift (10.29.0) - Flutter (1.0.0) - GoogleUtilities/AppDelegateSwizzler (7.13.3): - GoogleUtilities/Environment @@ -48,7 +1031,91 @@ PODS: - GoogleUtilities/Reachability (7.13.3): - GoogleUtilities/Logger - GoogleUtilities/Privacy + - "gRPC-C++ (1.62.5)": + - "gRPC-C++/Implementation (= 1.62.5)" + - "gRPC-C++/Interface (= 1.62.5)" + - "gRPC-C++/Implementation (1.62.5)": + - abseil/algorithm/container (~> 1.20240116.2) + - abseil/base/base (~> 1.20240116.2) + - abseil/base/config (~> 1.20240116.2) + - abseil/base/core_headers (~> 1.20240116.2) + - abseil/cleanup/cleanup (~> 1.20240116.2) + - abseil/container/flat_hash_map (~> 1.20240116.2) + - abseil/container/flat_hash_set (~> 1.20240116.2) + - abseil/container/inlined_vector (~> 1.20240116.2) + - abseil/flags/flag (~> 1.20240116.2) + - abseil/flags/marshalling (~> 1.20240116.2) + - abseil/functional/any_invocable (~> 1.20240116.2) + - abseil/functional/bind_front (~> 1.20240116.2) + - abseil/functional/function_ref (~> 1.20240116.2) + - abseil/hash/hash (~> 1.20240116.2) + - abseil/memory/memory (~> 1.20240116.2) + - abseil/meta/type_traits (~> 1.20240116.2) + - abseil/random/bit_gen_ref (~> 1.20240116.2) + - abseil/random/distributions (~> 1.20240116.2) + - abseil/random/random (~> 1.20240116.2) + - abseil/status/status (~> 1.20240116.2) + - abseil/status/statusor (~> 1.20240116.2) + - abseil/strings/cord (~> 1.20240116.2) + - abseil/strings/str_format (~> 1.20240116.2) + - abseil/strings/strings (~> 1.20240116.2) + - abseil/synchronization/synchronization (~> 1.20240116.2) + - abseil/time/time (~> 1.20240116.2) + - abseil/types/optional (~> 1.20240116.2) + - abseil/types/span (~> 1.20240116.2) + - abseil/types/variant (~> 1.20240116.2) + - abseil/utility/utility (~> 1.20240116.2) + - "gRPC-C++/Interface (= 1.62.5)" + - "gRPC-C++/Privacy (= 1.62.5)" + - gRPC-Core (= 1.62.5) + - "gRPC-C++/Interface (1.62.5)" + - "gRPC-C++/Privacy (1.62.5)" + - gRPC-Core (1.62.5): + - gRPC-Core/Implementation (= 1.62.5) + - gRPC-Core/Interface (= 1.62.5) + - gRPC-Core/Implementation (1.62.5): + - abseil/algorithm/container (~> 1.20240116.2) + - abseil/base/base (~> 1.20240116.2) + - abseil/base/config (~> 1.20240116.2) + - abseil/base/core_headers (~> 1.20240116.2) + - abseil/cleanup/cleanup (~> 1.20240116.2) + - abseil/container/flat_hash_map (~> 1.20240116.2) + - abseil/container/flat_hash_set (~> 1.20240116.2) + - abseil/container/inlined_vector (~> 1.20240116.2) + - abseil/flags/flag (~> 1.20240116.2) + - abseil/flags/marshalling (~> 1.20240116.2) + - abseil/functional/any_invocable (~> 1.20240116.2) + - abseil/functional/bind_front (~> 1.20240116.2) + - abseil/functional/function_ref (~> 1.20240116.2) + - abseil/hash/hash (~> 1.20240116.2) + - abseil/memory/memory (~> 1.20240116.2) + - abseil/meta/type_traits (~> 1.20240116.2) + - abseil/random/bit_gen_ref (~> 1.20240116.2) + - abseil/random/distributions (~> 1.20240116.2) + - abseil/random/random (~> 1.20240116.2) + - abseil/status/status (~> 1.20240116.2) + - abseil/status/statusor (~> 1.20240116.2) + - abseil/strings/cord (~> 1.20240116.2) + - abseil/strings/str_format (~> 1.20240116.2) + - abseil/strings/strings (~> 1.20240116.2) + - abseil/synchronization/synchronization (~> 1.20240116.2) + - abseil/time/time (~> 1.20240116.2) + - abseil/types/optional (~> 1.20240116.2) + - abseil/types/span (~> 1.20240116.2) + - abseil/types/variant (~> 1.20240116.2) + - abseil/utility/utility (~> 1.20240116.2) + - BoringSSL-GRPC (= 0.0.32) + - gRPC-Core/Interface (= 1.62.5) + - gRPC-Core/Privacy (= 1.62.5) + - gRPC-Core/Interface (1.62.5) + - gRPC-Core/Privacy (1.62.5) - GTMSessionFetcher/Core (3.5.0) + - leveldb-library (1.22.5) + - nanopb (2.30910.0): + - nanopb/decode (= 2.30910.0) + - nanopb/encode (= 2.30910.0) + - nanopb/decode (2.30910.0) + - nanopb/encode (2.30910.0) - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS @@ -59,6 +1126,7 @@ PODS: - FlutterMacOS DEPENDENCIES: + - cloud_firestore (from `.symlinks/plugins/cloud_firestore/ios`) - firebase_auth (from `.symlinks/plugins/firebase_auth/ios`) - firebase_core (from `.symlinks/plugins/firebase_core/ios`) - Flutter (from `Flutter`) @@ -67,17 +1135,29 @@ DEPENDENCIES: SPEC REPOS: trunk: + - abseil + - BoringSSL-GRPC - Firebase - FirebaseAppCheckInterop - FirebaseAuth - FirebaseCore + - FirebaseCoreExtension - FirebaseCoreInternal + - FirebaseFirestore + - FirebaseFirestoreInternal + - FirebaseSharedSwift - GoogleUtilities + - "gRPC-C++" + - gRPC-Core - GTMSessionFetcher + - leveldb-library + - nanopb - PromisesObjC - RecaptchaInterop EXTERNAL SOURCES: + cloud_firestore: + :path: ".symlinks/plugins/cloud_firestore/ios" firebase_auth: :path: ".symlinks/plugins/firebase_auth/ios" firebase_core: @@ -90,16 +1170,27 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" SPEC CHECKSUMS: + abseil: d121da9ef7e2ff4cab7666e76c5a3e0915ae08c3 + BoringSSL-GRPC: 1e2348957acdbcad360b80a264a90799984b2ba6 + cloud_firestore: 9bdbf5a952da93fad19c89f4a497bff41b3f7df2 Firebase: cec914dab6fd7b1bd8ab56ea07ce4e03dd251c2d firebase_auth: 6a09851aca5fcbb1f3745bed7ec88f221c52c3e9 firebase_core: 57aeb91680e5d5e6df6b888064be7c785f146efb FirebaseAppCheckInterop: 6a1757cfd4067d8e00fccd14fcc1b8fd78cfac07 FirebaseAuth: e2ebfaf9fb4638a1c9a3b0efd17d1b90943987cd FirebaseCore: 30e9c1cbe3d38f5f5e75f48bfcea87d7c358ec16 + FirebaseCoreExtension: 705ca5b14bf71d2564a0ddc677df1fc86ffa600f FirebaseCoreInternal: df84dd300b561c27d5571684f389bf60b0a5c934 + FirebaseFirestore: f258936f52d712337233182b90042a76ff48dce0 + FirebaseFirestoreInternal: f43d25cc04835ec3aa1885f4fc946a1a4f9e1c56 + FirebaseSharedSwift: 20530f495084b8d840f78a100d8c5ee613375f6e Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15 + "gRPC-C++": e725ef63c4475d7cdb7e2cf16eb0fde84bd9ee51 + gRPC-Core: eee4be35df218649fe66d721a05a7f27a28f069b GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 + leveldb-library: e8eadf9008a61f9e1dde3978c086d2b6d9b9dc28 + nanopb: 438bc412db1928dac798aa6fd75726007be04262 path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 RecaptchaInterop: 7d1a4a01a6b2cb1610a47ef3f85f0c411434cb21 diff --git a/lib/app/view/app.dart b/lib/app/view/app.dart index 9000e69..0b1d4dc 100644 --- a/lib/app/view/app.dart +++ b/lib/app/view/app.dart @@ -12,14 +12,17 @@ class App extends StatelessWidget { required SudokuAPI apiClient, required PuzzleRepository puzzleRepository, required AuthenticationRepository authenticationRepository, + required PlayerRepository playerRepository, super.key, }) : _apiClient = apiClient, _puzzleRepository = puzzleRepository, - _authenticationRepository = authenticationRepository; + _authenticationRepository = authenticationRepository, + _playerRepository = playerRepository; final SudokuAPI _apiClient; final PuzzleRepository _puzzleRepository; final AuthenticationRepository _authenticationRepository; + final PlayerRepository _playerRepository; @override Widget build(BuildContext context) { @@ -34,6 +37,9 @@ class App extends StatelessWidget { RepositoryProvider.value( value: _authenticationRepository, ), + RepositoryProvider.value( + value: _playerRepository, + ), ], child: const AppView(), ); diff --git a/lib/bootstrap.dart b/lib/bootstrap.dart index 85b5228..adaebcd 100644 --- a/lib/bootstrap.dart +++ b/lib/bootstrap.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:developer'; import 'package:bloc/bloc.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; @@ -11,6 +12,7 @@ import 'package:sudoku/app_bloc_observer.dart'; /// The type definition for the builder widget. typedef BootstrapBuilder = FutureOr Function( FirebaseAuth firebaseAuth, + FirebaseFirestore firestore, ); /// Bootstrap is responsible for any common setup and calls @@ -37,7 +39,12 @@ Future bootstrap(BootstrapBuilder builder) async { await runZonedGuarded( () async { Bloc.observer = const AppBlocObserver(); - runApp(await builder(FirebaseAuth.instance)); + runApp( + await builder( + FirebaseAuth.instance, + FirebaseFirestore.instance, + ), + ); }, (error, stackTrace) => log(error.toString(), stackTrace: stackTrace), ); diff --git a/lib/colors/colors.dart b/lib/colors/colors.dart index 48ff07d..3e86f1b 100644 --- a/lib/colors/colors.dart +++ b/lib/colors/colors.dart @@ -9,10 +9,10 @@ abstract class SudokuColors { static const lightPurple = Color(0xFF9089FC); /// Dark Pink - static const darkPink = Color(0xFFFF38B0); + static const darkPink = Color(0xFFFC1FA4); /// Dark Purple - static const darkPurple = Color(0xFF7A57FD); + static const darkPurple = Color(0xFF5E33FD); /// Green static const green = Color(0xFF388E3C); diff --git a/lib/home/bloc/home_bloc.dart b/lib/home/bloc/home_bloc.dart index 820eec5..4a3c234 100644 --- a/lib/home/bloc/home_bloc.dart +++ b/lib/home/bloc/home_bloc.dart @@ -14,16 +14,24 @@ class HomeBloc extends Bloc { HomeBloc({ required SudokuAPI apiClient, required PuzzleRepository puzzleRepository, + required AuthenticationRepository authenticationRepository, + required PlayerRepository playerRepository, }) : _apiClient = apiClient, _puzzleRepository = puzzleRepository, + _authenticationRepository = authenticationRepository, + _playerRepository = playerRepository, super(const HomeState()) { on(_onSudokuCreationRequested); - on(_onSubscriptionRequested); + on(_onPuzzleSubscriptionRequested); on(_onUnfinishedPuzzleResumed); + on(_onPlayerSubscriptionRequested); + on(_onNewPuzzleAttempted); } final SudokuAPI _apiClient; final PuzzleRepository _puzzleRepository; + final AuthenticationRepository _authenticationRepository; + final PlayerRepository _playerRepository; FutureOr _onSudokuCreationRequested( SudokuCreationRequested event, @@ -73,7 +81,7 @@ class HomeBloc extends Bloc { } } - FutureOr _onSubscriptionRequested( + FutureOr _onPuzzleSubscriptionRequested( UnfinishedPuzzleSubscriptionRequested event, Emitter emit, ) async { @@ -114,4 +122,31 @@ class HomeBloc extends Bloc { ), ); } + + FutureOr _onPlayerSubscriptionRequested( + PlayerSubscriptionRequested event, + Emitter emit, + ) async { + final userId = _authenticationRepository.currentUser.id; + await emit.forEach( + _playerRepository.getPlayer(userId), + onData: (player) => state.copyWith( + player: () => player, + ), + onError: (_, __) => state.copyWith( + player: () => Player.empty, + ), + ); + } + + FutureOr _onNewPuzzleAttempted( + NewPuzzleAttempted event, + Emitter emit, + ) async { + final userId = _authenticationRepository.currentUser.id; + final updatedPlayer = state.player.updateAttemptCount(event.difficulty); + try { + await _playerRepository.updatePlayer(userId, updatedPlayer); + } catch (_) {} + } } diff --git a/lib/home/bloc/home_event.dart b/lib/home/bloc/home_event.dart index 167a9ea..de7e764 100644 --- a/lib/home/bloc/home_event.dart +++ b/lib/home/bloc/home_event.dart @@ -23,3 +23,16 @@ final class UnfinishedPuzzleSubscriptionRequested extends HomeEvent { final class UnfinishedPuzzleResumed extends HomeEvent { const UnfinishedPuzzleResumed(); } + +final class PlayerSubscriptionRequested extends HomeEvent { + const PlayerSubscriptionRequested(); +} + +final class NewPuzzleAttempted extends HomeEvent { + const NewPuzzleAttempted(this.difficulty); + + final Difficulty difficulty; + + @override + List get props => [difficulty]; +} diff --git a/lib/home/bloc/home_state.dart b/lib/home/bloc/home_state.dart index 2701a5d..2b28633 100644 --- a/lib/home/bloc/home_state.dart +++ b/lib/home/bloc/home_state.dart @@ -11,12 +11,14 @@ class HomeState extends Equatable { this.sudokuCreationStatus = SudokuCreationStatus.initial, this.sudokuCreationError, this.unfinishedPuzzle, + this.player = Player.empty, }); final Difficulty? difficulty; final SudokuCreationStatus sudokuCreationStatus; final SudokuCreationErrorType? sudokuCreationError; final Puzzle? unfinishedPuzzle; + final Player player; @override List get props => [ @@ -24,6 +26,7 @@ class HomeState extends Equatable { sudokuCreationStatus, sudokuCreationError, unfinishedPuzzle, + player, ]; HomeState copyWith({ @@ -31,6 +34,7 @@ class HomeState extends Equatable { SudokuCreationStatus Function()? sudokuCreationStatus, SudokuCreationErrorType? Function()? sudokuCreationError, Puzzle? Function()? unfinishedPuzzle, + Player Function()? player, }) { return HomeState( difficulty: difficulty != null ? difficulty() : this.difficulty, @@ -42,6 +46,7 @@ class HomeState extends Equatable { : this.sudokuCreationError, unfinishedPuzzle: unfinishedPuzzle != null ? unfinishedPuzzle() : this.unfinishedPuzzle, + player: player != null ? player() : this.player, ); } } diff --git a/lib/home/home.dart b/lib/home/home.dart index 9291b53..211abf0 100644 --- a/lib/home/home.dart +++ b/lib/home/home.dart @@ -1,2 +1,3 @@ export 'bloc/home_bloc.dart'; export 'view/home_page.dart'; +export 'widgets/widgets.dart'; diff --git a/lib/home/view/home_page.dart b/lib/home/view/home_page.dart index 1a826c6..0499bdb 100644 --- a/lib/home/view/home_page.dart +++ b/lib/home/view/home_page.dart @@ -1,5 +1,3 @@ -import 'dart:developer'; - import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -26,8 +24,12 @@ class HomePage extends StatelessWidget { return BlocProvider( create: (context) => HomeBloc( apiClient: context.read(), + authenticationRepository: context.read(), puzzleRepository: context.read(), - )..add(const UnfinishedPuzzleSubscriptionRequested()), + playerRepository: context.read(), + ) + ..add(const UnfinishedPuzzleSubscriptionRequested()) + ..add(const PlayerSubscriptionRequested()), child: const HomeView(), ); } @@ -282,16 +284,6 @@ class HighlightedSection extends StatelessWidget { (HomeBloc bloc) => bloc.state.unfinishedPuzzle, ); - final dailyChallengeWidget = HighlightedSectionItem( - key: const Key('daily_challenge_widget'), - elevatedButtonkey: const Key('daily_challenge_widget_elevated_button'), - iconAsset: Assets.dailyChallengeIcon, - title: l10n.dailyChallengeTitle, - subtitle: l10n.dailyChallengeSubtitle, - buttonText: 'Play', - onButtonPressed: () => log('daily_challenge'), - ); - final resumePuzzleWidget = HighlightedSectionItem( key: const Key('resume_puzzle_widget'), elevatedButtonkey: const Key('resume_puzzle_widget_elevated_button'), @@ -313,7 +305,7 @@ class HighlightedSection extends StatelessWidget { ), child: Row( children: [ - Expanded(child: dailyChallengeWidget), + const Expanded(child: PlayerInfoWidget()), const SizedBox(width: 16), Expanded(child: resumePuzzleWidget), ], @@ -328,7 +320,7 @@ class HighlightedSection extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - dailyChallengeWidget, + const PlayerInfoWidget(), const SizedBox(height: 24), resumePuzzleWidget, ], @@ -339,7 +331,7 @@ class HighlightedSection extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 32), child: Row( children: [ - Expanded(child: dailyChallengeWidget), + const Expanded(child: PlayerInfoWidget()), const SizedBox(width: 32), Expanded(child: resumePuzzleWidget), ], @@ -537,9 +529,11 @@ class CreateGameSection extends StatelessWidget { iconAsset: Assets.easyPuzzleIcon, title: l10n.createEasyGameTitle, caption: l10n.createEasyGameCaption, - onButtonPressed: () => context.read().add( - const SudokuCreationRequested(Difficulty.easy), - ), + onButtonPressed: () { + context.read() + ..add(const SudokuCreationRequested(Difficulty.easy)) + ..add(const NewPuzzleAttempted(Difficulty.easy)); + }, ), CreateGameSectionItem( key: const Key('create_game_medium_mode'), @@ -547,9 +541,11 @@ class CreateGameSection extends StatelessWidget { iconAsset: Assets.mediumPuzzleIcon, title: l10n.createMediumGameTitle, caption: l10n.createMediumGameCaption, - onButtonPressed: () => context.read().add( - const SudokuCreationRequested(Difficulty.medium), - ), + onButtonPressed: () { + context.read() + ..add(const SudokuCreationRequested(Difficulty.medium)) + ..add(const NewPuzzleAttempted(Difficulty.medium)); + }, ), CreateGameSectionItem( key: const Key('create_game_difficult_mode'), @@ -557,9 +553,11 @@ class CreateGameSection extends StatelessWidget { iconAsset: Assets.difficultPuzzleIcon, title: l10n.createDifficultGameTitle, caption: l10n.createDifficultGameCaption, - onButtonPressed: () => context.read().add( - const SudokuCreationRequested(Difficulty.difficult), - ), + onButtonPressed: () { + context.read() + ..add(const SudokuCreationRequested(Difficulty.difficult)) + ..add(const NewPuzzleAttempted(Difficulty.difficult)); + }, ), CreateGameSectionItem( key: const Key('create_game_expert_mode'), @@ -567,9 +565,11 @@ class CreateGameSection extends StatelessWidget { iconAsset: Assets.expertPuzzleIcon, title: l10n.createExpertGameTitle, caption: l10n.createExpertGameCaption, - onButtonPressed: () => context.read().add( - const SudokuCreationRequested(Difficulty.expert), - ), + onButtonPressed: () { + context.read() + ..add(const SudokuCreationRequested(Difficulty.expert)) + ..add(const NewPuzzleAttempted(Difficulty.expert)); + }, ), ]; diff --git a/lib/home/widgets/player_info.dart b/lib/home/widgets/player_info.dart new file mode 100644 index 0000000..549bfb7 --- /dev/null +++ b/lib/home/widgets/player_info.dart @@ -0,0 +1,255 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:sudoku/assets/assets.dart'; +import 'package:sudoku/colors/colors.dart'; +import 'package:sudoku/home/home.dart'; +import 'package:sudoku/layout/layout.dart'; +import 'package:sudoku/typography/typography.dart'; +import 'package:sudoku/widgets/widgets.dart'; + +class PlayerInfoWidget extends StatelessWidget { + const PlayerInfoWidget({super.key}); + + @override + Widget build(BuildContext context) { + final player = context.select((HomeBloc bloc) => bloc.state.player); + + return ResponsiveLayoutBuilder( + small: (_, child) => child!, + medium: (_, __) => DecoratedBox( + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [ + SudokuColors.darkPurple, + SudokuColors.darkPink, + ], + begin: Alignment.bottomLeft, + end: Alignment.topRight, + ), + borderRadius: BorderRadius.circular(16), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + const Expanded( + flex: 2, + child: SudokuIcon( + iconAsset: Assets.dailyChallengeIcon, + scaleFactor: 1.8, + ), + ), + const SizedBox(width: 32), + Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + 'Player Stats', + style: SudokuTextStyle.bodyText1.copyWith( + fontWeight: SudokuFontWeight.bold, + color: Colors.white, + ), + maxLines: 1, + ), + Text( + 'Solved / Attempted', + style: SudokuTextStyle.bodyText1.copyWith( + fontWeight: SudokuFontWeight.medium, + fontSize: 12, + color: Colors.white, + ), + maxLines: 1, + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: AttemptedVsSolved( + label: 'Easy', + attempted: player.easyAttempted, + solved: player.easySolved, + ), + ), + const SizedBox(width: 32), + Expanded( + child: AttemptedVsSolved( + label: 'Medium', + attempted: player.mediumAttempted, + solved: player.mediumSolved, + ), + ), + ], + ), + const SizedBox(height: 4), + Row( + children: [ + Expanded( + child: AttemptedVsSolved( + label: 'Difficult', + attempted: player.difficultAttempted, + solved: player.difficultSolved, + ), + ), + const SizedBox(width: 32), + Expanded( + child: AttemptedVsSolved( + label: 'Expert', + attempted: player.expertAttempted, + solved: player.expertSolved, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ), + large: (_, child) => child!, + child: (layoutSize) { + final cardHeight = switch (layoutSize) { + ResponsiveLayoutSize.small => 252.0, + ResponsiveLayoutSize.medium => 216.0, + ResponsiveLayoutSize.large => 472.0, + }; + + final padding = switch (layoutSize) { + ResponsiveLayoutSize.small => 16.0, + ResponsiveLayoutSize.medium => 16.0, + ResponsiveLayoutSize.large => 24.0, + }; + + final titleTextStyle = switch (layoutSize) { + ResponsiveLayoutSize.small => SudokuTextStyle.bodyText1, + _ => SudokuTextStyle.headline2, + }; + + final iconScaleFactor = switch (layoutSize) { + ResponsiveLayoutSize.small => 1.0, + _ => 1.76, + }; + + return SizedBox( + height: cardHeight, + child: DecoratedBox( + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [ + SudokuColors.darkPurple, + SudokuColors.darkPink, + ], + begin: Alignment.bottomLeft, + end: Alignment.topRight, + ), + borderRadius: BorderRadius.circular(16), + ), + child: Padding( + padding: EdgeInsets.all(padding), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + SudokuIcon( + iconAsset: Assets.dailyChallengeIcon, + scaleFactor: iconScaleFactor, + ), + const SizedBox(height: 16), + Text( + 'Player Stats', + style: titleTextStyle.copyWith( + fontWeight: SudokuFontWeight.bold, + color: Colors.white, + ), + maxLines: 1, + ), + Text( + 'Solved / Attempted', + style: titleTextStyle.copyWith( + fontWeight: SudokuFontWeight.medium, + fontSize: 12, + color: Colors.white, + ), + maxLines: 1, + ), + const SizedBox(height: 8), + AttemptedVsSolved( + label: 'Easy', + attempted: player.easyAttempted, + solved: player.easySolved, + ), + const SizedBox(height: 2), + AttemptedVsSolved( + label: 'Medium', + attempted: player.mediumAttempted, + solved: player.mediumSolved, + ), + const SizedBox(height: 2), + AttemptedVsSolved( + label: 'Difficult', + attempted: player.difficultAttempted, + solved: player.difficultSolved, + ), + const SizedBox(height: 2), + AttemptedVsSolved( + label: 'Expert', + attempted: player.expertAttempted, + solved: player.expertSolved, + ), + ], + ), + ), + ), + ); + }, + ); + } +} + +class AttemptedVsSolved extends StatelessWidget { + const AttemptedVsSolved({ + required this.label, + required this.attempted, + required this.solved, + super.key, + }); + + final String label; + final int attempted; + final int solved; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text( + label, + style: SudokuTextStyle.subtitle2.copyWith( + fontWeight: SudokuFontWeight.semiBold, + color: Colors.white, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + Flexible( + child: Text( + '$solved / $attempted', + style: SudokuTextStyle.subtitle2.copyWith( + fontWeight: SudokuFontWeight.semiBold, + color: Colors.white, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ); + } +} diff --git a/lib/home/widgets/widgets.dart b/lib/home/widgets/widgets.dart new file mode 100644 index 0000000..92d6481 --- /dev/null +++ b/lib/home/widgets/widgets.dart @@ -0,0 +1 @@ +export 'player_info.dart'; diff --git a/lib/main_development.dart b/lib/main_development.dart index a0159cd..dfb350c 100644 --- a/lib/main_development.dart +++ b/lib/main_development.dart @@ -20,7 +20,7 @@ void main() async { ); unawaited( - bootstrap((firebaseAuth) async { + bootstrap((firebaseAuth, firestore) async { final apiClient = SudokuDioClient(baseUrl: Env.apiBaseUrl); final cacheClient = CacheClient(); @@ -47,10 +47,16 @@ void main() async { storageClient: storageClient, ); + final playerRepository = PlayerRepository( + firestore: firestore, + cacheClient: cacheClient, + ); + return App( apiClient: apiClient, puzzleRepository: puzzleRepository, authenticationRepository: authenticationRepository, + playerRepository: playerRepository, ); }), ); diff --git a/lib/main_production.dart b/lib/main_production.dart index cc77439..960e66d 100644 --- a/lib/main_production.dart +++ b/lib/main_production.dart @@ -20,7 +20,7 @@ void main() async { ); unawaited( - bootstrap((firebaseAuth) async { + bootstrap((firebaseAuth, firestore) async { final apiClient = SudokuDioClient(baseUrl: Env.apiBaseUrl); final cacheClient = CacheClient(); @@ -47,10 +47,16 @@ void main() async { storageClient: storageClient, ); + final playerRepository = PlayerRepository( + firestore: firestore, + cacheClient: cacheClient, + ); + return App( apiClient: apiClient, puzzleRepository: puzzleRepository, authenticationRepository: authenticationRepository, + playerRepository: playerRepository, ); }), ); diff --git a/lib/main_staging.dart b/lib/main_staging.dart index c74ee60..e98c3ae 100644 --- a/lib/main_staging.dart +++ b/lib/main_staging.dart @@ -20,7 +20,7 @@ void main() async { ); unawaited( - bootstrap((firebaseAuth) async { + bootstrap((firebaseAuth, firestore) async { final apiClient = SudokuDioClient(baseUrl: Env.apiBaseUrl); final cacheClient = CacheClient(); @@ -47,10 +47,16 @@ void main() async { storageClient: storageClient, ); + final playerRepository = PlayerRepository( + firestore: firestore, + cacheClient: cacheClient, + ); + return App( apiClient: apiClient, puzzleRepository: puzzleRepository, authenticationRepository: authenticationRepository, + playerRepository: playerRepository, ); }), ); diff --git a/lib/models/models.dart b/lib/models/models.dart index 79b26e1..cfee609 100644 --- a/lib/models/models.dart +++ b/lib/models/models.dart @@ -2,6 +2,7 @@ export 'block.dart'; export 'difficulty.dart'; export 'hint.dart'; export 'json_map.dart'; +export 'player.dart'; export 'position.dart'; export 'sudoku.dart'; export 'ticker.dart'; diff --git a/lib/models/player.dart b/lib/models/player.dart new file mode 100644 index 0000000..725f66d --- /dev/null +++ b/lib/models/player.dart @@ -0,0 +1,191 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart' show immutable; +import 'package:json_annotation/json_annotation.dart'; +import 'package:sudoku/models/models.dart'; + +part 'player.g.dart'; + +/// {@template player} +/// Player model. +/// {@endtemplate} +@immutable +@JsonSerializable(fieldRename: FieldRename.snake) +class Player extends Equatable { + /// {@macro player} + const Player({ + this.name = '', + this.easyAttempted = 0, + this.easySolved = 0, + this.mediumAttempted = 0, + this.mediumSolved = 0, + this.difficultAttempted = 0, + this.difficultSolved = 0, + this.expertAttempted = 0, + this.expertSolved = 0, + this.totalAttempted = 0, + this.totalSolved = 0, + }); + + /// Name of the player. + /// + /// Defaults to empty-string. + final String name; + + /// Number of easy puzzles created by the player. + /// + /// Defaults to 0. + final int easyAttempted; + + /// Number of easy puzzles solved by the player. + /// + /// Defaults to 0. + final int easySolved; + + /// Number of medium puzzles created by the player. + /// + /// Defaults to 0. + final int mediumAttempted; + + /// Number of medium puzzles solved by the player. + /// + /// Defaults to 0. + final int mediumSolved; + + /// Number of difficult puzzles created by the player. + /// + /// Defaults to 0. + final int difficultAttempted; + + /// Number of difficult puzzles solved by the player. + /// + /// Defaults to 0. + final int difficultSolved; + + /// Number of expert puzzles created by the player. + /// + /// Defaults to 0. + final int expertAttempted; + + /// Number of expert puzzles solved by the player. + /// + /// Defaults to 0. + final int expertSolved; + + /// Number of total puzzles created by the player. + /// + /// Defaults to 0. + final int totalAttempted; + + /// Number of total puzzles solved by the player. + /// + /// Defaults to 0. + final int totalSolved; + + /// Represents an empty player object. + static const empty = Player(); + + /// Deserializes the given [JsonMap] into a [Player]. + static Player fromJson(JsonMap json) => _$PlayerFromJson(json); + + /// Converts this [Player] into a [JsonMap]. + JsonMap toJson() => _$PlayerToJson(this); + + /// Helper method that increase attempt count depending upon + /// provided difficulty level. + Player updateAttemptCount(Difficulty difficulty) { + switch (difficulty) { + case Difficulty.easy: + return copyWith( + easyAttempted: easyAttempted + 1, + totalAttempted: totalAttempted + 1, + ); + case Difficulty.medium: + return copyWith( + mediumAttempted: mediumAttempted + 1, + totalAttempted: totalAttempted + 1, + ); + case Difficulty.difficult: + return copyWith( + difficultAttempted: difficultAttempted + 1, + totalAttempted: totalAttempted + 1, + ); + case Difficulty.expert: + return copyWith( + expertAttempted: expertAttempted + 1, + totalAttempted: totalAttempted + 1, + ); + } + } + + /// Helper method that increase solved count depending upon + /// provided difficulty level. + Player updateSolvedCount(Difficulty difficulty) { + switch (difficulty) { + case Difficulty.easy: + return copyWith( + easySolved: easySolved + 1, + totalSolved: totalSolved + 1, + ); + case Difficulty.medium: + return copyWith( + mediumSolved: mediumSolved + 1, + totalSolved: totalSolved + 1, + ); + case Difficulty.difficult: + return copyWith( + difficultSolved: difficultSolved + 1, + totalSolved: totalSolved + 1, + ); + case Difficulty.expert: + return copyWith( + expertSolved: expertSolved + 1, + totalSolved: totalSolved + 1, + ); + } + } + + /// Returns a new instance of the [Player] with updated value. + /// + /// {@macro player} + Player copyWith({ + String? name, + int? easyAttempted, + int? easySolved, + int? mediumAttempted, + int? mediumSolved, + int? difficultAttempted, + int? difficultSolved, + int? expertAttempted, + int? expertSolved, + int? totalAttempted, + int? totalSolved, + }) { + return Player( + name: name ?? this.name, + easyAttempted: easyAttempted ?? this.easyAttempted, + easySolved: easySolved ?? this.easySolved, + mediumAttempted: mediumAttempted ?? this.mediumAttempted, + mediumSolved: mediumSolved ?? this.mediumSolved, + difficultAttempted: difficultAttempted ?? this.difficultAttempted, + difficultSolved: difficultSolved ?? this.difficultSolved, + expertAttempted: expertAttempted ?? this.expertAttempted, + expertSolved: expertSolved ?? this.expertSolved, + totalAttempted: totalAttempted ?? this.totalAttempted, + totalSolved: totalSolved ?? this.totalSolved, + ); + } + + @override + List get props => [ + name, + easyAttempted, + easySolved, + mediumAttempted, + mediumSolved, + difficultAttempted, + difficultSolved, + expertAttempted, + expertSolved, + ]; +} diff --git a/lib/models/player.g.dart b/lib/models/player.g.dart new file mode 100644 index 0000000..7c20f56 --- /dev/null +++ b/lib/models/player.g.dart @@ -0,0 +1,36 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file + +part of 'player.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Player _$PlayerFromJson(Map json) => Player( + name: json['name'] as String? ?? '', + easyAttempted: (json['easy_attempted'] as num?)?.toInt() ?? 0, + easySolved: (json['easy_solved'] as num?)?.toInt() ?? 0, + mediumAttempted: (json['medium_attempted'] as num?)?.toInt() ?? 0, + mediumSolved: (json['medium_solved'] as num?)?.toInt() ?? 0, + difficultAttempted: (json['difficult_attempted'] as num?)?.toInt() ?? 0, + difficultSolved: (json['difficult_solved'] as num?)?.toInt() ?? 0, + expertAttempted: (json['expert_attempted'] as num?)?.toInt() ?? 0, + expertSolved: (json['expert_solved'] as num?)?.toInt() ?? 0, + totalAttempted: (json['total_attempted'] as num?)?.toInt() ?? 0, + totalSolved: (json['total_solved'] as num?)?.toInt() ?? 0, + ); + +Map _$PlayerToJson(Player instance) => { + 'name': instance.name, + 'easy_attempted': instance.easyAttempted, + 'easy_solved': instance.easySolved, + 'medium_attempted': instance.mediumAttempted, + 'medium_solved': instance.mediumSolved, + 'difficult_attempted': instance.difficultAttempted, + 'difficult_solved': instance.difficultSolved, + 'expert_attempted': instance.expertAttempted, + 'expert_solved': instance.expertSolved, + 'total_attempted': instance.totalAttempted, + 'total_solved': instance.totalSolved, + }; diff --git a/lib/puzzle/bloc/puzzle_bloc.dart b/lib/puzzle/bloc/puzzle_bloc.dart index 0608975..c3e9a0f 100644 --- a/lib/puzzle/bloc/puzzle_bloc.dart +++ b/lib/puzzle/bloc/puzzle_bloc.dart @@ -12,10 +12,14 @@ part 'puzzle_state.dart'; class PuzzleBloc extends Bloc { PuzzleBloc({ - required PuzzleRepository puzzleRepository, required SudokuAPI apiClient, - }) : _puzzleRepository = puzzleRepository, - _apiClient = apiClient, + required PuzzleRepository puzzleRepository, + required AuthenticationRepository authenticationRepository, + required PlayerRepository playerRepository, + }) : _apiClient = apiClient, + _puzzleRepository = puzzleRepository, + _authenticationRepository = authenticationRepository, + _playerRepository = playerRepository, super(const PuzzleState()) { on(_onPuzzleInitialized); on(_onSudokuBlockSelected); @@ -26,9 +30,10 @@ class PuzzleBloc extends Bloc { on(_onUnfinishedPuzzleSaveRequested); } - final PuzzleRepository _puzzleRepository; - final SudokuAPI _apiClient; + final PuzzleRepository _puzzleRepository; + final AuthenticationRepository _authenticationRepository; + final PlayerRepository _playerRepository; void _onPuzzleInitialized( PuzzleInitialized event, @@ -51,10 +56,10 @@ class PuzzleBloc extends Bloc { ); } - void _onSudokuInputEntered( + Future _onSudokuInputEntered( SudokuInputEntered event, Emitter emit, - ) { + ) async { final selectedBlock = state.selectedBlock; if (selectedBlock == null || selectedBlock.isGenerated == true) return; @@ -102,6 +107,13 @@ class PuzzleBloc extends Bloc { selectedBlock: () => null, ), ); + final currentUser = _authenticationRepository.currentUser; + final currentPlayer = _playerRepository.currentPlayer; + + final updatedPlayer = currentPlayer.updateSolvedCount( + state.puzzle.difficulty, + ); + await _playerRepository.updatePlayer(currentUser.id, updatedPlayer); } else { emit( state.copyWith( diff --git a/lib/puzzle/view/puzzle_page.dart b/lib/puzzle/view/puzzle_page.dart index c32052f..39b4b38 100644 --- a/lib/puzzle/view/puzzle_page.dart +++ b/lib/puzzle/view/puzzle_page.dart @@ -22,8 +22,10 @@ class PuzzlePage extends StatelessWidget { providers: [ BlocProvider( create: (context) => PuzzleBloc( - puzzleRepository: context.read(), apiClient: context.read(), + puzzleRepository: context.read(), + authenticationRepository: context.read(), + playerRepository: context.read(), )..add(const PuzzleInitialized()), ), BlocProvider( diff --git a/lib/repository/player_repository.dart b/lib/repository/player_repository.dart new file mode 100644 index 0000000..48f9130 --- /dev/null +++ b/lib/repository/player_repository.dart @@ -0,0 +1,51 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:flutter/foundation.dart' show visibleForTesting; +import 'package:sudoku/cache/cache.dart'; +import 'package:sudoku/models/models.dart'; + +const _collection = 'players'; + +/// {@template player_repository} +/// Repository which manages player. +/// {@endtemplate} +class PlayerRepository { + /// {@macro player_repository} + PlayerRepository({ + required FirebaseFirestore firestore, + required CacheClient cacheClient, + }) : _playersCollection = firestore.collection(_collection), + _cache = cacheClient; + + final CollectionReference> _playersCollection; + final CacheClient _cache; + + /// Player cache key. + /// Should only be used for testing purposes. + @visibleForTesting + static const playerCacheKey = '__player_cache_key__'; + + /// Returns a [Stream] of the [Player] with the `userId`. + Stream getPlayer(String userId) { + return _playersCollection.doc(userId).snapshots().map((snapshot) { + if (!snapshot.exists) { + _cache.write(key: playerCacheKey, value: Player.empty); + return Player.empty; + } + + final player = Player.fromJson(snapshot.data()!); + _cache.write(key: playerCacheKey, value: player); + return Player.fromJson(snapshot.data()!); + }); + } + + /// Returns the current cached player. + /// Defaults to [Player.empty] if there is no cached player. + Player get currentPlayer { + return _cache.read(key: playerCacheKey) ?? Player.empty; + } + + /// Updates information of the [player] with the `userId`. + Future updatePlayer(String userId, Player player) { + return _playersCollection.doc(userId).set(player.toJson()); + } +} diff --git a/lib/repository/repository.dart b/lib/repository/repository.dart index 62a869a..9adb69a 100644 --- a/lib/repository/repository.dart +++ b/lib/repository/repository.dart @@ -1,2 +1,3 @@ export 'authentication_repository.dart'; +export 'player_repository.dart'; export 'puzzle_repository.dart'; diff --git a/pubspec.lock b/pubspec.lock index 4b2314f..0ce0711 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -25,6 +25,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.4.1" + antlr4: + dependency: transitive + description: + name: antlr4 + sha256: "752b4a6e4ad97953652a2b2bbf5377f46c94b579d3372b50080c7e5858234a05" + url: "https://pub.dev" + source: hosted + version: "4.13.2" args: dependency: transitive description: @@ -129,6 +137,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.9.2" + cel: + dependency: transitive + description: + name: cel + sha256: "2b5adb2c4866311817f6444996a67fd14f016cfcfb57a8d9c7fb0defeea2eb62" + url: "https://pub.dev" + source: hosted + version: "0.5.3" characters: dependency: transitive description: @@ -153,6 +169,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + cloud_firestore: + dependency: "direct main" + description: + name: cloud_firestore + sha256: a7782612945fffa07cb99cf2e1e152f6c6152ee57fc201a7249e08a97a875bdb + url: "https://pub.dev" + source: hosted + version: "5.2.0" + cloud_firestore_platform_interface: + dependency: transitive + description: + name: cloud_firestore_platform_interface + sha256: "4ea584875e9c192559a21a8198727ad333b2b85b22af8ca2fa8b0b112de069dc" + url: "https://pub.dev" + source: hosted + version: "6.3.0" + cloud_firestore_web: + dependency: transitive + description: + name: cloud_firestore_web + sha256: "530066aa006b7d23e4cd8312d08cef996f80e6cbb0f1c2e7256b5fa200404127" + url: "https://pub.dev" + source: hosted + version: "4.1.0" code_builder: dependency: transitive description: @@ -257,6 +297,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + fake_cloud_firestore: + dependency: "direct dev" + description: + name: fake_cloud_firestore + sha256: "59dfb28913a6f3234033698833f55b6fbe73dfefe56794ae7e35e18f658ef9be" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + fake_firebase_security_rules: + dependency: transitive + description: + name: fake_firebase_security_rules + sha256: "7a9011d42d99848ece92784fed5a20167ad76da66de2c1102a2bc053e66b0305" + url: "https://pub.dev" + source: hosted + version: "0.5.3" ffi: dependency: transitive description: @@ -493,6 +549,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" + logger: + dependency: transitive + description: + name: logger + sha256: "697d067c60c20999686a0add96cf6aba723b3aa1f83ecf806a8097231529ec32" + url: "https://pub.dev" + source: hosted + version: "2.4.0" logging: dependency: transitive description: @@ -533,6 +597,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" + mock_exceptions: + dependency: transitive + description: + name: mock_exceptions + sha256: "6e3e623712d2c6106ffe9e14732912522b565ddaa82a8dcee6cd4441b5984056" + url: "https://pub.dev" + source: hosted + version: "0.8.2" mockingjay: dependency: "direct dev" description: @@ -549,6 +621,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + more: + dependency: transitive + description: + name: more + sha256: b9372ef11909cb88a7a29e221d96fbe17eb04394f022e374a4299a6c72946108 + url: "https://pub.dev" + source: hosted + version: "4.2.0" nested: dependency: transitive description: @@ -677,6 +757,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + quiver: + dependency: transitive + description: + name: quiver + sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 + url: "https://pub.dev" + source: hosted + version: "3.2.1" recase: dependency: transitive description: @@ -685,14 +773,22 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" + rx: + dependency: transitive + description: + name: rx + sha256: "7f54bd39cc63a01c770c1de4b6ce8e135eb13119614cba2216bd9a93ccd29e56" + url: "https://pub.dev" + source: hosted + version: "0.4.0" rxdart: dependency: "direct main" description: name: rxdart - sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" url: "https://pub.dev" source: hosted - version: "0.28.0" + version: "0.27.7" shared_preferences: dependency: "direct main" description: @@ -898,6 +994,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + tuple: + dependency: transitive + description: + name: tuple + sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 + url: "https://pub.dev" + source: hosted + version: "2.0.2" typed_data: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 0b27e32..6ed57fc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,6 +11,7 @@ environment: dependencies: bloc: ^8.1.3 + cloud_firestore: ^5.2.0 dio: ^5.5.0+1 envied: ^0.5.4+1 equatable: ^2.0.5 @@ -26,7 +27,7 @@ dependencies: intl: ^0.19.0 json_annotation: ^4.9.0 loading_indicator: ^3.1.1 - rxdart: ^0.28.0 + rxdart: ^0.27.1 shared_preferences: ^2.2.3 dev_dependencies: @@ -34,6 +35,7 @@ dev_dependencies: build_runner: ^2.4.11 envied_generator: ^0.5.4+1 fake_async: ^1.3.1 + fake_cloud_firestore: ^3.0.2 firebase_core_platform_interface: ^5.2.0 flutter_test: sdk: flutter diff --git a/test/app/view/app_test.dart b/test/app/view/app_test.dart index edb598a..e058d94 100644 --- a/test/app/view/app_test.dart +++ b/test/app/view/app_test.dart @@ -3,6 +3,7 @@ import 'package:mocktail/mocktail.dart'; import 'package:sudoku/api/api.dart'; import 'package:sudoku/app/app.dart'; import 'package:sudoku/home/home.dart'; +import 'package:sudoku/models/models.dart'; import 'package:sudoku/puzzle/puzzle.dart'; import 'package:sudoku/repository/repository.dart'; @@ -11,20 +12,31 @@ import '../../helpers/helpers.dart'; void main() { group('App', () { late Puzzle puzzle; + late User user; late SudokuAPI apiClient; + late PuzzleRepository puzzleRepository; late AuthenticationRepository authenticationRepository; + late PlayerRepository playerRepository; setUp(() { puzzle = MockPuzzle(); + user = MockUser(); apiClient = MockSudokuAPI(); puzzleRepository = MockPuzzleRepository(); authenticationRepository = MockAuthenticationRepository(); + playerRepository = MockPlayerRepository(); when(() => puzzleRepository.getPuzzleFromLocalMemory()).thenAnswer( (_) => Stream.value(puzzle), ); + + when(() => user.id).thenReturn('mock-user'); + when(() => authenticationRepository.currentUser).thenReturn(user); + when(() => playerRepository.getPlayer(any())).thenAnswer( + (_) => Stream.value(MockPlayer()), + ); }); testWidgets('renders HomePage', (tester) async { @@ -33,6 +45,7 @@ void main() { apiClient: apiClient, puzzleRepository: puzzleRepository, authenticationRepository: authenticationRepository, + playerRepository: playerRepository, ), ); expect(find.byType(HomePage), findsOneWidget); diff --git a/test/helpers/mocks.dart b/test/helpers/mocks.dart index b07268a..08ba722 100644 --- a/test/helpers/mocks.dart +++ b/test/helpers/mocks.dart @@ -64,3 +64,9 @@ class MockUserCredential extends Mock implements firebase_auth.UserCredential {} class MockFirebaseCore extends Mock with MockPlatformInterfaceMixin implements FirebasePlatform {} + +class MockPlayerRepository extends Mock implements PlayerRepository {} + +class MockUser extends Mock implements User {} + +class MockPlayer extends Mock implements Player {} diff --git a/test/helpers/pump_app.dart b/test/helpers/pump_app.dart index 0022164..5900b86 100644 --- a/test/helpers/pump_app.dart +++ b/test/helpers/pump_app.dart @@ -17,6 +17,7 @@ extension PumpApp on WidgetTester { SudokuAPI? apiClient, PuzzleRepository? puzzleRepository, AuthenticationRepository? authenticationRepository, + PlayerRepository? playerRepository, HomeBloc? homeBloc, TimerBloc? timerBloc, PuzzleBloc? puzzleBloc, @@ -35,6 +36,9 @@ extension PumpApp on WidgetTester { RepositoryProvider.value( value: authenticationRepository ?? MockAuthenticationRepository(), ), + RepositoryProvider.value( + value: playerRepository ?? MockPlayerRepository(), + ), ], child: MultiBlocProvider( providers: [ diff --git a/test/home/bloc/home_bloc_test.dart b/test/home/bloc/home_bloc_test.dart index cc18326..b3ca4fd 100644 --- a/test/home/bloc/home_bloc_test.dart +++ b/test/home/bloc/home_bloc_test.dart @@ -11,11 +11,20 @@ import 'package:sudoku/repository/repository.dart'; import '../../helpers/helpers.dart'; +class _FakePlayer extends Fake implements Player {} + void main() { group('HomeBloc', () { late Puzzle puzzle; late SudokuAPI apiClient; + late User user; + late Player player; + late PuzzleRepository puzzleRepository; + late AuthenticationRepository authenticationRepository; + late PlayerRepository playerRepository; + + const mockUserId = 'mock-user'; const sudoku = Sudoku( blocks: [ @@ -32,6 +41,11 @@ void main() { apiClient = MockSudokuAPI(); puzzleRepository = MockPuzzleRepository(); + user = MockUser(); + player = MockPlayer(); + authenticationRepository = MockAuthenticationRepository(); + playerRepository = MockPlayerRepository(); + when(() => apiClient.createSudoku(difficulty: any(named: 'difficulty'))) .thenAnswer( (_) => Future.value(sudoku), @@ -42,16 +56,29 @@ void main() { when(() => puzzleRepository.clearPuzzleInLocalMemory()).thenAnswer( (_) async {}, ); + + when(() => user.id).thenReturn(mockUserId); + when(() => authenticationRepository.currentUser).thenReturn(user); + + when(() => playerRepository.getPlayer(any())).thenAnswer( + (_) => Stream.value(player), + ); + when(() => playerRepository.updatePlayer(any(), any())).thenAnswer( + (_) async {}, + ); }); setUpAll(() { registerFallbackValue(Difficulty.easy); + registerFallbackValue(_FakePlayer()); }); HomeBloc buildBloc() { return HomeBloc( apiClient: apiClient, puzzleRepository: puzzleRepository, + authenticationRepository: authenticationRepository, + playerRepository: playerRepository, ); } @@ -239,5 +266,58 @@ void main() { ], ); }); + + group('PlayerSubscriptionRequested', () { + blocTest( + 'starts listening to getPlayer from PlayerRepository', + build: buildBloc, + act: (bloc) => bloc.add(PlayerSubscriptionRequested()), + verify: (_) { + verify(() => playerRepository.getPlayer(mockUserId)).called(1); + }, + ); + + blocTest( + 'emits state with updated [player] when repository ' + 'getPlayer emits a new player object', + build: buildBloc, + act: (bloc) => bloc.add(PlayerSubscriptionRequested()), + expect: () => [ + HomeState(player: player), + ], + ); + + blocTest( + 'emits state with empty player when repository ' + 'getPlayer emits an error', + build: buildBloc, + setUp: () { + when(() => playerRepository.getPlayer(any())) + .thenAnswer((_) => Stream.error(Exception())); + }, + act: (bloc) => bloc.add(PlayerSubscriptionRequested()), + expect: () => [ + HomeState(player: Player.empty), + ], + ); + }); + + group('NewPuzzleAttempted', () { + blocTest( + 'calls the [updatePlayer] method from playerRepository with ' + 'userId and updated player', + build: buildBloc, + seed: () => HomeState(player: Player.empty), + act: (bloc) => bloc.add(NewPuzzleAttempted(Difficulty.medium)), + verify: (_) { + verify( + () => playerRepository.updatePlayer( + mockUserId, + Player(mediumAttempted: 1), + ), + ).called(1); + }, + ); + }); }); } diff --git a/test/home/bloc/home_event_test.dart b/test/home/bloc/home_event_test.dart index 7c16a8d..1400b6b 100644 --- a/test/home/bloc/home_event_test.dart +++ b/test/home/bloc/home_event_test.dart @@ -53,5 +53,37 @@ void main() { ); }); }); + + group('PlayerSubscriptionRequested', () { + test('supports value equality', () { + expect( + PlayerSubscriptionRequested(), + equals(PlayerSubscriptionRequested()), + ); + }); + + test('props are correct', () { + expect( + PlayerSubscriptionRequested().props, + equals([]), + ); + }); + }); + + group('NewPuzzleAttempted', () { + test('supports value equality', () { + expect( + NewPuzzleAttempted(Difficulty.easy), + equals(NewPuzzleAttempted(Difficulty.easy)), + ); + }); + + test('props are correct', () { + expect( + NewPuzzleAttempted(Difficulty.easy).props, + equals([Difficulty.easy]), + ); + }); + }); }); } diff --git a/test/home/bloc/home_state_test.dart b/test/home/bloc/home_state_test.dart index 08c32d0..b9ff691 100644 --- a/test/home/bloc/home_state_test.dart +++ b/test/home/bloc/home_state_test.dart @@ -14,6 +14,7 @@ void main() { SudokuCreationStatus? sudokuCreationStatus, SudokuCreationErrorType? sudokuCreationError, Puzzle? unfinishedPuzzle, + Player? player, }) { return HomeState( difficulty: difficulty, @@ -21,6 +22,7 @@ void main() { sudokuCreationStatus ?? SudokuCreationStatus.initial, sudokuCreationError: sudokuCreationError, unfinishedPuzzle: unfinishedPuzzle, + player: player ?? Player.empty, ); } @@ -37,6 +39,7 @@ void main() { SudokuCreationStatus.initial, null, null, + Player.empty, ], ), ); @@ -54,6 +57,7 @@ void main() { sudokuCreationStatus: null, sudokuCreationError: null, unfinishedPuzzle: null, + player: null, ), equals(createSubject()), ); @@ -67,6 +71,7 @@ void main() { sudokuCreationStatus: () => SudokuCreationStatus.inProgress, sudokuCreationError: () => SudokuCreationErrorType.unexpected, unfinishedPuzzle: () => puzzle, + player: () => Player(easyAttempted: 5), ), equals( createSubject( @@ -74,6 +79,7 @@ void main() { sudokuCreationStatus: SudokuCreationStatus.inProgress, sudokuCreationError: SudokuCreationErrorType.unexpected, unfinishedPuzzle: puzzle, + player: Player(easyAttempted: 5), ), ), ); diff --git a/test/home/view/home_page_test.dart b/test/home/view/home_page_test.dart index 635ee78..a7ab8f7 100644 --- a/test/home/view/home_page_test.dart +++ b/test/home/view/home_page_test.dart @@ -14,12 +14,8 @@ import '../../helpers/helpers.dart'; void main() { group('HomePage', () { - const dailyChallengeKey = Key('daily_challenge_widget'); const resumePuzzleKey = Key('resume_puzzle_widget'); - const dailyChallengeElevatedButtonKey = Key( - 'daily_challenge_widget_elevated_button', - ); const resumePuzzleElevatedButtonKey = Key( 'resume_puzzle_widget_elevated_button', ); @@ -32,7 +28,11 @@ void main() { late PuzzleBloc puzzleBloc; late PuzzleState puzzleState; late Puzzle puzzle; + + late User user; late PuzzleRepository puzzleRepository; + late AuthenticationRepository authenticationRepository; + late PlayerRepository playerRepository; setUp(() { homeBloc = MockHomeBloc(); @@ -40,6 +40,10 @@ void main() { puzzle = MockPuzzle(); homeState = MockHomeState(); + user = MockUser(); + authenticationRepository = MockAuthenticationRepository(); + playerRepository = MockPlayerRepository(); + when(() => homeBloc.state).thenReturn(homeState); when(puzzleRepository.fetchPuzzleFromCache).thenReturn(puzzle); when(() => puzzleRepository.getPuzzleFromLocalMemory()).thenAnswer( @@ -52,25 +56,47 @@ void main() { when(() => puzzle.totalSecondsElapsed).thenReturn(12); when(() => puzzleState.puzzle).thenReturn(puzzle); when(() => puzzleBloc.state).thenReturn(puzzleState); + + when(() => user.id).thenReturn('mock-user'); + when(() => authenticationRepository.currentUser).thenReturn(user); + + when(() => playerRepository.getPlayer(any())).thenAnswer( + (_) => Stream.value(Player.empty), + ); }); testWidgets('renders on a large layout', (tester) async { tester.setLargeDisplaySize(); - await tester.pumpApp(HomePage(), puzzleRepository: puzzleRepository); + await tester.pumpApp( + HomePage(), + puzzleRepository: puzzleRepository, + authenticationRepository: authenticationRepository, + playerRepository: playerRepository, + ); expect(find.byType(HomeView), findsOneWidget); }); testWidgets('renders on a medium layout', (tester) async { tester.setMediumDisplaySize(); - await tester.pumpApp(HomePage(), puzzleRepository: puzzleRepository); + await tester.pumpApp( + HomePage(), + puzzleRepository: puzzleRepository, + authenticationRepository: authenticationRepository, + playerRepository: playerRepository, + ); expect(find.byType(HomeView), findsOneWidget); }); testWidgets('renders on a small layout', (tester) async { tester.setSmallDisplaySize(); - await tester.pumpApp(HomePage(), puzzleRepository: puzzleRepository); + await tester.pumpApp( + HomePage(), + puzzleRepository: puzzleRepository, + authenticationRepository: authenticationRepository, + playerRepository: playerRepository, + ); expect(find.byType(HomeView), findsOneWidget); }); @@ -176,36 +202,6 @@ void main() { }, ); - group('Daily Challenge', () { - late HomeBloc homeBloc; - - setUp(() { - homeBloc = MockHomeBloc(); - when(() => homeBloc.state).thenReturn(const HomeState()); - }); - - testWidgets('exists in the widget tree', (tester) async { - await tester.pumpApp(HomeView(), homeBloc: homeBloc); - expect(find.byKey(dailyChallengeKey), findsOneWidget); - }); - - testWidgets('onPressed is defined', (tester) async { - await tester.pumpApp(HomeView(), homeBloc: homeBloc); - final finder = find.byWidgetPredicate( - (widget) => - widget is SudokuElevatedButton && - widget.key == dailyChallengeElevatedButtonKey && - widget.onPressed != null, - ); - expect( - finder, - findsOneWidget, - ); - await tester.tap(finder); - await tester.pumpAndSettle(); - }); - }); - group('Resume Puzzle', () { late HomeBloc homeBloc; @@ -236,12 +232,17 @@ void main() { testWidgets( 'adds [UnfinishedPuzzleResumed] when unfinishedPuzzle is not null', (tester) async { + tester.setLargeDisplaySize(); + when(() => homeState.unfinishedPuzzle).thenReturn(puzzle); + when(() => homeState.player).thenReturn(Player.empty); when(() => homeBloc.state).thenReturn(homeState); - await tester.pumpApp(HomeView(), homeBloc: homeBloc); + await tester.pumpApp(HomeViewLayout(), homeBloc: homeBloc); final finder = find.byKey(resumePuzzleElevatedButtonKey); + await tester.ensureVisible(finder); + await tester.tap(finder); await tester.pumpAndSettle(); diff --git a/test/models/player_test.dart b/test/models/player_test.dart new file mode 100644 index 0000000..6056f59 --- /dev/null +++ b/test/models/player_test.dart @@ -0,0 +1,221 @@ +// ignore_for_file: prefer_const_constructors, avoid_redundant_argument_values + +import 'package:flutter_test/flutter_test.dart'; +import 'package:sudoku/models/models.dart'; + +void main() { + group('$Player', () { + Player createSubject({ + String? name, + int? easyAttempted, + int? easySolved, + int? mediumAttempted, + int? mediumSolved, + int? difficultAttempted, + int? difficultSolved, + int? expertAttempted, + int? expertSolved, + int? totalAttempted, + int? totalSolved, + }) { + return Player( + name: name ?? '', + easyAttempted: easyAttempted ?? 0, + easySolved: easySolved ?? 0, + mediumAttempted: mediumAttempted ?? 0, + mediumSolved: mediumSolved ?? 0, + difficultAttempted: difficultAttempted ?? 0, + difficultSolved: difficultSolved ?? 0, + expertAttempted: expertAttempted ?? 0, + expertSolved: expertSolved ?? 0, + totalAttempted: totalAttempted ?? 0, + totalSolved: totalSolved ?? 0, + ); + } + + test('constructor works correctly', () { + expect(createSubject, returnsNormally); + }); + + test('props are correct', () { + expect( + createSubject().props, + equals( + [ + '', + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ], + ), + ); + }); + + test('supports value equality', () { + expect(createSubject(), equals(createSubject())); + }); + + test('empty returns an empty player object', () { + expect(Player.empty, equals(createSubject())); + }); + + test('toJson works correctly', () { + expect( + createSubject().toJson(), + equals({ + 'name': '', + 'easy_attempted': 0, + 'easy_solved': 0, + 'medium_attempted': 0, + 'medium_solved': 0, + 'difficult_attempted': 0, + 'difficult_solved': 0, + 'expert_attempted': 0, + 'expert_solved': 0, + 'total_attempted': 0, + 'total_solved': 0, + }), + ); + }); + + test('fromJson works correctly', () { + expect( + Player.fromJson({ + 'name': '', + 'easy_attempted': 0, + 'easy_solved': 0, + 'medium_attempted': 0, + 'medium_solved': 0, + 'difficult_attempted': 0, + 'difficult_solved': 0, + 'expert_attempted': 0, + 'expert_solved': 0, + 'total_attempted': 0, + 'total_solved': 0, + }), + equals(createSubject()), + ); + }); + + group('updateAttemptCount', () { + test('updates easy attempt count with total attempt', () { + expect( + createSubject().updateAttemptCount(Difficulty.easy), + equals(createSubject(easyAttempted: 1, totalAttempted: 1)), + ); + }); + + test('updates medium attempt count with total attempt', () { + expect( + createSubject().updateAttemptCount(Difficulty.medium), + equals(createSubject(mediumAttempted: 1, totalAttempted: 1)), + ); + }); + + test('updates difficult attempt count with total attempt', () { + expect( + createSubject().updateAttemptCount(Difficulty.difficult), + equals(createSubject(difficultAttempted: 1, totalAttempted: 1)), + ); + }); + + test('updates expert attempt count with total attempt', () { + expect( + createSubject().updateAttemptCount(Difficulty.expert), + equals(createSubject(expertAttempted: 1, totalAttempted: 1)), + ); + }); + }); + + group('updateSolvedCount', () { + test('updates easy solved count with total solved', () { + expect( + createSubject().updateSolvedCount(Difficulty.easy), + equals(createSubject(easySolved: 1, totalSolved: 1)), + ); + }); + + test('updates medium solved count with total solved', () { + expect( + createSubject().updateSolvedCount(Difficulty.medium), + equals(createSubject(mediumSolved: 1, totalSolved: 1)), + ); + }); + + test('updates difficult solved count with total solved', () { + expect( + createSubject().updateSolvedCount(Difficulty.difficult), + equals(createSubject(difficultSolved: 1, totalSolved: 1)), + ); + }); + + test('updates expert solved count with total solved', () { + expect( + createSubject().updateSolvedCount(Difficulty.expert), + equals(createSubject(expertSolved: 1, totalSolved: 1)), + ); + }); + }); + + group('copyWith', () { + test('returns same object if no argument is passed', () { + expect(createSubject().copyWith(), equals(createSubject())); + }); + + test('returns the old value for each parameter if null is provided', () { + expect( + createSubject().copyWith( + name: null, + easyAttempted: null, + easySolved: null, + mediumAttempted: null, + mediumSolved: null, + difficultAttempted: null, + difficultSolved: null, + expertAttempted: null, + expertSolved: null, + totalAttempted: null, + totalSolved: null, + ), + equals(createSubject()), + ); + }); + + test('returns the updated copy of this for every non-null parameter', () { + expect( + createSubject().copyWith( + name: 'test', + easyAttempted: 1, + easySolved: 2, + mediumAttempted: 3, + mediumSolved: 4, + difficultAttempted: 5, + difficultSolved: 6, + expertAttempted: 7, + expertSolved: 8, + totalAttempted: 9, + ), + equals( + createSubject( + name: 'test', + easyAttempted: 1, + easySolved: 2, + mediumAttempted: 3, + mediumSolved: 4, + difficultAttempted: 5, + difficultSolved: 6, + expertAttempted: 7, + expertSolved: 8, + totalAttempted: 9, + ), + ), + ); + }); + }); + }); +} diff --git a/test/puzzle/bloc/puzzle_bloc_test.dart b/test/puzzle/bloc/puzzle_bloc_test.dart index 5651042..7963952 100644 --- a/test/puzzle/bloc/puzzle_bloc_test.dart +++ b/test/puzzle/bloc/puzzle_bloc_test.dart @@ -16,24 +16,32 @@ class _FakeSudoku extends Fake implements Sudoku {} class _FakePuzzle extends Fake implements Puzzle {} +class _FakePlayer extends Fake implements Player {} + void main() { group('PuzzleBloc', () { late Block block; late Sudoku sudoku; late Puzzle puzzle; late Hint hint; + late User user; late SudokuAPI apiClient; late PuzzleRepository repository; + late AuthenticationRepository authenticationRepository; + late PlayerRepository playerRepository; setUp(() { block = MockBlock(); sudoku = MockSudoku(); puzzle = MockPuzzle(); hint = MockHint(); + user = MockUser(); apiClient = MockSudokuAPI(); repository = MockPuzzleRepository(); + authenticationRepository = MockAuthenticationRepository(); + playerRepository = MockPlayerRepository(); when(() => sudoku.blocksToHighlight(any())).thenReturn([block]); when(() => puzzle.sudoku).thenReturn(sudoku); @@ -41,18 +49,27 @@ void main() { when( () => repository.storePuzzleInLocalMemory(puzzle: any(named: 'puzzle')), ).thenAnswer((_) async {}); + when(() => user.id).thenReturn('mock-user'); + when(() => authenticationRepository.currentUser).thenReturn(user); + when(() => playerRepository.currentPlayer).thenReturn(Player.empty); + when(() => playerRepository.updatePlayer(any(), any())).thenAnswer( + (_) async {}, + ); }); setUpAll(() { registerFallbackValue(_FakeBlock()); registerFallbackValue(_FakeSudoku()); registerFallbackValue(_FakePuzzle()); + registerFallbackValue(_FakePlayer()); }); PuzzleBloc buildBloc() { return PuzzleBloc( apiClient: apiClient, puzzleRepository: repository, + authenticationRepository: authenticationRepository, + playerRepository: playerRepository, ); } diff --git a/test/repository/player_repository_test.dart b/test/repository/player_repository_test.dart new file mode 100644 index 0000000..9706481 --- /dev/null +++ b/test/repository/player_repository_test.dart @@ -0,0 +1,155 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:fake_cloud_firestore/fake_cloud_firestore.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:sudoku/cache/cache.dart'; +import 'package:sudoku/models/models.dart'; +import 'package:sudoku/repository/repository.dart'; + +import '../helpers/helpers.dart'; + +void main() { + group('PlayerRepository', () { + late FakeFirebaseFirestore firestore; + late CacheClient cacheClient; + late PlayerRepository playerRepository; + + late List ids; + late List players; + + setUp(() async { + firestore = FakeFirebaseFirestore(); + cacheClient = MockCacheClient(); + playerRepository = PlayerRepository( + firestore: firestore, + cacheClient: cacheClient, + ); + + players = [ + Player( + name: 'player-1', + easyAttempted: 1, + easySolved: 1, + mediumAttempted: 3, + mediumSolved: 2, + difficultAttempted: 1, + difficultSolved: 1, + expertAttempted: 7, + expertSolved: 2, + totalAttempted: 12, + totalSolved: 6, + ), + Player( + name: 'player-2', + easyAttempted: 1, + easySolved: 1, + mediumAttempted: 6, + mediumSolved: 3, + difficultAttempted: 1, + difficultSolved: 1, + expertAttempted: 7, + expertSolved: 3, + totalAttempted: 18, + totalSolved: 8, + ), + Player( + name: 'player-3', + easyAttempted: 3, + easySolved: 1, + mediumAttempted: 3, + mediumSolved: 2, + difficultAttempted: 1, + difficultSolved: 1, + expertAttempted: 4, + expertSolved: 2, + totalAttempted: 11, + totalSolved: 6, + ), + ]; + + ids = [ + 'userId-1', + 'userId-2', + 'userId-3', + ]; + + for (var i = 0; i < ids.length; i++) { + await firestore.doc('players/${ids[i]}').set( + players[i].toJson(), + ); + } + }); + + test('can be instantiated', () { + expect(playerRepository, isNotNull); + }); + + group('getPlayer', () { + test('returns empty player when userId is not present', () { + expect( + playerRepository.getPlayer('no-player'), + emits(Player.empty), + ); + }); + + test('returns player with the provided id', () { + expect( + playerRepository.getPlayer('userId-2'), + emits(players[1]), + ); + }); + + test( + 'writes the player info into cache when player is available', + () async { + await playerRepository.getPlayer(ids[2]).first; + verify( + () => cacheClient.write( + key: PlayerRepository.playerCacheKey, + value: players[2], + ), + ).called(1); + }, + ); + + test( + 'writes the player info into cache when player is unavailable', + () async { + await playerRepository.getPlayer('no-player').first; + verify( + () => cacheClient.write( + key: PlayerRepository.playerCacheKey, + value: Player.empty, + ), + ).called(1); + }, + ); + }); + + group('currentPlayer', () { + test('returns player from cache', () { + when( + () => cacheClient.read(key: PlayerRepository.playerCacheKey), + ).thenReturn(players[0]); + final player = playerRepository.currentPlayer; + expect(player, equals(players[0])); + }); + + test('returns empty player when cache is empty', () { + when( + () => cacheClient.read(key: PlayerRepository.playerCacheKey), + ).thenReturn(null); + final player = playerRepository.currentPlayer; + expect(player, equals(Player.empty)); + }); + }); + + group('updatePlayer', () { + test('works', () async { + await playerRepository.updatePlayer('userId-2', players[2]); + expect(playerRepository.getPlayer('userId-2'), emits(players[2])); + }); + }); + }); +}