diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..b9eeef8 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,13 @@ +name: Run Tests +on: [push, workflow_dispatch] +jobs: + drive: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: subosito/flutter-action@v2 + with: + channel: beta + cache: true + + - run: flutter test \ No newline at end of file diff --git a/.github/workflows/web.yml b/.github/workflows/web.yml index dd0fd4d..6b74560 100644 --- a/.github/workflows/web.yml +++ b/.github/workflows/web.yml @@ -20,6 +20,9 @@ jobs: channel: beta cache: true + - name: Run Tests + run: flutter test + - name: Install dependencies run: flutter pub get diff --git a/.idx/dev.nix b/.idx/dev.nix new file mode 100644 index 0000000..7051cf9 --- /dev/null +++ b/.idx/dev.nix @@ -0,0 +1,41 @@ +{pkgs}: { + channel = "stable-23.11"; + packages = [ + pkgs.nodePackages.firebase-tools + pkgs.jdk17 + pkgs.unzip + ]; + idx.extensions = [ + + ]; + idx.previews = { + previews = { + web = { + command = [ + "flutter" + "run" + "--machine" + "-d" + "web-server" + "--web-hostname" + "0.0.0.0" + "--web-port" + "$PORT" + ]; + manager = "flutter"; + }; + android = { + command = [ + "flutter" + "run" + "--machine" + "-d" + "android" + "-d" + "emulator-5554" + ]; + manager = "flutter"; + }; + }; + }; +} \ No newline at end of file diff --git a/analysis_options.yaml b/analysis_options.yaml index b8e7339..e19c4b6 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,27 +1,11 @@ -# This file configures the analyzer, which statically analyzes Dart code to -# check for errors, warnings, and lints. -# -# The issues identified by the analyzer are surfaced in the UI of Dart-enabled -# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be -# invoked from the command line by running `flutter analyze`. +# include: package:very_good_analysis/analysis_options.yaml +include: package:solid_lints/analysis_options.yaml -# The following line activates a set of recommended lints for Flutter apps, -# packages, and plugins designed to encourage good coding practices. -include: package:very_good_analysis/analysis_options.yaml linter: - # The lint rules applied to this project can be customized in the - # section below to disable rules from the `package:flutter_lints/flutter.yaml` - # included above or to enable additional rules. A list of all available lints - # and their documentation is published at https://dart.dev/lints. - # - # Instead of disabling a lint rule for the entire project in the - # section below, it can also be suppressed for a single line of code - # or a specific dart file by using the `// ignore: name_of_lint` and - # `// ignore_for_file: name_of_lint` syntax on the line or in the file - # producing the lint. rules: public_member_api_docs: false + sort_constructors_first: false use_setters_to_change_properties: false one_member_abstracts: false always_use_package_imports: false @@ -31,4 +15,5 @@ analyzer: plugins: - custom_lint exclude: - - 'lib/l10n/*.dart' + - member_ordering: false + - 'lib/l10n/*.dart' \ No newline at end of file diff --git a/coverage/lcov.info b/coverage/lcov.info new file mode 100644 index 0000000..90f9738 --- /dev/null +++ b/coverage/lcov.info @@ -0,0 +1,1355 @@ +SF:lib/constants.dart +DA:10,0 +LF:1 +LH:0 +end_of_record +SF:lib/core/providers/client_network.dart +DA:20,0 +DA:22,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:31,0 +DA:33,0 +DA:34,0 +DA:37,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:47,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:61,0 +DA:63,0 +DA:65,0 +DA:67,0 +DA:80,0 +DA:81,0 +DA:82,1 +DA:83,1 +DA:84,0 +DA:85,0 +DA:86,0 +DA:93,0 +DA:95,0 +DA:96,0 +DA:98,0 +DA:99,0 +DA:101,0 +DA:102,0 +DA:104,0 +DA:105,0 +DA:106,0 +DA:107,0 +DA:108,0 +DA:109,0 +DA:110,0 +DA:111,0 +DA:112,0 +LF:49 +LH:2 +end_of_record +SF:lib/core/providers/client_network.g.dart +DA:9,0 +DA:13,2 +DA:14,1 +LF:3 +LH:2 +end_of_record +SF:lib/common_widgets/app_back_button.dart +DA:7,6 +DA:9,0 +DA:11,0 +DA:15,0 +DA:16,0 +DA:17,0 +DA:20,0 +DA:21,0 +LF:8 +LH:1 +end_of_record +SF:lib/core/extensions/is_ios_or_macos_platform_extension.dart +DA:3,0 +DA:4,0 +DA:5,0 +LF:3 +LH:0 +end_of_record +SF:lib/core/extensions/theme_of_context_extension.dart +DA:8,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:14,0 +DA:17,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:50,0 +DA:53,0 +DA:54,0 +DA:55,0 +LF:35 +LH:0 +end_of_record +SF:lib/common_widgets/app_cupertino_button.dart +DA:10,0 +DA:23,0 +DA:46,0 +DA:48,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:64,0 +DA:65,0 +DA:66,0 +DA:68,0 +DA:69,0 +DA:70,0 +DA:71,0 +DA:72,0 +DA:73,0 +DA:74,0 +DA:76,0 +DA:77,0 +DA:78,0 +DA:80,0 +DA:82,0 +DA:100,0 +DA:105,0 +DA:106,0 +DA:107,0 +DA:108,0 +LF:30 +LH:0 +end_of_record +SF:lib/common_widgets/app_cupertino_sliver_navigation_bar.dart +DA:13,0 +DA:26,0 +DA:28,0 +DA:30,0 +DA:32,0 +DA:33,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:46,0 +DA:47,0 +DA:51,0 +DA:52,0 +DA:56,0 +DA:57,0 +DA:58,0 +LF:20 +LH:0 +end_of_record +SF:lib/core/extensions/js_bottom_padding_extension.dart +DA:1,0 +DA:2,0 +DA:3,0 +DA:4,0 +LF:4 +LH:0 +end_of_record +SF:lib/features/home/widgets/settings_modal_sheet.dart +DA:12,0 +DA:16,0 +DA:22,0 +DA:28,0 +DA:33,0 +DA:35,0 +DA:37,0 +DA:39,0 +DA:41,0 +DA:42,0 +DA:45,0 +DA:46,0 +DA:50,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:80,6 +DA:82,0 +DA:84,0 +DA:86,0 +DA:88,0 +DA:89,0 +DA:90,0 +DA:92,0 +DA:96,0 +DA:97,0 +DA:98,0 +DA:99,0 +DA:100,0 +DA:101,0 +DA:104,0 +DA:106,0 +DA:108,0 +DA:109,0 +DA:111,0 +DA:112,0 +DA:115,0 +LF:40 +LH:1 +end_of_record +SF:lib/utils/screen_size.dart +DA:8,0 +DA:9,0 +DA:10,0 +DA:12,0 +DA:13,0 +DA:14,0 +DA:15,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:24,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:36,0 +DA:37,0 +DA:52,0 +LF:19 +LH:0 +end_of_record +SF:lib/common_widgets/app_fade_in_image.dart +DA:18,0 +DA:37,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:48,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:57,0 +DA:58,0 +DA:59,0 +DA:65,0 +DA:82,0 +DA:84,0 +DA:85,0 +DA:86,0 +DA:88,0 +DA:89,0 +DA:90,0 +DA:93,0 +DA:94,0 +DA:99,0 +DA:100,0 +DA:104,0 +DA:105,0 +LF:33 +LH:0 +end_of_record +SF:lib/core/extensions/app_localization_extension.dart +DA:6,0 +LF:1 +LH:0 +end_of_record +SF:lib/utils/app_loggers.dart +DA:4,0 +DA:5,3 +DA:6,0 +DA:7,0 +DA:8,0 +DA:12,0 +DA:14,0 +DA:16,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:22,0 +DA:23,0 +DA:25,0 +DA:28,0 +DA:33,0 +DA:34,0 +DA:38,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:44,0 +DA:49,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:58,0 +LF:29 +LH:1 +end_of_record +SF:lib/common_widgets/app_web_padding.dart +DA:11,0 +DA:21,0 +DA:30,0 +DA:41,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:50,0 +DA:52,0 +DA:56,0 +DA:61,0 +DA:77,0 +DA:86,0 +DA:90,0 +DA:93,0 +DA:94,0 +DA:95,0 +DA:96,0 +DA:97,0 +DA:98,0 +DA:99,0 +DA:100,0 +DA:102,0 +DA:107,0 +LF:27 +LH:0 +end_of_record +SF:lib/l10n/app_localizations.dart +DA:65,0 +DA:69,0 +DA:70,0 +DA:636,6 +DA:638,0 +DA:640,0 +DA:643,0 +DA:644,0 +DA:646,0 +DA:650,0 +DA:654,0 +DA:655,0 +DA:656,0 +DA:657,0 +DA:658,0 +DA:661,0 +LF:16 +LH:1 +end_of_record +SF:lib/core/extensions/text_lines_extension.dart +DA:6,0 +DA:7,0 +DA:8,0 +DA:10,0 +DA:13,0 +DA:15,0 +DA:18,0 +DA:24,0 +DA:26,0 +LF:9 +LH:0 +end_of_record +SF:lib/core/theme/app_theme.dart +DA:9,0 +DA:31,0 +DA:32,0 +DA:34,0 +DA:35,0 +DA:38,0 +DA:57,0 +DA:58,0 +DA:61,0 +DA:62,0 +DA:63,0 +DA:66,0 +DA:69,0 +DA:72,0 +DA:73,0 +DA:74,0 +DA:75,0 +DA:76,0 +DA:77,0 +DA:78,0 +DA:80,0 +DA:81,0 +DA:83,0 +DA:98,0 +DA:106,0 +DA:113,0 +DA:120,0 +DA:125,0 +DA:126,0 +DA:127,0 +DA:128,0 +DA:129,0 +DA:130,0 +DA:134,0 +DA:136,0 +DA:140,0 +DA:141,0 +DA:142,0 +DA:143,0 +DA:144,0 +DA:145,0 +LF:41 +LH:0 +end_of_record +SF:lib/localization/language.dart +DA:12,0 +DA:13,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:33,1 +DA:34,1 +DA:35,1 +DA:36,1 +DA:37,1 +DA:40,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:47,1 +DA:48,2 +DA:50,4 +DA:55,0 +DA:56,0 +DA:57,0 +DA:58,0 +DA:59,0 +LF:33 +LH:8 +end_of_record +SF:lib/localization/language_app_controller.dart +DA:10,1 +DA:12,4 +DA:13,1 +DA:14,4 +DA:16,1 +DA:17,3 +DA:18,1 +DA:22,1 +DA:25,4 +DA:26,1 +DA:27,1 +DA:29,2 +DA:31,2 +LF:13 +LH:13 +end_of_record +SF:lib/localization/language_app_controller.g.dart +DA:9,0 +DA:14,2 +DA:15,1 +LF:3 +LH:2 +end_of_record +SF:lib/core/providers/client_network/network_client_adapter.dart +DA:5,0 +DA:6,0 +DA:7,0 +DA:8,0 +DA:9,0 +DA:15,0 +DA:16,0 +LF:7 +LH:0 +end_of_record +SF:lib/core/providers/prefs.g.dart +DA:9,1 +DA:13,9 +LF:2 +LH:2 +end_of_record +SF:lib/core/providers/prefs.dart +DA:6,1 +DA:8,1 +LF:2 +LH:2 +end_of_record +SF:lib/core/theme/theme_mode_controller.dart +DA:11,1 +DA:14,4 +DA:15,1 +DA:18,1 +DA:19,1 +DA:23,0 +DA:29,1 +DA:30,1 +DA:31,1 +DA:32,4 +DA:33,1 +DA:34,1 +DA:35,3 +DA:36,1 +DA:37,1 +DA:38,1 +DA:39,4 +DA:40,1 +DA:41,1 +DA:42,1 +DA:43,4 +DA:44,1 +DA:45,1 +DA:49,1 +DA:50,4 +DA:52,1 +DA:53,1 +DA:54,1 +DA:55,1 +DA:56,1 +DA:57,1 +DA:58,1 +DA:59,1 +DA:60,1 +DA:67,1 +DA:70,5 +DA:73,3 +DA:79,1 +DA:80,4 +DA:81,2 +DA:82,1 +DA:83,2 +DA:85,1 +LF:43 +LH:42 +end_of_record +SF:lib/core/theme/theme_mode_controller.g.dart +DA:9,0 +DA:14,2 +DA:15,1 +DA:26,1 +DA:30,2 +DA:31,1 +LF:6 +LH:5 +end_of_record +SF:lib/features/home/widgets/settings_theme_section.dart +DA:9,6 +DA:11,0 +DA:13,0 +DA:14,0 +DA:16,0 +DA:17,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:23,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:33,0 +DA:35,0 +DA:36,0 +DA:38,0 +DA:39,0 +DA:41,0 +DA:43,0 +DA:45,0 +DA:47,0 +DA:49,0 +DA:54,0 +DA:55,0 +DA:57,0 +DA:58,0 +DA:61,0 +DA:63,0 +DA:64,0 +DA:65,0 +DA:68,0 +DA:69,0 +DA:71,0 +DA:73,0 +DA:75,0 +DA:76,0 +DA:77,0 +LF:39 +LH:1 +end_of_record +SF:lib/features/instruments/details/instrument_details_controller.g.dart +DA:9,0 +DA:14,0 +DA:16,0 +DA:18,0 +DA:20,0 +DA:21,0 +DA:24,0 +DA:26,0 +DA:28,0 +DA:29,0 +DA:49,6 +DA:55,0 +DA:58,0 +DA:62,0 +DA:66,0 +DA:69,0 +DA:74,0 +DA:79,0 +DA:80,0 +DA:85,0 +DA:86,0 +DA:91,0 +DA:99,0 +DA:103,0 +DA:112,0 +DA:114,0 +DA:115,0 +DA:128,0 +DA:136,0 +DA:140,0 +DA:144,0 +DA:145,0 +DA:149,0 +DA:151,0 +DA:153,0 +DA:154,0 +DA:155,0 +DA:160,0 +DA:165,0 +DA:167,0 +DA:170,0 +DA:173,0 +DA:176,0 +DA:179,0 +DA:180,0 +DA:181,0 +DA:182,0 +DA:183,0 +DA:184,0 +DA:185,0 +DA:186,0 +DA:190,0 +DA:192,0 +DA:195,0 +DA:197,0 +DA:198,0 +DA:200,0 +DA:213,0 +DA:215,0 +DA:216,0 +LF:60 +LH:1 +end_of_record +SF:lib/features/instruments/details/instrument_details_controller.dart +DA:9,0 +DA:11,0 +DA:12,0 +DA:13,0 +DA:14,0 +DA:16,0 +LF:6 +LH:0 +end_of_record +SF:lib/features/instruments/instrument.dart +DA:21,0 +LF:1 +LH:0 +end_of_record +SF:lib/features/instruments/instruments_tab_controller.dart +DA:11,0 +DA:13,0 +DA:16,0 +DA:17,0 +DA:18,0 +DA:19,0 +DA:20,0 +LF:7 +LH:0 +end_of_record +SF:lib/features/instruments/instruments_tab_controller.g.dart +DA:9,0 +DA:14,0 +LF:2 +LH:0 +end_of_record +SF:lib/features/instruments/details/instrument_details_page.dart +DA:19,0 +DA:25,0 +DA:27,0 +DA:35,0 +DA:37,0 +DA:38,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:52,0 +DA:54,0 +DA:56,0 +DA:63,0 +DA:64,0 +DA:65,0 +DA:66,0 +DA:67,0 +DA:68,0 +DA:72,0 +DA:74,0 +DA:76,0 +DA:77,0 +DA:84,0 +DA:86,0 +DA:87,0 +DA:88,0 +DA:92,0 +DA:93,0 +DA:101,0 +DA:102,0 +DA:103,0 +DA:110,0 +DA:112,0 +DA:114,0 +DA:115,0 +DA:116,0 +DA:117,0 +DA:120,0 +DA:121,0 +DA:122,0 +DA:123,0 +DA:125,0 +DA:126,0 +DA:127,0 +DA:129,0 +DA:130,0 +DA:148,0 +DA:153,0 +LF:54 +LH:0 +end_of_record +SF:lib/features/instruments/details/widgets/instrument_details_summary.dart +DA:5,0 +DA:12,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:17,0 +DA:19,0 +DA:20,0 +DA:22,0 +DA:23,0 +DA:24,0 +LF:11 +LH:0 +end_of_record +SF:lib/features/instruments/details/widgets/instrument_header_images.dart +DA:12,0 +DA:22,0 +DA:24,0 +DA:26,0 +DA:28,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:41,0 +DA:42,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:53,0 +DA:57,0 +DA:58,0 +DA:59,0 +DA:61,0 +DA:62,0 +DA:74,0 +DA:75,0 +DA:77,0 +DA:79,0 +DA:80,0 +DA:82,0 +DA:83,0 +DA:84,0 +DA:85,0 +DA:93,0 +DA:97,0 +DA:99,0 +DA:100,0 +DA:111,0 +DA:112,0 +DA:114,0 +DA:115,0 +DA:116,0 +DA:117,0 +DA:118,0 +DA:119,0 +DA:120,0 +DA:121,0 +DA:122,0 +DA:141,0 +DA:146,0 +DA:147,0 +DA:148,0 +DA:149,0 +DA:154,0 +LF:55 +LH:0 +end_of_record +SF:lib/utils/immutable_list.dart +DA:8,0 +DA:10,0 +DA:12,0 +DA:14,0 +DA:15,0 +LF:5 +LH:0 +end_of_record +SF:lib/features/instruments/instruments_repo.dart +DA:10,0 +DA:12,0 +DA:24,0 +DA:26,0 +DA:29,0 +DA:31,0 +DA:32,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:39,0 +DA:43,0 +DA:46,0 +DA:47,0 +DA:48,0 +DA:51,0 +DA:53,0 +LF:17 +LH:0 +end_of_record +SF:lib/features/instruments/instruments_repo.g.dart +DA:9,0 +DA:13,3 +LF:2 +LH:1 +end_of_record +SF:lib/l10n/app_localizations_en.dart +DA:5,0 +DA:7,0 +DA:10,0 +DA:13,0 +DA:16,0 +DA:19,0 +DA:22,0 +DA:25,0 +DA:28,0 +DA:31,0 +DA:34,0 +DA:37,0 +DA:40,0 +DA:43,0 +DA:46,0 +DA:49,0 +DA:52,0 +DA:55,0 +DA:58,0 +DA:61,0 +DA:64,0 +DA:67,0 +DA:70,0 +DA:73,0 +DA:76,0 +DA:79,0 +DA:82,0 +DA:85,0 +DA:88,0 +DA:91,0 +DA:94,0 +DA:97,0 +DA:100,0 +DA:103,0 +DA:106,0 +DA:109,0 +DA:112,0 +DA:115,0 +DA:118,0 +DA:121,0 +DA:124,0 +DA:127,0 +DA:130,0 +DA:133,0 +DA:136,0 +DA:139,0 +DA:142,0 +DA:145,0 +DA:148,0 +DA:151,0 +DA:154,0 +DA:157,0 +DA:160,0 +DA:163,0 +DA:166,0 +DA:169,0 +DA:172,0 +DA:175,0 +DA:178,0 +DA:181,0 +DA:184,0 +DA:187,0 +DA:190,0 +DA:193,0 +DA:196,0 +DA:199,0 +DA:202,0 +DA:205,0 +DA:208,0 +DA:211,0 +DA:214,0 +DA:217,0 +DA:220,0 +DA:223,0 +DA:226,0 +DA:229,0 +DA:232,0 +DA:235,0 +DA:238,0 +DA:241,0 +DA:244,0 +DA:247,0 +DA:250,0 +DA:253,0 +DA:256,0 +DA:259,0 +DA:262,0 +DA:265,0 +DA:268,0 +DA:271,0 +LF:90 +LH:0 +end_of_record +SF:lib/l10n/app_localizations_es.dart +DA:5,0 +DA:7,0 +DA:10,0 +DA:13,0 +DA:16,0 +DA:19,0 +DA:22,0 +DA:25,0 +DA:28,0 +DA:31,0 +DA:34,0 +DA:37,0 +DA:40,0 +DA:43,0 +DA:46,0 +DA:49,0 +DA:52,0 +DA:55,0 +DA:58,0 +DA:61,0 +DA:64,0 +DA:67,0 +DA:70,0 +DA:73,0 +DA:76,0 +DA:79,0 +DA:82,0 +DA:85,0 +DA:88,0 +DA:91,0 +DA:94,0 +DA:97,0 +DA:100,0 +DA:103,0 +DA:106,0 +DA:109,0 +DA:112,0 +DA:115,0 +DA:118,0 +DA:121,0 +DA:124,0 +DA:127,0 +DA:130,0 +DA:133,0 +DA:136,0 +DA:139,0 +DA:142,0 +DA:145,0 +DA:148,0 +DA:151,0 +DA:154,0 +DA:157,0 +DA:160,0 +DA:163,0 +DA:166,0 +DA:169,0 +DA:172,0 +DA:175,0 +DA:178,0 +DA:181,0 +DA:184,0 +DA:187,0 +DA:190,0 +DA:193,0 +DA:196,0 +DA:199,0 +DA:202,0 +DA:205,0 +DA:208,0 +DA:211,0 +DA:214,0 +DA:217,0 +DA:220,0 +DA:223,0 +DA:226,0 +DA:229,0 +DA:232,0 +DA:235,0 +DA:238,0 +DA:241,0 +DA:244,0 +DA:247,0 +DA:250,0 +DA:253,0 +DA:256,0 +DA:259,0 +DA:262,0 +DA:265,0 +DA:268,0 +DA:271,0 +LF:90 +LH:0 +end_of_record +SF:lib/l10n/app_localizations_ja.dart +DA:5,0 +DA:7,0 +DA:10,0 +DA:13,0 +DA:16,0 +DA:19,0 +DA:22,0 +DA:25,0 +DA:28,0 +DA:31,0 +DA:34,0 +DA:37,0 +DA:40,0 +DA:43,0 +DA:46,0 +DA:49,0 +DA:52,0 +DA:55,0 +DA:58,0 +DA:61,0 +DA:64,0 +DA:67,0 +DA:70,0 +DA:73,0 +DA:76,0 +DA:79,0 +DA:82,0 +DA:85,0 +DA:88,0 +DA:91,0 +DA:94,0 +DA:97,0 +DA:100,0 +DA:103,0 +DA:106,0 +DA:109,0 +DA:112,0 +DA:115,0 +DA:118,0 +DA:121,0 +DA:124,0 +DA:127,0 +DA:130,0 +DA:133,0 +DA:136,0 +DA:139,0 +DA:142,0 +DA:145,0 +DA:148,0 +DA:151,0 +DA:154,0 +DA:157,0 +DA:160,0 +DA:163,0 +DA:166,0 +DA:169,0 +DA:172,0 +DA:175,0 +DA:178,0 +DA:181,0 +DA:184,0 +DA:187,0 +DA:190,0 +DA:193,0 +DA:196,0 +DA:199,0 +DA:202,0 +DA:205,0 +DA:208,0 +DA:211,0 +DA:214,0 +DA:217,0 +DA:220,0 +DA:223,0 +DA:226,0 +DA:229,0 +DA:232,0 +DA:235,0 +DA:238,0 +DA:241,0 +DA:244,0 +DA:247,0 +DA:250,0 +DA:253,0 +DA:256,0 +DA:259,0 +DA:262,0 +DA:265,0 +DA:268,0 +DA:271,0 +LF:90 +LH:0 +end_of_record +SF:lib/l10n/app_localizations_pt.dart +DA:5,0 +DA:7,0 +DA:10,0 +DA:13,0 +DA:16,0 +DA:19,0 +DA:22,0 +DA:25,0 +DA:28,0 +DA:31,0 +DA:34,0 +DA:37,0 +DA:40,0 +DA:43,0 +DA:46,0 +DA:49,0 +DA:52,0 +DA:55,0 +DA:58,0 +DA:61,0 +DA:64,0 +DA:67,0 +DA:70,0 +DA:73,0 +DA:76,0 +DA:79,0 +DA:82,0 +DA:85,0 +DA:88,0 +DA:91,0 +DA:94,0 +DA:97,0 +DA:100,0 +DA:103,0 +DA:106,0 +DA:109,0 +DA:112,0 +DA:115,0 +DA:118,0 +DA:121,0 +DA:124,0 +DA:127,0 +DA:130,0 +DA:133,0 +DA:136,0 +DA:139,0 +DA:142,0 +DA:145,0 +DA:148,0 +DA:151,0 +DA:154,0 +DA:157,0 +DA:160,0 +DA:163,0 +DA:166,0 +DA:169,0 +DA:172,0 +DA:175,0 +DA:178,0 +DA:181,0 +DA:184,0 +DA:187,0 +DA:190,0 +DA:193,0 +DA:196,0 +DA:199,0 +DA:202,0 +DA:205,0 +DA:208,0 +DA:211,0 +DA:214,0 +DA:217,0 +DA:220,0 +DA:223,0 +DA:226,0 +DA:229,0 +DA:232,0 +DA:235,0 +DA:238,0 +DA:241,0 +DA:244,0 +DA:247,0 +DA:250,0 +DA:253,0 +DA:256,0 +DA:259,0 +DA:262,0 +DA:265,0 +DA:268,0 +DA:271,0 +LF:90 +LH:0 +end_of_record +SF:lib/features/schools/school.dart +DA:44,0 +DA:119,0 +DA:120,0 +DA:122,0 +DA:123,0 +DA:124,0 +DA:129,2 +DA:131,0 +DA:133,0 +DA:135,0 +DA:136,0 +DA:138,0 +DA:139,0 +DA:140,0 +DA:141,0 +DA:144,0 +LF:16 +LH:1 +end_of_record +SF:lib/features/schools/schools_repo.dart +DA:9,0 +DA:11,0 +DA:26,0 +DA:28,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:46,0 +DA:48,0 +DA:49,0 +DA:50,0 +DA:53,0 +LF:16 +LH:0 +end_of_record +SF:lib/features/schools/schools_repo.g.dart +DA:9,0 +DA:13,3 +LF:2 +LH:1 +end_of_record +SF:lib/features/schools/school_color_hook.dart +DA:8,2 +DA:10,0 +DA:12,0 +DA:13,0 +DA:14,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:58,0 +DA:59,0 +LF:41 +LH:1 +end_of_record diff --git a/lib/common_widgets/app_animated_linear_gradient.dart b/lib/common_widgets/app_animated_linear_gradient.dart index 49bb3b4..e264832 100644 --- a/lib/common_widgets/app_animated_linear_gradient.dart +++ b/lib/common_widgets/app_animated_linear_gradient.dart @@ -22,28 +22,21 @@ class AppAnimatedLinearGradient extends StatefulWidget { class _AppAnimatedLinearGradientState extends State with SingleTickerProviderStateMixin { - late AnimationController _controller; - late Animation _animation; + late final _controller = + AnimationController(duration: widget.duration, vsync: this); + late final _animation = Tween(begin: 0, end: 1).animate(_controller); @override void initState() { super.initState(); - _controller = AnimationController(duration: widget.duration, vsync: this) - ..repeat(reverse: true); - _animation = Tween(begin: 0, end: 1).animate(_controller); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); + _controller.repeat(reverse: true); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _animation, - builder: (context, child) { + builder: (_, __) { return CustomPaint( painter: LinearGradientPainter( colors: widget.colors, @@ -54,19 +47,29 @@ class _AppAnimatedLinearGradientState extends State }, ); } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } } class LinearGradientPainter extends CustomPainter { + final List colors; + final double percent; + final Alignment begin; + final Alignment end; + + // Double the width to allow for a continuous flow + static const widthMultiplier = 2; + LinearGradientPainter({ required this.colors, required this.percent, this.begin = Alignment.topLeft, this.end = Alignment.bottomRight, }); - final List colors; - final double percent; - final Alignment begin; - final Alignment end; @override void paint(Canvas canvas, Size size) { @@ -81,7 +84,7 @@ class LinearGradientPainter extends CustomPainter { final shaderRect = Rect.fromLTWH( -size.width * percent, // Shift the left boundary of the gradient 0, - size.width * 2, // Double the width to allow for a continuous flow + size.width * widthMultiplier, size.height, ); diff --git a/lib/common_widgets/app_animation_wrapper.dart b/lib/common_widgets/app_animation_wrapper.dart index 72b7d7c..1bb54c0 100644 --- a/lib/common_widgets/app_animation_wrapper.dart +++ b/lib/common_widgets/app_animation_wrapper.dart @@ -5,10 +5,15 @@ class AppAnimationWrapper extends StatefulWidget { required this.child, super.key, this.keepAlive = false, + this.duration = const Duration(milliseconds: 350), }); final bool keepAlive; final Widget child; + final Duration duration; + + static const start = 0.6; + static const end = 1.0; @override State createState() => _AppAnimationWrapperState(); @@ -16,21 +21,27 @@ class AppAnimationWrapper extends StatefulWidget { class _AppAnimationWrapperState extends State with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin { - late final AnimationController _controller; - late final Animation _animation; + late final AnimationController _controller = AnimationController( + vsync: this, + duration: widget.duration, + )..forward(); + late final Animation _animation = Tween( + begin: AppAnimationWrapper.start, + end: AppAnimationWrapper.end, + ).animate( + CurvedAnimation( + parent: _controller, + curve: Curves.easeInOut, + ), + ); @override - void initState() { - super.initState(); - _controller = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 350), - )..forward(); - _animation = Tween(begin: 0.6, end: 1).animate( - CurvedAnimation( - parent: _controller, - curve: Curves.easeInOut, - ), + Widget build(BuildContext context) { + super.build(context); + + return ScaleTransition( + scale: _animation, + child: widget.child, ); } @@ -40,15 +51,6 @@ class _AppAnimationWrapperState extends State super.dispose(); } - @override - Widget build(BuildContext context) { - super.build(context); - return ScaleTransition( - scale: _animation, - child: widget.child, - ); - } - @override bool get wantKeepAlive => widget.keepAlive; } diff --git a/lib/common_widgets/app_async_widget.dart b/lib/common_widgets/app_async_widget.dart index 9b0a8a8..8deec35 100644 --- a/lib/common_widgets/app_async_widget.dart +++ b/lib/common_widgets/app_async_widget.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sliver_tools/sliver_tools.dart'; -import '../extensions/app_localization_extension.dart'; +import '../core/extensions/app_localization_extension.dart'; import 'app_cupertino_button.dart'; import 'app_infinite_rotation_animation.dart'; @@ -77,6 +77,7 @@ class _AppAsyncSliverWidgetState extends State> { @override Widget build(BuildContext context) { final isRefreshing = widget.asyncValue.isRefreshing; + return SliverAnimatedSwitcher( duration: kThemeAnimationDuration, child: switch (widget.asyncValue) { @@ -129,7 +130,7 @@ class _AppAsyncSliverWidgetState extends State> { Future errorRetry() async { setState(() => isLoading = true); - widget.onErrorRetry!(); + widget.onErrorRetry?.call(); await Future.delayed(const Duration(milliseconds: 700)); setState(() => isLoading = false); } diff --git a/lib/common_widgets/app_back_button.dart b/lib/common_widgets/app_back_button.dart index bfa1ba6..ac8a4c1 100644 --- a/lib/common_widgets/app_back_button.dart +++ b/lib/common_widgets/app_back_button.dart @@ -1,7 +1,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import '../extensions/is_ios_or_macos_platform_extension.dart'; -import '../extensions/theme_of_context_extension.dart'; +import '../core/extensions/is_ios_or_macos_platform_extension.dart'; +import '../core/extensions/theme_of_context_extension.dart'; class AppBackButton extends StatelessWidget { const AppBackButton({super.key}); diff --git a/lib/common_widgets/app_cupertino_button.dart b/lib/common_widgets/app_cupertino_button.dart index e840c4a..29f26d4 100644 --- a/lib/common_widgets/app_cupertino_button.dart +++ b/lib/common_widgets/app_cupertino_button.dart @@ -2,17 +2,10 @@ import 'package:flex_color_scheme/flex_color_scheme.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import '../extensions/theme_of_context_extension.dart'; +import '../core/extensions/theme_of_context_extension.dart'; ///extended CupertinoButton to pass null values in the minimumSize and padding -enum CupertinoButtonType { - plain, - gray, - tinted, - filled, -} - class AppCupertinoButton extends StatelessWidget { const AppCupertinoButton({ required this.child, @@ -24,7 +17,7 @@ class AppCupertinoButton extends StatelessWidget { this.padding = EdgeInsets.zero, this.minSize, this.borderRadius = const BorderRadius.all(Radius.circular(32)), - this.type, + this.type = CupertinoButtonType.plain, }); const AppCupertinoButton.tinted({ @@ -48,11 +41,14 @@ class AppCupertinoButton extends StatelessWidget { final Color? color; final Color? disabledColor; final BorderRadius borderRadius; - final CupertinoButtonType? type; + final CupertinoButtonType type; @override Widget build(BuildContext context) { - final typeSize = type == null ? 0.0 : kMinInteractiveDimensionCupertino; + final typeSize = type == CupertinoButtonType.plain + ? 0.0 + : kMinInteractiveDimensionCupertino; + return Theme( data: Theme.of(context).copyWith( cupertinoOverrideTheme: CupertinoThemeData( @@ -74,7 +70,7 @@ class AppCupertinoButton extends StatelessWidget { padding: padding, minSize: minSize ?? typeSize, borderRadius: borderRadius, - color: _calculateColor(context), + color: type.calculateColor(color, context), child: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -91,12 +87,24 @@ class AppCupertinoButton extends StatelessWidget { ), ); } +} + +enum CupertinoButtonType { + plain, + gray, + tinted, + filled; + + static const tintedOpacity = 0.2; - Color? _calculateColor(BuildContext context) { - return switch (type) { - CupertinoButtonType.plain || null => null, + Color? calculateColor( + Color? color, + BuildContext context, + ) { + return switch (this) { + CupertinoButtonType.plain => null, CupertinoButtonType.gray => CupertinoColors.systemFill, - CupertinoButtonType.tinted => color!.withOpacity(0.2), + CupertinoButtonType.tinted => color?.withOpacity(tintedOpacity), CupertinoButtonType.filled => context.colorScheme.primary.darken() }; } diff --git a/lib/common_widgets/app_cupertino_sliver_navigation_bar.dart b/lib/common_widgets/app_cupertino_sliver_navigation_bar.dart index cf146f7..f5242da 100644 --- a/lib/common_widgets/app_cupertino_sliver_navigation_bar.dart +++ b/lib/common_widgets/app_cupertino_sliver_navigation_bar.dart @@ -2,10 +2,10 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import '../extensions/is_ios_or_macos_platform_extension.dart'; -import '../extensions/js_bottom_padding_extension.dart' - if (dart.library.js_interop) '../extensions/js_bottom_padding_extension_web.dart'; -import '../extensions/theme_of_context_extension.dart'; +import '../core/extensions/is_ios_or_macos_platform_extension.dart'; +import '../core/extensions/js_bottom_padding_extension.dart' + if (dart.library.js_interop) '../core/extensions/js_bottom_padding_extension_web.dart'; +import '../core/extensions/theme_of_context_extension.dart'; import '../features/home/widgets/settings_modal_sheet.dart'; import '../utils/screen_size.dart'; @@ -26,6 +26,7 @@ class AppCupertinoSliverNavigationBar extends StatelessWidget { @override Widget build(BuildContext context) { final screenSize = context.screenSize; + return CupertinoSliverNavigationBar( backgroundColor: Colors.transparent, largeTitle: Text( @@ -38,7 +39,7 @@ class AppCupertinoSliverNavigationBar extends StatelessWidget { transitionBetweenRoutes: transitionBetweenRoutes, border: Border( bottom: BorderSide( - color: context.customColors.inverseTextColor!.withOpacity(0.1), + color: context.customInverseTextColor.withOpacity(0.1), ), ), padding: kIsWeb diff --git a/lib/common_widgets/app_fade_in_image.dart b/lib/common_widgets/app_fade_in_image.dart index db036eb..6a637d0 100644 --- a/lib/common_widgets/app_fade_in_image.dart +++ b/lib/common_widgets/app_fade_in_image.dart @@ -4,9 +4,9 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:transparent_image/transparent_image.dart'; -import '../extensions/app_localization_extension.dart'; -import '../extensions/theme_of_context_extension.dart'; -import '../utils/main_logger.dart'; +import '../core/extensions/app_localization_extension.dart'; +import '../core/extensions/theme_of_context_extension.dart'; +import '../utils/app_loggers.dart'; typedef ImageErrorWidgetBuilder = Widget Function( BuildContext context, @@ -44,6 +44,7 @@ class AppFadeInImage extends StatelessWidget { imageErrorBuilder: imageErrorBuilder ?? (context, error, stackTrace) { logViews.info('Error loading image:', error, stackTrace); + return AppErrorImageBuilder( height: height, textColor: context.colorScheme.onErrorContainer, diff --git a/lib/common_widgets/app_image.dart b/lib/common_widgets/app_image.dart deleted file mode 100644 index a008711..0000000 --- a/lib/common_widgets/app_image.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:extended_image/extended_image.dart'; -import 'package:flutter/material.dart'; - -class AppImageNetwork extends StatelessWidget { - const AppImageNetwork( - this.imageUrl, { - super.key, - }); - final String imageUrl; - - @override - Widget build(BuildContext context) { - return Image( - fit: BoxFit.cover, - image: ExtendedNetworkImageProvider( - imageUrl, - cache: true, - ), - ); - } -} diff --git a/lib/common_widgets/app_infinite_rotation_animation.dart b/lib/common_widgets/app_infinite_rotation_animation.dart index d720711..86018f5 100644 --- a/lib/common_widgets/app_infinite_rotation_animation.dart +++ b/lib/common_widgets/app_infinite_rotation_animation.dart @@ -19,14 +19,16 @@ class AppInfiniteRotationAnimation extends StatefulWidget { class _AppInfiniteRotationAnimationState extends State with SingleTickerProviderStateMixin { - late AnimationController _controller; + late final _controller = AnimationController( + duration: widget.duration, + vsync: this, + ); @override - void initState() { - super.initState(); - _controller = AnimationController( - duration: widget.duration, - vsync: this, + Widget build(BuildContext context) { + return RotationTransition( + turns: _controller, + child: widget.child, ); } @@ -47,12 +49,4 @@ class _AppInfiniteRotationAnimationState _controller.dispose(); super.dispose(); } - - @override - Widget build(BuildContext context) { - return RotationTransition( - turns: _controller, - child: widget.child, - ); - } } diff --git a/lib/common_widgets/app_loading_indicator.dart b/lib/common_widgets/app_loading_indicator.dart index 188c040..56975b6 100644 --- a/lib/common_widgets/app_loading_indicator.dart +++ b/lib/common_widgets/app_loading_indicator.dart @@ -34,6 +34,7 @@ class AppLoadingIndicator extends StatelessWidget { ), ); } + return AnimatedSize( duration: const Duration(milliseconds: 300), child: !showLoading diff --git a/lib/common_widgets/app_web_padding.dart b/lib/common_widgets/app_web_padding.dart index 1ce47b0..1eafb27 100644 --- a/lib/common_widgets/app_web_padding.dart +++ b/lib/common_widgets/app_web_padding.dart @@ -2,8 +2,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import '../extensions/js_bottom_padding_extension.dart' - if (dart.library.js_interop) '../extensions/js_bottom_padding_extension_web.dart'; +import '../core/extensions/js_bottom_padding_extension.dart' + if (dart.library.js_interop) '../core/extensions/js_bottom_padding_extension.dart'; /// Widget that calls the calls to JS to get the insets of web /// https://github.com/flutter/flutter/issues/84833#issuecomment-1679737846 @@ -41,29 +41,18 @@ class AppWebPadding extends StatelessWidget { @override Widget build(BuildContext context) { if (kIsWeb) { - return color != null - ? ColoredBox( - color: color!, - child: Padding( - padding: EdgeInsets.only( - bottom: bottom ? bottomInset() : 0, - top: top ? topInset() : 0, - left: left ? leftInset() : 0, - right: right ? rightInset() : 0, - ), - child: child, - ), - ) - : Padding( - padding: EdgeInsets.only( - bottom: bottom ? bottomInset() : 0, - top: top ? topInset() : 0, - left: left ? leftInset() : 0, - right: right ? rightInset() : 0, - ), - child: child, - ); + return Container( + color: color, + padding: EdgeInsets.only( + bottom: bottom ? bottomInset() : 0, + top: top ? topInset() : 0, + left: left ? leftInset() : 0, + right: right ? rightInset() : 0, + ), + child: child, + ); } + return child; } } @@ -101,29 +90,20 @@ class WebPaddingSliver extends StatelessWidget { @override Widget build(BuildContext context) { if (kIsWeb) { - return color != null - ? DecoratedSliver( - decoration: BoxDecoration(color: color), - sliver: SliverPadding( - padding: EdgeInsets.only( - bottom: bottom ? bottomInset() : 0, - top: top ? topInset() : 0, - left: left ? leftInset() : 0, - right: right ? rightInset() : 0, - ), - sliver: sliver, - ), - ) - : SliverPadding( - padding: EdgeInsets.only( - bottom: bottom ? bottomInset() : 0, - top: top ? topInset() : 0, - left: left ? leftInset() : 0, - right: right ? rightInset() : 0, - ), - sliver: sliver, - ); + DecoratedSliver( + decoration: BoxDecoration(color: color), + sliver: SliverPadding( + padding: EdgeInsets.only( + bottom: bottom ? bottomInset() : 0, + top: top ? topInset() : 0, + left: left ? leftInset() : 0, + right: right ? rightInset() : 0, + ), + sliver: sliver, + ), + ); } + return sliver; } } diff --git a/lib/common_widgets/cupertino_sheet.dart b/lib/common_widgets/cupertino_sheet_route.dart similarity index 98% rename from lib/common_widgets/cupertino_sheet.dart rename to lib/common_widgets/cupertino_sheet_route.dart index 94205ed..a79451d 100644 --- a/lib/common_widgets/cupertino_sheet.dart +++ b/lib/common_widgets/cupertino_sheet_route.dart @@ -1,3 +1,5 @@ +// ignore_for_file: avoid_non_null_assertion, avoid_returning_widgets, +// ignore_for_file: avoid_unnecessary_type_assertions // Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -62,6 +64,7 @@ class _CupertinoSheetDecorationBuilder extends StatelessWidget { @override Widget build(BuildContext context) { final topPadding = MediaQuery.paddingOf(context).top; + return SafeArea( bottom: false, minimum: EdgeInsets.only(top: topPadding + _kPreviousRouteVisibleOffset), @@ -103,6 +106,14 @@ class _CupertinoSheetDecorationBuilder extends StatelessWidget { /// /// * [AppCupertinoSheetPage], which is the [Page] version of this class class CupertinoSheetRoute extends SheetRoute { + final SheetController _sheetController = SheetController(); + + @override + Color? get barrierColor => Colors.transparent; + + @override + bool get barrierDismissible => true; + CupertinoSheetRoute({ required WidgetBuilder builder, super.stops, @@ -113,7 +124,7 @@ class CupertinoSheetRoute extends SheetRoute { super.draggable = true, super.fit, }) : super( - builder: (BuildContext context) { + builder: (_) { return _CupertinoSheetDecorationBuilder( backgroundColor: backgroundColor, topRadius: _kCupertinoSheetTopRadius, @@ -124,19 +135,11 @@ class CupertinoSheetRoute extends SheetRoute { initialExtent: initialStop, ); - final SheetController _sheetController = SheetController(); - @override SheetController createSheetController() { return _sheetController; } - @override - Color? get barrierColor => Colors.transparent; - - @override - bool get barrierDismissible => true; - @override Widget buildSheet(BuildContext context, Widget child) { SheetPhysics? effectivePhysics = BouncingSheetPhysics( @@ -152,6 +155,7 @@ class CupertinoSheetRoute extends SheetRoute { final topPadding = MediaQuery.paddingOf(context).top; final topMargin = math.max(_kSheetMinimalOffset, topPadding) + _kPreviousRouteVisibleOffset; + return Sheet.raw( initialExtent: initialExtent, decorationBuilder: decorationBuilder, @@ -172,18 +176,20 @@ class CupertinoSheetRoute extends SheetRoute { ) { final topPadding = MediaQuery.paddingOf(context).top; final double topOffset = math.max(_kSheetMinimalOffset, topPadding); + return AnimatedBuilder( animation: secondaryAnimation, child: CupertinoUserInterfaceLevel( data: CupertinoUserInterfaceLevelData.elevated, child: child, ), - builder: (BuildContext context, Widget? child) { + builder: (_, Widget? child) { final progress = secondaryAnimation.value; final scale = 1 - progress / 10; final distanceWithScale = (topOffset + _kPreviousRouteVisibleOffset) * 0.9; final offset = Offset(0, progress * (topOffset - distanceWithScale)); + return Transform.translate( offset: offset, child: Transform.scale( @@ -252,6 +258,7 @@ class CupertinoSheetBottomRouteTransition extends StatelessWidget { // Round corners for iPhone devices from X to the newest version final isRoundedDevice = defaultTargetPlatform == TargetPlatform.iOS && topPadding > _kRoundedDeviceStatusBarHeight; + return isRoundedDevice ? _kRoundedDeviceRadius : Radius.zero; } @@ -277,6 +284,7 @@ class CupertinoSheetBottomRouteTransition extends StatelessWidget { final radius = progress == 0 ? Radius.zero : Radius.lerp(deviceCorner, _kCupertinoSheetTopRadius, progress)!; + return Stack( children: [ Container(color: CupertinoColors.black), @@ -323,6 +331,14 @@ class CupertinoSheetBottomRouteTransition extends StatelessWidget { /// /// * [CupertinoSheetRoute], which is the [PageRoute] version of this class class AppCupertinoSheetPage extends Page { + /// The content to be shown in the [Route] created by this page. + final Widget child; + + /// {@macro flutter.widgets.modalRoute.maintainState} + final bool maintainState; + + final SheetFit fit; + /// Creates a material page. const AppCupertinoSheetPage({ required this.child, @@ -333,14 +349,6 @@ class AppCupertinoSheetPage extends Page { super.arguments, }); - /// The content to be shown in the [Route] created by this page. - final Widget child; - - /// {@macro flutter.widgets.modalRoute.maintainState} - final bool maintainState; - - final SheetFit fit; - @override Route createRoute(BuildContext context) { return _PageBasedCupertinoSheetRoute(page: this); @@ -352,6 +360,13 @@ class AppCupertinoSheetPage extends Page { // This route uses the builder from the page to build its content. This ensures // the content is up to date after page updates. class _PageBasedCupertinoSheetRoute extends CupertinoSheetRoute { + AppCupertinoSheetPage get _page => settings as AppCupertinoSheetPage; + + @override + bool get maintainState => _page.maintainState; + + @override + String get debugLabel => '${super.debugLabel}(${_page.name})'; _PageBasedCupertinoSheetRoute({ required AppCupertinoSheetPage page, super.stops, @@ -367,12 +382,4 @@ class _PageBasedCupertinoSheetRoute extends CupertinoSheetRoute { .child; }, ); - - AppCupertinoSheetPage get _page => settings as AppCupertinoSheetPage; - - @override - bool get maintainState => _page.maintainState; - - @override - String get debugLabel => '${super.debugLabel}(${_page.name})'; } diff --git a/lib/constants.dart b/lib/constants.dart new file mode 100644 index 0000000..c72196e --- /dev/null +++ b/lib/constants.dart @@ -0,0 +1,11 @@ +final class Constants { + // Network + static const baseUrlPath = 'https://samba.deno.dev'; + static const connectTimeout = Duration(seconds: 2); + static const receiveTimeout = Duration(seconds: 3); + + /// Duration(milliseconds: 300) + static const debouncerDelay = Duration(milliseconds: 300); + + Constants._(); +} diff --git a/lib/extensions/app_localization_extension.dart b/lib/core/extensions/app_localization_extension.dart similarity index 57% rename from lib/extensions/app_localization_extension.dart rename to lib/core/extensions/app_localization_extension.dart index 7224b6e..3edff2a 100644 --- a/lib/extensions/app_localization_extension.dart +++ b/lib/core/extensions/app_localization_extension.dart @@ -1,7 +1,7 @@ // app_localizations_context.dart import 'package:flutter/widgets.dart'; -import '../l10n/app_localizations.dart'; +import '../../l10n/app_localizations.dart'; -extension LocalizedBuildContext on BuildContext { +extension AppLocalizationExtension on BuildContext { AppLocalizations get loc => AppLocalizations.of(this); } diff --git a/lib/core/extensions/context_snackbar.dart b/lib/core/extensions/context_snackbar.dart new file mode 100644 index 0000000..efd2036 --- /dev/null +++ b/lib/core/extensions/context_snackbar.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; + +extension ContextSnackBar on BuildContext { + void showSnackBarText({ + required Widget content, + Key? key, + Color? backgroundColor, + double? elevation, + EdgeInsetsGeometry? padding, + double? width, + ShapeBorder? shape, + }) { + ScaffoldMessenger.of(this).clearSnackBars(); + ScaffoldMessenger.of(this).showSnackBar( + SnackBar( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + behavior: SnackBarBehavior.floating, + content: content, + key: key, + backgroundColor: backgroundColor, + elevation: elevation, + padding: padding, + width: width, + shape: shape, + ), + ); + } +} diff --git a/lib/extensions/hardcoded_extension.dart b/lib/core/extensions/hardcoded_extension.dart similarity index 70% rename from lib/extensions/hardcoded_extension.dart rename to lib/core/extensions/hardcoded_extension.dart index 10d4540..a60a151 100644 --- a/lib/extensions/hardcoded_extension.dart +++ b/lib/core/extensions/hardcoded_extension.dart @@ -1,4 +1,4 @@ -extension HardcodeExtension on String { +extension HardcodedExtension on String { /// Returns the string itself, works to add translation later String get hardcoded => this; } diff --git a/lib/extensions/intl_extension.dart b/lib/core/extensions/intl_extension.dart similarity index 99% rename from lib/extensions/intl_extension.dart rename to lib/core/extensions/intl_extension.dart index 0470a51..842441c 100644 --- a/lib/extensions/intl_extension.dart +++ b/lib/core/extensions/intl_extension.dart @@ -27,6 +27,7 @@ extension IntlExtension on DateTime { Localizations.localeOf(context).languageCode, ).add_Hm().format(this); } + return DateFormat.yMMMd( Localizations.localeOf(context).languageCode, ).add_Hm().format(this); @@ -49,6 +50,7 @@ extension OrdinalExtension on int { }, (_) => '$this', }; + return spaceAfter ? '$ordinal ' : ordinal; } diff --git a/lib/extensions/is_ios_or_macos_platform_extension.dart b/lib/core/extensions/is_ios_or_macos_platform_extension.dart similarity index 100% rename from lib/extensions/is_ios_or_macos_platform_extension.dart rename to lib/core/extensions/is_ios_or_macos_platform_extension.dart diff --git a/lib/extensions/js_bottom_padding_extension.dart b/lib/core/extensions/js_bottom_padding_extension.dart similarity index 100% rename from lib/extensions/js_bottom_padding_extension.dart rename to lib/core/extensions/js_bottom_padding_extension.dart diff --git a/lib/extensions/js_bottom_padding_extension_web.dart b/lib/core/extensions/js_bottom_padding_extension_web.dart similarity index 100% rename from lib/extensions/js_bottom_padding_extension_web.dart rename to lib/core/extensions/js_bottom_padding_extension_web.dart diff --git a/lib/extensions/router_extension.dart b/lib/core/extensions/router_extension.dart similarity index 87% rename from lib/extensions/router_extension.dart rename to lib/core/extensions/router_extension.dart index 5443efc..cabee04 100644 --- a/lib/extensions/router_extension.dart +++ b/lib/core/extensions/router_extension.dart @@ -1,11 +1,12 @@ import 'package:go_router/go_router.dart'; -extension GoRouteExtension on GoRoute { +extension RouterExtension on GoRoute { String addPathParameters(Map parameters) { var newPath = path; for (final entry in parameters.entries) { newPath = newPath.replaceAll(':${entry.key}', entry.value); } + return newPath; } } diff --git a/lib/extensions/string_extension.dart b/lib/core/extensions/string_extensions.dart similarity index 93% rename from lib/extensions/string_extension.dart rename to lib/core/extensions/string_extensions.dart index de6ffe3..a2e02a2 100644 --- a/lib/extensions/string_extension.dart +++ b/lib/core/extensions/string_extensions.dart @@ -1,4 +1,4 @@ -extension EnumExtension on String { +extension StringExtensions on String { String get capitalize => '${this[0].toUpperCase()}${substring(1)}'; } diff --git a/lib/core/extensions/text_lines_extension.dart b/lib/core/extensions/text_lines_extension.dart new file mode 100644 index 0000000..99611de --- /dev/null +++ b/lib/core/extensions/text_lines_extension.dart @@ -0,0 +1,28 @@ +import 'package:flutter/widgets.dart'; + +const _defaultLineHeight = 20.0; + +extension TextLinesExtension on String { + int calculateLines(BuildContext context, {double? width, TextStyle? style}) { + final textPainter = TextPainter( + text: TextSpan( + text: this, + style: style ?? DefaultTextStyle.of(context).style, + ), + textDirection: TextDirection.ltr, + )..layout(maxWidth: width ?? MediaQuery.of(context).size.width); + + return textPainter.computeLineMetrics().length; + } + + double calculateHeightByLines( + BuildContext context, { + double? width, + TextStyle? style, + double paddingHeight = 0, + }) { + final lines = calculateLines(context, width: width, style: style); + + return (lines * (style?.height ?? _defaultLineHeight)) + paddingHeight; + } +} diff --git a/lib/core/extensions/theme_of_context_extension.dart b/lib/core/extensions/theme_of_context_extension.dart new file mode 100644 index 0000000..f57f4ae --- /dev/null +++ b/lib/core/extensions/theme_of_context_extension.dart @@ -0,0 +1,56 @@ +// ignore_for_file: avoid_non_null_assertion +import 'package:flutter/material.dart'; + +import '../theme/app_theme.dart'; +import 'app_localization_extension.dart'; + +extension ThemeOfContextExtension on BuildContext { + ThemeData get theme => Theme.of(this); + + Brightness get brightness => theme.brightness; + bool get brightnessIsDark => brightness == Brightness.dark; + bool get brightnessIsLight => brightness == Brightness.light; + + ColorScheme get colorScheme => theme.colorScheme; + + // TextStyles non nullables https://github.com/flutter/flutter/issues/86807 + TextTheme get textTheme => theme.textTheme; + TextStyle get headlineLarge => textTheme.headlineLarge!; + TextStyle get headlineMedium => textTheme.headlineMedium!; + TextStyle get headlineSmall => textTheme.headlineSmall!; + TextStyle get titleSmall => textTheme.titleSmall!; + TextStyle get titleMedium => textTheme.titleMedium!; + TextStyle get titleLarge => textTheme.titleLarge!; + TextStyle get bodySmall => textTheme.bodySmall!; + TextStyle get bodyMedium => textTheme.bodyMedium!; + TextStyle get bodyLarge => textTheme.bodyLarge!; + TextStyle get labelLarge => textTheme.labelLarge!; + TextStyle get labelMedium => textTheme.labelMedium!; + TextStyle get labelSmall => textTheme.labelSmall!; + + // Custom colors non nullables https://github.com/flutter/flutter/issues/75695 + AppCustomColors get customColors => theme.extension()!; + Color get customTextColor => customColors.textColor!; + Color get customInverseTextColor => customColors.inverseTextColor!; + Color get customGoldColor => customColors.goldColor!; + Color get customSilverColor => customColors.silverColor!; + Color get customBronzeColor => customColors.bronzeColor!; +} + +extension ThemeModeExtension on ThemeMode { + String label(BuildContext context) => switch (this) { + ThemeMode.system => context.loc.system, + ThemeMode.light => context.loc.light, + ThemeMode.dark => context.loc.dark + }; + + IconData get icon => switch (this) { + ThemeMode.system => Icons.brightness_medium, + ThemeMode.light => Icons.light_mode, + ThemeMode.dark => Icons.dark_mode + }; + + bool get isDark => this == ThemeMode.dark; + bool get isLight => this == ThemeMode.light; + bool get isSystem => this == ThemeMode.system; +} diff --git a/lib/core/client_network_provider.dart b/lib/core/providers/client_network.dart similarity index 77% rename from lib/core/client_network_provider.dart rename to lib/core/providers/client_network.dart index 38c8af4..f850e26 100644 --- a/lib/core/client_network_provider.dart +++ b/lib/core/providers/client_network.dart @@ -6,24 +6,23 @@ import 'package:dio_cache_interceptor_hive_store/dio_cache_interceptor_hive_stor import 'package:path_provider/path_provider.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import '../localization/language.dart'; -import '../localization/language_app_provider.dart'; -import '../utils/main_logger.dart'; -import 'get_native_adapter.dart' - if (dart.library.js_interop) 'get_native_adapter_web.dart'; +import '../../constants.dart'; +import '../../localization/language.dart'; +import '../../localization/language_app_controller.dart'; +import '../../utils/app_loggers.dart'; +import 'client_network/network_client_adapter.dart' + if (dart.library.js_interop) 'client_network/network_client_adapter_web.dart'; -part 'client_network_provider.g.dart'; - -const _baseUrlPath = 'https://samba.deno.dev'; -const _connectTimeout = Duration(seconds: 2); -const _receiveTimeout = Duration(seconds: 3); +part 'client_network.g.dart'; @Riverpod(keepAlive: true) class ClientNetwork extends _$ClientNetwork { @override - Future build() async { - final language = ref.watch(languageAppProvider).value!.languageCode; + FutureOr build() async { final cacheDirPath = await _getTemporaryDirectory(); + final futureLanguage = + await ref.watch(languageAppControllerProvider.future); + final language = futureLanguage.languageCode; final cache = CacheOptions( store: BackupCacheStore( primary: MemCacheStore(), @@ -33,14 +32,12 @@ class ClientNetwork extends _$ClientNetwork { ); final options = BaseOptions( baseUrl: Endpoint.basePath.path, - connectTimeout: _connectTimeout, - receiveTimeout: _receiveTimeout, - queryParameters: { - 'language': language, - }, + connectTimeout: Constants.connectTimeout, + receiveTimeout: Constants.receiveTimeout, + queryParameters: {'language': language}, ); final dio = Dio(options) - ..httpClientAdapter = getNativeAdapter(cronetHttp2: true) + ..httpClientAdapter = getNativeAdapter() ..interceptors.add(DioCacheInterceptor(options: cache)) ..interceptors.add( LogInterceptor( @@ -64,9 +61,11 @@ class ClientNetwork extends _$ClientNetwork { Future _getTemporaryDirectory() async { try { final dir = await getTemporaryDirectory(); + return dir.path; } catch (e) { logNetwork.info('Error getting temporary directory: $e'); + return null; } } @@ -81,7 +80,7 @@ enum Endpoint { String get pathId => '$path/'; String get pathSearch => '$path/search'; String get path => switch (this) { - basePath => _baseUrlPath, + basePath => Constants.baseUrlPath, parades => '/parades', instruments => '/instruments', schools => '/schools', @@ -89,18 +88,19 @@ enum Endpoint { } class AppNetworkError extends Error { + final String message; + AppNetworkError(this.message); AppNetworkError.fromNetworkClientException(Object e) : message = messageFromDio(e); - final String message; - @override String toString() => message; static String messageFromDio(Object e) { if (e is! DioException) return 'Unknown error 🤷'; + return switch (e.type) { DioExceptionType.badCertificate => 'Bad certificate 📜', DioExceptionType.connectionTimeout => 'Connection timeout ⏰', diff --git a/lib/core/client_network_provider.g.dart b/lib/core/providers/client_network.g.dart similarity index 89% rename from lib/core/client_network_provider.g.dart rename to lib/core/providers/client_network.g.dart index 2be02a2..f303266 100644 --- a/lib/core/client_network_provider.g.dart +++ b/lib/core/providers/client_network.g.dart @@ -1,12 +1,12 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'client_network_provider.dart'; +part of 'client_network.dart'; // ************************************************************************** // RiverpodGenerator // ************************************************************************** -String _$clientNetworkHash() => r'e3f371ef67d6880104ff979657ee9463af9d8a01'; +String _$clientNetworkHash() => r'355f8709de49a11d80859fabaf293746bafcec1f'; /// See also [ClientNetwork]. @ProviderFor(ClientNetwork) diff --git a/lib/core/get_native_adapter.dart b/lib/core/providers/client_network/network_client_adapter.dart similarity index 90% rename from lib/core/get_native_adapter.dart rename to lib/core/providers/client_network/network_client_adapter.dart index 7202933..10d18ea 100644 --- a/lib/core/get_native_adapter.dart +++ b/lib/core/providers/client_network/network_client_adapter.dart @@ -2,13 +2,14 @@ import 'package:native_dio_adapter/native_dio_adapter.dart'; const _maxCacheSize = 2 * 1024 * 1024; // 2MB -NativeAdapter getNativeAdapter({required bool cronetHttp2}) { +NativeAdapter getNativeAdapter() { return NativeAdapter( createCupertinoConfiguration: () { final config = URLSessionConfiguration.ephemeralSessionConfiguration() ..cache = URLCache.withCapacity( memoryCapacity: _maxCacheSize, ); + return config; }, createCronetEngine: () { diff --git a/lib/core/get_native_adapter_web.dart b/lib/core/providers/client_network/network_client_adapter_web.dart similarity index 86% rename from lib/core/get_native_adapter_web.dart rename to lib/core/providers/client_network/network_client_adapter_web.dart index 4e9da46..924afe9 100644 --- a/lib/core/get_native_adapter_web.dart +++ b/lib/core/providers/client_network/network_client_adapter_web.dart @@ -7,5 +7,6 @@ import 'package:fetch_client/fetch_client.dart'; // ignore: implementation_imports import 'package:native_dio_adapter/src/conversion_layer_adapter.dart'; -HttpClientAdapter getNativeAdapter({bool? cronetHttp2}) => +// ignore: avoid_unused_parameters +HttpClientAdapter getNativeAdapter() => ConversionLayerAdapter(FetchClient(mode: RequestMode.cors)); diff --git a/lib/core/providers/initialization.dart b/lib/core/providers/initialization.dart new file mode 100644 index 0000000..b178d57 --- /dev/null +++ b/lib/core/providers/initialization.dart @@ -0,0 +1,21 @@ +import 'package:flutter/foundation.dart'; +import 'package:logging/logging.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +import '../../utils/app_loggers.dart'; +import '../../utils/immutable_list.dart'; +import '../../utils/register_error_handler.dart'; +import 'prefs.dart'; + +part 'initialization.g.dart'; + +@Riverpod(keepAlive: true) +Future initialization(InitializationRef ref) async { + registerErrorHandlers(); + initializeFICMappers(); + if (kDebugMode) initLoggers(Level.FINE, {}); + ref.onDispose(() { + ref.invalidate(prefsProvider); + }); + await ref.watch(prefsProvider.future); +} diff --git a/lib/initialization_page.g.dart b/lib/core/providers/initialization.g.dart similarity index 89% rename from lib/initialization_page.g.dart rename to lib/core/providers/initialization.g.dart index 03ea771..09c9ee7 100644 --- a/lib/initialization_page.g.dart +++ b/lib/core/providers/initialization.g.dart @@ -1,12 +1,12 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'initialization_page.dart'; +part of 'initialization.dart'; // ************************************************************************** // RiverpodGenerator // ************************************************************************** -String _$initializationHash() => r'4d1c86ca1ab823a045818e9ecd8f37380d3fbbfe'; +String _$initializationHash() => r'c3e3a01b85067c9aeb106e4564cd04295fd6642a'; /// See also [initialization]. @ProviderFor(initialization) diff --git a/lib/core/shared_preferences_provider.dart b/lib/core/providers/prefs.dart similarity index 50% rename from lib/core/shared_preferences_provider.dart rename to lib/core/providers/prefs.dart index 6fff40e..3efbc63 100644 --- a/lib/core/shared_preferences_provider.dart +++ b/lib/core/providers/prefs.dart @@ -1,8 +1,9 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:shared_preferences/shared_preferences.dart'; -part 'shared_preferences_provider.g.dart'; +part 'prefs.g.dart'; @Riverpod(keepAlive: true) -Future sharedPreferences(SharedPreferencesRef ref) => - SharedPreferences.getInstance(); +Future prefs(PrefsRef _) { + return SharedPreferences.getInstance(); +} diff --git a/lib/router/go_router.g.dart b/lib/core/providers/prefs.g.dart similarity index 62% rename from lib/router/go_router.g.dart rename to lib/core/providers/prefs.g.dart index a55830b..cd1432f 100644 --- a/lib/router/go_router.g.dart +++ b/lib/core/providers/prefs.g.dart @@ -1,24 +1,24 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'go_router.dart'; +part of 'prefs.dart'; // ************************************************************************** // RiverpodGenerator // ************************************************************************** -String _$goRouterHash() => r'3c17b1afc4de4f7b29a826183ea2903eb57b40e0'; +String _$prefsHash() => r'5fa192c20a47a46153bec9274d10ff8a5807f867'; -/// See also [goRouter]. -@ProviderFor(goRouter) -final goRouterProvider = AutoDisposeProvider.internal( - goRouter, - name: r'goRouterProvider', +/// See also [prefs]. +@ProviderFor(prefs) +final prefsProvider = FutureProvider.internal( + prefs, + name: r'prefsProvider', debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') ? null : _$goRouterHash, + const bool.fromEnvironment('dart.vm.product') ? null : _$prefsHash, dependencies: null, allTransitiveDependencies: null, ); -typedef GoRouterRef = AutoDisposeProviderRef; +typedef PrefsRef = FutureProviderRef; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/lib/theme/theme_data.dart b/lib/core/theme/app_theme.dart similarity index 98% rename from lib/theme/theme_data.dart rename to lib/core/theme/app_theme.dart index f7d6bd9..ee58245 100644 --- a/lib/theme/theme_data.dart +++ b/lib/core/theme/app_theme.dart @@ -3,6 +3,8 @@ import 'package:flex_color_scheme/flex_color_scheme.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +const _iOSFontLetterSpacing = -1.5; + class AppTheme { static ThemeData get lightTheme => FlexThemeData.light( useMaterial3: true, @@ -79,7 +81,7 @@ class AppTheme { .navLargeTitleTextStyle // fixes a small bug with spacing .copyWith( - letterSpacing: -1.5, + letterSpacing: _iOSFontLetterSpacing, ), ), ); @@ -134,6 +136,7 @@ class AppCustomColors extends ThemeExtension { if (other is! AppCustomColors) { return this; } + return AppCustomColors( textColor: Color.lerp(textColor, other.textColor, t), inverseTextColor: Color.lerp(inverseTextColor, other.inverseTextColor, t), diff --git a/lib/theme/theme_provider.dart b/lib/core/theme/theme_mode_controller.dart similarity index 68% rename from lib/theme/theme_provider.dart rename to lib/core/theme/theme_mode_controller.dart index 673c1ad..db961d7 100644 --- a/lib/theme/theme_provider.dart +++ b/lib/core/theme/theme_mode_controller.dart @@ -1,24 +1,27 @@ import 'package:flutter/material.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import '../core/shared_preferences_provider.dart'; -import '../utils/main_logger.dart'; -part 'theme_provider.g.dart'; +import '../../utils/app_loggers.dart'; +import '../providers/prefs.dart'; + +part 'theme_mode_controller.g.dart'; @Riverpod(keepAlive: true) -class AppThemeMode extends _$AppThemeMode { +class ThemeModeController extends _$ThemeModeController { @override ThemeMode build() { try { - final mode = - ref.watch(sharedPreferencesProvider).value!.getString('theme_mode'); + final prefs = ref.watch(prefsProvider).value; + final mode = prefs?.getString('theme_mode'); + return switch (mode) { 'light' => ThemeMode.light, 'dark' => ThemeMode.dark, - 'system' || (_) => ThemeMode.system, + (_) => ThemeMode.system, }; } catch (e) { logViews.finest('$e'); + return ThemeMode.system; } } @@ -26,25 +29,25 @@ class AppThemeMode extends _$AppThemeMode { Future toggleTheme() async { switch (state) { case ThemeMode.system: - final prefs = ref.read(sharedPreferencesProvider).requireValue; + final prefs = ref.read(prefsProvider).requireValue; await prefs.setString('theme_mode', 'light'); ref .read(appThemeTrueBlackProvider.notifier) .toggleTrueBlack(forceState: false); state = ThemeMode.light; case ThemeMode.light: - final prefs = ref.read(sharedPreferencesProvider).requireValue; + final prefs = ref.read(prefsProvider).requireValue; await prefs.setString('theme_mode', 'dark'); state = ThemeMode.dark; case ThemeMode.dark: - final prefs = ref.read(sharedPreferencesProvider).requireValue; + final prefs = ref.read(prefsProvider).requireValue; await prefs.remove('theme_mode'); state = ThemeMode.system; } } void setTheme(ThemeMode mode) { - final prefs = ref.read(sharedPreferencesProvider).requireValue; + final prefs = ref.read(prefsProvider).requireValue; switch (mode) { case ThemeMode.system: prefs.remove('theme_mode'); @@ -64,19 +67,17 @@ class AppThemeTrueBlack extends _$AppThemeTrueBlack { @override bool build() { try { - return ref - .watch(sharedPreferencesProvider) - .value! - .getBool('true_black') ?? + return ref.watch(prefsProvider).requireValue.getBool('true_black') ?? false; } catch (e) { logViews.finest('$e'); + return false; } } void toggleTrueBlack({bool? forceState}) { - final prefs = ref.read(sharedPreferencesProvider).requireValue; + final prefs = ref.read(prefsProvider).requireValue; state = forceState ?? !state; if (state) { prefs.setBool('true_black', state); diff --git a/lib/theme/theme_provider.g.dart b/lib/core/theme/theme_mode_controller.g.dart similarity index 63% rename from lib/theme/theme_provider.g.dart rename to lib/core/theme/theme_mode_controller.g.dart index eb9d5fa..79ac38e 100644 --- a/lib/theme/theme_provider.g.dart +++ b/lib/core/theme/theme_mode_controller.g.dart @@ -1,26 +1,29 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'theme_provider.dart'; +part of 'theme_mode_controller.dart'; // ************************************************************************** // RiverpodGenerator // ************************************************************************** -String _$appThemeModeHash() => r'429f7a40d1df8304a535214708cfb66f66f08773'; +String _$themeModeControllerHash() => + r'dff5d6a2e47b086ae84bc400b4308dbce1143d7e'; -/// See also [AppThemeMode]. -@ProviderFor(AppThemeMode) -final appThemeModeProvider = NotifierProvider.internal( - AppThemeMode.new, - name: r'appThemeModeProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') ? null : _$appThemeModeHash, +/// See also [ThemeModeController]. +@ProviderFor(ThemeModeController) +final themeModeControllerProvider = + NotifierProvider.internal( + ThemeModeController.new, + name: r'themeModeControllerProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$themeModeControllerHash, dependencies: null, allTransitiveDependencies: null, ); -typedef _$AppThemeMode = Notifier; -String _$appThemeTrueBlackHash() => r'c71eaecafef02c241b4e8c996cde320440828fe2'; +typedef _$ThemeModeController = Notifier; +String _$appThemeTrueBlackHash() => r'be89cfe33db2954b047f1644c4b5af256171b836'; /// See also [AppThemeTrueBlack]. @ProviderFor(AppThemeTrueBlack) diff --git a/lib/extensions/context_snackbar.dart b/lib/extensions/context_snackbar.dart deleted file mode 100644 index e3d802f..0000000 --- a/lib/extensions/context_snackbar.dart +++ /dev/null @@ -1,66 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'theme_of_context_extension.dart'; - -extension ContextSnackBar on BuildContext { - void showSnackBar({ - required Widget content, - Key? key, - Color? backgroundColor, - double? elevation, - EdgeInsetsGeometry? margin, - EdgeInsetsGeometry? padding, - double? width, - ShapeBorder? shape, - HitTestBehavior? hitTestBehavior, - SnackBarBehavior? behavior, - }) { - ScaffoldMessenger.of(this).clearSnackBars(); - ScaffoldMessenger.of(this).showSnackBar( - SnackBar( - margin: - margin ?? const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - behavior: behavior ?? SnackBarBehavior.floating, - content: content, - key: key, - backgroundColor: backgroundColor, - elevation: elevation, - padding: padding, - width: width, - shape: shape, - ), - ); - } - - void showSnackBarText( - String text, { - Key? key, - Color? backgroundColor, - double? elevation, - EdgeInsetsGeometry? margin, - EdgeInsetsGeometry? padding, - double? width, - ShapeBorder? shape, - HitTestBehavior? hitTestBehavior, - SnackBarBehavior? behavior, - }) => - showSnackBar( - content: Text( - text, - style: textTheme.titleSmall!.copyWith( - color: colorScheme.onSurface, - fontWeight: FontWeight.w700, - fontStyle: FontStyle.italic, - ), - ), - key: key, - backgroundColor: backgroundColor, - elevation: elevation, - margin: margin, - padding: padding, - width: width, - shape: shape, - hitTestBehavior: hitTestBehavior, - behavior: behavior, - ); -} diff --git a/lib/extensions/theme_of_context_extension.dart b/lib/extensions/theme_of_context_extension.dart deleted file mode 100644 index 4c485de..0000000 --- a/lib/extensions/theme_of_context_extension.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:flutter/material.dart'; - -import '../theme/theme_data.dart'; -import 'app_localization_extension.dart'; - -extension TextThemeOfContextExtension on BuildContext { - TextTheme get textTheme => Theme.of(this).textTheme; - - Brightness get brightness => Theme.of(this).brightness; - bool get brightnessIsDark => brightness == Brightness.dark; - bool get brightnessIsLight => brightness == Brightness.light; - - ColorScheme get colorScheme => Theme.of(this).colorScheme; - AppCustomColors get customColors => - Theme.of(this).extension()!; -} - -extension ThemeModeExtension on ThemeMode { - String label(BuildContext context) => switch (this) { - ThemeMode.system => context.loc.system, - ThemeMode.light => context.loc.light, - ThemeMode.dark => context.loc.dark - }; - - IconData get icon => switch (this) { - ThemeMode.system => Icons.brightness_medium, - ThemeMode.light => Icons.light_mode, - ThemeMode.dark => Icons.dark_mode - }; - - bool get isDark => this == ThemeMode.dark; - bool get isLight => this == ThemeMode.light; - bool get isSystem => this == ThemeMode.system; -} diff --git a/lib/features/home/home_page.dart b/lib/features/home/home_page.dart index fde8ae1..d23154b 100644 --- a/lib/features/home/home_page.dart +++ b/lib/features/home/home_page.dart @@ -4,7 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../common_widgets/app_web_padding.dart'; -import '../../extensions/theme_of_context_extension.dart'; +import '../../core/extensions/theme_of_context_extension.dart'; import '../../utils/immutable_list.dart'; import '../../utils/screen_size.dart'; import 'home_page_controller.dart'; @@ -19,6 +19,7 @@ class HomePage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final screenSize = context.screenSize; + return Scaffold( body: screenSize.isSmall ? navigationShell diff --git a/lib/features/home/home_page_controller.dart b/lib/features/home/home_page_controller.dart index f8fadf8..2f123c7 100644 --- a/lib/features/home/home_page_controller.dart +++ b/lib/features/home/home_page_controller.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import '../../extensions/app_localization_extension.dart'; +import '../../core/extensions/app_localization_extension.dart'; import '../instruments/instruments_tab_page.dart'; import '../parades/parades_tab_page.dart'; import '../schools/schools_tab_page.dart'; @@ -8,7 +8,7 @@ import '../schools/schools_tab_page.dart'; part 'home_page_controller.g.dart'; @Riverpod(keepAlive: true) -class CurrentTab extends _$CurrentTab { +class HomePageController extends _$HomePageController { @override ///Change for a pattern or record of hometab and boolean is is top diff --git a/lib/features/home/home_page_controller.g.dart b/lib/features/home/home_page_controller.g.dart index 3b8d3e0..9c8c691 100644 --- a/lib/features/home/home_page_controller.g.dart +++ b/lib/features/home/home_page_controller.g.dart @@ -6,20 +6,22 @@ part of 'home_page_controller.dart'; // RiverpodGenerator // ************************************************************************** -String _$currentTabHash() => r'2442f2e51bdc146b1b868464167636c0b2f2e3fa'; +String _$homePageControllerHash() => + r'df7d702c78f616ab4d2965468a89774e7e551e54'; -/// See also [CurrentTab]. -@ProviderFor(CurrentTab) -final currentTabProvider = - NotifierProvider.internal( - CurrentTab.new, - name: r'currentTabProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') ? null : _$currentTabHash, +/// See also [HomePageController]. +@ProviderFor(HomePageController) +final homePageControllerProvider = NotifierProvider.internal( + HomePageController.new, + name: r'homePageControllerProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$homePageControllerHash, dependencies: null, allTransitiveDependencies: null, ); -typedef _$CurrentTab = Notifier<({HomeTab tab, bool topRoute})>; +typedef _$HomePageController = Notifier<({HomeTab tab, bool topRoute})>; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/lib/features/home/widgets/adaptive_navigation_bar.dart b/lib/features/home/widgets/adaptive_navigation_bar.dart index 1143361..10e5ed0 100644 --- a/lib/features/home/widgets/adaptive_navigation_bar.dart +++ b/lib/features/home/widgets/adaptive_navigation_bar.dart @@ -2,8 +2,8 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import '../../../common_widgets/app_web_padding.dart'; -import '../../../extensions/is_ios_or_macos_platform_extension.dart'; -import '../../../extensions/theme_of_context_extension.dart'; +import '../../../core/extensions/is_ios_or_macos_platform_extension.dart'; +import '../../../core/extensions/theme_of_context_extension.dart'; import '../../../utils/immutable_list.dart'; import '../home_page_controller.dart'; @@ -22,9 +22,10 @@ class AdaptiveNavigationBar extends StatelessWidget { @override Widget build(BuildContext context) { + const maxScaleFactor = 1.8; if (kIsCupertino) { return MediaQuery.withClampedTextScaling( - maxScaleFactor: 1.8, + maxScaleFactor: maxScaleFactor, child: AnimatedTheme( data: Theme.of(context), child: AppWebPadding.only( @@ -48,6 +49,7 @@ class AdaptiveNavigationBar extends StatelessWidget { ), ); } + return AppWebPadding.only( color: context.colorScheme.primaryContainer, bottom: true, diff --git a/lib/features/home/widgets/adaptive_navigation_rail.dart b/lib/features/home/widgets/adaptive_navigation_rail.dart index 7117459..c694c7c 100644 --- a/lib/features/home/widgets/adaptive_navigation_rail.dart +++ b/lib/features/home/widgets/adaptive_navigation_rail.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import '../../../extensions/theme_of_context_extension.dart'; +import '../../../core/extensions/theme_of_context_extension.dart'; import '../../../utils/immutable_list.dart'; import '../../../utils/screen_size.dart'; import '../home_page_controller.dart'; @@ -28,6 +28,7 @@ class AdaptiveNavigationRail extends StatelessWidget { final footerHeight = height <= ScreenSize.smallHeight ? AdaptiveNavigationRailFooter.heightCollapsed : AdaptiveNavigationRailFooter.heightFull; + return ColoredBox( color: context.colorScheme.surface, child: Column( diff --git a/lib/features/home/widgets/adaptive_navigation_rail_footer.dart b/lib/features/home/widgets/adaptive_navigation_rail_footer.dart index 5268e65..b8e98b4 100644 --- a/lib/features/home/widgets/adaptive_navigation_rail_footer.dart +++ b/lib/features/home/widgets/adaptive_navigation_rail_footer.dart @@ -3,11 +3,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:pull_down_button/pull_down_button.dart'; -import '../../../extensions/app_localization_extension.dart'; -import '../../../extensions/theme_of_context_extension.dart'; +import '../../../core/extensions/app_localization_extension.dart'; +import '../../../core/extensions/theme_of_context_extension.dart'; +import '../../../core/theme/theme_mode_controller.dart'; import '../../../localization/language.dart'; -import '../../../localization/language_app_provider.dart'; -import '../../../theme/theme_provider.dart'; +import '../../../localization/language_app_controller.dart'; import '../../../utils/screen_size.dart'; import 'settings_modal_sheet.dart'; @@ -21,139 +21,150 @@ class AdaptiveNavigationRailFooter extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final locale = WidgetsBinding.instance.platformDispatcher.locale; final screenSize = context.screenSize; - final themeMode = ref.watch(appThemeModeProvider); + final themeMode = ref.watch(themeModeControllerProvider); final trueBlack = ref.watch(appThemeTrueBlackProvider); - if (MediaQuery.sizeOf(context).height < ScreenSize.smallHeight) { - return Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - InkWell( - onTap: () => showSettingModalSheet( - context, - padding: const EdgeInsets.symmetric(vertical: 24), - ), - child: CupertinoListTile.notched( - title: screenSize.isLarge - ? Text( - context.loc.settingsTitle, - style: context.textTheme.titleMedium, - ) - : const Icon(CupertinoIcons.settings), - leading: screenSize.isLarge ? Icon(themeMode.icon) : null, - ), - ), - ], - ); + + if (context.isSmallHeight) { + return AdaptiveRailSmallHeight(isLarge: false, themeMode: themeMode); } + return Column( - mainAxisAlignment: MainAxisAlignment.end, children: [ - PullDownButton( - scrollController: ScrollController(), - itemBuilder: (context) => [ - for (final language in Language.values) - PullDownMenuItem.selectable( - icon: language.languageCode == locale.languageCode - ? CupertinoIcons.device_phone_portrait - : null, - title: language.name(context), - subtitle: language.nativeName, - selected: language == ref.watch(languageAppProvider).value, - onTap: () { - ref.read(languageAppProvider.notifier).setLanguage( - language, - isSameAsPlatform: - language.languageCode == locale.languageCode, - ); - }, - ), - ], - buttonBuilder: (context, showMenu) => CupertinoListTile.notched( - onTap: showMenu, - leading: screenSize.isLarge - ? Icon( - CupertinoIcons.flag, - color: context.colorScheme.onSurface, - ) - : null, - title: !screenSize.isLarge - ? Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Icon( - CupertinoIcons.flag, - color: context.colorScheme.onSurface, - ), - ) - : Text( - context.loc.language, - style: themeMode.isLight - ? context.textTheme.titleMedium! - .copyWith(color: Colors.grey) - : context.textTheme.titleMedium, - ), - ), + RailPullDown( + locale: locale, + themeMode: themeMode, + isLarge: screenSize.isLarge, ), InkWell( onTap: themeMode.isLight ? null - : () async => ref - .read(appThemeTrueBlackProvider.notifier) - .toggleTrueBlack(), + : ref.read(appThemeTrueBlackProvider.notifier).toggleTrueBlack, child: CupertinoListTile.notched( trailing: IgnorePointer( - child: screenSize.isMedium - ? null - : Switch.adaptive( - value: trueBlack, - applyCupertinoTheme: true, - onChanged: themeMode.isLight - ? null - : (_) => ref - .read(appThemeTrueBlackProvider.notifier) - .toggleTrueBlack(), - ), + child: Switch.adaptive( + value: trueBlack, + applyCupertinoTheme: true, + onChanged: themeMode.isLight + ? null + : (_) => ref + .read(appThemeTrueBlackProvider.notifier) + .toggleTrueBlack(), + ), + ), + leading: Icon( + CupertinoIcons.moon_stars, + color: themeMode.isLight + ? context.colorScheme.onSurface.withOpacity(0.5) + : context.colorScheme.onSurface, + ), + title: Text( + context.loc.themeTrueBlack, + style: themeMode.isLight + ? context.titleMedium.copyWith(color: Colors.grey) + : context.textTheme.titleMedium, ), - leading: screenSize.isLarge - ? Icon( - CupertinoIcons.moon_stars, - color: themeMode.isLight - ? context.colorScheme.onSurface.withOpacity(0.5) - : context.colorScheme.onSurface, - ) - : null, - title: screenSize.isMedium - ? Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Icon( - CupertinoIcons.moon_stars, - color: themeMode.isLight - ? context.colorScheme.onSurface.withOpacity(0.5) - : context.colorScheme.onSurface, - ), - ) - : Text( - context.loc.themeTrueBlack, - style: themeMode.isLight - ? context.textTheme.titleMedium! - .copyWith(color: Colors.grey) - : context.textTheme.titleMedium, - ), ), ), InkWell( - onTap: () async => - ref.read(appThemeModeProvider.notifier).toggleTheme(), + onTap: ref.read(themeModeControllerProvider.notifier).toggleTheme, + child: CupertinoListTile.notched( + title: Text( + themeMode.label(context), + style: context.textTheme.titleMedium, + ), + leading: Icon(themeMode.icon), + ), + ), + ], + ); + } +} + +class RailPullDown extends ConsumerWidget { + const RailPullDown({ + required this.locale, + required this.themeMode, + required this.isLarge, + super.key, + }); + + final Locale locale; + final ThemeMode themeMode; + final bool isLarge; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return PullDownButton( + scrollController: ScrollController(), + itemBuilder: (context) => [ + for (final language in Language.values) + PullDownMenuItem.selectable( + icon: language.languageCode == locale.languageCode + ? CupertinoIcons.device_phone_portrait + : null, + title: language.name(context), + subtitle: language.nativeName, + selected: + language == ref.watch(languageAppControllerProvider).value, + onTap: () { + ref + .read(languageAppControllerProvider.notifier) + .setLanguage(language); + }, + ), + ], + buttonBuilder: (context, showMenu) => CupertinoListTile.notched( + onTap: showMenu, + leading: isLarge + ? Icon( + CupertinoIcons.flag, + color: context.colorScheme.onSurface, + ) + : null, + title: !isLarge + ? Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Icon( + CupertinoIcons.flag, + color: context.colorScheme.onSurface, + ), + ) + : Text( + context.loc.language, + style: themeMode.isLight + ? context.titleMedium.copyWith(color: Colors.grey) + : context.textTheme.titleMedium, + ), + ), + ); + } +} + +class AdaptiveRailSmallHeight extends StatelessWidget { + final bool isLarge; + final ThemeMode themeMode; + + const AdaptiveRailSmallHeight({ + required this.isLarge, + required this.themeMode, + super.key, + }); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + InkWell( + onTap: () => showSettingModalSheet(context), child: CupertinoListTile.notched( - title: screenSize.isLarge + title: isLarge ? Text( - themeMode.label(context), + context.loc.settingsTitle, style: context.textTheme.titleMedium, ) - : Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Icon(themeMode.icon), - ), - leading: screenSize.isLarge ? Icon(themeMode.icon) : null, + : const Icon(CupertinoIcons.settings), + leading: isLarge ? Icon(themeMode.icon) : null, ), ), ], diff --git a/lib/features/home/widgets/settings_modal_sheet.dart b/lib/features/home/widgets/settings_modal_sheet.dart index e981c3f..2365ec0 100644 --- a/lib/features/home/widgets/settings_modal_sheet.dart +++ b/lib/features/home/widgets/settings_modal_sheet.dart @@ -3,15 +3,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../common_widgets/app_cupertino_button.dart'; -import '../../../extensions/app_localization_extension.dart'; -import '../../../extensions/theme_of_context_extension.dart'; +import '../../../core/extensions/app_localization_extension.dart'; +import '../../../core/extensions/theme_of_context_extension.dart'; import '../../../localization/language.dart'; -import '../../../localization/language_app_provider.dart'; +import '../../../localization/language_app_controller.dart'; import 'settings_theme_section.dart'; void showSettingModalSheet( BuildContext context, { - EdgeInsets padding = const EdgeInsets.only(top: 24), bool showAsDialog = true, }) { showModalBottomSheet( @@ -20,7 +19,20 @@ void showSettingModalSheet( useRootNavigator: true, showDragHandle: false, isScrollControlled: true, - builder: (context) => SingleChildScrollView( + builder: (_) => SettingsModalSheet(showAsDialog: showAsDialog), + ); +} + +class SettingsModalSheet extends StatelessWidget { + final bool showAsDialog; + const SettingsModalSheet({ + super.key, + this.showAsDialog = true, + }); + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( padding: const EdgeInsets.only(bottom: 32), controller: PrimaryScrollController.of(context), physics: const ClampingScrollPhysics(), @@ -31,7 +43,7 @@ void showSettingModalSheet( automaticallyImplyLeading: false, elevation: 0, title: Text(context.loc.settingsTitle), - titleTextStyle: context.textTheme.titleLarge!.copyWith( + titleTextStyle: context.titleLarge.copyWith( fontWeight: FontWeight.bold, ), backgroundColor: @@ -60,8 +72,8 @@ void showSettingModalSheet( const SettingsLanguageSection(), ], ), - ), - ); + ); + } } class SettingsLanguageSection extends ConsumerWidget { @@ -86,16 +98,15 @@ class SettingsLanguageSection extends ConsumerWidget { CupertinoListTile.notched( backgroundColor: context.colorScheme.surface, leading: Icon( - ref.watch(languageAppProvider).value == language + ref.watch(languageAppControllerProvider).value == language ? CupertinoIcons.check_mark_circled : null, color: context.colorScheme.primary, ), onTap: () { - ref.read(languageAppProvider.notifier).setLanguage( - language, - isSameAsPlatform: language.isSameAsPlatform, - ); + ref + .read(languageAppControllerProvider.notifier) + .setLanguage(language); }, title: Text( language.nativeName, diff --git a/lib/features/home/widgets/settings_theme_section.dart b/lib/features/home/widgets/settings_theme_section.dart index fa17c0a..0ecc159 100644 --- a/lib/features/home/widgets/settings_theme_section.dart +++ b/lib/features/home/widgets/settings_theme_section.dart @@ -1,16 +1,16 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../../extensions/app_localization_extension.dart'; -import '../../../extensions/theme_of_context_extension.dart'; -import '../../../theme/theme_provider.dart'; +import '../../../core/extensions/app_localization_extension.dart'; +import '../../../core/extensions/theme_of_context_extension.dart'; +import '../../../core/theme/theme_mode_controller.dart'; class SettingsThemeSection extends ConsumerWidget { const SettingsThemeSection({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final themeMode = ref.watch(appThemeModeProvider); + final themeMode = ref.watch(themeModeControllerProvider); final trueBlack = ref.watch(appThemeTrueBlackProvider); return CupertinoListSection.insetGrouped( @@ -30,10 +30,10 @@ class SettingsThemeSection extends ConsumerWidget { title: CupertinoSegmentedControl( padding: EdgeInsets.zero, // This represents a currently selected segmented control. - groupValue: ref.watch(appThemeModeProvider), + groupValue: ref.watch(themeModeControllerProvider), // Callback that sets the selected segmented control. onValueChanged: (ThemeMode value) { - ref.read(appThemeModeProvider.notifier).setTheme(value); + ref.read(themeModeControllerProvider.notifier).setTheme(value); }, children: { ThemeMode.light: Padding( diff --git a/lib/features/instruments/details/instrument_details_providers.dart b/lib/features/instruments/details/instrument_details_controller.dart similarity index 62% rename from lib/features/instruments/details/instrument_details_providers.dart rename to lib/features/instruments/details/instrument_details_controller.dart index 46af2ad..e5a2cfd 100644 --- a/lib/features/instruments/details/instrument_details_providers.dart +++ b/lib/features/instruments/details/instrument_details_controller.dart @@ -1,20 +1,21 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; import '../instrument.dart'; -import '../instruments_tab_providers.dart'; +import '../instruments_tab_controller.dart'; -part 'instrument_details_providers.g.dart'; +part 'instrument_details_controller.g.dart'; @riverpod -class InstrumentDetails extends _$InstrumentDetails { +class InstrumentDetailsController extends _$InstrumentDetailsController { @override Instrument build(int id) { final instrument = ref - .watch(instrumentsTabProvider) + .watch(instrumentsTabControllerProvider) .value ?.firstWhere((element) => element.id == id); if (instrument == null) { throw Exception('Instrument not found'); } + return instrument; } } diff --git a/lib/features/instruments/details/instrument_details_providers.g.dart b/lib/features/instruments/details/instrument_details_controller.g.dart similarity index 53% rename from lib/features/instruments/details/instrument_details_providers.g.dart rename to lib/features/instruments/details/instrument_details_controller.g.dart index b7c62f9..e587770 100644 --- a/lib/features/instruments/details/instrument_details_providers.g.dart +++ b/lib/features/instruments/details/instrument_details_controller.g.dart @@ -1,12 +1,13 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'instrument_details_providers.dart'; +part of 'instrument_details_controller.dart'; // ************************************************************************** // RiverpodGenerator // ************************************************************************** -String _$instrumentDetailsHash() => r'8cd61b14528469a50013bae24788ac4938ee68b7'; +String _$instrumentDetailsControllerHash() => + r'62aacbc96a4ab812e1f78ce8707a195dfd47d1d7'; /// Copied from Dart SDK class _SystemHash { @@ -29,7 +30,7 @@ class _SystemHash { } } -abstract class _$InstrumentDetails +abstract class _$InstrumentDetailsController extends BuildlessAutoDisposeNotifier { late final int id; @@ -38,14 +39,14 @@ abstract class _$InstrumentDetails ); } -/// See also [InstrumentDetails]. -@ProviderFor(InstrumentDetails) -const instrumentDetailsProvider = InstrumentDetailsFamily(); +/// See also [InstrumentDetailsController]. +@ProviderFor(InstrumentDetailsController) +const instrumentDetailsControllerProvider = InstrumentDetailsControllerFamily(); -/// See also [InstrumentDetails]. -class InstrumentDetailsFamily extends Family { - /// See also [InstrumentDetails]. - const InstrumentDetailsFamily(); +/// See also [InstrumentDetailsController]. +class InstrumentDetailsControllerFamily extends Family { + /// See also [InstrumentDetailsController]. + const InstrumentDetailsControllerFamily(); static const Iterable? _dependencies = null; @@ -59,21 +60,21 @@ class InstrumentDetailsFamily extends Family { _allTransitiveDependencies; @override - String? get name => r'instrumentDetailsProvider'; + String? get name => r'instrumentDetailsControllerProvider'; - /// See also [InstrumentDetails]. - InstrumentDetailsProvider call( + /// See also [InstrumentDetailsController]. + InstrumentDetailsControllerProvider call( int id, ) { - return InstrumentDetailsProvider( + return InstrumentDetailsControllerProvider( id, ); } @visibleForOverriding @override - InstrumentDetailsProvider getProviderOverride( - covariant InstrumentDetailsProvider provider, + InstrumentDetailsControllerProvider getProviderOverride( + covariant InstrumentDetailsControllerProvider provider, ) { return call( provider.id, @@ -81,48 +82,50 @@ class InstrumentDetailsFamily extends Family { } /// Enables overriding the behavior of this provider, no matter the parameters. - Override overrideWith(InstrumentDetails Function() create) { - return _$InstrumentDetailsFamilyOverride(this, create); + Override overrideWith(InstrumentDetailsController Function() create) { + return _$InstrumentDetailsControllerFamilyOverride(this, create); } } -class _$InstrumentDetailsFamilyOverride implements FamilyOverride { - _$InstrumentDetailsFamilyOverride(this.overriddenFamily, this.create); +class _$InstrumentDetailsControllerFamilyOverride implements FamilyOverride { + _$InstrumentDetailsControllerFamilyOverride( + this.overriddenFamily, this.create); - final InstrumentDetails Function() create; + final InstrumentDetailsController Function() create; @override - final InstrumentDetailsFamily overriddenFamily; + final InstrumentDetailsControllerFamily overriddenFamily; @override - InstrumentDetailsProvider getProviderOverride( - covariant InstrumentDetailsProvider provider, + InstrumentDetailsControllerProvider getProviderOverride( + covariant InstrumentDetailsControllerProvider provider, ) { return provider._copyWith(create); } } -/// See also [InstrumentDetails]. -class InstrumentDetailsProvider - extends AutoDisposeNotifierProviderImpl { - /// See also [InstrumentDetails]. - InstrumentDetailsProvider( +/// See also [InstrumentDetailsController]. +class InstrumentDetailsControllerProvider + extends AutoDisposeNotifierProviderImpl { + /// See also [InstrumentDetailsController]. + InstrumentDetailsControllerProvider( int id, ) : this._internal( - () => InstrumentDetails()..id = id, - from: instrumentDetailsProvider, - name: r'instrumentDetailsProvider', + () => InstrumentDetailsController()..id = id, + from: instrumentDetailsControllerProvider, + name: r'instrumentDetailsControllerProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null - : _$instrumentDetailsHash, - dependencies: InstrumentDetailsFamily._dependencies, + : _$instrumentDetailsControllerHash, + dependencies: InstrumentDetailsControllerFamily._dependencies, allTransitiveDependencies: - InstrumentDetailsFamily._allTransitiveDependencies, + InstrumentDetailsControllerFamily._allTransitiveDependencies, id: id, ); - InstrumentDetailsProvider._internal( + InstrumentDetailsControllerProvider._internal( super.create, { required super.name, required super.dependencies, @@ -136,7 +139,7 @@ class InstrumentDetailsProvider @override Instrument runNotifierBuild( - covariant InstrumentDetails notifier, + covariant InstrumentDetailsController notifier, ) { return notifier.build( id, @@ -144,10 +147,10 @@ class InstrumentDetailsProvider } @override - Override overrideWith(InstrumentDetails Function() create) { + Override overrideWith(InstrumentDetailsController Function() create) { return ProviderOverride( origin: this, - override: InstrumentDetailsProvider._internal( + override: InstrumentDetailsControllerProvider._internal( () => create()..id = id, from: from, name: null, @@ -165,15 +168,15 @@ class InstrumentDetailsProvider } @override - AutoDisposeNotifierProviderElement + AutoDisposeNotifierProviderElement createElement() { - return _InstrumentDetailsProviderElement(this); + return _InstrumentDetailsControllerProviderElement(this); } - InstrumentDetailsProvider _copyWith( - InstrumentDetails Function() create, + InstrumentDetailsControllerProvider _copyWith( + InstrumentDetailsController Function() create, ) { - return InstrumentDetailsProvider._internal( + return InstrumentDetailsControllerProvider._internal( () => create()..id = id, name: name, dependencies: dependencies, @@ -186,7 +189,7 @@ class InstrumentDetailsProvider @override bool operator ==(Object other) { - return other is InstrumentDetailsProvider && other.id == id; + return other is InstrumentDetailsControllerProvider && other.id == id; } @override @@ -198,18 +201,19 @@ class InstrumentDetailsProvider } } -mixin InstrumentDetailsRef on AutoDisposeNotifierProviderRef { +mixin InstrumentDetailsControllerRef + on AutoDisposeNotifierProviderRef { /// The parameter `id` of this provider. int get id; } -class _InstrumentDetailsProviderElement - extends AutoDisposeNotifierProviderElement - with InstrumentDetailsRef { - _InstrumentDetailsProviderElement(super.provider); +class _InstrumentDetailsControllerProviderElement + extends AutoDisposeNotifierProviderElement with InstrumentDetailsControllerRef { + _InstrumentDetailsControllerProviderElement(super.provider); @override - int get id => (origin as InstrumentDetailsProvider).id; + int get id => (origin as InstrumentDetailsControllerProvider).id; } // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/lib/features/instruments/details/instrument_details_page.dart b/lib/features/instruments/details/instrument_details_page.dart index 407d32c..f8b8708 100644 --- a/lib/features/instruments/details/instrument_details_page.dart +++ b/lib/features/instruments/details/instrument_details_page.dart @@ -5,41 +5,45 @@ import 'package:sliver_tools/sliver_tools.dart'; import '../../../common_widgets/app_back_button.dart'; import '../../../common_widgets/app_cupertino_sliver_navigation_bar.dart'; import '../../../common_widgets/app_web_padding.dart'; -import '../../../extensions/app_localization_extension.dart'; +import '../../../core/extensions/app_localization_extension.dart'; +import '../../../core/extensions/text_lines_extension.dart'; import '../../../utils/screen_size.dart'; -import 'instrument_details_providers.dart'; +import '../instrument.dart'; +import 'instrument_details_controller.dart'; import 'widgets/instrument_details_summary.dart'; import 'widgets/instrument_header_images.dart'; typedef InstrumentId = int; -enum InstrumentDetailsTab { summary, learning } - class InstrumentDetailsPage extends ConsumerStatefulWidget { const InstrumentDetailsPage({required this.id, super.key}); final InstrumentId id; static const path = 'details/:id'; + static const bottomPaddingContent = 200.0; @override ConsumerState createState() => _InstrumentDetailsPageState(); } +enum InstrumentDetailsTab { summary, learning } + class _InstrumentDetailsPageState extends ConsumerState { final _controller = ScrollController(); @override Widget build(BuildContext context) { - final value = ref.watch(instrumentDetailsProvider(widget.id)); + final value = ref.watch(instrumentDetailsControllerProvider(widget.id)); final screenConstraint = ScreenSize.lg.value; const imageHeight = 80.0; + return DefaultTabController( length: InstrumentDetailsTab.values.length, child: Scaffold( body: NestedScrollView( controller: _controller, - headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { + headerSliverBuilder: (BuildContext context, _) { return [ SliverOverlapAbsorber( handle: NestedScrollView.sliverOverlapAbsorberHandleFor( @@ -114,16 +118,8 @@ class _InstrumentDetailsPageState extends ConsumerState { top: 8, ), sliver: SliverToBoxAdapter( - // TODO(hectorAguero): hardcoded to avoid overscroll child: SizedBox( - height: value.translatedDescription - .calculateLines( - context, - width: screenConstraint, - ) - .toDouble() * - 20 + - 200, + height: _height(value, context, screenConstraint), child: TabBarView( physics: const ClampingScrollPhysics(), children: [ @@ -148,17 +144,16 @@ class _InstrumentDetailsPageState extends ConsumerState { ), ); } -} -extension TextLinesExtension on String { - int calculateLines(BuildContext context, {double? width, TextStyle? style}) { - final textPainter = TextPainter( - text: TextSpan( - text: this, - style: style ?? DefaultTextStyle.of(context).style, - ), - textDirection: TextDirection.ltr, - )..layout(maxWidth: width ?? MediaQuery.of(context).size.width); - return textPainter.computeLineMetrics().length; + double _height( + Instrument value, + BuildContext context, + double screenConstraint, + ) { + return value.translatedDescription.calculateHeightByLines( + context, + width: screenConstraint, + paddingHeight: InstrumentDetailsPage.bottomPaddingContent, + ); } } diff --git a/lib/features/instruments/details/widgets/instrument_details_learning.dart b/lib/features/instruments/details/widgets/instrument_details_learning.dart index e8ecfdb..f027efe 100644 --- a/lib/features/instruments/details/widgets/instrument_details_learning.dart +++ b/lib/features/instruments/details/widgets/instrument_details_learning.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import '../../../../extensions/hardcoded_extension.dart'; +import '../../../../core/extensions/hardcoded_extension.dart'; class InstrumentDetailsLearning extends StatelessWidget { const InstrumentDetailsLearning({super.key}); diff --git a/lib/features/instruments/details/widgets/instrument_details_summary.dart b/lib/features/instruments/details/widgets/instrument_details_summary.dart index 1b8676b..596ca6a 100644 --- a/lib/features/instruments/details/widgets/instrument_details_summary.dart +++ b/lib/features/instruments/details/widgets/instrument_details_summary.dart @@ -15,6 +15,7 @@ class InstrumentDetailsSummary extends StatelessWidget { builder: (context, constraints) { final padding = (constraints.maxWidth - ScreenSize.md.value) .clamp(16.0, ScreenSize.md.value); + return SingleChildScrollView( padding: EdgeInsets.symmetric(vertical: 16, horizontal: padding), physics: const NeverScrollableScrollPhysics(), diff --git a/lib/features/instruments/details/widgets/instrument_header_images.dart b/lib/features/instruments/details/widgets/instrument_header_images.dart index a3fcf44..849c0c4 100644 --- a/lib/features/instruments/details/widgets/instrument_header_images.dart +++ b/lib/features/instruments/details/widgets/instrument_header_images.dart @@ -3,7 +3,7 @@ import 'package:extended_image/extended_image.dart'; import 'package:flutter/material.dart'; import '../../../../common_widgets/app_fade_in_image.dart'; -import '../../../../extensions/theme_of_context_extension.dart'; +import '../../../../core/extensions/theme_of_context_extension.dart'; import '../../../../utils/immutable_list.dart'; import '../../../../utils/screen_size.dart'; import '../../instrument.dart'; @@ -24,6 +24,7 @@ class InstrumentHeaderImages extends StatelessWidget { final imageQuantity = context.screenSize.isSmall ? 2 : 3; final largeImageHeight = imageHeight * imageQuantity + (imageQuantity - 1) * 16; + return Padding( padding: const EdgeInsets.all(24), child: Column( diff --git a/lib/features/instruments/instrument.dart b/lib/features/instruments/instrument.dart index 69c720f..3cbecff 100644 --- a/lib/features/instruments/instrument.dart +++ b/lib/features/instruments/instrument.dart @@ -6,17 +6,6 @@ part 'instrument.mapper.dart'; @MappableClass() class Instrument with InstrumentMappable { - Instrument({ - required this.id, - required this.name, - required this.description, - required this.imageUrl, - required this.gallery, - required this.type, - required this.translatedName, - required this.translatedDescription, - }); - final int id; final String name; final String type; @@ -28,4 +17,15 @@ class Instrument with InstrumentMappable { static const fromMap = InstrumentMapper.fromMap; static const fromJson = InstrumentMapper.fromJson; + + Instrument({ + required this.id, + required this.name, + required this.description, + required this.imageUrl, + required this.gallery, + required this.type, + required this.translatedName, + required this.translatedDescription, + }); } diff --git a/lib/features/instruments/instruments_repo.dart b/lib/features/instruments/instruments_repo.dart index 8252065..c38a647 100644 --- a/lib/features/instruments/instruments_repo.dart +++ b/lib/features/instruments/instruments_repo.dart @@ -1,6 +1,6 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; -import '../../core/client_network_provider.dart'; +import '../../core/providers/client_network.dart'; import '../../utils/immutable_list.dart'; import 'details/instrument_details_page.dart'; import 'instrument.dart'; @@ -9,7 +9,7 @@ part 'instruments_repo.g.dart'; @riverpod InstrumentsRepo instrumentsRepo(InstrumentsRepoRef ref) { - return InstrumentRepoImpls(ref); + return InstrumentsRepoImpls(ref); } abstract class InstrumentsRepo { @@ -18,21 +18,22 @@ abstract class InstrumentsRepo { Future getDetails(InstrumentId id); } -class InstrumentRepoImpls implements InstrumentsRepo { - InstrumentRepoImpls(this.ref); - +class InstrumentsRepoImpls implements InstrumentsRepo { final InstrumentsRepoRef ref; + InstrumentsRepoImpls(this.ref); + @override Future> getInstruments() async { try { - final response = await ref - .watch(clientNetworkProvider) - .value! - .get>(Endpoint.instruments.path); - final data = response.data!.cast>(); + final dio = await ref.watch(clientNetworkProvider.future); + final response = + await dio.get>(Endpoint.instruments.path); + final data = response.data ?? []; + return ImmutableList([ - for (final item in data) Instrument.fromMap(item), + for (final item in data.cast>()) + Instrument.fromMap(item), ]); } catch (e) { throw AppNetworkError.fromNetworkClientException(e); @@ -42,13 +43,12 @@ class InstrumentRepoImpls implements InstrumentsRepo { @override Future getDetails(InstrumentId id) async { try { - final response = await ref - .watch(clientNetworkProvider) - .value! - .get>( - '${Endpoint.instruments.pathId}/$id', - ); - return Instrument.fromMap(response.data!); + final dio = await ref.watch(clientNetworkProvider.future); + final response = await dio.get>( + '${Endpoint.instruments.pathId}/$id', + ); + + return Instrument.fromMap(response.data ?? {}); } catch (e) { throw AppNetworkError.fromNetworkClientException(e); } diff --git a/lib/features/instruments/instruments_repo.g.dart b/lib/features/instruments/instruments_repo.g.dart index d877104..79950bc 100644 --- a/lib/features/instruments/instruments_repo.g.dart +++ b/lib/features/instruments/instruments_repo.g.dart @@ -6,7 +6,7 @@ part of 'instruments_repo.dart'; // RiverpodGenerator // ************************************************************************** -String _$instrumentsRepoHash() => r'c18f42f8a1f93748803582ffb4297d1c76dfce20'; +String _$instrumentsRepoHash() => r'be669548808c3c86e523d75611dc91bdc21f2387'; /// See also [instrumentsRepo]. @ProviderFor(instrumentsRepo) diff --git a/lib/features/instruments/instruments_tab_providers.dart b/lib/features/instruments/instruments_tab_controller.dart similarity index 82% rename from lib/features/instruments/instruments_tab_providers.dart rename to lib/features/instruments/instruments_tab_controller.dart index e9d889c..396adbf 100644 --- a/lib/features/instruments/instruments_tab_providers.dart +++ b/lib/features/instruments/instruments_tab_controller.dart @@ -4,10 +4,10 @@ import '../../utils/immutable_list.dart'; import 'instrument.dart'; import 'instruments_repo.dart'; -part 'instruments_tab_providers.g.dart'; +part 'instruments_tab_controller.g.dart'; @riverpod -class InstrumentsTab extends _$InstrumentsTab { +class InstrumentsTabController extends _$InstrumentsTabController { @override FutureOr> build() async { return await ref.watch(instrumentsRepoProvider).getInstruments(); diff --git a/lib/features/instruments/instruments_tab_providers.g.dart b/lib/features/instruments/instruments_tab_controller.g.dart similarity index 54% rename from lib/features/instruments/instruments_tab_providers.g.dart rename to lib/features/instruments/instruments_tab_controller.g.dart index 55a7d23..7f099a4 100644 --- a/lib/features/instruments/instruments_tab_providers.g.dart +++ b/lib/features/instruments/instruments_tab_controller.g.dart @@ -1,26 +1,28 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'instruments_tab_providers.dart'; +part of 'instruments_tab_controller.dart'; // ************************************************************************** // RiverpodGenerator // ************************************************************************** -String _$instrumentsTabHash() => r'7e4f3788aa70d663dfd0ab1363840ff64e8bd1d1'; +String _$instrumentsTabControllerHash() => + r'af042f105ac11e3ed447c7fb429bd9abe1a3aaf1'; -/// See also [InstrumentsTab]. -@ProviderFor(InstrumentsTab) -final instrumentsTabProvider = AutoDisposeAsyncNotifierProvider>.internal( - InstrumentsTab.new, - name: r'instrumentsTabProvider', +/// See also [InstrumentsTabController]. +@ProviderFor(InstrumentsTabController) +final instrumentsTabControllerProvider = AutoDisposeAsyncNotifierProvider< + InstrumentsTabController, ImmutableList>.internal( + InstrumentsTabController.new, + name: r'instrumentsTabControllerProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null - : _$instrumentsTabHash, + : _$instrumentsTabControllerHash, dependencies: null, allTransitiveDependencies: null, ); -typedef _$InstrumentsTab = AutoDisposeAsyncNotifier>; +typedef _$InstrumentsTabController + = AutoDisposeAsyncNotifier>; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/lib/features/instruments/instruments_tab_page.dart b/lib/features/instruments/instruments_tab_page.dart index acd2cb4..9a498d0 100644 --- a/lib/features/instruments/instruments_tab_page.dart +++ b/lib/features/instruments/instruments_tab_page.dart @@ -7,10 +7,10 @@ import 'package:sliver_tools/sliver_tools.dart'; import '../../common_widgets/app_async_widget.dart'; import '../../common_widgets/app_cupertino_sliver_navigation_bar.dart'; import '../../common_widgets/app_web_padding.dart'; -import '../../extensions/app_localization_extension.dart'; +import '../../core/extensions/app_localization_extension.dart'; import '../../utils/screen_size.dart'; import '../home/home_page_controller.dart'; -import 'instruments_tab_providers.dart'; +import 'instruments_tab_controller.dart'; import 'widgets/instrument_list_tile.dart'; class InstrumentsTabPage extends ConsumerWidget { @@ -35,8 +35,9 @@ class InstrumentsTabPage extends ConsumerWidget { SliverAnimatedSwitcher( duration: const Duration(milliseconds: 300), child: AppAsyncSliverWidget( - asyncValue: ref.watch(instrumentsTabProvider), - onErrorRetry: () => ref.invalidate(instrumentsTabProvider), + asyncValue: ref.watch(instrumentsTabControllerProvider), + onErrorRetry: () => + ref.invalidate(instrumentsTabControllerProvider), child: (value) => WebPaddingSliver.only( right: true, sliver: SliverSafeArea( @@ -46,6 +47,7 @@ class InstrumentsTabPage extends ConsumerWidget { itemCount: value.length, itemBuilder: (context, index) { final instrument = value[index]; + return InstrumentListTile( title: instrument.translatedName, originalTitle: instrument.name, diff --git a/lib/features/instruments/widgets/instrument_list_tile.dart b/lib/features/instruments/widgets/instrument_list_tile.dart index 4997e66..e369ce3 100644 --- a/lib/features/instruments/widgets/instrument_list_tile.dart +++ b/lib/features/instruments/widgets/instrument_list_tile.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import '../../../common_widgets/app_fade_in_image.dart'; -import '../../../extensions/theme_of_context_extension.dart'; +import '../../../core/extensions/theme_of_context_extension.dart'; typedef ImageUrl = String; @@ -76,34 +76,26 @@ class _InstrumentListTileState extends State { children: [ TextSpan( text: widget.title, - style: Theme.of(context) - .textTheme - .headlineSmall! - .copyWith( - fontWeight: FontWeight.bold, - color: context.colorScheme.onSurfaceVariant, - ), + style: context.headlineSmall.copyWith( + fontWeight: FontWeight.bold, + color: context.colorScheme.onSurfaceVariant, + ), ), if (widget.title != widget.originalTitle) TextSpan( text: ' ${widget.originalTitle}', - style: Theme.of(context) - .textTheme - .titleLarge! - .copyWith( - color: - context.colorScheme.onSurfaceVariant, - ), + style: context.titleLarge.copyWith( + color: context.colorScheme.onSurfaceVariant, + ), ), ], style: Theme.of(context).textTheme.headlineSmall, ), maxLines: 1, overflow: TextOverflow.ellipsis, - style: - Theme.of(context).textTheme.headlineSmall!.copyWith( - fontWeight: FontWeight.bold, - ), + style: context.headlineSmall.copyWith( + fontWeight: FontWeight.bold, + ), ), ), Row( diff --git a/lib/features/parades/parade.dart b/lib/features/parades/parade.dart index 745ac18..19201bf 100644 --- a/lib/features/parades/parade.dart +++ b/lib/features/parades/parade.dart @@ -9,38 +9,6 @@ typedef ParadeId = int; @MappableClass() class Parade with ParadeMappable { - Parade({ - required this.id, - required this.schoolId, - required this.carnivalId, - required this.carnivalName, - required this.enredo, - required this.carnavalescos, - required this.division, - required this.divisionNumber, - required this.subdivisionNumber, - required this.paradeYear, - required this.date, - required this.championParade, - required this.components, - required this.numberOfWings, - required this.numberOfFloats, - required this.numberOfTripods, - required this.placing, - required this.relegated, - required this.promoted, - required this.champion, - required this.performanceOrder, - required this.performanceDay, - required this.points, - required this.details, - required this.translatedCarnivalName, - required this.translatedEnredo, - required this.translatedDivision, - required this.translatedCarnavalescos, - required this.school, - }); - final ParadeId id; final SchoolId schoolId; final int carnivalId; @@ -73,10 +41,50 @@ class Parade with ParadeMappable { static const fromMap = ParadeMapper.fromMap; static const fromJson = ParadeMapper.fromJson; + + Parade({ + required this.id, + required this.schoolId, + required this.carnivalId, + required this.carnivalName, + required this.enredo, + required this.carnavalescos, + required this.division, + required this.divisionNumber, + required this.subdivisionNumber, + required this.paradeYear, + required this.date, + required this.championParade, + required this.components, + required this.numberOfWings, + required this.numberOfFloats, + required this.numberOfTripods, + required this.placing, + required this.relegated, + required this.promoted, + required this.champion, + required this.performanceOrder, + required this.performanceDay, + required this.points, + required this.details, + required this.translatedCarnivalName, + required this.translatedEnredo, + required this.translatedDivision, + required this.translatedCarnavalescos, + required this.school, + }); } @MappableClass() class ParadeQueryParams with ParadeQueryParamsMappable { + final String? language; + final String? filter; + final String? sort; + final String? sortOrder; + final String? search; + final int? page; + final int? pageSize; + ParadeQueryParams({ this.language, this.filter, @@ -86,12 +94,4 @@ class ParadeQueryParams with ParadeQueryParamsMappable { this.page, this.pageSize, }); - - final String? language; - final String? filter; - final String? sort; - final String? sortOrder; - final String? search; - final int? page; - final int? pageSize; } diff --git a/lib/features/parades/parade_extension.dart b/lib/features/parades/parade_extension.dart index 63738a4..325fee1 100644 --- a/lib/features/parades/parade_extension.dart +++ b/lib/features/parades/parade_extension.dart @@ -1,19 +1,26 @@ import 'package:flutter/material.dart'; -import '../../extensions/theme_of_context_extension.dart'; +import '../../core/extensions/theme_of_context_extension.dart'; import '../schools/school.dart'; import 'parade.dart'; +const int _firstPlacing = 1; +const int _secondPlacing = 2; +const int _thirdPlacing = 3; +const int _fourthPlacing = 4; +const int _fifthPlacing = 5; +const int _sixthPlacing = 6; + extension ParadeExtension on Parade { String get getPerformanceIcon => divisionNumber == SchoolDivision.especial ? _firstDivisionIcon : _otherDivisionIcon; String get _firstDivisionIcon => switch ((placing, relegated, champion)) { - (1, _, _) => '🏆', - (2, _, _) => '🥈', - (3, _, _) => '🥉', - (4 || 5 || 6, _, false) => '🏅', + (_firstPlacing, _, _) => '🏆', + (_secondPlacing, _, _) => '🥈', + (_thirdPlacing, _, _) => '🥉', + (_fourthPlacing || _fifthPlacing || _sixthPlacing, _, false) => '🏅', (_, true, _) => '🔻', _ => '🎗️', }; @@ -21,8 +28,8 @@ extension ParadeExtension on Parade { String get _otherDivisionIcon => switch ((placing, relegated, champion)) { (1, _, true) => '🏆', (1, _, false) => '🥇', - (2, _, _) => '🥈', - (3, _, _) => '🥉', + (_secondPlacing, _, _) => '🥈', + (_thirdPlacing, _, _) => '🥉', (_, true, _) => '🔻', _ => '🎗️', }; @@ -40,13 +47,19 @@ extension ParadeExtension on Parade { promoted: promoted, champion: champion )) { - (placing: 1, relegated: _, promoted: _, champion: true) => - context.customColors.goldColor!, - (placing: 1, relegated: _, promoted: true, champion: false) => - context.customColors.silverColor!, - (placing: 2, relegated: _, promoted: true, champion: _) => - context.customColors.silverColor!, - (placing: 2 || 3 || 4 || 5, relegated: _, promoted: true, champion: _) => + (placing: _firstPlacing, relegated: _, promoted: _, champion: true) => + context.customGoldColor, + (placing: _firstPlacing, relegated: _, promoted: true, champion: false) => + context.customSilverColor, + (placing: _secondPlacing, relegated: _, promoted: true, champion: _) => + context.customSilverColor, + ( + placing: + _secondPlacing || _thirdPlacing || _fourthPlacing || _fifthPlacing, + relegated: _, + promoted: true, + champion: _ + ) => context.colorScheme.secondary, (placing: _, relegated: true, promoted: _, champion: _) => context.colorScheme.errorContainer, @@ -56,9 +69,13 @@ extension ParadeExtension on Parade { Color _specialMedalColor(BuildContext context) { return switch ((placing: placing, relegated: relegated)) { - (placing: 1, relegated: _) => context.customColors.goldColor!, - (placing: 2, relegated: _) => context.customColors.silverColor!, - (placing: 3 || 4 || 5 || 6, relegated: _) => + (placing: _firstPlacing, relegated: _) => context.customGoldColor, + (placing: _secondPlacing, relegated: _) => context.customSilverColor, + ( + placing: + _thirdPlacing || _fourthPlacing || _fifthPlacing || _sixthPlacing, + relegated: _ + ) => context.colorScheme.tertiaryContainer, (placing: _, relegated: true) => context.colorScheme.errorContainer, _ => context.colorScheme.primaryContainer, diff --git a/lib/features/parades/parades_repo.dart b/lib/features/parades/parades_repo.dart index 0517bd6..ede60e0 100644 --- a/lib/features/parades/parades_repo.dart +++ b/lib/features/parades/parades_repo.dart @@ -1,6 +1,6 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; -import '../../core/client_network_provider.dart'; +import '../../core/providers/client_network.dart'; import '../../utils/immutable_list.dart'; import 'parade.dart'; @@ -20,26 +20,29 @@ abstract class ParadesRepo { } class ParadesRepoImpl implements ParadesRepo { - ParadesRepoImpl(this.ref); - final ParadesRepoRef ref; + ParadesRepoImpl(this.ref); @override Future> getParades({ ParadeQueryParams? queryParams, }) async { try { - final response = - await ref.watch(clientNetworkProvider).value!.get>( + final dio = await ref.watch(clientNetworkProvider.future); + final page = queryParams?.page; + final pageSize = queryParams?.pageSize; + final response = await dio.get>( Endpoint.parades.path, queryParameters: { - if (queryParams?.page != null) 'page': queryParams!.page, - if (queryParams?.pageSize != null) 'pageSize': queryParams!.pageSize, + if (page != null) 'page': page, + if (pageSize != null) 'pageSize': pageSize, }, ); - final data = response.data!.cast>(); + final data = response.data ?? >[]; + return ImmutableList([ - for (final item in data) Parade.fromMap(item), + for (final item in data.cast>()) + Parade.fromMap(item), ]); } catch (e) { throw AppNetworkError.fromNetworkClientException(e); @@ -49,11 +52,11 @@ class ParadesRepoImpl implements ParadesRepo { @override Future getParade(int id, {ParadeQueryParams? queryParams}) async { try { - final response = await ref - .watch(clientNetworkProvider) - .value! - .get>('${Endpoint.parades.pathId}/$id'); - return Parade.fromMap(response.data!); + final dio = await ref.watch(clientNetworkProvider.future); + final response = + await dio.get>('${Endpoint.parades.pathId}/$id'); + + return Parade.fromMap(response.data ?? {}); } catch (e) { throw AppNetworkError.fromNetworkClientException(e); } diff --git a/lib/features/parades/parades_tab_providers.dart b/lib/features/parades/parades_tab_controller.dart similarity index 81% rename from lib/features/parades/parades_tab_providers.dart rename to lib/features/parades/parades_tab_controller.dart index c68ba98..cad89a7 100644 --- a/lib/features/parades/parades_tab_providers.dart +++ b/lib/features/parades/parades_tab_controller.dart @@ -1,14 +1,14 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; +import '../../utils/app_loggers.dart'; import '../../utils/immutable_list.dart'; -import '../../utils/main_logger.dart'; import 'parade.dart'; import 'parades_repo.dart'; -part 'parades_tab_providers.g.dart'; +part 'parades_tab_controller.g.dart'; @riverpod -class Parades extends _$Parades { +class ParadesTabController extends _$ParadesTabController { static const _pageSize = 10; @override @@ -19,19 +19,23 @@ class Parades extends _$Parades { } Future fetchNextPage({int pageSize = _pageSize}) async { + final current = state.value ?? const IList.empty(); try { final parades = await getParades( - page: state.value!.length ~/ pageSize + 1, + page: current.length ~/ pageSize + 1, pageSize: pageSize, ); if (parades.isNotEmpty) { - state = AsyncData(ImmutableList([...state.value!, ...parades])); + state = AsyncData(ImmutableList([...current, ...parades])); + return true; } ref.read(paradesTabReachedLimitProvider.notifier).setReachedLimit(); + return false; } catch (e, st) { logViews.warning('Failed to fetch next page $e', e, st); + return null; } } @@ -48,8 +52,10 @@ class Parades extends _$Parades { ); if (parades.isEmpty || parades.length < pageSize) { ref.read(paradesTabReachedLimitProvider.notifier).setReachedLimit(); + return parades; } + return parades; } } @@ -64,7 +70,7 @@ class ParadesTabReachedLimit extends _$ParadesTabReachedLimit { } } -final currentParadeProvider = Provider((ref) { +final currentParadeProvider = Provider((_) { throw UnimplementedError(); }); diff --git a/lib/features/parades/parades_tab_providers.g.dart b/lib/features/parades/parades_tab_controller.g.dart similarity index 74% rename from lib/features/parades/parades_tab_providers.g.dart rename to lib/features/parades/parades_tab_controller.g.dart index 8d2a7d6..81c463e 100644 --- a/lib/features/parades/parades_tab_providers.g.dart +++ b/lib/features/parades/parades_tab_controller.g.dart @@ -1,26 +1,29 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'parades_tab_providers.dart'; +part of 'parades_tab_controller.dart'; // ************************************************************************** // RiverpodGenerator // ************************************************************************** -String _$paradesHash() => r'c49a4e922bbd7721ea995b6fd49483dbffd81cea'; +String _$paradesTabControllerHash() => + r'6c5d4cd7a66b471fdd03be710654b7eea981fc4e'; -/// See also [Parades]. -@ProviderFor(Parades) -final paradesProvider = - AutoDisposeAsyncNotifierProvider>.internal( - Parades.new, - name: r'paradesProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') ? null : _$paradesHash, +/// See also [ParadesTabController]. +@ProviderFor(ParadesTabController) +final paradesTabControllerProvider = AutoDisposeAsyncNotifierProvider< + ParadesTabController, ImmutableList>.internal( + ParadesTabController.new, + name: r'paradesTabControllerProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$paradesTabControllerHash, dependencies: null, allTransitiveDependencies: null, ); -typedef _$Parades = AutoDisposeAsyncNotifier>; +typedef _$ParadesTabController + = AutoDisposeAsyncNotifier>; String _$paradesTabReachedLimitHash() => r'e24c9ae9f3318555dd548f146cde6bc7ff0fb670'; diff --git a/lib/features/parades/parades_tab_page.dart b/lib/features/parades/parades_tab_page.dart index a54bded..759db41 100644 --- a/lib/features/parades/parades_tab_page.dart +++ b/lib/features/parades/parades_tab_page.dart @@ -7,12 +7,12 @@ import '../../common_widgets/app_animation_wrapper.dart'; import '../../common_widgets/app_async_widget.dart'; import '../../common_widgets/app_cupertino_sliver_navigation_bar.dart'; import '../../common_widgets/app_loading_indicator.dart'; -import '../../extensions/app_localization_extension.dart'; +import '../../core/extensions/app_localization_extension.dart'; import '../../utils/debouncer.dart'; import '../../utils/screen_size.dart'; import '../home/home_page_controller.dart'; import '../schools/school.dart'; -import 'parades_tab_providers.dart'; +import 'parades_tab_controller.dart'; import 'widgets/parade_item.dart'; import 'widgets/parade_item_year_line.dart'; @@ -27,32 +27,24 @@ class ParadesTabPage extends ConsumerStatefulWidget { } class _ParadesTabPageState extends ConsumerState { - final _debouncer = Debouncer(defaultDelay); + final _debouncer = Debouncer(); final _listController = ListController(); - ScrollController? controller; + late final ScrollController controller = PrimaryScrollController.of(context); @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { - controller = PrimaryScrollController.of(context); - controller?.addListener(_loadMoreListener); + controller.addListener(_loadMoreListener); }); } - @override - void dispose() { - _debouncer.dispose(); - _listController.dispose(); - controller?.removeListener(_loadMoreListener); - super.dispose(); - } - void _loadMoreListener() { - final position = controller!.position; + final position = controller.position; if (position.pixels == position.maxScrollExtent) { if (!ref.read(paradesTabReachedLimitProvider)) { - _debouncer.run(ref.read(paradesProvider.notifier).fetchNextPage); + _debouncer + .run(ref.read(paradesTabControllerProvider.notifier).fetchNextPage); } } } @@ -71,18 +63,19 @@ class _ParadesTabPageState extends ConsumerState { ), ), AppAsyncSliverWidget( - asyncValue: ref.watch(paradesProvider), + asyncValue: ref.watch(paradesTabControllerProvider), onErrorRetry: () async => Future.delayed(const Duration(milliseconds: 500), () { - ref.invalidate(paradesProvider); + ref.invalidate(paradesTabControllerProvider); }), child: (value) => SliverCrossAxisConstrained( maxCrossAxisExtent: ScreenSize.md.value, child: SuperSliverList.builder( itemCount: value.length, listController: _listController, - itemBuilder: (context, index) { + itemBuilder: (_, index) { final parade = value[index]; + return ProviderScope( overrides: [ currentParadeProvider.overrideWithValue(value[index]), @@ -125,15 +118,11 @@ class _ParadesTabPageState extends ConsumerState { ); } - void animateToItem(int index) { - _listController.animateToItem( - index: index, - scrollController: controller!, - alignment: 0.5, - // You can provide duration and curve depending on the estimated - // distance between currentPosition and the target item position. - duration: (estimatedDistance) => const Duration(milliseconds: 250), - curve: (estimatedDistance) => Curves.easeInOut, - ); + @override + void dispose() { + _debouncer.dispose(); + _listController.dispose(); + controller.removeListener(_loadMoreListener); + super.dispose(); } } diff --git a/lib/features/parades/widgets/parade_item.dart b/lib/features/parades/widgets/parade_item.dart index b72991b..fe5f349 100644 --- a/lib/features/parades/widgets/parade_item.dart +++ b/lib/features/parades/widgets/parade_item.dart @@ -5,12 +5,12 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../common_widgets/app_animated_linear_gradient.dart'; import '../../../common_widgets/app_fade_in_image.dart'; -import '../../../extensions/app_localization_extension.dart'; -import '../../../extensions/theme_of_context_extension.dart'; +import '../../../core/extensions/app_localization_extension.dart'; +import '../../../core/extensions/theme_of_context_extension.dart'; import '../../schools/school_extensions.dart'; import '../parade.dart'; import '../parade_extension.dart'; -import '../parades_tab_providers.dart'; +import '../parades_tab_controller.dart'; import 'parade_item_bottom_row.dart'; import 'parade_item_sidebar.dart'; @@ -23,6 +23,7 @@ class ParadeItem extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final parade = ref.watch(currentParadeProvider); final medalColor = parade.medalColor(context); + return Card( clipBehavior: Clip.antiAlias, margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), @@ -71,6 +72,7 @@ class ParadeItemContent extends StatelessWidget { @override Widget build(BuildContext context) { final medalColor = parade.medalColor(context); + return Stack( children: [ Column( @@ -99,7 +101,7 @@ class ParadeItemContent extends StatelessWidget { child: AppFadeInImage( parade.school.imageUrl, fadeInDuration: const Duration(milliseconds: 300), - imageErrorBuilder: (context, error, stackTrace) { + imageErrorBuilder: (_, __, ___) { return const SizedBox(); }, ), @@ -131,6 +133,7 @@ class ParadeItemBadge extends StatelessWidget { Widget build(BuildContext context) { final medalColor = parade.medalColor(context); final divisionName = parade.divisionNumber.shortName(context); + return DecoratedBox( decoration: BoxDecoration( borderRadius: const BorderRadius.only( @@ -155,7 +158,7 @@ class ParadeItemBadge extends StatelessWidget { padding: const EdgeInsets.all(8), child: Text( divisionName, - style: context.textTheme.labelSmall!.copyWith( + style: context.labelSmall.copyWith( color: context.colorScheme.onSurface, fontWeight: FontWeight.bold, ), @@ -181,6 +184,7 @@ class ParadeItemTextContentHeader extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final showOriginal = ref.watch(paradeShowOriginalProvider); final enredo = showOriginal ? parade.enredo : parade.translatedEnredo; + return Padding( padding: padding, child: Column( @@ -197,7 +201,7 @@ class ParadeItemTextContentHeader extends ConsumerWidget { ? '${parade.school.name}\n' : '${parade.school.translatedName}\n', maxLines: 2, - style: context.textTheme.labelSmall!.copyWith( + style: context.labelSmall.copyWith( color: context.colorScheme.onSurfaceVariant, ), strutStyle: const StrutStyle( @@ -210,7 +214,7 @@ class ParadeItemTextContentHeader extends ConsumerWidget { padding: const EdgeInsets.symmetric(horizontal: 8), child: Text( parade.divisionNumber.shortName(context), - style: context.textTheme.labelSmall!.copyWith( + style: context.labelSmall.copyWith( color: Colors.transparent, ), ), @@ -227,7 +231,7 @@ class ParadeItemTextContentHeader extends ConsumerWidget { TextSpan(text: parade.details), ], ), - style: context.textTheme.titleSmall!.copyWith( + style: context.titleSmall.copyWith( fontWeight: FontWeight.w700, ), strutStyle: const StrutStyle( @@ -254,6 +258,7 @@ class ParadeItemTextContentDetails extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final showOriginal = ref.watch(paradeShowOriginalProvider); + return Padding( padding: padding, child: Column( @@ -289,13 +294,13 @@ class ParadeItemTextContentDetails extends ConsumerWidget { children: [ TextSpan( text: '${context.loc.schoolComponents}: ', - style: context.textTheme.labelSmall!.copyWith( + style: context.labelSmall.copyWith( fontWeight: FontWeight.w400, ), ), TextSpan( text: parade.components.toString(), - style: context.textTheme.labelSmall!.copyWith( + style: context.labelSmall.copyWith( fontWeight: FontWeight.w400, ), ), @@ -333,7 +338,7 @@ class ParadeItemTextContentDetails extends ConsumerWidget { : '', ), ], - style: context.textTheme.labelSmall!.copyWith( + style: context.labelSmall.copyWith( fontWeight: FontWeight.w300, fontStyle: FontStyle.italic, ), diff --git a/lib/features/parades/widgets/parade_item_bottom_row.dart b/lib/features/parades/widgets/parade_item_bottom_row.dart index 48efd48..63a2e62 100644 --- a/lib/features/parades/widgets/parade_item_bottom_row.dart +++ b/lib/features/parades/widgets/parade_item_bottom_row.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import '../../../common_widgets/app_animated_linear_gradient.dart'; -import '../../../extensions/app_localization_extension.dart'; -import '../../../extensions/intl_extension.dart'; -import '../../../extensions/theme_of_context_extension.dart'; +import '../../../core/extensions/app_localization_extension.dart'; +import '../../../core/extensions/intl_extension.dart'; +import '../../../core/extensions/theme_of_context_extension.dart'; import '../parade.dart'; import '../parade_extension.dart'; @@ -20,6 +20,7 @@ class ParadeItemBottomRow extends StatelessWidget { final medalColor = parade.medalColor(context); final day = parade.performanceDay.intlOrdinal(context); final order = parade.performanceOrder.intlOrdinal(context); + return AppAnimatedLinearGradient( duration: const Duration(seconds: 10), colors: [ @@ -54,7 +55,7 @@ class ParadeItemBottomRow extends StatelessWidget { '$order${context.loc.schoolToParade}', maxLines: 1, overflow: TextOverflow.ellipsis, - style: context.textTheme.labelMedium!.copyWith( + style: context.labelMedium.copyWith( fontStyle: FontStyle.italic, ), ), diff --git a/lib/features/parades/widgets/parade_item_sidebar.dart b/lib/features/parades/widgets/parade_item_sidebar.dart index 3fe9532..f908b6b 100644 --- a/lib/features/parades/widgets/parade_item_sidebar.dart +++ b/lib/features/parades/widgets/parade_item_sidebar.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; -import '../../../extensions/app_localization_extension.dart'; -import '../../../extensions/intl_extension.dart'; -import '../../../extensions/theme_of_context_extension.dart'; +import '../../../core/extensions/app_localization_extension.dart'; +import '../../../core/extensions/intl_extension.dart'; +import '../../../core/extensions/theme_of_context_extension.dart'; import '../parade.dart'; import '../parade_extension.dart'; @@ -19,6 +19,7 @@ class ParadeItemSideBar extends StatelessWidget { @override Widget build(BuildContext context) { final medalColor = parade.medalColor(context); + return Container( width: 48, height: double.infinity, @@ -64,7 +65,7 @@ class ParadeItemSideBar extends StatelessWidget { TextSpan( children: [ TextSpan( - style: context.textTheme.headlineSmall!.copyWith( + style: context.headlineSmall.copyWith( color: context.colorScheme.onSurface, fontWeight: FontWeight.w600, ), diff --git a/lib/features/parades/widgets/parade_item_year_line.dart b/lib/features/parades/widgets/parade_item_year_line.dart index e2a2e93..f7544f4 100644 --- a/lib/features/parades/widgets/parade_item_year_line.dart +++ b/lib/features/parades/widgets/parade_item_year_line.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import '../../../extensions/theme_of_context_extension.dart'; +import '../../../core/extensions/theme_of_context_extension.dart'; class ParadeItemYearLine extends StatelessWidget { const ParadeItemYearLine({ diff --git a/lib/features/schools/details/school_details_page.dart b/lib/features/schools/details/school_details_page.dart index 93673ed..3f50e53 100644 --- a/lib/features/schools/details/school_details_page.dart +++ b/lib/features/schools/details/school_details_page.dart @@ -4,14 +4,14 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../common_widgets/app_cupertino_button.dart'; import '../../../common_widgets/app_page_indicator.dart'; -import '../../../extensions/app_localization_extension.dart'; -import '../../../extensions/intl_extension.dart'; -import '../../../extensions/string_extension.dart'; -import '../../../extensions/theme_of_context_extension.dart'; +import '../../../core/extensions/app_localization_extension.dart'; +import '../../../core/extensions/intl_extension.dart'; +import '../../../core/extensions/string_extensions.dart'; +import '../../../core/extensions/theme_of_context_extension.dart'; import '../school.dart'; import '../school_extensions.dart'; import '../widgets/school_flag.dart'; -import 'schools_details_providers.dart'; +import 'schools_details_controller.dart'; class SchoolDetailsPage extends ConsumerStatefulWidget { const SchoolDetailsPage({ @@ -34,7 +34,8 @@ class _SchoolDetailsPageState extends ConsumerState { @override Widget build(BuildContext context) { - final school = ref.watch(selectedSchoolProvider(widget.id)); + final school = ref.watch(schoolsDetailsControllerProvider(widget.id)); + return ListView( shrinkWrap: true, children: [ @@ -68,7 +69,7 @@ class _SchoolDetailsPageState extends ConsumerState { child: Column( children: [ LayoutBuilder( - builder: (context, constraints) => ConstrainedBox( + builder: (_, constraints) => ConstrainedBox( constraints: BoxConstraints( maxHeight: ((constraints.maxWidth - 20) / 3) * 2, ), @@ -82,7 +83,7 @@ class _SchoolDetailsPageState extends ConsumerState { itemCount: imageCount, onPageChanged: (value) => currentImage.value = value, - itemBuilder: (context, index) { + itemBuilder: (_, __) { return Padding( padding: const EdgeInsets.symmetric( horizontal: 12, @@ -102,7 +103,7 @@ class _SchoolDetailsPageState extends ConsumerState { ), ValueListenableBuilder( valueListenable: currentImage, - builder: (context, index, child) { + builder: (_, index, __) { return AppPageIndicator( pageCount: imageCount, currentPage: index, @@ -116,7 +117,7 @@ class _SchoolDetailsPageState extends ConsumerState { ), ValueListenableBuilder( valueListenable: showOriginal, - builder: (context, value, child) { + builder: (_, value, __) { return SchoolDetailsText( school: school, showOriginal: value, @@ -155,6 +156,9 @@ class SchoolDetailsText extends StatefulWidget { class _SchoolDetailsTextState extends State { @override Widget build(BuildContext context) { + final school = widget.school; + final foundationDate = school.foundationDate; + return AnimatedSwitcher( duration: kThemeAnimationDuration, child: Padding( @@ -176,7 +180,7 @@ class _SchoolDetailsTextState extends State { : '${widget.school.name}${'\n'}', maxLines: 2, overflow: TextOverflow.ellipsis, - style: context.textTheme.headlineMedium! + style: context.headlineMedium .copyWith(fontWeight: FontWeight.w600), ), ), @@ -190,7 +194,7 @@ class _SchoolDetailsTextState extends State { children: [ Text( '${widget.school.firstDivisionChampionships}', - style: context.textTheme.headlineMedium!.copyWith( + style: context.headlineMedium.copyWith( color: context.customColors.goldColor, fontWeight: FontWeight.bold, height: 1, @@ -235,11 +239,11 @@ class _SchoolDetailsTextState extends State { ? widget.school.symbols.join(', ') : widget.school.translatedSymbols.join(', '), ), - if (widget.school.foundationDate != null) + if (foundationDate != null) SchoolTextTile( icon: Icons.date_range_outlined, title: '${context.loc.schoolFoundation}: ', - content: widget.school.foundationDate!.intlShort(context), + content: foundationDate.intlShort(context), ), if (widget.school.godmotherSchool.isNotEmpty) SchoolTextTile( @@ -352,10 +356,10 @@ class SchoolTextTile extends StatelessWidget { TextSpan(text: title), TextSpan( text: content, - style: context.textTheme.bodyLarge, + style: context.bodyLarge, ), ], - style: context.textTheme.bodyLarge!.copyWith( + style: context.bodyLarge.copyWith( fontWeight: FontWeight.w600, ), ), diff --git a/lib/features/schools/details/schools_details_controller.dart b/lib/features/schools/details/schools_details_controller.dart new file mode 100644 index 0000000..525b5b5 --- /dev/null +++ b/lib/features/schools/details/schools_details_controller.dart @@ -0,0 +1,15 @@ +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import '../school.dart'; +import '../schools_tab_controller.dart'; + +part 'schools_details_controller.g.dart'; + +@riverpod +class SchoolsDetailsController extends _$SchoolsDetailsController { + @override + School build(int id) { + final school = ref.watch(schoolsTabControllerProvider).requireValue; + + return school.firstWhere((school) => school.id == id); + } +} diff --git a/lib/features/schools/details/schools_details_providers.g.dart b/lib/features/schools/details/schools_details_controller.g.dart similarity index 53% rename from lib/features/schools/details/schools_details_providers.g.dart rename to lib/features/schools/details/schools_details_controller.g.dart index e3080b6..8a7d63d 100644 --- a/lib/features/schools/details/schools_details_providers.g.dart +++ b/lib/features/schools/details/schools_details_controller.g.dart @@ -1,12 +1,13 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'schools_details_providers.dart'; +part of 'schools_details_controller.dart'; // ************************************************************************** // RiverpodGenerator // ************************************************************************** -String _$selectedSchoolHash() => r'caae5ffd4b733525c963b09b2b47726e3a1920ec'; +String _$schoolsDetailsControllerHash() => + r'29336ca2472c1525ae7bf07f6539f5557d5e486b'; /// Copied from Dart SDK class _SystemHash { @@ -29,7 +30,8 @@ class _SystemHash { } } -abstract class _$SelectedSchool extends BuildlessAutoDisposeNotifier { +abstract class _$SchoolsDetailsController + extends BuildlessAutoDisposeNotifier { late final int id; School build( @@ -37,14 +39,14 @@ abstract class _$SelectedSchool extends BuildlessAutoDisposeNotifier { ); } -/// See also [SelectedSchool]. -@ProviderFor(SelectedSchool) -const selectedSchoolProvider = SelectedSchoolFamily(); +/// See also [SchoolsDetailsController]. +@ProviderFor(SchoolsDetailsController) +const schoolsDetailsControllerProvider = SchoolsDetailsControllerFamily(); -/// See also [SelectedSchool]. -class SelectedSchoolFamily extends Family { - /// See also [SelectedSchool]. - const SelectedSchoolFamily(); +/// See also [SchoolsDetailsController]. +class SchoolsDetailsControllerFamily extends Family { + /// See also [SchoolsDetailsController]. + const SchoolsDetailsControllerFamily(); static const Iterable? _dependencies = null; @@ -58,21 +60,21 @@ class SelectedSchoolFamily extends Family { _allTransitiveDependencies; @override - String? get name => r'selectedSchoolProvider'; + String? get name => r'schoolsDetailsControllerProvider'; - /// See also [SelectedSchool]. - SelectedSchoolProvider call( + /// See also [SchoolsDetailsController]. + SchoolsDetailsControllerProvider call( int id, ) { - return SelectedSchoolProvider( + return SchoolsDetailsControllerProvider( id, ); } @visibleForOverriding @override - SelectedSchoolProvider getProviderOverride( - covariant SelectedSchoolProvider provider, + SchoolsDetailsControllerProvider getProviderOverride( + covariant SchoolsDetailsControllerProvider provider, ) { return call( provider.id, @@ -80,48 +82,48 @@ class SelectedSchoolFamily extends Family { } /// Enables overriding the behavior of this provider, no matter the parameters. - Override overrideWith(SelectedSchool Function() create) { - return _$SelectedSchoolFamilyOverride(this, create); + Override overrideWith(SchoolsDetailsController Function() create) { + return _$SchoolsDetailsControllerFamilyOverride(this, create); } } -class _$SelectedSchoolFamilyOverride implements FamilyOverride { - _$SelectedSchoolFamilyOverride(this.overriddenFamily, this.create); +class _$SchoolsDetailsControllerFamilyOverride implements FamilyOverride { + _$SchoolsDetailsControllerFamilyOverride(this.overriddenFamily, this.create); - final SelectedSchool Function() create; + final SchoolsDetailsController Function() create; @override - final SelectedSchoolFamily overriddenFamily; + final SchoolsDetailsControllerFamily overriddenFamily; @override - SelectedSchoolProvider getProviderOverride( - covariant SelectedSchoolProvider provider, + SchoolsDetailsControllerProvider getProviderOverride( + covariant SchoolsDetailsControllerProvider provider, ) { return provider._copyWith(create); } } -/// See also [SelectedSchool]. -class SelectedSchoolProvider - extends AutoDisposeNotifierProviderImpl { - /// See also [SelectedSchool]. - SelectedSchoolProvider( +/// See also [SchoolsDetailsController]. +class SchoolsDetailsControllerProvider + extends AutoDisposeNotifierProviderImpl { + /// See also [SchoolsDetailsController]. + SchoolsDetailsControllerProvider( int id, ) : this._internal( - () => SelectedSchool()..id = id, - from: selectedSchoolProvider, - name: r'selectedSchoolProvider', + () => SchoolsDetailsController()..id = id, + from: schoolsDetailsControllerProvider, + name: r'schoolsDetailsControllerProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null - : _$selectedSchoolHash, - dependencies: SelectedSchoolFamily._dependencies, + : _$schoolsDetailsControllerHash, + dependencies: SchoolsDetailsControllerFamily._dependencies, allTransitiveDependencies: - SelectedSchoolFamily._allTransitiveDependencies, + SchoolsDetailsControllerFamily._allTransitiveDependencies, id: id, ); - SelectedSchoolProvider._internal( + SchoolsDetailsControllerProvider._internal( super.create, { required super.name, required super.dependencies, @@ -135,7 +137,7 @@ class SelectedSchoolProvider @override School runNotifierBuild( - covariant SelectedSchool notifier, + covariant SchoolsDetailsController notifier, ) { return notifier.build( id, @@ -143,10 +145,10 @@ class SelectedSchoolProvider } @override - Override overrideWith(SelectedSchool Function() create) { + Override overrideWith(SchoolsDetailsController Function() create) { return ProviderOverride( origin: this, - override: SelectedSchoolProvider._internal( + override: SchoolsDetailsControllerProvider._internal( () => create()..id = id, from: from, name: null, @@ -164,14 +166,15 @@ class SelectedSchoolProvider } @override - AutoDisposeNotifierProviderElement createElement() { - return _SelectedSchoolProviderElement(this); + AutoDisposeNotifierProviderElement + createElement() { + return _SchoolsDetailsControllerProviderElement(this); } - SelectedSchoolProvider _copyWith( - SelectedSchool Function() create, + SchoolsDetailsControllerProvider _copyWith( + SchoolsDetailsController Function() create, ) { - return SelectedSchoolProvider._internal( + return SchoolsDetailsControllerProvider._internal( () => create()..id = id, name: name, dependencies: dependencies, @@ -184,7 +187,7 @@ class SelectedSchoolProvider @override bool operator ==(Object other) { - return other is SelectedSchoolProvider && other.id == id; + return other is SchoolsDetailsControllerProvider && other.id == id; } @override @@ -196,18 +199,18 @@ class SelectedSchoolProvider } } -mixin SelectedSchoolRef on AutoDisposeNotifierProviderRef { +mixin SchoolsDetailsControllerRef on AutoDisposeNotifierProviderRef { /// The parameter `id` of this provider. int get id; } -class _SelectedSchoolProviderElement - extends AutoDisposeNotifierProviderElement - with SelectedSchoolRef { - _SelectedSchoolProviderElement(super.provider); +class _SchoolsDetailsControllerProviderElement + extends AutoDisposeNotifierProviderElement + with SchoolsDetailsControllerRef { + _SchoolsDetailsControllerProviderElement(super.provider); @override - int get id => (origin as SelectedSchoolProvider).id; + int get id => (origin as SchoolsDetailsControllerProvider).id; } // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/lib/features/schools/details/schools_details_providers.dart b/lib/features/schools/details/schools_details_providers.dart deleted file mode 100644 index cdc4c9b..0000000 --- a/lib/features/schools/details/schools_details_providers.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'package:riverpod_annotation/riverpod_annotation.dart'; -import '../school.dart'; -import '../schools_tab_providers.dart'; - -part 'schools_details_providers.g.dart'; - -@riverpod -class SelectedSchool extends _$SelectedSchool { - @override - School build(int id) { - final school = ref.watch(schoolsProvider).value!.firstWhere( - (school) => school.id == id, - ); - return school; - } -} diff --git a/lib/features/schools/school.dart b/lib/features/schools/school.dart index a1aab80..22b91cc 100644 --- a/lib/features/schools/school.dart +++ b/lib/features/schools/school.dart @@ -1,7 +1,7 @@ import 'package:dart_mappable/dart_mappable.dart'; import 'package:flutter/material.dart'; -import '../../extensions/app_localization_extension.dart'; +import '../../core/extensions/app_localization_extension.dart'; import '../../utils/immutable_list.dart'; import 'school_color_hook.dart'; @@ -12,31 +12,6 @@ typedef SchoolId = int; @MappableClass() class School with SchoolMappable { - const School({ - required this.id, - required this.name, - required this.translatedName, - required this.imageUrl, - required this.foundationDate, - required this.godmotherSchool, - required this.colors, - required this.colorsCode, - required this.symbols, - required this.carnivalCategory, - required this.currentDivision, - required this.divisionNumber, - required this.subdivisionNumber, - required this.firstDivisionChampionships, - required this.country, - required this.leagueLocation, - required this.lastPosition, - required this.translatedColors, - required this.translatedSymbols, - required this.translatedGodmotherSchool, - required this.translatedLeagueLocation, - required this.translatedCountry, - }); - final SchoolId id; final String name; final String imageUrl; @@ -58,13 +33,38 @@ class School with SchoolMappable { final String translatedGodmotherSchool; final String translatedCountry; final String translatedLeagueLocation; - @MappableField(hook: ColorHook(), key: 'colors') + @MappableField(hook: SchoolColorHook(), key: 'colors') final ImmutableList colorsCode; @MappableField(key: 'divisionNumber') final SchoolDivision currentDivision; static const fromMap = SchoolMapper.fromMap; static const fromJson = SchoolMapper.fromJson; + + const School({ + required this.id, + required this.name, + required this.translatedName, + required this.imageUrl, + required this.foundationDate, + required this.godmotherSchool, + required this.colors, + required this.colorsCode, + required this.symbols, + required this.carnivalCategory, + required this.currentDivision, + required this.divisionNumber, + required this.subdivisionNumber, + required this.firstDivisionChampionships, + required this.country, + required this.leagueLocation, + required this.lastPosition, + required this.translatedColors, + required this.translatedSymbols, + required this.translatedGodmotherSchool, + required this.translatedLeagueLocation, + required this.translatedCountry, + }); } @MappableEnum(caseStyle: CaseStyle.upperCase) @@ -79,25 +79,37 @@ enum SchoolCategory { blocoDeRua, } +class _SchoolDivisionConstants { + static const int especial = 1; + static const int ouro = 2; + static const int prata = 3; + static const int bronze = 4; + static const int avaliacao = 5; + static const int mirins = 6; + static const int blocosDeEnredo1 = 7; + static const int blocosDeEnredo2 = 8; + static const int blocosDeRua = 9; +} + @MappableEnum() enum SchoolDivision { - @MappableValue(1) + @MappableValue(_SchoolDivisionConstants.especial) especial, - @MappableValue(2) + @MappableValue(_SchoolDivisionConstants.ouro) ouro, - @MappableValue(3) + @MappableValue(_SchoolDivisionConstants.prata) prata, - @MappableValue(4) + @MappableValue(_SchoolDivisionConstants.bronze) bronze, - @MappableValue(5) + @MappableValue(_SchoolDivisionConstants.avaliacao) avaliacao, - @MappableValue(6) + @MappableValue(_SchoolDivisionConstants.mirins) mirins, - @MappableValue(7) + @MappableValue(_SchoolDivisionConstants.blocosDeEnredo1) blocosDeEnredo1, - @MappableValue(8) + @MappableValue(_SchoolDivisionConstants.blocosDeEnredo2) blocosDeEnredo2, - @MappableValue(9) + @MappableValue(_SchoolDivisionConstants.blocosDeRua) blocosDeRua } @@ -122,8 +134,9 @@ class DateTimeHook extends MappingHook { //1946/6/24 if (value.isEmpty) return null; final data = value.trim().split('/'); + return DateTime( - int.parse(data[0]), + int.parse(data.first), data.length > 1 ? int.parse(data[1]) : 1, data.length > 2 ? int.parse(data[2]) : 1, ); diff --git a/lib/features/schools/school.mapper.dart b/lib/features/schools/school.mapper.dart index 033ad7a..9671753 100644 --- a/lib/features/schools/school.mapper.dart +++ b/lib/features/schools/school.mapper.dart @@ -79,23 +79,23 @@ class SchoolDivisionMapper extends EnumMapper { @override SchoolDivision decode(dynamic value) { switch (value) { - case 1: + case _SchoolDivisionConstants.especial: return SchoolDivision.especial; - case 2: + case _SchoolDivisionConstants.ouro: return SchoolDivision.ouro; - case 3: + case _SchoolDivisionConstants.prata: return SchoolDivision.prata; - case 4: + case _SchoolDivisionConstants.bronze: return SchoolDivision.bronze; - case 5: + case _SchoolDivisionConstants.avaliacao: return SchoolDivision.avaliacao; - case 6: + case _SchoolDivisionConstants.mirins: return SchoolDivision.mirins; - case 7: + case _SchoolDivisionConstants.blocosDeEnredo1: return SchoolDivision.blocosDeEnredo1; - case 8: + case _SchoolDivisionConstants.blocosDeEnredo2: return SchoolDivision.blocosDeEnredo2; - case 9: + case _SchoolDivisionConstants.blocosDeRua: return SchoolDivision.blocosDeRua; default: throw MapperException.unknownEnumValue(value); @@ -106,23 +106,23 @@ class SchoolDivisionMapper extends EnumMapper { dynamic encode(SchoolDivision self) { switch (self) { case SchoolDivision.especial: - return 1; + return _SchoolDivisionConstants.especial; case SchoolDivision.ouro: - return 2; + return _SchoolDivisionConstants.ouro; case SchoolDivision.prata: - return 3; + return _SchoolDivisionConstants.prata; case SchoolDivision.bronze: - return 4; + return _SchoolDivisionConstants.bronze; case SchoolDivision.avaliacao: - return 5; + return _SchoolDivisionConstants.avaliacao; case SchoolDivision.mirins: - return 6; + return _SchoolDivisionConstants.mirins; case SchoolDivision.blocosDeEnredo1: - return 7; + return _SchoolDivisionConstants.blocosDeEnredo1; case SchoolDivision.blocosDeEnredo2: - return 8; + return _SchoolDivisionConstants.blocosDeEnredo2; case SchoolDivision.blocosDeRua: - return 9; + return _SchoolDivisionConstants.blocosDeRua; } } } @@ -171,7 +171,7 @@ class SchoolMapper extends ClassMapperBase { Field('colors', _$colors); static IList _$colorsCode(School v) => v.colorsCode; static const Field> _f$colorsCode = - Field('colorsCode', _$colorsCode, key: 'colors', hook: ColorHook()); + Field('colorsCode', _$colorsCode, key: 'colors', hook: SchoolColorHook()); static IList _$symbols(School v) => v.symbols; static const Field> _f$symbols = Field('symbols', _$symbols); diff --git a/lib/features/schools/school_color_hook.dart b/lib/features/schools/school_color_hook.dart index 15426f4..4f0e599 100644 --- a/lib/features/schools/school_color_hook.dart +++ b/lib/features/schools/school_color_hook.dart @@ -1,11 +1,11 @@ import 'package:dart_mappable/dart_mappable.dart'; import 'package:flutter/material.dart'; +import '../../utils/app_loggers.dart'; import '../../utils/immutable_list.dart'; -import '../../utils/main_logger.dart'; -class ColorHook extends MappingHook { - const ColorHook(); +class SchoolColorHook extends MappingHook { + const SchoolColorHook(); @override Object? beforeDecode(Object? value) { @@ -14,6 +14,7 @@ class ColorHook extends MappingHook { for (final color in value) _getColor(color as String), ]); } + return value; } @@ -56,6 +57,7 @@ class ColorHook extends MappingHook { Color _defaultColor(String color) { logColorParse.info('Color not parsed $color'); + return Colors.white; } } diff --git a/lib/features/schools/school_extensions.dart b/lib/features/schools/school_extensions.dart index 45c894a..06b19f1 100644 --- a/lib/features/schools/school_extensions.dart +++ b/lib/features/schools/school_extensions.dart @@ -2,10 +2,31 @@ library; import 'package:flutter/material.dart'; -import '../../extensions/app_localization_extension.dart'; -import '../../extensions/string_extension.dart'; +import '../../core/extensions/app_localization_extension.dart'; +import '../../core/extensions/string_extensions.dart'; import 'school.dart'; +extension SchoolExtensions on School { + bool searchLogic(String search) { + if (name.removeAccents.contains(search)) { + return true; + } + if (symbols.join(' ').removeAccents.contains(search)) { + return true; + } + + if (godmotherSchool.removeAccents.contains(search)) { + return true; + } + + if (colorsCode.join(' ').removeAccents.contains(search)) { + return true; + } + + return false; + } +} + extension SchoolDivisionExtension on SchoolDivision { String fullName(BuildContext context) => switch (this) { (SchoolDivision.especial) => context.loc.schoolDivisionSpecialFullName, @@ -36,24 +57,3 @@ extension SchoolDivisionExtension on SchoolDivision { (SchoolDivision.blocosDeRua) => context.loc.schoolDivisionStreetBloco, }; } - -extension SearchLogicSchoolExtension on School { - bool searchLogic(String search) { - if (name.removeAccents.contains(search)) { - return true; - } - if (symbols.join(' ').removeAccents.contains(search)) { - return true; - } - - if (godmotherSchool.removeAccents.contains(search)) { - return true; - } - - if (colorsCode.join(' ').removeAccents.contains(search)) { - return true; - } - - return false; - } -} diff --git a/lib/features/schools/schools_repo.dart b/lib/features/schools/schools_repo.dart index 8e8eac0..9fe5506 100644 --- a/lib/features/schools/schools_repo.dart +++ b/lib/features/schools/schools_repo.dart @@ -1,6 +1,6 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; -import '../../core/client_network_provider.dart'; +import '../../core/providers/client_network.dart'; import '../../utils/immutable_list.dart'; import 'school.dart'; @@ -21,10 +21,10 @@ abstract class SchoolsRepo { } class SchoolsRepoImpls implements SchoolsRepo { - SchoolsRepoImpls(this.ref); - final SchoolsRepoRef ref; + SchoolsRepoImpls(this.ref); + @override Future> getSchools({ required int page, @@ -33,7 +33,7 @@ class SchoolsRepoImpls implements SchoolsRepo { required String search, }) async { try { - final networkClient = ref.watch(clientNetworkProvider).requireValue; + final networkClient = await ref.watch(clientNetworkProvider.future); final response = await networkClient.get>( search.isEmpty ? Endpoint.schools.path : Endpoint.schools.pathSearch, queryParameters: { @@ -43,9 +43,11 @@ class SchoolsRepoImpls implements SchoolsRepo { // if (sort.isNotEmpty) 'sort': sort, }, ); - final data = response.data!.cast>(); + final data = response.data ?? >[]; + return ImmutableList([ - for (final item in data) School.fromMap(item), + for (final item in data.cast>()) + School.fromMap(item), ]); } catch (e) { throw AppNetworkError.fromNetworkClientException(e); diff --git a/lib/features/schools/schools_tab_providers.dart b/lib/features/schools/schools_tab_controller.dart similarity index 79% rename from lib/features/schools/schools_tab_providers.dart rename to lib/features/schools/schools_tab_controller.dart index d905165..1c98217 100644 --- a/lib/features/schools/schools_tab_providers.dart +++ b/lib/features/schools/schools_tab_controller.dart @@ -1,37 +1,46 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; -import '../../core/shared_preferences_provider.dart'; +import '../../core/providers/prefs.dart'; +import '../../utils/app_loggers.dart'; import '../../utils/immutable_list.dart'; -import '../../utils/main_logger.dart'; import 'school.dart'; import 'schools_repo.dart'; import 'widgets/school_card.dart'; -part 'schools_tab_providers.g.dart'; +part 'schools_tab_controller.g.dart'; @riverpod -class Schools extends _$Schools { +class SchoolsTabController extends _$SchoolsTabController { static const _pageSize = 12; @override - FutureOr> build() async { + Future> build() async { return getSchools(pageSize: _pageSize); } Future fetchNextPage({int pageSize = _pageSize}) async { + final current = state.value ?? const ImmutableList.empty(); try { final schools = await getSchools( - page: state.value!.length ~/ pageSize + 1, + page: current.length ~/ pageSize + 1, pageSize: pageSize, ); if (schools.isNotEmpty) { - state = AsyncData(ImmutableList([...state.value!, ...schools])); + state = AsyncData( + ImmutableList([ + ...current, + ...schools, + ]), + ); + return true; } ref.read(schoolReachedMaxProvider.notifier).reached(); + return false; } catch (e, st) { logViews.warning('Failed to fetch next page $e', e, st); + return null; } } @@ -56,8 +65,10 @@ class Schools extends _$Schools { ); if (schools.isEmpty || schools.length < pageSize) { ref.read(schoolReachedMaxProvider.notifier).reached(); + return schools; } + return schools; } } @@ -66,24 +77,26 @@ class Schools extends _$Schools { class FavoriteSchools extends _$FavoriteSchools { @override ImmutableList build() { - final prefs = ref.watch(sharedPreferencesProvider).value!; - return ImmutableList(prefs.getStringList('favoriteSchools') ?? []); + final prefs = ref.watch(prefsProvider).value; + + return ImmutableList(prefs?.getStringList('favoriteSchools') ?? []); } void toggleFavorite(SchoolId id) { - final prefs = ref.watch(sharedPreferencesProvider).value!; - final favoriteSchools = prefs.getStringList('favoriteSchools') ?? []; + final prefs = ref.watch(prefsProvider).value; + final favoriteSchools = prefs?.getStringList('favoriteSchools') ?? []; if (favoriteSchools.contains('$id')) { favoriteSchools.remove('$id'); } else { favoriteSchools.add('$id'); } - prefs.setStringList('favoriteSchools', favoriteSchools); + prefs?.setStringList('favoriteSchools', favoriteSchools); state = ImmutableList(favoriteSchools); } bool isFavorite(SchoolId id) { final favoriteSchools = state; + return favoriteSchools.contains('$id'); } } @@ -92,8 +105,9 @@ class FavoriteSchools extends _$FavoriteSchools { class SchoolDivisions extends _$SchoolDivisions { @override Map build() { - final schools = ref.watch(schoolsProvider).valueOrNull; + final schools = ref.watch(schoolsTabControllerProvider).valueOrNull; final divisions = schools?.map((e) => e.currentDivision).toSet() ?? {}; + return { for (final division in divisions) division: true, }; @@ -122,14 +136,12 @@ class SchoolDivisions extends _$SchoolDivisions { @riverpod class SearchedSchool extends _$SearchedSchool { @override - String build() { - return ''; - } + String build() => ''; void setSearch(String search) { if (state != search.trim()) { state = search.trim(); - ref.read(schoolsProvider.notifier).searchSchools(); + ref.read(schoolsTabControllerProvider.notifier).searchSchools(); } } } @@ -160,13 +172,14 @@ class ShowOnlyFavoriteSchools extends _$ShowOnlyFavoriteSchools { final filteredSchoolsProvider = Provider.autoDispose>((ref) { final filter = ref.watch(schoolDivisionsProvider); - final schools = ref.watch(schoolsProvider).valueOrNull; + final schools = ref.watch(schoolsTabControllerProvider).valueOrNull; final favoritesIds = ref.watch(favoriteSchoolsProvider); final onlyFavorites = ref.watch(showOnlyFavoriteSchoolsProvider); if (schools == null) return ImmutableList(const []); + return ImmutableList([ for (final school in schools) - if (filter[school.currentDivision]! && + if ((filter[school.currentDivision] ?? false) && (!onlyFavorites || favoritesIds.contains('${school.id}'))) school, ]); @@ -186,4 +199,4 @@ class SchoolReachedMax extends _$SchoolReachedMax { /// This ensures that when we add/remove/edit schools, only what the /// impacted widgets rebuilds, instead of the entire list of items. final currentSchoolProvider = - Provider((ref) => throw UnimplementedError()); + Provider((_) => throw UnimplementedError()); diff --git a/lib/features/schools/schools_tab_providers.g.dart b/lib/features/schools/schools_tab_controller.g.dart similarity index 81% rename from lib/features/schools/schools_tab_providers.g.dart rename to lib/features/schools/schools_tab_controller.g.dart index b651ba5..d6bca2b 100644 --- a/lib/features/schools/schools_tab_providers.g.dart +++ b/lib/features/schools/schools_tab_controller.g.dart @@ -1,27 +1,30 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'schools_tab_providers.dart'; +part of 'schools_tab_controller.dart'; // ************************************************************************** // RiverpodGenerator // ************************************************************************** -String _$schoolsHash() => r'086f0ba5dcd4a1dd3d0df75ac87caf1005817b44'; +String _$schoolsTabControllerHash() => + r'86239ab4bdf4f3bbf10ab430895ff410a2efcafe'; -/// See also [Schools]. -@ProviderFor(Schools) -final schoolsProvider = - AutoDisposeAsyncNotifierProvider>.internal( - Schools.new, - name: r'schoolsProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') ? null : _$schoolsHash, +/// See also [SchoolsTabController]. +@ProviderFor(SchoolsTabController) +final schoolsTabControllerProvider = AutoDisposeAsyncNotifierProvider< + SchoolsTabController, ImmutableList>.internal( + SchoolsTabController.new, + name: r'schoolsTabControllerProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$schoolsTabControllerHash, dependencies: null, allTransitiveDependencies: null, ); -typedef _$Schools = AutoDisposeAsyncNotifier>; -String _$favoriteSchoolsHash() => r'2734a25c2df1d78c93a6853617a63d8f0d28f1a3'; +typedef _$SchoolsTabController + = AutoDisposeAsyncNotifier>; +String _$favoriteSchoolsHash() => r'bbf5471b1c45dfd748df53c883c674586c4d7a3b'; /// See also [FavoriteSchools]. @ProviderFor(FavoriteSchools) @@ -37,7 +40,7 @@ final favoriteSchoolsProvider = AutoDisposeNotifierProvider>; -String _$schoolDivisionsHash() => r'790ca8cc34132ada189c3bdfaa8f3e68046700f0'; +String _$schoolDivisionsHash() => r'a37f839f4ee8d6e192affdb0cb63e1fbabe0aabe'; /// See also [SchoolDivisions]. @ProviderFor(SchoolDivisions) @@ -53,7 +56,7 @@ final schoolDivisionsProvider = AutoDisposeNotifierProvider>; -String _$searchedSchoolHash() => r'1109496d2bb599150694d30dc372748c439e63bc'; +String _$searchedSchoolHash() => r'23d65b61351b411c9230f5f87ffe7ba56348b0a6'; /// See also [SearchedSchool]. @ProviderFor(SearchedSchool) diff --git a/lib/features/schools/schools_tab_page.dart b/lib/features/schools/schools_tab_page.dart index 1b3c5b4..45330a1 100644 --- a/lib/features/schools/schools_tab_page.dart +++ b/lib/features/schools/schools_tab_page.dart @@ -3,7 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../common_widgets/app_loading_indicator.dart'; import '../../utils/debouncer.dart'; import '../home/home_page_controller.dart'; -import 'schools_tab_providers.dart'; +import 'schools_tab_controller.dart'; import 'widgets/school_filter_chips.dart'; import 'widgets/schools_tab_body.dart'; import 'widgets/schools_tab_navbar.dart'; @@ -20,30 +20,24 @@ class SchoolsTabPage extends ConsumerStatefulWidget { } class _SchoolsTabState extends ConsumerState { - final _debouncer = Debouncer(defaultDelay); - ScrollController? controller; + final _debouncer = Debouncer(); + late final ScrollController controller = PrimaryScrollController.of(context); @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { - controller = PrimaryScrollController.of(context); - controller?.addListener(_loadMoreListener); + controller.addListener(_loadMoreListener); }); } - @override - void dispose() { - _debouncer.dispose(); - controller?.removeListener(_loadMoreListener); - super.dispose(); - } - void _loadMoreListener() { - final position = controller!.position; + final position = controller.position; if (position.pixels == position.maxScrollExtent) { if (!ref.read(schoolReachedMaxProvider)) { - _debouncer.run(ref.read(schoolsProvider.notifier).fetchNextPage); + _debouncer.run( + ref.read(schoolsTabControllerProvider.notifier).fetchNextPage, + ); } } } @@ -51,6 +45,7 @@ class _SchoolsTabState extends ConsumerState { @override Widget build(BuildContext context) { final focus = FocusScope.of(context); + return Scaffold( body: GestureDetector( onTap: () => focus.hasFocus ? focus.unfocus() : null, @@ -67,6 +62,13 @@ class _SchoolsTabState extends ConsumerState { ), ); } + + @override + void dispose() { + _debouncer.dispose(); + controller.removeListener(_loadMoreListener); + super.dispose(); + } } class SchoolsTabLoadMoreIndicator extends ConsumerWidget { @@ -77,7 +79,8 @@ class SchoolsTabLoadMoreIndicator extends ConsumerWidget { final reachedLimit = ref.watch(schoolReachedMaxProvider); final isNotEmpty = ref.watch(filteredSchoolsProvider).isNotEmpty; final isFiltered = ref.watch(filteredSchoolsProvider).length != - ref.watch(schoolsProvider).valueOrNull?.length; + ref.watch(schoolsTabControllerProvider).valueOrNull?.length; + return AppLoadingIndicator( showLoading: !reachedLimit && isNotEmpty && !isFiltered, sliver: true, diff --git a/lib/features/schools/widgets/school_card.dart b/lib/features/schools/widgets/school_card.dart index e0a28c1..0ad0457 100644 --- a/lib/features/schools/widgets/school_card.dart +++ b/lib/features/schools/widgets/school_card.dart @@ -3,14 +3,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; -import '../../../extensions/app_localization_extension.dart'; -import '../../../extensions/intl_extension.dart'; -import '../../../extensions/string_extension.dart'; -import '../../../extensions/theme_of_context_extension.dart'; +import '../../../core/extensions/app_localization_extension.dart'; +import '../../../core/extensions/intl_extension.dart'; +import '../../../core/extensions/string_extensions.dart'; +import '../../../core/extensions/theme_of_context_extension.dart'; import '../../../utils/screen_size.dart'; import '../school.dart'; import '../school_extensions.dart'; -import '../schools_tab_providers.dart'; +import '../schools_tab_controller.dart'; import 'school_flag.dart'; class SchoolCard extends ConsumerStatefulWidget { @@ -32,6 +32,7 @@ class _SchoolCardState extends ConsumerState { @override Widget build(BuildContext context) { final school = ref.watch(currentSchoolProvider); + return Padding( padding: widget.margin ?? const EdgeInsets.only(bottom: 4), child: Card( @@ -47,7 +48,7 @@ class _SchoolCardState extends ConsumerState { onLongPress: school.name == school.translatedName ? null : () => showOriginal.value = !showOriginal.value, - child: Container( + child: DecoratedBox( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, @@ -80,7 +81,7 @@ class _SchoolCardState extends ConsumerState { ), ValueListenableBuilder( valueListenable: showOriginal, - builder: (context, value, child) { + builder: (_, value, __) { return SchoolInfoCard( school: school, showOriginal: value, @@ -125,19 +126,19 @@ class SchoolInfoCard extends StatelessWidget { '${school.translatedName}' '${context.screenSize.isLarge ? '\n' : ' '}', maxLines: 2, - style: Theme.of(context).textTheme.titleLarge!.copyWith( - color: colorScheme.onSurface, - fontWeight: FontWeight.bold, - ), + style: context.titleLarge.copyWith( + color: colorScheme.onSurface, + fontWeight: FontWeight.bold, + ), ) : Text( '${school.name}' '${!context.screenSize.isLarge ? '\n' : ' '}', maxLines: 2, - style: Theme.of(context).textTheme.titleLarge!.copyWith( - color: colorScheme.onSurface, - fontWeight: FontWeight.bold, - ), + style: context.titleLarge.copyWith( + color: colorScheme.onSurface, + fontWeight: FontWeight.bold, + ), ), ), ), @@ -164,26 +165,26 @@ class SchoolInfoCard extends StatelessWidget { ), maxLines: 2, overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.titleMedium!.copyWith( - color: colorScheme.onSurface, - ), + style: context.titleMedium.copyWith( + color: colorScheme.onSurface, + ), ), ), ), Flexible( child: Consumer( - builder: (context, ref, child) { + builder: (context, ref, _) { final sort = ref.watch(selectedSchoolSortProvider); + return Padding( padding: const EdgeInsets.only(bottom: 8, right: 8), child: TextButton( onPressed: null, style: TextButton.styleFrom( - textStyle: - Theme.of(context).textTheme.labelLarge!.copyWith( - wordSpacing: -1, - letterSpacing: -0.5, - ), + textStyle: context.labelLarge.copyWith( + wordSpacing: -1, + letterSpacing: -0.5, + ), ), child: Text.rich( TextSpan( @@ -212,6 +213,7 @@ class SchoolInfoCard extends StatelessWidget { extension SelectedSchoolSortExtension on SchoolSort { String getSortedValue(School school, BuildContext context) { + final foundation = school.foundationDate; switch (this) { case SchoolSort.lastPerformance: if (school.lastPosition == 0) { @@ -223,8 +225,8 @@ extension SelectedSchoolSortExtension on SchoolSort { case SchoolSort.name: case SchoolSort.foundationDate: case SchoolSort.location: - if (school.foundationDate != null) { - return school.foundationDate!.intlShort(context); + if (foundation != null) { + return foundation.intlShort(context); } return ''; } diff --git a/lib/features/schools/widgets/school_filter_chips.dart b/lib/features/schools/widgets/school_filter_chips.dart index 00afe00..f7a813e 100644 --- a/lib/features/schools/widgets/school_filter_chips.dart +++ b/lib/features/schools/widgets/school_filter_chips.dart @@ -3,11 +3,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sliver_tools/sliver_tools.dart'; -import '../../../extensions/app_localization_extension.dart'; -import '../../../extensions/theme_of_context_extension.dart'; +import '../../../core/extensions/app_localization_extension.dart'; +import '../../../core/extensions/theme_of_context_extension.dart'; import '../../../utils/screen_size.dart'; import '../school_extensions.dart'; -import '../schools_tab_providers.dart'; +import '../schools_tab_controller.dart'; class SchoolFilterChips extends ConsumerWidget { const SchoolFilterChips({ @@ -21,6 +21,7 @@ class SchoolFilterChips extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final selectedDivisions = ref.watch(schoolDivisionsProvider); final padding = MediaQuery.paddingOf(context); + return SliverCrossAxisConstrained( maxCrossAxisExtent: ScreenSize.lg.value, child: SliverToBoxAdapter( @@ -43,11 +44,9 @@ class SchoolFilterChips extends ConsumerWidget { selected: ref.watch(showOnlyFavoriteSchoolsProvider), label: Text(context.loc.schoolFavorites), selectedColor: context.colorScheme.primaryContainer, - onSelected: (value) { - ref - .read(showOnlyFavoriteSchoolsProvider.notifier) - .toggleShowFavorites(); - }, + onSelected: (_) => ref + .read(showOnlyFavoriteSchoolsProvider.notifier) + .toggleShowFavorites(), ), ), Padding( diff --git a/lib/features/schools/widgets/school_flag.dart b/lib/features/schools/widgets/school_flag.dart index 8b3f7f3..54dfde1 100644 --- a/lib/features/schools/widgets/school_flag.dart +++ b/lib/features/schools/widgets/school_flag.dart @@ -3,10 +3,10 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../common_widgets/app_fade_in_image.dart'; -import '../../../extensions/app_localization_extension.dart'; -import '../../../extensions/theme_of_context_extension.dart'; +import '../../../core/extensions/app_localization_extension.dart'; +import '../../../core/extensions/theme_of_context_extension.dart'; import '../school.dart'; -import '../schools_tab_providers.dart'; +import '../schools_tab_controller.dart'; class SchoolFlag extends ConsumerWidget { const SchoolFlag({ @@ -27,6 +27,7 @@ class SchoolFlag extends ConsumerWidget { final isFavorite = ref.watch( favoriteSchoolsProvider.select((v) => v.contains('${school.id}')), ); + return Stack( children: [ ClipRRect( @@ -136,7 +137,7 @@ class EmptyImage extends StatelessWidget { const SizedBox(height: 8), Text( context.loc.noImage, - style: context.textTheme.titleLarge!.copyWith( + style: context.titleLarge.copyWith( color: context.colorScheme.onPrimaryContainer, ), ), diff --git a/lib/features/schools/widgets/schools_empty_list.dart b/lib/features/schools/widgets/schools_empty_list.dart index f044d38..cde75b8 100644 --- a/lib/features/schools/widgets/schools_empty_list.dart +++ b/lib/features/schools/widgets/schools_empty_list.dart @@ -1,15 +1,17 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../../extensions/app_localization_extension.dart'; -import '../../../extensions/theme_of_context_extension.dart'; -import '../schools_tab_providers.dart'; +import '../../../core/extensions/app_localization_extension.dart'; +import '../../../core/extensions/theme_of_context_extension.dart'; +import '../schools_tab_controller.dart'; class SchoolsEmptyList extends ConsumerWidget { const SchoolsEmptyList({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { + final schools = ref.watch(schoolsTabControllerProvider).value; + return SliverFillRemaining( child: Center( child: Padding( @@ -17,11 +19,11 @@ class SchoolsEmptyList extends ConsumerWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - if (ref.watch(schoolsProvider).value?.isEmpty ?? false) ...[ + if (schools == null || schools.isEmpty) ...[ const SizedBox(height: 8), Text( context.loc.noSchoolsFound, - style: context.textTheme.titleMedium!.copyWith( + style: context.titleMedium.copyWith( color: context.colorScheme.onSurfaceVariant, ), textAlign: TextAlign.center, @@ -31,7 +33,7 @@ class SchoolsEmptyList extends ConsumerWidget { if (ref.watch(favoriteSchoolsProvider).isEmpty) Text( context.loc.noFavoriteSchools, - style: context.textTheme.titleMedium!.copyWith( + style: context.titleMedium.copyWith( color: context.colorScheme.onSurfaceVariant, ), textAlign: TextAlign.center, @@ -39,7 +41,7 @@ class SchoolsEmptyList extends ConsumerWidget { else Text( context.loc.noFilteredSchools, - style: context.textTheme.titleMedium!.copyWith( + style: context.titleMedium.copyWith( color: context.colorScheme.onSurfaceVariant, ), textAlign: TextAlign.center, diff --git a/lib/features/schools/widgets/schools_tab_body.dart b/lib/features/schools/widgets/schools_tab_body.dart index 1d1c844..868d028 100644 --- a/lib/features/schools/widgets/schools_tab_body.dart +++ b/lib/features/schools/widgets/schools_tab_body.dart @@ -5,12 +5,12 @@ import 'package:sliver_tools/sliver_tools.dart'; import '../../../common_widgets/app_animation_wrapper.dart'; import '../../../common_widgets/app_async_widget.dart'; -import '../../../extensions/js_bottom_padding_extension.dart' - if (dart.library.js_interop) '../../../extensions/js_bottom_padding_extension_web.dart'; +import '../../../core/extensions/js_bottom_padding_extension.dart' + if (dart.library.js_interop) '../../../core/extensions/js_bottom_padding_extension_web.dart'; import '../../../utils/immutable_list.dart'; import '../../../utils/screen_size.dart'; import '../school.dart'; -import '../schools_tab_providers.dart'; +import '../schools_tab_controller.dart'; import 'school_card.dart'; import 'schools_empty_list.dart'; @@ -22,11 +22,12 @@ class SchoolsTabBody extends ConsumerWidget { return SliverSafeArea( top: false, sliver: AppAsyncSliverWidget( - asyncValue: ref.watch(schoolsProvider), - onErrorRetry: () => ref.invalidate(schoolsProvider), - child: (value) => Consumer( - builder: (context, ref, child) { + asyncValue: ref.watch(schoolsTabControllerProvider), + onErrorRetry: () => ref.invalidate(schoolsTabControllerProvider), + child: (_) => Consumer( + builder: (_, ref, __) { final schools = ref.watch(filteredSchoolsProvider); + return SliverCrossAxisConstrained( maxCrossAxisExtent: ScreenSize.lg.value, child: SliverPadding( @@ -61,9 +62,10 @@ class SliverSchoolsList extends StatelessWidget { Widget build(BuildContext context) { return SliverDynamicHeightGridView( itemCount: schools.length, - crossAxisCount: gridCrossAxisCount(context), - builder: (context, index) { + crossAxisCount: context.screenSize.defaultCrossAxisCount, + builder: (_, index) { final school = schools[index]; + return AppAnimationWrapper( child: ProviderScope( overrides: [ @@ -75,12 +77,4 @@ class SliverSchoolsList extends StatelessWidget { }, ); } - - static int gridCrossAxisCount(BuildContext context) { - return switch (context.screenSize) { - ScreenSize.xs => 1, - ScreenSize.md => 2, - ScreenSize.lg => 3 - }; - } } diff --git a/lib/features/schools/widgets/schools_tab_navbar.dart b/lib/features/schools/widgets/schools_tab_navbar.dart index 5b828d2..9a16d40 100644 --- a/lib/features/schools/widgets/schools_tab_navbar.dart +++ b/lib/features/schools/widgets/schools_tab_navbar.dart @@ -4,8 +4,9 @@ import 'package:sliver_tools/sliver_tools.dart'; import '../../../common_widgets/app_cupertino_button.dart'; import '../../../common_widgets/app_cupertino_sliver_navigation_bar.dart'; -import '../../../extensions/app_localization_extension.dart'; -import '../../../extensions/hardcoded_extension.dart'; +import '../../../core/extensions/app_localization_extension.dart'; +import '../../../core/extensions/hardcoded_extension.dart'; +import '../../../utils/app_loggers.dart'; import '../../../utils/screen_size.dart'; class SchoolsTabNavBar extends StatelessWidget { @@ -21,15 +22,17 @@ class SchoolsTabNavBar extends StatelessWidget { largeTitle: context.loc.schoolsTitle, leading: PullDownButton( // menuOffset: context.screenSize.currentRailWidth, - itemBuilder: (context) => [ + itemBuilder: (_) => [ // TODO(hectorAguero): Should get this from the Data PullDownMenuItem.selectable( title: '🇧🇷 Rio de Janeiro'.hardcoded, selected: true, - onTap: () {}, + onTap: () { + logViews.info('Selected Rio de Janeiro'); + }, ), ], - buttonBuilder: (context, showMenu) => AppCupertinoButton( + buttonBuilder: (_, showMenu) => AppCupertinoButton( onPressed: showMenu, child: const Icon(CupertinoIcons.ellipsis_circle), ), diff --git a/lib/features/schools/widgets/schools_tab_search_header.dart b/lib/features/schools/widgets/schools_tab_search_header.dart index d8ef297..b3a4e86 100644 --- a/lib/features/schools/widgets/schools_tab_search_header.dart +++ b/lib/features/schools/widgets/schools_tab_search_header.dart @@ -5,12 +5,12 @@ import 'package:sliver_tools/sliver_tools.dart'; import '../../../common_widgets/app_cupertino_button.dart'; import '../../../common_widgets/app_web_padding.dart'; -import '../../../extensions/app_localization_extension.dart'; -import '../../../extensions/theme_of_context_extension.dart'; +import '../../../core/extensions/app_localization_extension.dart'; +import '../../../core/extensions/theme_of_context_extension.dart'; import '../../../utils/screen_size.dart'; import '../../home/widgets/adaptive_navigation_rail.dart'; import '../school.dart'; -import '../schools_tab_providers.dart'; +import '../schools_tab_controller.dart'; class SchoolsTabSearchHeader extends ConsumerStatefulWidget { const SchoolsTabSearchHeader({super.key}); @@ -24,16 +24,11 @@ class _SchoolsTabSearchHeaderState extends ConsumerState { late final controller = TextEditingController(); - @override - void dispose() { - super.dispose(); - controller.dispose(); - } - @override Widget build(BuildContext context) { // Watch to not dispose the provider ref.watch(searchedSchoolProvider); + return WebPaddingSliver.only( right: true, sliver: SliverSafeArea( @@ -112,14 +107,24 @@ class _SchoolsTabSearchHeaderState ); } + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + Rect calculateRect(BuildContext context) { + const top = 100.0; + const height = 48.0; if (context.screenSize.isLarge) { final left = ScreenSize.lg.value + AdaptiveNavigationRail.largeRailWidth; + const largeWidth = -48.0; - return Rect.fromLTWH(left, 100, -48, 48); + return Rect.fromLTWH(left, top, largeWidth, height); } + const width = 48.0; final left = MediaQuery.sizeOf(context).width - 48; - return Rect.fromLTWH(left, 100, 48, 48); + return Rect.fromLTWH(left, top, width, height); } } diff --git a/lib/initialization_page.dart b/lib/initialization.dart similarity index 58% rename from lib/initialization_page.dart rename to lib/initialization.dart index c66c3dd..baeea1b 100644 --- a/lib/initialization_page.dart +++ b/lib/initialization.dart @@ -1,65 +1,23 @@ import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:logging/logging.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'core/shared_preferences_provider.dart'; -import 'extensions/app_localization_extension.dart'; +import 'core/extensions/app_localization_extension.dart'; +import 'core/providers/initialization.dart'; +import 'core/theme/theme_mode_controller.dart'; import 'features/home/widgets/adaptive_navigation_rail.dart'; -import 'theme/theme_provider.dart'; -import 'utils/immutable_list.dart'; -import 'utils/main_logger.dart'; import 'utils/screen_size.dart'; -part 'initialization_page.g.dart'; - -@Riverpod(keepAlive: true) -Future initialization(InitializationRef ref) async { - registerErrorHandlers(); - initializeFICMappers(); - if (kDebugMode) initLoggers(Level.FINE, {}); - ref.onDispose(() { - ref.invalidate(sharedPreferencesProvider); - }); - await ref.watch(sharedPreferencesProvider.future); -} - -void registerErrorHandlers() { - // * Show some error UI if any uncaught exception happens - FlutterError.onError = (FlutterErrorDetails details) { - FlutterError.presentError(details); - debugPrint(details.toString()); - }; - // * Handle errors from the underlying platform/OS - PlatformDispatcher.instance.onError = (Object error, StackTrace stack) { - debugPrint(error.toString()); - return true; - }; - // * Show some error UI when any widget in the app fails to build - ErrorWidget.builder = (FlutterErrorDetails details) { - return Scaffold( - appBar: AppBar( - backgroundColor: Colors.red, - title: const Text('An error occurred'), - ), - body: Center(child: Text(details.toString())), - ); - }; -} - -/// Widget class to manage asynchronous app initialization -class InitializationPage extends ConsumerWidget { - const InitializationPage({required this.onLoaded, super.key}); +class Initialization extends ConsumerWidget { + const Initialization({required this.onLoaded, super.key}); final WidgetBuilder onLoaded; static const path = '/startup'; @override Widget build(BuildContext context, WidgetRef ref) { + ref.watch(themeModeControllerProvider); final initProvider = ref.watch(initializationProvider); - ref.watch(appThemeModeProvider); return AnimatedSwitcher( duration: const Duration(milliseconds: 300), @@ -83,12 +41,12 @@ class AppStartupLoadingWidget extends StatelessWidget { @override Widget build(BuildContext context) { final screenSize = context.screenSize; - final padding = MediaQuery.paddingOf(context); + return Scaffold( appBar: AppBar( elevation: 0, - toolbarHeight: - kMinInteractiveDimensionCupertino + padding.top.clamp(52, 100), + toolbarHeight: kMinInteractiveDimensionCupertino + + MediaQuery.paddingOf(context).top.clamp(52, 100), ), bottomNavigationBar: screenSize.isSmall ? null diff --git a/lib/localization/language.dart b/lib/localization/language.dart index 988fd20..4a36139 100644 --- a/lib/localization/language.dart +++ b/lib/localization/language.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import '../extensions/app_localization_extension.dart'; +import '../core/extensions/app_localization_extension.dart'; enum Language { en, @@ -45,9 +45,9 @@ extension LanguageExtension on Language { }; bool get isSameAsPlatform { - final platformLanguage = - WidgetsBinding.instance.platformDispatcher.locale.languageCode; - return languageCode == platformLanguage; + final platformLanguage = WidgetsBinding.instance.platformDispatcher; + + return languageCode == platformLanguage.locale.languageCode; } } diff --git a/lib/localization/language_app_provider.dart b/lib/localization/language_app_controller.dart similarity index 50% rename from lib/localization/language_app_provider.dart rename to lib/localization/language_app_controller.dart index 99f2731..96202ea 100644 --- a/lib/localization/language_app_provider.dart +++ b/lib/localization/language_app_controller.dart @@ -1,17 +1,18 @@ import 'package:flutter/widgets.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import '../core/shared_preferences_provider.dart'; +import '../core/providers/prefs.dart'; import 'language.dart'; -part 'language_app_provider.g.dart'; +part 'language_app_controller.g.dart'; @Riverpod(keepAlive: true) -class LanguageApp extends _$LanguageApp { +class LanguageAppController extends _$LanguageAppController { @override - FutureOr build() async { - final prefs = await ref.watch(sharedPreferencesProvider.future); + FutureOr build() async { + final prefs = await ref.watch(prefsProvider.future); final localeKey = prefs.getString('locale') ?? WidgetsBinding.instance.platformDispatcher.locale.languageCode; + return Language.values.firstWhere( (e) => e.languageCode == localeKey, orElse: () => Language.en, @@ -19,14 +20,13 @@ class LanguageApp extends _$LanguageApp { } Future setLanguage( - Language language, { - required bool isSameAsPlatform, - }) async { - final prefs = ref.read(sharedPreferencesProvider).value; - if (isSameAsPlatform) { - await prefs!.remove('locale'); + Language language, + ) async { + final prefs = await ref.read(prefsProvider.future); + if (language.isSameAsPlatform) { + await prefs.remove('locale'); } else { - await prefs!.setString('locale', language.languageCode); + await prefs.setString('locale', language.languageCode); } state = AsyncData(language); } diff --git a/lib/core/shared_preferences_provider.g.dart b/lib/localization/language_app_controller.g.dart similarity index 59% rename from lib/core/shared_preferences_provider.g.dart rename to lib/localization/language_app_controller.g.dart index 4eec3e9..20d7237 100644 --- a/lib/core/shared_preferences_provider.g.dart +++ b/lib/localization/language_app_controller.g.dart @@ -1,25 +1,27 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'shared_preferences_provider.dart'; +part of 'language_app_controller.dart'; // ************************************************************************** // RiverpodGenerator // ************************************************************************** -String _$sharedPreferencesHash() => r'25eceea0052302f519f44a896409ba30ede45562'; +String _$languageAppControllerHash() => + r'b305e4beb2aa7ffa06066585974ffbabde50577d'; -/// See also [sharedPreferences]. -@ProviderFor(sharedPreferences) -final sharedPreferencesProvider = FutureProvider.internal( - sharedPreferences, - name: r'sharedPreferencesProvider', +/// See also [LanguageAppController]. +@ProviderFor(LanguageAppController) +final languageAppControllerProvider = + AsyncNotifierProvider.internal( + LanguageAppController.new, + name: r'languageAppControllerProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null - : _$sharedPreferencesHash, + : _$languageAppControllerHash, dependencies: null, allTransitiveDependencies: null, ); -typedef SharedPreferencesRef = FutureProviderRef; +typedef _$LanguageAppController = AsyncNotifier; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/lib/main.dart b/lib/main.dart index 8a015dd..1865cad 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,4 @@ -import 'package:country_picker/country_picker.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; @@ -6,46 +6,38 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; // ignore:depend_on_referenced_packages import 'package:flutter_web_plugins/url_strategy.dart'; -import 'extensions/app_localization_extension.dart'; +import 'core/extensions/app_localization_extension.dart'; +import 'core/theme/app_theme.dart'; +import 'core/theme/theme_mode_controller.dart'; import 'l10n/app_localizations.dart'; import 'localization/language.dart'; -import 'localization/language_app_provider.dart'; -import 'router/go_router.dart'; -import 'theme/theme_data.dart'; -import 'theme/theme_provider.dart'; +import 'localization/language_app_controller.dart'; +import 'routing/app_router.dart'; void main() { usePathUrlStrategy(); - runApp(const ProviderScope(child: MainApp())); } -///This widget is the root of your application. -class MainApp extends ConsumerStatefulWidget { +// ignore: prefer_match_file_name +class MainApp extends ConsumerWidget { const MainApp({super.key}); - @override - ConsumerState createState() => _MainAppState(); -} - -class _MainAppState extends ConsumerState { - @override - void initState() { - super.initState(); + void _initAndroid() { + if (TargetPlatform.android != defaultTargetPlatform) return; SystemChrome.setSystemUIOverlayStyle( - const SystemUiOverlayStyle( - systemNavigationBarColor: Colors.transparent, - ), + const SystemUiOverlayStyle(systemNavigationBarColor: Colors.transparent), ); SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); } @override - Widget build(BuildContext context) { - final router = ref.watch(goRouterProvider); - final themeMode = ref.watch(appThemeModeProvider); + Widget build(BuildContext context, WidgetRef ref) { + _initAndroid(); + final router = ref.watch(appRouterProvider); + final themeMode = ref.watch(themeModeControllerProvider); final isTrueBlack = ref.watch(appThemeTrueBlackProvider); - final language = ref.watch(languageAppProvider).valueOrNull; + final language = ref.watch(languageAppControllerProvider).valueOrNull; return MaterialApp.router( routerConfig: router, @@ -57,15 +49,14 @@ class _MainAppState extends ConsumerState { themeMode: themeMode, themeAnimationStyle: AnimationStyle.noAnimation, localizationsDelegates: const [ - CountryLocalizations.delegate, AppLocalizations.delegate, GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], - builder: (context, child) => MediaQuery.withClampedTextScaling( + builder: (_, child) => MediaQuery.withClampedTextScaling( maxScaleFactor: 2, - child: child!, + child: child ?? const SizedBox.shrink(), ), supportedLocales: AppLocalizations.supportedLocales, locale: language?.locale, diff --git a/lib/router/go_router.dart b/lib/router/go_router.dart deleted file mode 100644 index a0a0a3e..0000000 --- a/lib/router/go_router.dart +++ /dev/null @@ -1,182 +0,0 @@ -import 'package:fast_immutable_collections/fast_immutable_collections.dart'; -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -import '../common_widgets/cupertino_sheet.dart'; -import '../features/home/home_page.dart'; -import '../features/home/home_page_controller.dart'; -import '../features/instruments/details/instrument_details_page.dart'; -import '../features/instruments/instruments_tab_page.dart'; -import '../features/parades/parades_tab_page.dart'; -import '../features/schools/details/school_details_page.dart'; -import '../features/schools/schools_tab_page.dart'; -import '../initialization_page.dart'; - -part 'go_router.g.dart'; - -/// Required by StatefulShellRoute in GoRouter -final _rootNavigatorKey = GlobalKey(debugLabel: 'root'); - -@riverpod -GoRouter goRouter(GoRouterRef ref) { - final controllers = IMap( - {for (final tab in HomeTab.values) tab.name: ScrollController()}, - ); - final initProvider = ref.watch(initializationProvider); - return GoRouter( - navigatorKey: _rootNavigatorKey, - initialLocation: HomeTab.instruments.path, - errorPageBuilder: (context, state) => const NoTransitionPage( - child: Scaffold(body: Text('404 - Not Found')), - ), - redirect: (context, state) { - if (initProvider.isLoading) { - return InitializationPage.path; - } - if (initProvider.hasError) { - return InitializationPage.path; - } - final path = state.fullPath?.split('/'); - final topPath = path?.sublist(0, 2).join('/'); - if (path != null && HomeTab.values.any((v) => v.path == topPath)) { - final home = ref.read(currentTabProvider); - final nextTab = HomeTab.values.firstWhere( - (t) => t.path == topPath, - ); - final top = path.length <= 2; - if (home.tab.path != nextTab.path || home.topRoute != top) { - Future.microtask( - () => ref.read(currentTabProvider.notifier).set(nextTab, top: top), - ); - } - if (home.tab.path == nextTab.path && home.topRoute == top) { - Future.microtask( - () => _scrollTabToTheTop(controllers[nextTab.name]!), - ); - } - - return null; - } - return null; - }, - routes: [ - GoRoute( - path: InitializationPage.path, - pageBuilder: (context, state) => NoTransitionPage( - child: InitializationPage( - // * Just a placeholder, route will be managed by GoRouter - onLoaded: (_) => const SizedBox.shrink(), - ), - ), - ), - StatefulShellRoute.indexedStack( - pageBuilder: (_, __, shell) => NoTransitionPage(child: HomePage(shell)), - branches: [ - StatefulShellBranch( - routes: [ - GoRoute( - path: InstrumentsTabPage.path, - builder: (_, __) => PrimaryScrollController( - controller: controllers[InstrumentsTabPage.tab.name]!, - child: const InstrumentsTabPage(), - ), - routes: [ - GoRoute( - path: InstrumentDetailsPage.path, - onExit: (context) { - Future.microtask( - () => ref - .read(currentTabProvider.notifier) - .set(InstrumentsTabPage.tab, top: true), - ); - return Future.value(true); - }, - builder: (_, state) => InstrumentDetailsPage( - id: int.parse(state.pathParameters['id']!), - ), - ), - ], - ), - ], - ), - StatefulShellBranch( - routes: [ - GoRoute( - path: ParadesTabPage.path, - builder: (_, __) => PrimaryScrollController( - controller: controllers[ParadesTabPage.tab.name]!, - child: const ParadesTabPage(), - ), - ), - ], - ), - StatefulShellBranch( - routes: [ - GoRoute( - path: SchoolsTabPage.path, - builder: (context, state) { - return PrimaryScrollController( - controller: controllers[SchoolsTabPage.tab.name]!, - child: const SchoolsTabPage(), - ); - }, - routes: [ - GoRoute( - path: SchoolDetailsPage.path, - pageBuilder: (context, state) { - return AppCupertinoSheetPage( - child: SchoolDetailsPage( - id: int.parse(state.pathParameters['id']!), - ), - ); - }, - onExit: (context) { - Future.microtask( - () => ref - .read(currentTabProvider.notifier) - .set(SchoolsTabPage.tab, top: true), - ); - return Future.value(true); - }, - ), - ], - ), - ], - ), - ], - ), - ], - ); -} - -void _scrollTabToTheTop(ScrollController controller) { - if (controller.hasClients) { - controller.animateTo( - 0, - duration: const Duration(milliseconds: 350), - curve: Curves.easeInOut, - ); - } -} - -class SheetPage extends Page { - const SheetPage({ - required this.child, - }) : super(key: const ValueKey('SheetPage')); - - final Widget child; - - static const String routeName = 'Modal Sheet'; - - @override - Route createRoute(BuildContext context) { - return ModalBottomSheetRoute( - settings: this, - builder: (context) => child, - isScrollControlled: true, - ); - } - - @override - String get name => routeName; -} diff --git a/lib/routing/app_router.dart b/lib/routing/app_router.dart new file mode 100644 index 0000000..7f68aaa --- /dev/null +++ b/lib/routing/app_router.dart @@ -0,0 +1,174 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import '../common_widgets/cupertino_sheet_route.dart'; +import '../core/providers/initialization.dart'; +import '../features/home/home_page.dart'; +import '../features/home/home_page_controller.dart'; +import '../features/instruments/details/instrument_details_page.dart'; +import '../features/instruments/instruments_tab_page.dart'; +import '../features/parades/parades_tab_page.dart'; +import '../features/schools/details/school_details_page.dart'; +import '../features/schools/schools_tab_page.dart'; +import '../initialization.dart'; + +part 'app_router.g.dart'; + +/// Required by StatefulShellRoute in GoRouter +final _rootNavigatorKey = GlobalKey(debugLabel: 'root'); + +@riverpod +class AppRouter extends _$AppRouter { + @override + GoRouter build() { + final controllers = { + for (final tab in HomeTab.values) tab.name: ScrollController(), + }; + ref.watch(initializationProvider); + + return GoRouter( + navigatorKey: _rootNavigatorKey, + initialLocation: HomeTab.instruments.path, + errorPageBuilder: (_, __) => const NoTransitionPage( + child: Scaffold(body: Text('404 - Not Found')), + ), + redirect: (_, state) => _redirect(state, controllers), + routes: [ + GoRoute( + path: Initialization.path, + pageBuilder: (_, __) => NoTransitionPage( + child: Initialization( + // * Just a placeholder, route will be managed by GoRouter + onLoaded: (_) => const SizedBox.shrink(), + ), + ), + ), + StatefulShellRoute.indexedStack( + pageBuilder: (_, __, shell) => + NoTransitionPage(child: HomePage(shell)), + branches: [ + StatefulShellBranch( + routes: [ + GoRoute( + path: InstrumentsTabPage.path, + builder: (_, __) => PrimaryScrollController( + controller: controllers[InstrumentsTabPage.tab.name]!, + child: const InstrumentsTabPage(), + ), + routes: [ + GoRoute( + path: InstrumentDetailsPage.path, + onExit: (_, __) { + Future.microtask( + () => ref + .read(homePageControllerProvider.notifier) + .set(InstrumentsTabPage.tab, top: true), + ); + + return Future.value(true); + }, + builder: (_, state) => InstrumentDetailsPage( + id: int.parse(state.pathParameters['id']!), + ), + ), + ], + ), + ], + ), + StatefulShellBranch( + routes: [ + GoRoute( + path: ParadesTabPage.path, + builder: (_, __) => PrimaryScrollController( + controller: controllers[ParadesTabPage.tab.name]!, + child: const ParadesTabPage(), + ), + ), + ], + ), + StatefulShellBranch( + routes: [ + GoRoute( + path: SchoolsTabPage.path, + builder: (_, __) { + return PrimaryScrollController( + controller: controllers[SchoolsTabPage.tab.name]!, + child: const SchoolsTabPage(), + ); + }, + routes: [ + GoRoute( + path: SchoolDetailsPage.path, + pageBuilder: (_, state) { + return AppCupertinoSheetPage( + child: SchoolDetailsPage( + id: int.parse(state.pathParameters['id']!), + ), + ); + }, + onExit: (_, __) { + Future.microtask( + () => ref + .read(homePageControllerProvider.notifier) + .set(SchoolsTabPage.tab, top: true), + ); + + return Future.value(true); + }, + ), + ], + ), + ], + ), + ], + ), + ], + ); + } + + void _scrollTabToTheTop(ScrollController controller) { + if (controller.hasClients) { + controller.animateTo( + 0, + duration: const Duration(milliseconds: 350), + curve: Curves.easeInOut, + ); + } + } + + FutureOr _redirect( + GoRouterState state, + Map controllers, + ) async { + final init = ref.watch(initializationProvider); + if (init.isLoading) { + return Initialization.path; + } + if (init.hasError) { + return Initialization.path; + } + final path = state.fullPath?.split('/'); + final topPath = path?.sublist(0, 2).join('/'); + if (path != null && HomeTab.values.any((v) => v.path == topPath)) { + final home = ref.read(homePageControllerProvider); + final nextTab = HomeTab.values.firstWhere((t) => t.path == topPath); + final top = path.length <= 2; + if (home.tab.path != nextTab.path || home.topRoute != top) { + await Future.microtask( + () => ref + .read(homePageControllerProvider.notifier) + .set(nextTab, top: top), + ); + } + if (home.tab.path == nextTab.path && home.topRoute == top) { + await Future.microtask( + () => _scrollTabToTheTop(controllers[nextTab.name]!), + ); + } + + return null; + } + + return null; + } +} diff --git a/lib/localization/language_app_provider.g.dart b/lib/routing/app_router.g.dart similarity index 59% rename from lib/localization/language_app_provider.g.dart rename to lib/routing/app_router.g.dart index 09d93a2..df26be6 100644 --- a/lib/localization/language_app_provider.g.dart +++ b/lib/routing/app_router.g.dart @@ -1,25 +1,25 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'language_app_provider.dart'; +part of 'app_router.dart'; // ************************************************************************** // RiverpodGenerator // ************************************************************************** -String _$languageAppHash() => r'e30aa5ff94a3c5293ac17e9904bc63cab026eb1e'; +String _$appRouterHash() => r'9eba2f50346adf432762ce32a67d505104b7374e'; -/// See also [LanguageApp]. -@ProviderFor(LanguageApp) -final languageAppProvider = - AsyncNotifierProvider.internal( - LanguageApp.new, - name: r'languageAppProvider', +/// See also [AppRouter]. +@ProviderFor(AppRouter) +final appRouterProvider = + AutoDisposeNotifierProvider.internal( + AppRouter.new, + name: r'appRouterProvider', debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') ? null : _$languageAppHash, + const bool.fromEnvironment('dart.vm.product') ? null : _$appRouterHash, dependencies: null, allTransitiveDependencies: null, ); -typedef _$LanguageApp = AsyncNotifier; +typedef _$AppRouter = AutoDisposeNotifier; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/lib/routing/refresh_listenable.dart b/lib/routing/refresh_listenable.dart new file mode 100644 index 0000000..74d2fdd --- /dev/null +++ b/lib/routing/refresh_listenable.dart @@ -0,0 +1,22 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; + +/// This class was imported from the migration guide for GoRouter 5.0 +class RefreshListenable extends ChangeNotifier { + // ignore: avoid_late_keyword + late final StreamSubscription _subscription; + + RefreshListenable(Stream stream) { + notifyListeners(); + _subscription = stream.asBroadcastStream().listen( + (dynamic _) => notifyListeners(), + ); + } + + @override + void dispose() { + _subscription.cancel(); + super.dispose(); + } +} diff --git a/lib/utils/main_logger.dart b/lib/utils/app_loggers.dart similarity index 94% rename from lib/utils/main_logger.dart rename to lib/utils/app_loggers.dart index f3665b9..18e4d71 100644 --- a/lib/utils/main_logger.dart +++ b/lib/utils/app_loggers.dart @@ -47,9 +47,10 @@ void deactivateLoggers(Set loggers) { } void _printLog(LogRecord record) { + const padLimit = 3; print( '(${record.time.second}.' - '${record.time.millisecond.toString().padLeft(3, '0')})' + '${record.time.millisecond.toString().padLeft(padLimit, '0')})' ' ${record.loggerName} > ${record.level.name}: ${record.message}', ); } diff --git a/lib/utils/debouncer.dart b/lib/utils/debouncer.dart index 146f777..0839932 100644 --- a/lib/utils/debouncer.dart +++ b/lib/utils/debouncer.dart @@ -1,14 +1,15 @@ import 'dart:async'; -/// Duration(milliseconds: 300) -const defaultDelay = Duration(milliseconds: 300); +import '../constants.dart'; class Debouncer { - Debouncer(this.delay); final Duration delay; Timer? _timer; bool _isFirstCall = true; + /// Debouncer constructor with default delay of 300 milliseconds + Debouncer([this.delay = Constants.debouncerDelay]); + void run(void Function() action) { if (_isFirstCall) { action.call(); diff --git a/lib/utils/pagination_scroll_controller.dart b/lib/utils/pagination_scroll_controller.dart deleted file mode 100644 index 176561d..0000000 --- a/lib/utils/pagination_scroll_controller.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:flutter/material.dart'; - -typedef AsyncBoolFunction = Future Function(); - -class PaginationScrollController { - late ScrollController scrollController; - bool isLoading = false; - bool stopLoading = false; - int currentPage = 1; - double boundaryOffset = 0.5; - late AsyncBoolFunction loadAction; - - void init({ - required AsyncBoolFunction onLoadMore, - ScrollController? controller, - }) { - loadAction = onLoadMore; - scrollController = controller ?? ScrollController() - ..addListener(scrollListener); - } - - void dispose() { - scrollController - ..removeListener(scrollListener) - ..dispose(); - } - - void scrollListener() { - if (!stopLoading) { - //load more data - if (scrollController.offset >= - scrollController.position.maxScrollExtent * boundaryOffset && - !isLoading) { - isLoading = true; - loadAction().then((bool shouldStop) { - isLoading = false; - currentPage++; - boundaryOffset = 1 - 1 / (currentPage * 2); - if (shouldStop == true) { - stopLoading = true; - } - }); - } - } - } -} diff --git a/lib/utils/register_error_handler.dart b/lib/utils/register_error_handler.dart new file mode 100644 index 0000000..abd6c75 --- /dev/null +++ b/lib/utils/register_error_handler.dart @@ -0,0 +1,29 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; + +import 'app_loggers.dart'; + +void registerErrorHandlers() { + // * Show some error UI if any uncaught exception happens + FlutterError.onError = (FlutterErrorDetails details) { + FlutterError.presentError(details); + logViews.severe(details.toString()); + }; + // * Handle errors from the underlying platform/OS + PlatformDispatcher.instance.onError = (Object error, StackTrace stack) { + logViews.severe(error.toString(), error, stack); + + return true; + }; + // * Show some error UI when any widget in the app fails to build + ErrorWidget.builder = (FlutterErrorDetails details) { + return Scaffold( + appBar: AppBar( + backgroundColor: Colors.red, + title: const Text('An error occurred'), + ), + body: Center(child: Text(details.toString())), + ); + }; +} diff --git a/lib/utils/screen_size.dart b/lib/utils/screen_size.dart index 49fe54e..7b038a5 100644 --- a/lib/utils/screen_size.dart +++ b/lib/utils/screen_size.dart @@ -1,12 +1,5 @@ import 'package:flutter/material.dart'; -/// For mobile in landscape for non scrollable content -const _smallHeight = 500.0; - -const _smallWidth = 600.0; -const _mediumWidth = 900.0; -const _largeWidth = 1200.0; - enum ScreenSize { xs, md, @@ -17,12 +10,18 @@ enum ScreenSize { bool get isLarge => this == ScreenSize.lg; double get value => switch (this) { - (ScreenSize.xs) => _smallWidth, - (ScreenSize.md) => _mediumWidth, - (ScreenSize.lg) => _largeWidth, + (ScreenSize.xs) => ScreenConstants.smallWidth, + (ScreenSize.md) => ScreenConstants.mediumWidth, + (ScreenSize.lg) => ScreenConstants.largeWidth, + }; + + int get defaultCrossAxisCount => switch (this) { + (ScreenSize.xs) => ScreenConstants.smallAxisCount, + (ScreenSize.md) => ScreenConstants.mediumAxisCount, + (ScreenSize.lg) => ScreenConstants.largeAxisCount, }; - static double get smallHeight => _smallHeight; + static double get smallHeight => ScreenConstants.smallHeight; } extension MediaQueryExtension on BuildContext { @@ -30,6 +29,25 @@ extension MediaQueryExtension on BuildContext { final width = MediaQuery.sizeOf(this).width; if (width < ScreenSize.xs.value) return ScreenSize.xs; if (width < ScreenSize.md.value) return ScreenSize.md; + return ScreenSize.lg; } + + bool get isSmallHeight => + MediaQuery.sizeOf(this).height < ScreenSize.smallHeight; +} + +final class ScreenConstants { + /// For mobile in landscape for non scrollable content + static const smallHeight = 500.0; + + static const smallWidth = 600.0; + static const mediumWidth = 900.0; + static const largeWidth = 1200.0; + + static const smallAxisCount = 1; + static const mediumAxisCount = 2; + static const largeAxisCount = 3; + + ScreenConstants._(); } diff --git a/pubspec.lock b/pubspec.lock index bd356ed..db3d582 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -37,10 +37,10 @@ packages: dependency: transitive description: name: archive - sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" + sha256: "0763b45fa9294197a2885c8567927e2830ade852e5c896fd4ab7e0e348d0f373" url: "https://pub.dev" source: hosted - version: "3.4.10" + version: "3.5.0" args: dependency: transitive description: @@ -193,14 +193,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" - country_picker: - dependency: "direct main" - description: - name: country_picker - sha256: "98a7cddb9413293d9aea13829120686c0a37c8854ba20088dc9f66ad28b503eb" - url: "https://pub.dev" - source: hosted - version: "2.0.25" cronet_http: dependency: transitive description: @@ -325,10 +317,10 @@ packages: dependency: "direct main" description: name: easy_image_viewer - sha256: "750bb85e0a34504557d378a616110540caeec2324490fc040709589219e75834" + sha256: eca828c492d580f9bd775495b281cf6481d34109b73010f1e9a3db87c4d2e5f7 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.5.0" extended_image: dependency: "direct main" description: @@ -489,10 +481,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: "771c8feb40ad0ef639973d7ecf1b43d55ffcedb2207fd43fab030f5639e40446" + sha256: "9e0f7d1a3e7dc5010903e330fbc5497872c4c3cf6626381d69083cc1d5113c1e" url: "https://pub.dev" source: hosted - version: "13.2.4" + version: "14.0.2" graphs: dependency: transitive description: @@ -541,6 +533,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + http_mock_adapter: + dependency: "direct dev" + description: + name: http_mock_adapter + sha256: "46399c78bd4a0af071978edd8c502d7aeeed73b5fb9860bca86b5ed647a63c1b" + url: "https://pub.dev" + source: hosted + version: "0.6.1" http_multi_server: dependency: transitive description: @@ -609,10 +609,10 @@ packages: dependency: transitive description: name: json_annotation - sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" url: "https://pub.dev" source: hosted - version: "4.8.1" + version: "4.9.0" leak_tracker: dependency: transitive description: @@ -637,6 +637,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" + logger: + dependency: transitive + description: + name: logger + sha256: "8c94b8c219e7e50194efc8771cd0e9f10807d8d3e219af473d89b06cc2ee4e04" + url: "https://pub.dev" + source: hosted + version: "2.2.0" logging: dependency: "direct main" description: @@ -677,6 +685,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" + mocktail: + dependency: "direct dev" + description: + name: mocktail + sha256: c4b5007d91ca4f67256e720cb1b6d704e79a510183a12fa551021f652577dce6 + url: "https://pub.dev" + source: hosted + version: "1.0.3" native_dio_adapter: dependency: "direct main" description: @@ -773,14 +789,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" - pointycastle: - dependency: transitive - description: - name: pointycastle - sha256: "70fe966348fe08c34bf929582f1d8247d9d9408130723206472b4687227e4333" - url: "https://pub.dev" - source: hosted - version: "3.8.0" pool: dependency: transitive description: @@ -809,10 +817,10 @@ packages: dependency: "direct main" description: name: pull_down_button - sha256: "235b302701ce029fd9e9470975069376a6700935bb47a5f1b3ec8a5efba07e6f" + sha256: "48b928203afdeafa4a8be5dc96980523bc8a2ddbd04569f766071a722be22379" url: "https://pub.dev" source: hosted - version: "0.9.3" + version: "0.9.4" riverpod: dependency: transitive description: @@ -954,6 +962,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.12" + solid_lints: + dependency: "direct dev" + description: + name: solid_lints + sha256: "3873959a106ca8683c9b64bfb331d318699fdae0e5c79a4420205f06f403f764" + url: "https://pub.dev" + source: hosted + version: "0.1.2" source_gen: dependency: transitive description: @@ -1110,10 +1126,10 @@ packages: dependency: transitive description: name: vm_service - sha256: a75f83f14ad81d5fe4b3319710b90dec37da0e22612326b696c9e1b8f34bbf48 + sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" url: "https://pub.dev" source: hosted - version: "14.2.0" + version: "14.2.1" watcher: dependency: transitive description: @@ -1134,10 +1150,10 @@ packages: dependency: transitive description: name: web_socket - sha256: "6bbfb36ea811dac44d18511648687d5334cbad736f45880520f34995861d9b11" + sha256: bfe704c186c6e32a46f6607f94d079cd0b747b9a489fceeecc93cd3adb98edd5 url: "https://pub.dev" source: hosted - version: "0.1.1" + version: "0.1.3" web_socket_channel: dependency: transitive description: @@ -1150,10 +1166,10 @@ packages: dependency: transitive description: name: win32 - sha256: "0a989dc7ca2bb51eac91e8fd00851297cfffd641aa7538b165c62637ca0eaa4a" + sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb" url: "https://pub.dev" source: hosted - version: "5.4.0" + version: "5.5.0" xdg_directories: dependency: transitive description: @@ -1180,4 +1196,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.3.3 <4.0.0" - flutter: ">=3.22.0-0.1.pre" + flutter: ">=3.22.0-0.3.pre" diff --git a/pubspec.yaml b/pubspec.yaml index 6b34d49..d20bbd0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,10 +7,9 @@ version: 1.0.0+1 environment: sdk: '>=3.3.3 <4.0.0' - flutter: 3.22.0-0.1.pre + flutter: 3.22.0-0.3.pre dependencies: - country_picker: ^2.0.25 cupertino_icons: ^1.0.6 dart_mappable: ^4.2.2 dio: ^5.4.3 @@ -29,7 +28,7 @@ dependencies: sdk: flutter flutter_riverpod: ^3.0.0-dev.3 flutter_staggered_grid_view: ^0.7.0 - go_router: ^13.2.1 + go_router: ^14.0.2 intl: ^0.19.0 logging: ^1.2.0 native_dio_adapter: ^1.3.0 @@ -49,11 +48,13 @@ dev_dependencies: flutter_native_splash: ^2.4.0 flutter_test: sdk: flutter + http_mock_adapter: ^0.6.1 icons_launcher: ^2.1.7 + mocktail: ^1.0.3 riverpod_generator: ^3.0.0-dev.11 riverpod_lint: ^3.0.0-dev.4 + solid_lints: ^0.1.2 very_good_analysis: ^5.1.0 - flutter: uses-material-design: true diff --git a/test/analysis_options.yaml b/test/analysis_options.yaml new file mode 100644 index 0000000..6e3f377 --- /dev/null +++ b/test/analysis_options.yaml @@ -0,0 +1 @@ +include: package:solid_lints/analysis_options_test.yaml \ No newline at end of file diff --git a/test/core/providers/client_network_provider_test.dart b/test/core/providers/client_network_provider_test.dart new file mode 100644 index 0000000..02112cf --- /dev/null +++ b/test/core/providers/client_network_provider_test.dart @@ -0,0 +1,79 @@ +// ignore_for_file: depend_on_referenced_packages +// ignore_for_file: invalid_use_of_visible_for_overriding_member + +import 'package:batucadapp/constants.dart'; +import 'package:batucadapp/core/providers/client_network.dart'; +import 'package:dio/dio.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http_mock_adapter/http_mock_adapter.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../utils.dart'; + +class MockClientNetwork extends AsyncNotifier + with Mock + implements ClientNetwork {} + +// a generic Listener class, used to track a provider notifying its listeners +class Listener extends Mock { + void call(T? previous, T next); +} + +void main() { + const dioOkResponseCode = 200; + group('ClientNetwork build', () { + test('Dio is configured with correct base URL and language', () async { + final baseOptions = BaseOptions( + baseUrl: Endpoint.basePath.path, + connectTimeout: Constants.connectTimeout, + receiveTimeout: Constants.receiveTimeout, + queryParameters: {'language': 'en'}, + ); + final dio = Dio(baseOptions); + final mock = MockClientNetwork(); + when(mock.build).thenReturn(dio); + + final container = createContainer( + overrides: [clientNetworkProvider.overrideWith(() => mock)], + ); + + // create a listener + final listener = Listener>(); + // listen to the provider and call [listener] whenever its value changes + container.listen( + clientNetworkProvider, + listener, + fireImmediately: true, + ); + + final readDio = await container.read(clientNetworkProvider.future); + DioAdapter(dio: readDio).onGet( + Endpoint.basePath.path, + (server) => server.reply(dioOkResponseCode, 'OK'), + ); + + final response = await readDio.get(Endpoint.basePath.path); + + // verify + verify(() => listener(null, AsyncData(dio))); + // verify that the listener is no longer called + verifyNoMoreInteractions(listener); + expect(response.data, 'OK'); + expect( + readDio.options, + isA() + .having( + (options) => options.baseUrl, + 'baseUrl', + Endpoint.basePath.path, + ) + .having( + (options) => options.queryParameters, + 'queryParameters', + {'language': 'en'}, + ), + ); + }); + }); +} diff --git a/test/core/providers/prefs_provider_test.dart b/test/core/providers/prefs_provider_test.dart new file mode 100644 index 0000000..b3e2aaf --- /dev/null +++ b/test/core/providers/prefs_provider_test.dart @@ -0,0 +1,56 @@ +import 'package:batucadapp/core/providers/prefs.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +void main() { + ProviderContainer makeProviderContainer(MockSharedPreferences prefs) { + final container = ProviderContainer( + overrides: [prefsProvider.overrideWith((_) => prefs)], + ); + addTearDown(container.dispose); + + return container; + } + + test('Should return a SharedPreferences instance', () async { + final prefs = MockSharedPreferences(); + final container = makeProviderContainer(prefs); + await expectLater( + container.read(prefsProvider.future), + completion(prefs), + ); + }); +} + +class MockSharedPreferences extends Mock implements SharedPreferences { + //setMockInitialValues + static MockSharedPreferences setMockInitialValues( + Map values, + ) { + final mock = MockSharedPreferences(); + when(mock.getKeys).thenReturn(values.keys.toSet()); + for (final entry in values.entries) { + final key = entry.key; + final value = entry.value; + if (value is bool) { + when(() => mock.getBool(key)).thenReturn(value); + } else if (value is num) { + if (value is int) { + when(() => mock.getInt(key)).thenReturn(value); + } else if (value is double) { + when(() => mock.getDouble(key)).thenReturn(value); + } + } else if (value is String) { + when(() => mock.getString(key)).thenReturn(value); + } else if (value is List) { + when(() => mock.getStringList(key)).thenReturn(value); + } else { + throw UnimplementedError('Type ${value.runtimeType} not implemented'); + } + } + + return mock; + } +} diff --git a/test/core/theme/theme_provider_test.dart b/test/core/theme/theme_provider_test.dart new file mode 100644 index 0000000..39c2f3d --- /dev/null +++ b/test/core/theme/theme_provider_test.dart @@ -0,0 +1,169 @@ +import 'package:batucadapp/core/providers/prefs.dart'; +import 'package:batucadapp/core/theme/theme_mode_controller.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../providers/prefs_provider_test.dart'; + +void main() { + ProviderContainer makeProviderContainer( + MockSharedPreferences? mockSharedPreferences, + ) { + final container = ProviderContainer( + overrides: [ + if (mockSharedPreferences != null) + prefsProvider.overrideWith((_) => mockSharedPreferences), + themeModeControllerProvider.overrideWith(ThemeModeController.new), + ], + ); + addTearDown(container.dispose); + + return container; + } + + group('appThemeTrueBlackProvider tests', () { + test('Return false when SharedPreferences doesnt have a value', () { + final container = makeProviderContainer(null); + final trueBlack = container.read(appThemeTrueBlackProvider); + expect(trueBlack, false); + }); + + test('Return true when SharedPreferences has a value', () { + final initialValues = {'true_black': true}; + final mock = MockSharedPreferences.setMockInitialValues(initialValues); + final container = makeProviderContainer(mock); + final trueBlack = container.read(appThemeTrueBlackProvider); + expect(trueBlack, true); + }); + + test('Toggle trueBlack to true', () { + final mock = MockSharedPreferences(); + when(() => mock.setBool('true_black', true)) + .thenAnswer((_) async => true); + final container = makeProviderContainer(mock); + container.read(appThemeTrueBlackProvider.notifier).toggleTrueBlack(); + verify(() => mock.setBool('true_black', true)).called(1); + expect(container.read(appThemeTrueBlackProvider), true); + }); + + test('Toggle trueBlack to false', () { + final values = {'true_black': true}; + final mock = MockSharedPreferences.setMockInitialValues(values); + when(() => mock.remove('true_black')).thenAnswer((_) async => true); + final container = makeProviderContainer(mock); + container.read(appThemeTrueBlackProvider.notifier).toggleTrueBlack(); + expect(container.read(appThemeTrueBlackProvider), false); + }); + }); + + group('Test the build of the appThemeProvider', () { + test(' ThemeMode.system when SharedPreferences has no value', () async { + final mockSharedPreferences = MockSharedPreferences(); + final container = makeProviderContainer(mockSharedPreferences); + final themeMode = container.read(themeModeControllerProvider); + expect(themeMode, ThemeMode.system); + }); + + test(' ThemeMode.system when SharedPreferences is not init', () async { + final container = makeProviderContainer(null); + final themeMode = container.read(themeModeControllerProvider); + expect(themeMode, ThemeMode.system); + }); + + test(' ThemeMode.light when SharedPreferences has value light', () async { + final mockSharedPreferences = MockSharedPreferences(); + when(() => mockSharedPreferences.getString('theme_mode')) + .thenReturn('light'); + final container = makeProviderContainer(mockSharedPreferences); + final themeMode = container.read(themeModeControllerProvider); + expect(themeMode, ThemeMode.light); + }); + + test(' ThemeMode.dark when SharedPreferences has value dark', () async { + final mockSharedPreferences = MockSharedPreferences(); + when(() => mockSharedPreferences.getString('theme_mode')) + .thenReturn('dark'); + final container = makeProviderContainer(mockSharedPreferences); + final themeMode = container.read(themeModeControllerProvider); + expect(themeMode, ThemeMode.dark); + }); + }); + + group('Test the setTheme function of appThemeProvider ', () { + test('Set ThemeMode.system', () async { + final mockSharedPreferences = MockSharedPreferences(); + when(() => mockSharedPreferences.remove('theme_mode')) + .thenAnswer((_) async => true); + final container = makeProviderContainer(mockSharedPreferences); + expect(container.read(themeModeControllerProvider), ThemeMode.system); + container + .read(themeModeControllerProvider.notifier) + .setTheme(ThemeMode.system); + verify(() => mockSharedPreferences.remove('theme_mode')).called(1); + expect(container.read(themeModeControllerProvider), ThemeMode.system); + }); + + test('Set ThemeMode.light', () async { + final mockSharedPreferences = MockSharedPreferences(); + when(() => mockSharedPreferences.setString('theme_mode', 'light')) + .thenAnswer((_) async => true); + final container = makeProviderContainer(mockSharedPreferences); + container + .read(themeModeControllerProvider.notifier) + .setTheme(ThemeMode.light); + verify(() => mockSharedPreferences.setString('theme_mode', 'light')) + .called(1); + expect(container.read(themeModeControllerProvider), ThemeMode.light); + }); + + test('Set ThemeMode.dark', () async { + final mockSharedPreferences = MockSharedPreferences(); + when(() => mockSharedPreferences.setString('theme_mode', 'dark')) + .thenAnswer((_) async => true); + final container = makeProviderContainer(mockSharedPreferences); + container + .read(themeModeControllerProvider.notifier) + .setTheme(ThemeMode.dark); + verify(() => mockSharedPreferences.setString('theme_mode', 'dark')) + .called(1); + expect(container.read(themeModeControllerProvider), ThemeMode.dark); + }); + }); + + group('Apptheme toggleTheme tests', () { + test('Toggle theme from ThemeMode.system', () async { + final mockSharedPreferences = MockSharedPreferences(); + when(() => mockSharedPreferences.setString('theme_mode', 'light')) + .thenAnswer((_) async => true); + when(() => mockSharedPreferences.remove('true_black')) + .thenAnswer((_) async => true); + final container = makeProviderContainer(mockSharedPreferences); + await container.read(themeModeControllerProvider.notifier).toggleTheme(); + expect(container.read(themeModeControllerProvider), ThemeMode.light); + }); + + test('Toggle theme from ThemeMode.light', () async { + final initialValue = {'theme_mode': 'light'}; + final mockSharedPreferences = + MockSharedPreferences.setMockInitialValues(initialValue); + when(() => mockSharedPreferences.setString('theme_mode', 'dark')) + .thenAnswer((_) async => true); + final container = makeProviderContainer(mockSharedPreferences); + await container.read(themeModeControllerProvider.notifier).toggleTheme(); + expect(container.read(themeModeControllerProvider), ThemeMode.dark); + }); + + test('Toggle theme from ThemeMode.dark', () async { + final initialValue = {'theme_mode': 'dark'}; + final mockSharedPreferences = + MockSharedPreferences.setMockInitialValues(initialValue); + when(() => mockSharedPreferences.remove('theme_mode')) + .thenAnswer((_) async => true); + final container = makeProviderContainer(mockSharedPreferences); + await container.read(themeModeControllerProvider.notifier).toggleTheme(); + expect(container.read(themeModeControllerProvider), ThemeMode.system); + }); + }); +} diff --git a/test/features/instruments/instruments_repo_test.dart b/test/features/instruments/instruments_repo_test.dart new file mode 100644 index 0000000..6d1384d --- /dev/null +++ b/test/features/instruments/instruments_repo_test.dart @@ -0,0 +1,75 @@ +import 'package:batucadapp/features/instruments/instrument.dart'; +import 'package:batucadapp/features/instruments/instruments_repo.dart'; +import 'package:batucadapp/utils/immutable_list.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../utils.dart'; + +class InstrumentsRepoMock with Mock implements InstrumentsRepoImpls {} + +void main() { + group('Instruments Repository', () { + test( + ''' + When InstrumentsRepo is called + Should return a instance of InstrumentsRepoImpls + ''', + () { + final mock = InstrumentsRepoMock(); + final container = createContainer( + overrides: [instrumentsRepoProvider.overrideWith((_) => mock)], + ); + + expect( + container.read(instrumentsRepoProvider), + isA(), + ); + }, + ); + + test( + ''' + When getInstruments is called + Should return a immutable list of instruments + ''', + () { + final mock = InstrumentsRepoMock(); + when(mock.getInstruments).thenAnswer( + (_) async => ImmutableList(const []), + ); + final container = createContainer( + overrides: [instrumentsRepoProvider.overrideWith((_) => mock)], + ); + + expectLater( + container.read(instrumentsRepoProvider).getInstruments(), + completion(isA>()), + ); + verify(mock.getInstruments).called(1); + }, + ); + + test( + ''' + When getInstruments is called + Should return a immutable list of instruments + ''', + () async { + final mock = InstrumentsRepoMock(); + when(mock.getInstruments).thenThrow( + Exception("Error"), + ); + final container = createContainer( + overrides: [instrumentsRepoProvider.overrideWith((_) => mock)], + ); + + await expectLater( + () => container.read(instrumentsRepoProvider).getInstruments(), + throwsA(isA()), + ); + verify(mock.getInstruments).called(1); + }, + ); + }); +} diff --git a/test/features/schools/schools_repo_test.dart b/test/features/schools/schools_repo_test.dart new file mode 100644 index 0000000..891eb80 --- /dev/null +++ b/test/features/schools/schools_repo_test.dart @@ -0,0 +1,95 @@ +import 'package:batucadapp/features/schools/school.dart'; +import 'package:batucadapp/features/schools/schools_repo.dart'; +import 'package:batucadapp/utils/immutable_list.dart'; +import 'package:dio/dio.dart'; +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../utils.dart'; + +class SchoolsRepoMock with Mock implements SchoolsRepoImpls {} + +void main() { + group('Schools Repository', () { + test( + ''' + When SchoolsRepo is called + Should return a instance of SchoolsRepoImpls + ''', + () { + final mock = SchoolsRepoMock(); + final container = createContainer( + overrides: [schoolsRepoProvider.overrideWith((_) => mock)], + ); + + expect( + container.read(schoolsRepoProvider), + isA(), + ); + }, + ); + + test(''' + When SchoolsRepo.getSchools is called + Should return a list of Schools + ''', () async { + const page = 1; + const pageSize = 10; + final mock = SchoolsRepoMock(); + when( + () => mock.getSchools( + page: any(named: 'page'), + pageSize: any(named: 'pageSize'), + sort: any(named: 'sort'), + search: any(named: 'search'), + ), + ).thenAnswer((_) async => [].lock); + + final container = createContainer( + overrides: [schoolsRepoProvider.overrideWith((_) => mock)], + ); + + expect( + await container.read(schoolsRepoProvider).getSchools( + page: page, + pageSize: pageSize, + sort: 'name', + search: '', + ), + isA>(), + ); + }); + + test(''' + When SchoolsRepo.getSchools is called + Should throw an error + ''', () async { + const page = 1; + const pageSize = 10; + final mock = SchoolsRepoMock(); + when( + () => mock.getSchools( + page: any(named: 'page'), + pageSize: any(named: 'pageSize'), + sort: any(named: 'sort'), + search: any(named: 'search'), + ), + ).thenThrow(DioException(requestOptions: RequestOptions())); + + final container = createContainer( + overrides: [schoolsRepoProvider.overrideWith((_) => mock)], + ); + + expect( + () => container.read(schoolsRepoProvider).getSchools( + page: page, + pageSize: pageSize, + sort: 'name', + search: '', + ), + throwsA(isA()), + ); + }); + }); +} diff --git a/test/localization/language_app_provider_test.dart b/test/localization/language_app_provider_test.dart new file mode 100644 index 0000000..b66b0e1 --- /dev/null +++ b/test/localization/language_app_provider_test.dart @@ -0,0 +1,99 @@ +import 'package:batucadapp/core/providers/prefs.dart'; +import 'package:batucadapp/localization/language.dart'; +import 'package:batucadapp/localization/language_app_controller.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../core/providers/prefs_provider_test.dart'; + +ProviderContainer makeProviderContainer( + MockSharedPreferences? mockSharedPreferences, +) { + final container = ProviderContainer( + overrides: [ + if (mockSharedPreferences != null) + prefsProvider.overrideWith((_) => mockSharedPreferences), + languageAppControllerProvider.overrideWith(LanguageAppController.new), + ], + ); + addTearDown(container.dispose); + + return container; +} + +void main() { + setUpAll(TestWidgetsFlutterBinding.ensureInitialized); + + group('Build Language App Provider', () { + test('Return default when SharedPreferences doesnt have a value', () async { + final container = makeProviderContainer(MockSharedPreferences()); + final language = + await container.read(languageAppControllerProvider.future); + expect( + language.languageCode, + TestWidgetsFlutterBinding + .instance.platformDispatcher.locale.languageCode, + ); + }); + + test('Return default when SharedPreferences nor Flutter have a right value', + () async { + TestWidgetsFlutterBinding.instance.platformDispatcher.localeTestValue = + const Locale('und', ''); + final container = makeProviderContainer(MockSharedPreferences()); + final language = + await container.read(languageAppControllerProvider.future); + expect( + language.languageCode, + 'en', + ); + }); + + test('Return value from SharedPreferences', () async { + final initialData = { + 'locale': Language.pt.languageCode, + }; + final mockPrefs = MockSharedPreferences.setMockInitialValues(initialData); + final container = makeProviderContainer(mockPrefs); + final language = + await container.read(languageAppControllerProvider.future); + expect(language, Language.pt); + }); + }); + + group('Set Language', () { + test('Set custom language to SharedPreferences', () async { + TestWidgetsFlutterBinding.instance.platformDispatcher.localeTestValue = + const Locale('en', ''); + final initialData = {'locale': Language.pt.languageCode}; + final mock = MockSharedPreferences.setMockInitialValues(initialData); + when(() => mock.setString('locale', any())).thenAnswer((_) async => true); + final container = makeProviderContainer(mock); + final languageApp = + container.read(languageAppControllerProvider.notifier); + await languageApp.setLanguage(Language.es); + await expectLater( + container.read(languageAppControllerProvider.future), + completion(Language.es), + ); + }); + + test('Set default platform language', () async { + TestWidgetsFlutterBinding.instance.platformDispatcher.localeTestValue = + const Locale('es', ''); + final initialData = {'locale': Language.pt.languageCode}; + final mock = MockSharedPreferences.setMockInitialValues(initialData); + when(() => mock.remove('locale')).thenAnswer((_) async => true); + final container = makeProviderContainer(mock); + final languageApp = + container.read(languageAppControllerProvider.notifier); + await languageApp.setLanguage(Language.es); + await expectLater( + container.read(languageAppControllerProvider.future), + completion(Language.es), + ); + }); + }); +} diff --git a/test/utils.dart b/test/utils.dart new file mode 100644 index 0000000..c3fd97f --- /dev/null +++ b/test/utils.dart @@ -0,0 +1,20 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; + +ProviderContainer createContainer({ + ProviderContainer? parent, + List overrides = const [], + List? observers, +}) { + // Create a ProviderContainer, and optionally allow specifying parameters. + final container = ProviderContainer( + parent: parent, + overrides: overrides, + observers: observers, + ); + + // When the test ends, dispose the container. + addTearDown(container.dispose); + + return container; +}