From 6a9036ce5eed82cd312334ff698411bb85be6333 Mon Sep 17 00:00:00 2001 From: Vadim Date: Tue, 1 Aug 2023 12:58:43 +0200 Subject: [PATCH 01/17] Camera is taking too long to take a picture --- .../components/camera_container/bloc_container_camera.dart | 1 + pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/screens/metering/components/camera_container/bloc_container_camera.dart b/lib/screens/metering/components/camera_container/bloc_container_camera.dart index cafce13..fee9899 100644 --- a/lib/screens/metering/components/camera_container/bloc_container_camera.dart +++ b/lib/screens/metering/components/camera_container/bloc_container_camera.dart @@ -129,6 +129,7 @@ class CameraContainerBloc extends EvSourceBlocBase([ _cameraController!.getMinZoomLevel(), diff --git a/pubspec.yaml b/pubspec.yaml index 1f8ed64..0015829 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -9,7 +9,7 @@ environment: dependencies: app_settings: 4.2.0 bloc_concurrency: 0.2.2 - camera: 0.10.5 + camera: 0.10.5+2 clipboard: 0.1.3 dynamic_color: 1.6.5 exif: 3.1.4 From 50c2460f16494b69fc60a9c2eea632ca2273cdd8 Mon Sep 17 00:00:00 2001 From: vodemn Date: Tue, 1 Aug 2023 11:08:55 +0000 Subject: [PATCH 02/17] Version bump --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 0015829..e2c7b6d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: lightmeter description: A new Flutter project. publish_to: "none" -version: 0.12.2+33 +version: 0.12.3+34 environment: sdk: ">=3.0.0 <4.0.0" From c12cfb16976b69c19affc4f802852c936741f845 Mon Sep 17 00:00:00 2001 From: Vadim Date: Thu, 3 Aug 2023 22:46:01 +0200 Subject: [PATCH 03/17] Lock & release focus when taking a picture --- .../components/camera_container/bloc_container_camera.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/screens/metering/components/camera_container/bloc_container_camera.dart b/lib/screens/metering/components/camera_container/bloc_container_camera.dart index fee9899..9c8658d 100644 --- a/lib/screens/metering/components/camera_container/bloc_container_camera.dart +++ b/lib/screens/metering/components/camera_container/bloc_container_camera.dart @@ -129,7 +129,6 @@ class CameraContainerBloc extends EvSourceBlocBase([ _cameraController!.getMinZoomLevel(), @@ -206,7 +205,13 @@ class CameraContainerBloc extends EvSourceBlocBase _takePhoto() async { try { + // https://github.com/flutter/flutter/issues/84957#issuecomment-1661155095 + await _cameraController!.setFocusMode(FocusMode.locked); + await _cameraController!.setExposureMode(ExposureMode.locked); final file = await _cameraController!.takePicture(); + await _cameraController!.setFocusMode(FocusMode.auto); + await _cameraController!.setExposureMode(ExposureMode.auto); + final Uint8List bytes = await file.readAsBytes(); Directory(file.path).deleteSync(recursive: true); From 6e1aaf5acf0d707d6853048d07cda87a037457c2 Mon Sep 17 00:00:00 2001 From: vodemn Date: Thu, 3 Aug 2023 20:54:33 +0000 Subject: [PATCH 04/17] Version bump --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index e2c7b6d..315e72f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: lightmeter description: A new Flutter project. publish_to: "none" -version: 0.12.3+34 +version: 0.12.4+35 environment: sdk: ">=3.0.0 <4.0.0" From 8a71c8db13dbbd6b380b36091ec8d27bd15bd0d9 Mon Sep 17 00:00:00 2001 From: Vadim Date: Fri, 4 Aug 2023 16:17:40 +0200 Subject: [PATCH 05/17] Added switch animations to `MeteringScreen` --- lib/res/dimens.dart | 1 + .../widget_placeholder_camera_view.dart | 5 +- .../widget_container_camera.dart | 26 ++-- .../widget_list_exposure_pairs.dart | 112 +++++++++--------- .../widget_container_reading_value.dart | 15 ++- 5 files changed, 82 insertions(+), 77 deletions(-) diff --git a/lib/res/dimens.dart b/lib/res/dimens.dart index 1eefdc1..a1bb780 100644 --- a/lib/res/dimens.dart +++ b/lib/res/dimens.dart @@ -25,6 +25,7 @@ class Dimens { static const Duration durationM = Duration(milliseconds: 200); static const Duration durationML = Duration(milliseconds: 250); static const Duration durationL = Duration(milliseconds: 300); + static const Duration switchDuration = Duration(milliseconds: 100); static const double enabledOpacity = 1.0; static const double disabledOpacity = 0.38; diff --git a/lib/screens/metering/components/camera_container/components/camera_view_placeholder/widget_placeholder_camera_view.dart b/lib/screens/metering/components/camera_container/components/camera_view_placeholder/widget_placeholder_camera_view.dart index 058da74..a2914d2 100644 --- a/lib/screens/metering/components/camera_container/components/camera_view_placeholder/widget_placeholder_camera_view.dart +++ b/lib/screens/metering/components/camera_container/components/camera_view_placeholder/widget_placeholder_camera_view.dart @@ -10,10 +10,9 @@ class CameraViewPlaceholder extends StatelessWidget { @override Widget build(BuildContext context) { return Card( + color: error != null ? null : Colors.black, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(Dimens.borderRadiusM)), - child: Center( - child: error != null ? const Icon(Icons.no_photography) : const CircularProgressIndicator(), - ), + child: Center(child: error != null ? const Icon(Icons.no_photography) : null), ); } } diff --git a/lib/screens/metering/components/camera_container/widget_container_camera.dart b/lib/screens/metering/components/camera_container/widget_container_camera.dart index 2f8ef45..7ac43eb 100644 --- a/lib/screens/metering/components/camera_container/widget_container_camera.dart +++ b/lib/screens/metering/components/camera_container/widget_container_camera.dart @@ -110,17 +110,17 @@ class _CameraViewBuilder extends StatelessWidget { return AspectRatio( aspectRatio: PlatformConfig.cameraPreviewAspectRatio, child: BlocBuilder( - buildWhen: (previous, current) => - current is CameraLoadingState || - current is CameraInitializedState || - current is CameraErrorState, - builder: (context, state) { - if (state is CameraInitializedState) { - return Center(child: CameraView(controller: state.controller)); - } else { - return CameraViewPlaceholder(error: state is CameraErrorState ? state.error : null); - } - }, + buildWhen: (previous, current) => current is! CameraActiveState, + builder: (context, state) => Center( + child: AnimatedSwitcher( + duration: Dimens.durationM, + child: switch (state) { + CameraInitializedState() => CameraView(controller: state.controller), + CameraErrorState() => CameraViewPlaceholder(error: state.error), + _ => const CameraViewPlaceholder(error: null), + }, + ), + ), ), ); } @@ -161,11 +161,11 @@ class _CameraControlsBuilder extends StatelessWidget { }, ); } else { - child = const SizedBox.shrink(); + child = const Column(children: [Expanded(child: SizedBox.shrink())],); } return AnimatedSwitcher( - duration: Dimens.durationS, + duration: Dimens.switchDuration, child: child, ); }, diff --git a/lib/screens/metering/components/shared/exposure_pairs_list/widget_list_exposure_pairs.dart b/lib/screens/metering/components/shared/exposure_pairs_list/widget_list_exposure_pairs.dart index f2496b2..71b293e 100644 --- a/lib/screens/metering/components/shared/exposure_pairs_list/widget_list_exposure_pairs.dart +++ b/lib/screens/metering/components/shared/exposure_pairs_list/widget_list_exposure_pairs.dart @@ -12,70 +12,72 @@ class ExposurePairsList extends StatelessWidget { @override Widget build(BuildContext context) { - if (exposurePairs.isEmpty) { - return const EmptyExposurePairsList(); - } - return Stack( - alignment: Alignment.center, - children: [ - Positioned.fill( - child: ListView.builder( - key: ValueKey(exposurePairs.hashCode), - padding: const EdgeInsets.symmetric(vertical: Dimens.paddingL), - itemCount: exposurePairs.length, - itemBuilder: (_, index) => Stack( + return AnimatedSwitcher( + duration: Dimens.switchDuration, + child: exposurePairs.isEmpty + ? const EmptyExposurePairsList() + : Stack( alignment: Alignment.center, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: Align( - alignment: Alignment.centerLeft, - child: ExposurePairsListItem( - exposurePairs[index].aperture, - tickOnTheLeft: false, + Positioned.fill( + child: ListView.builder( + key: ValueKey(exposurePairs.hashCode), + padding: const EdgeInsets.symmetric(vertical: Dimens.paddingL), + itemCount: exposurePairs.length, + itemBuilder: (_, index) => Stack( + alignment: Alignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Align( + alignment: Alignment.centerLeft, + child: ExposurePairsListItem( + exposurePairs[index].aperture, + tickOnTheLeft: false, + ), + ), + ), + Expanded( + child: Align( + alignment: Alignment.centerLeft, + child: ExposurePairsListItem( + exposurePairs[index].shutterSpeed, + tickOnTheLeft: true, + ), + ), + ), + ], ), - ), - ), - Expanded( - child: Align( - alignment: Alignment.centerLeft, - child: ExposurePairsListItem( - exposurePairs[index].shutterSpeed, - tickOnTheLeft: true, + Positioned( + top: 0, + bottom: 0, + child: LayoutBuilder( + builder: (context, constraints) => Align( + alignment: index == 0 + ? Alignment.bottomCenter + : (index == exposurePairs.length - 1 + ? Alignment.topCenter + : Alignment.center), + child: SizedBox( + height: index == 0 || index == exposurePairs.length - 1 + ? constraints.maxHeight / 2 + : constraints.maxHeight, + child: ColoredBox( + color: Theme.of(context).colorScheme.onBackground, + child: const SizedBox(width: 1), + ), + ), + ), + ), ), - ), - ), - ], - ), - Positioned( - top: 0, - bottom: 0, - child: LayoutBuilder( - builder: (context, constraints) => Align( - alignment: index == 0 - ? Alignment.bottomCenter - : (index == exposurePairs.length - 1 - ? Alignment.topCenter - : Alignment.center), - child: SizedBox( - height: index == 0 || index == exposurePairs.length - 1 - ? constraints.maxHeight / 2 - : constraints.maxHeight, - child: ColoredBox( - color: Theme.of(context).colorScheme.onBackground, - child: const SizedBox(width: 1), - ), - ), + ], ), ), ), ], ), - ), - ), - ], ); } } diff --git a/lib/screens/metering/components/shared/readings_container/components/reading_value_container/widget_container_reading_value.dart b/lib/screens/metering/components/shared/readings_container/components/reading_value_container/widget_container_reading_value.dart index b40f666..3254456 100644 --- a/lib/screens/metering/components/shared/readings_container/components/reading_value_container/widget_container_reading_value.dart +++ b/lib/screens/metering/components/shared/readings_container/components/reading_value_container/widget_container_reading_value.dart @@ -72,12 +72,15 @@ class _ReadingValueBuilder extends StatelessWidget { softWrap: false, ), const SizedBox(height: Dimens.grid4), - Text( - reading.value, - style: textTheme.titleMedium?.copyWith(color: textColor), - maxLines: 1, - overflow: TextOverflow.ellipsis, - softWrap: false, + AnimatedSwitcher( + duration: Dimens.switchDuration, + child: Text( + reading.value, + style: textTheme.titleMedium?.copyWith(color: textColor), + maxLines: 1, + overflow: TextOverflow.ellipsis, + softWrap: false, + ), ) ], ); From 1310b78a54d85ba031577e8473f5de66098a6565 Mon Sep 17 00:00:00 2001 From: Vadim <44135514+vodemn@users.noreply.github.com> Date: Sat, 5 Aug 2023 21:11:23 +0200 Subject: [PATCH 06/17] ML-61 Delete artefacts after release creation (#96) * Replaced "Build ..." flow with "Create new release" * Renamed other flows --- .../workflows/{cd_dev.yml => build_apk.yml} | 10 +-- .../{cd_prod.yml => create_release.yml} | 76 +++++++++++++++---- .github/workflows/{ci.yml => pr_check.yml} | 0 3 files changed, 62 insertions(+), 24 deletions(-) rename .github/workflows/{cd_dev.yml => build_apk.yml} (91%) rename .github/workflows/{cd_prod.yml => create_release.yml} (72%) rename .github/workflows/{ci.yml => pr_check.yml} (100%) diff --git a/.github/workflows/cd_dev.yml b/.github/workflows/build_apk.yml similarity index 91% rename from .github/workflows/cd_dev.yml rename to .github/workflows/build_apk.yml index e0e6a25..c972954 100644 --- a/.github/workflows/cd_dev.yml +++ b/.github/workflows/build_apk.yml @@ -19,17 +19,11 @@ on: jobs: build: + name: Build .apk runs-on: macos-11 - timeout-minutes: 30 + timeout-minutes: 15 steps: - # - uses: shaunco/ssh-agent@git-repo-mapping - # with: - # ssh-private-key: | - # ${{ secrets.M3_LIGHTMETER_IAP_KEY }} - # repo-mappings: | - # github.com/vodemn/m3_lightmeter_iap - - uses: actions/checkout@v3 with: submodules: recursive diff --git a/.github/workflows/cd_prod.yml b/.github/workflows/create_release.yml similarity index 72% rename from .github/workflows/cd_prod.yml rename to .github/workflows/create_release.yml index 9e45644..6fe5d77 100644 --- a/.github/workflows/cd_prod.yml +++ b/.github/workflows/create_release.yml @@ -3,7 +3,9 @@ # separate terms of service, privacy policy, and support # documentation. -name: Build prod .aab & .apk +name: Create new release + +run-name: Release v${{ inputs.version }} on: workflow_dispatch: @@ -20,15 +22,9 @@ jobs: build: name: Build .apk & .aab runs-on: macos-11 - timeout-minutes: 30 - steps: - # - uses: shaunco/ssh-agent@git-repo-mapping - # with: - # ssh-private-key: | - # ${{ secrets.M3_LIGHTMETER_IAP_KEY }} - # repo-mappings: | - # github.com/vodemn/m3_lightmeter_iap + timeout-minutes: 15 + steps: - uses: actions/checkout@v3 with: submodules: recursive @@ -142,18 +138,66 @@ jobs: with: name: m3_lightmeter_apk + - name: Rename apk + run: mv app-prod-release.apk m3_lightmeter.apk + + - uses: ncipollo/release-action@v1.12.0 + with: + artifacts: "m3_lightmeter.apk" + skipIfReleaseExists: true + tag: "v${{ github.event.inputs.version }}" + + - name: Delete no longer used apk artifact + uses: geekyeggo/delete-artifact@v2 + with: + name: m3_lightmeter_apk + + extract-merged-native-libs: + name: Extract merged native libraries + needs: [build] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + - name: Download app bundle uses: actions/download-artifact@v3 with: name: m3_lightmeter_bundle - - name: Rename artifacts + - name: Extract & zip merged_native_libs run: | - mv app-prod-release.apk m3_lightmeter.apk - mv app-prod-release.aab m3_lightmeter.aab + unzip app-prod-release.aab + (cd base/lib && zip -r "$OLDPWD/merged_native_libs.zip" .) - - uses: ncipollo/release-action@v1.12.0 + - name: Zip app bundle and merged_native_libs + run: zip m3_lightmeter_release.zip app-prod-release.aab merged_native_libs.zip + + - name: Upload merged_native_libs.zip to artifacts + uses: actions/upload-artifact@v3 with: - artifacts: "m3_lightmeter.apk, m3_lightmeter.aab" - skipIfReleaseExists: true - tag: "v${{ github.event.inputs.version }}" + name: m3_lightmeter_release + path: m3_lightmeter_release.zip + + # TODO: this should be moved to `create-google-play-release` step when it is implemented + - name: Delete no longer used app bundle artifact + uses: geekyeggo/delete-artifact@v2 + with: + name: m3_lightmeter_bundle + + # TODO: Automate Google Play releases creation + create-google-play-release: + if: false + name: Create Google Play release + needs: [build, extract-merged-native-libs] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Download app bundle + uses: actions/download-artifact@v3 + with: + name: m3_lightmeter_bundle diff --git a/.github/workflows/ci.yml b/.github/workflows/pr_check.yml similarity index 100% rename from .github/workflows/ci.yml rename to .github/workflows/pr_check.yml From 886188bb9e7997bbf0595be6a1b3cbef545f278d Mon Sep 17 00:00:00 2001 From: Vadim <44135514+vodemn@users.noreply.github.com> Date: Sun, 6 Aug 2023 16:28:20 +0200 Subject: [PATCH 07/17] ML-95 Live histogram (#97) * Added histogram and separated camera view builder * Added histogram to `MeteringScreenLayoutConfig` * `ResolutionPreset.medium` -> `ResolutionPreset.low` * Adjusted histogram paddings --- .github/workflows/build_apk.yml | 2 +- .github/workflows/create_release.yml | 2 +- .vscode/launch.json | 4 +- .vscode/tasks.json | 6 +- README.md | 4 +- .../models/metering_screen_layout_config.dart | 10 +- lib/data/shared_prefs_service.dart | 1 + lib/l10n/intl_en.arb | 1 + lib/l10n/intl_fr.arb | 1 + lib/l10n/intl_ru.arb | 1 + lib/l10n/intl_zh.arb | 1 + .../bloc_container_camera.dart | 2 +- .../camera_view/widget_camera_view.dart | 10 +- .../widget_placeholder_camera_view.dart | 0 .../histogram/widget_histogram.dart | 132 ++++++++++++++++++ .../camera_preview/widget_camera_preview.dart | 62 ++++++++ .../widget_container_camera.dart | 26 ++-- ...ialog_metering_screen_layout_features.dart | 2 + .../metering_screen_layout_config_test.dart | 60 +++++--- test/data/shared_prefs_service_test.dart | 7 +- 20 files changed, 280 insertions(+), 54 deletions(-) rename lib/screens/metering/components/camera_container/components/{ => camera_preview/components}/camera_view/widget_camera_view.dart (86%) rename lib/screens/metering/components/camera_container/components/{ => camera_preview/components}/camera_view_placeholder/widget_placeholder_camera_view.dart (100%) create mode 100644 lib/screens/metering/components/camera_container/components/camera_preview/components/histogram/widget_histogram.dart create mode 100644 lib/screens/metering/components/camera_container/components/camera_preview/widget_camera_preview.dart diff --git a/.github/workflows/build_apk.yml b/.github/workflows/build_apk.yml index c972954..9157f90 100644 --- a/.github/workflows/build_apk.yml +++ b/.github/workflows/build_apk.yml @@ -76,7 +76,7 @@ jobs: - name: Build Apk env: FLAVOR: ${{ github.event.inputs.flavor }} - run: flutter build apk --release --flavor $FLAVOR --dart-define cameraPreviewAspectRatio=2/3 -t lib/main_$FLAVOR.dart + run: flutter build apk --release --flavor $FLAVOR --dart-define c -t lib/main_$FLAVOR.dart - name: Upload artifact uses: actions/upload-artifact@v3 diff --git a/.github/workflows/create_release.yml b/.github/workflows/create_release.yml index 6fe5d77..3851170 100644 --- a/.github/workflows/create_release.yml +++ b/.github/workflows/create_release.yml @@ -16,7 +16,7 @@ on: type: string env: - BUILD_ARGS: --release --flavor prod --dart-define cameraPreviewAspectRatio=2/3 -t lib/main_prod.dart + BUILD_ARGS: --release --flavor prod --dart-define cameraPreviewAspectRatio=240/320 -t lib/main_prod.dart jobs: build: diff --git a/.vscode/launch.json b/.vscode/launch.json index fb960be..822d34d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -12,7 +12,7 @@ "--flavor", "dev", "--dart-define", - "cameraPreviewAspectRatio=2/3", + "cameraPreviewAspectRatio=240/320", ], "program": "${workspaceFolder}/lib/main_dev.dart", }, @@ -36,7 +36,7 @@ "--flavor", "prod", "--dart-define", - "cameraPreviewAspectRatio=2/3", + "cameraPreviewAspectRatio=240/320", ], "program": "${workspaceFolder}/lib/main_prod.dart", }, diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 8615437..adaac0f 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -12,7 +12,7 @@ "dev", "--release", "--dart-define", - "cameraPreviewAspectRatio=2/3", + "cameraPreviewAspectRatio=240/320", "-t", "lib/main_dev.dart", ], @@ -28,7 +28,7 @@ "prod", "--release", "--dart-define", - "cameraPreviewAspectRatio=2/3", + "cameraPreviewAspectRatio=240/320", "-t", "lib/main_prod.dart", ], @@ -44,7 +44,7 @@ "prod", "--release", "--dart-define", - "cameraPreviewAspectRatio=2/3", + "cameraPreviewAspectRatio=240/320", "-t", "lib/main_prod.dart", ], diff --git a/README.md b/README.md index 2fef498..f629aa2 100644 --- a/README.md +++ b/README.md @@ -45,11 +45,11 @@ flutter pub get flutter pub run intl_utils:generate ``` -### 4. Build +### 4. Build (Android) You can build an apk by running the following command from the root of the repository: ```console -flutter build apk --release --flavor $FLAVOR --dart-define cameraPreviewAspectRatio=2/3 -t lib/main_$FLAVOR.dart +flutter build apk --release --flavor $FLAVOR --dart-define cameraPreviewAspectRatio=240/320 -t lib/main_$FLAVOR.dart ``` Just replace `$FLAVOR` with `dev` or `prod`. diff --git a/lib/data/models/metering_screen_layout_config.dart b/lib/data/models/metering_screen_layout_config.dart index 3410632..0aae055 100644 --- a/lib/data/models/metering_screen_layout_config.dart +++ b/lib/data/models/metering_screen_layout_config.dart @@ -1,11 +1,13 @@ -enum MeteringScreenLayoutFeature { extremeExposurePairs, filmPicker } +enum MeteringScreenLayoutFeature { extremeExposurePairs, filmPicker, histogram } typedef MeteringScreenLayoutConfig = Map; extension MeteringScreenLayoutConfigJson on MeteringScreenLayoutConfig { - static MeteringScreenLayoutConfig fromJson(Map data) => data.map( - (key, value) => MapEntry(MeteringScreenLayoutFeature.values[int.parse(key)], value as bool), - ); + static MeteringScreenLayoutConfig fromJson(Map data) => + { + for (final f in MeteringScreenLayoutFeature.values) + f: data[f.index.toString()] as bool? ?? true + }; Map toJson() => map((key, value) => MapEntry(key.index.toString(), value)); } diff --git a/lib/data/shared_prefs_service.dart b/lib/data/shared_prefs_service.dart index 8105d5c..8ae5487 100644 --- a/lib/data/shared_prefs_service.dart +++ b/lib/data/shared_prefs_service.dart @@ -97,6 +97,7 @@ class UserPreferencesService { return { MeteringScreenLayoutFeature.extremeExposurePairs: true, MeteringScreenLayoutFeature.filmPicker: true, + MeteringScreenLayoutFeature.histogram: true, }; } } diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 39700b8..1bfe8ed 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -38,6 +38,7 @@ "meteringScreenLayoutHint": "Hide elements on the metering screen that you don't need so that they don't waste exposure pairs list space.", "meteringScreenFeatureExtremeExposurePairs": "Fastest & shortest exposure pairs", "meteringScreenFeatureFilmPicker": "Film picker", + "meteringScreenFeatureHistogram": "Histogram", "film": "Film", "equipment": "Equipment", "equipmentProfileName": "Equipment profile name", diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index 5957d0d..f75d76b 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -38,6 +38,7 @@ "meteringScreenLayoutHint": "Masquer les éléments sur l'écran de mesure dont vous n'avez pas besoin pour qu'ils ne gaspillent pas de l'espace dans les paires d'exposition.", "meteringScreenFeatureExtremeExposurePairs": "Paires d'exposition les plus rapides et les plus courtes", "meteringScreenFeatureFilmPicker": "Sélecteur de film", + "meteringScreenFeatureHistogram": "Histogramme", "film": "Pellicule", "equipment": "Équipement", "equipmentProfileName": "Nom du profil de l'équipement", diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index f6b3833..3a67fc2 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -38,6 +38,7 @@ "meteringScreenLayoutHint": "Здесь вы можете скрыть некоторые ненужные или неиспользуемые элементы с главного экрана.", "meteringScreenFeatureExtremeExposurePairs": "Длинная и короткая выдержки", "meteringScreenFeatureFilmPicker": "Выбор пленки", + "meteringScreenFeatureHistogram": "Гистограмма", "film": "Пленка", "equipment": "Оборудование", "equipmentProfileName": "Название профиля", diff --git a/lib/l10n/intl_zh.arb b/lib/l10n/intl_zh.arb index b931906..5d020cf 100644 --- a/lib/l10n/intl_zh.arb +++ b/lib/l10n/intl_zh.arb @@ -38,6 +38,7 @@ "meteringScreenLayoutHint": "隐藏不需要的元素,以免浪费曝光列表空间", "meteringScreenFeatureExtremeExposurePairs": "最快 & 最慢曝光组合", "meteringScreenFeatureFilmPicker": "胶片选择", + "meteringScreenFeatureHistogram": "直方图", "film": "胶片", "equipment": "设备", "equipmentProfileName": "设备配置名称", diff --git a/lib/screens/metering/components/camera_container/bloc_container_camera.dart b/lib/screens/metering/components/camera_container/bloc_container_camera.dart index 9c8658d..991bdf6 100644 --- a/lib/screens/metering/components/camera_container/bloc_container_camera.dart +++ b/lib/screens/metering/components/camera_container/bloc_container_camera.dart @@ -123,7 +123,7 @@ class CameraContainerBloc extends EvSourceBlocBase camera.lensDirection == CameraLensDirection.back, orElse: () => cameras.last, ), - ResolutionPreset.medium, + ResolutionPreset.low, enableAudio: false, ); diff --git a/lib/screens/metering/components/camera_container/components/camera_view/widget_camera_view.dart b/lib/screens/metering/components/camera_container/components/camera_preview/components/camera_view/widget_camera_view.dart similarity index 86% rename from lib/screens/metering/components/camera_container/components/camera_view/widget_camera_view.dart rename to lib/screens/metering/components/camera_container/components/camera_preview/components/camera_view/widget_camera_view.dart index e443ad1..7c06062 100644 --- a/lib/screens/metering/components/camera_container/components/camera_view/widget_camera_view.dart +++ b/lib/screens/metering/components/camera_container/components/camera_preview/components/camera_view/widget_camera_view.dart @@ -14,10 +14,12 @@ class CameraView extends StatelessWidget { valueListenable: controller, builder: (_, __, ___) => AspectRatio( aspectRatio: _isLandscape(value) ? value.aspectRatio : (1 / value.aspectRatio), - child: RotatedBox( - quarterTurns: _getQuarterTurns(value), - child: controller.buildPreview(), - ), + child: value.isInitialized + ? RotatedBox( + quarterTurns: _getQuarterTurns(value), + child: controller.buildPreview(), + ) + : const SizedBox.shrink(), ), ); } diff --git a/lib/screens/metering/components/camera_container/components/camera_view_placeholder/widget_placeholder_camera_view.dart b/lib/screens/metering/components/camera_container/components/camera_preview/components/camera_view_placeholder/widget_placeholder_camera_view.dart similarity index 100% rename from lib/screens/metering/components/camera_container/components/camera_view_placeholder/widget_placeholder_camera_view.dart rename to lib/screens/metering/components/camera_container/components/camera_preview/components/camera_view_placeholder/widget_placeholder_camera_view.dart diff --git a/lib/screens/metering/components/camera_container/components/camera_preview/components/histogram/widget_histogram.dart b/lib/screens/metering/components/camera_container/components/camera_preview/components/histogram/widget_histogram.dart new file mode 100644 index 0000000..6964e77 --- /dev/null +++ b/lib/screens/metering/components/camera_container/components/camera_preview/components/histogram/widget_histogram.dart @@ -0,0 +1,132 @@ +import 'dart:math'; + +import 'package:camera/camera.dart'; +import 'package:flutter/material.dart'; +import 'package:lightmeter/res/dimens.dart'; + +class CameraHistogram extends StatefulWidget { + final CameraController controller; + + const CameraHistogram({required this.controller, super.key}); + + @override + _CameraHistogramState createState() => _CameraHistogramState(); +} + +class _CameraHistogramState extends State { + List histogramR = List.filled(256, 0); + List histogramG = List.filled(256, 0); + List histogramB = List.filled(256, 0); + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _startImageStream(); + }); + } + + @override + void dispose() { + widget.controller.stopImageStream(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + HistogramChannel( + color: Colors.red, + values: histogramR, + ), + const SizedBox(height: Dimens.grid4), + HistogramChannel( + color: Colors.green, + values: histogramG, + ), + const SizedBox(height: Dimens.grid4), + HistogramChannel( + color: Colors.blue, + values: histogramB, + ), + ], + ); + } + + void _startImageStream() { + widget.controller.startImageStream((CameraImage image) { + histogramR = List.filled(256, 0); + histogramG = List.filled(256, 0); + histogramB = List.filled(256, 0); + + final int uvRowStride = image.planes[1].bytesPerRow; + final int uvPixelStride = image.planes[1].bytesPerPixel!; + + for (int x = 0; x < image.width; x++) { + for (int y = 0; y < image.height; y++) { + final int uvIndex = uvPixelStride * (x / 2).floor() + uvRowStride * (y / 2).floor(); + final int index = y * image.width + x; + + final yp = image.planes[0].bytes[index]; + final up = image.planes[1].bytes[uvIndex]; + final vp = image.planes[2].bytes[uvIndex]; + + final r = yp + vp * 1436 / 1024 - 179; + final g = yp - up * 46549 / 131072 + 44 - vp * 93604 / 131072 + 91; + final b = yp + up * 1814 / 1024 - 227; + + histogramR[r.round().clamp(0, 255)]++; + histogramG[g.round().clamp(0, 255)]++; + histogramB[b.round().clamp(0, 255)]++; + } + } + + if (mounted) setState(() {}); + }); + } +} + +class HistogramChannel extends StatelessWidget { + final List values; + final Color color; + + final int _maxOccurences; + + HistogramChannel({ + required this.values, + required this.color, + super.key, + }) : _maxOccurences = values.reduce((value, element) => max(value, element)); + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final pixelWidth = constraints.maxWidth / values.length; + return Column( + children: [ + SizedBox( + height: Dimens.grid16, + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: values + .map( + (e) => SizedBox( + height: _maxOccurences == 0 ? 0 : Dimens.grid16 * (e / _maxOccurences), + width: pixelWidth, + child: ColoredBox(color: color), + ), + ) + .toList(), + ), + ), + const Divider(), + ], + ); + }, + ); + } +} diff --git a/lib/screens/metering/components/camera_container/components/camera_preview/widget_camera_preview.dart b/lib/screens/metering/components/camera_container/components/camera_preview/widget_camera_preview.dart new file mode 100644 index 0000000..288c678 --- /dev/null +++ b/lib/screens/metering/components/camera_container/components/camera_preview/widget_camera_preview.dart @@ -0,0 +1,62 @@ +import 'package:camera/camera.dart'; +import 'package:flutter/material.dart'; +import 'package:lightmeter/data/models/metering_screen_layout_config.dart'; +import 'package:lightmeter/platform_config.dart'; +import 'package:lightmeter/providers/metering_screen_layout_provider.dart'; +import 'package:lightmeter/res/dimens.dart'; +import 'package:lightmeter/screens/metering/components/camera_container/components/camera_preview/components/camera_view/widget_camera_view.dart'; +import 'package:lightmeter/screens/metering/components/camera_container/components/camera_preview/components/camera_view_placeholder/widget_placeholder_camera_view.dart'; +import 'package:lightmeter/screens/metering/components/camera_container/components/camera_preview/components/histogram/widget_histogram.dart'; +import 'package:lightmeter/screens/metering/components/camera_container/models/camera_error_type.dart'; + +class CameraPreview extends StatefulWidget { + final CameraController? controller; + final CameraErrorType? error; + + const CameraPreview({this.controller, this.error, super.key}); + + @override + State createState() => _CameraPreviewState(); +} + +class _CameraPreviewState extends State { + @override + Widget build(BuildContext context) { + return AspectRatio( + aspectRatio: PlatformConfig.cameraPreviewAspectRatio, + child: Center( + child: Stack( + children: [ + const CameraViewPlaceholder(error: null), + AnimatedSwitcher( + duration: Dimens.switchDuration, + child: widget.controller != null + ? ValueListenableBuilder( + valueListenable: widget.controller!, + builder: (_, __, ___) => widget.controller!.value.isInitialized + ? Stack( + alignment: Alignment.bottomCenter, + children: [ + CameraView(controller: widget.controller!), + if (MeteringScreenLayout.featureOf( + context, + MeteringScreenLayoutFeature.histogram, + )) + Positioned( + left: Dimens.grid8, + right: Dimens.grid8, + bottom: Dimens.grid16, + child: CameraHistogram(controller: widget.controller!), + ), + ], + ) + : const SizedBox.shrink(), + ) + : CameraViewPlaceholder(error: widget.error), + ), + ], + ), + ), + ); + } +} diff --git a/lib/screens/metering/components/camera_container/widget_container_camera.dart b/lib/screens/metering/components/camera_container/widget_container_camera.dart index 7ac43eb..a8f0a11 100644 --- a/lib/screens/metering/components/camera_container/widget_container_camera.dart +++ b/lib/screens/metering/components/camera_container/widget_container_camera.dart @@ -10,8 +10,7 @@ import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/screens/metering/components/camera_container/bloc_container_camera.dart'; import 'package:lightmeter/screens/metering/components/camera_container/components/camera_controls/widget_camera_controls.dart'; import 'package:lightmeter/screens/metering/components/camera_container/components/camera_controls_placeholder/widget_placeholder_camera_controls.dart'; -import 'package:lightmeter/screens/metering/components/camera_container/components/camera_view/widget_camera_view.dart'; -import 'package:lightmeter/screens/metering/components/camera_container/components/camera_view_placeholder/widget_placeholder_camera_view.dart'; +import 'package:lightmeter/screens/metering/components/camera_container/components/camera_preview/widget_camera_preview.dart'; import 'package:lightmeter/screens/metering/components/camera_container/event_container_camera.dart'; import 'package:lightmeter/screens/metering/components/camera_container/models/camera_error_type.dart'; import 'package:lightmeter/screens/metering/components/camera_container/state_container_camera.dart'; @@ -107,20 +106,11 @@ class _CameraViewBuilder extends StatelessWidget { @override Widget build(BuildContext context) { - return AspectRatio( - aspectRatio: PlatformConfig.cameraPreviewAspectRatio, - child: BlocBuilder( - buildWhen: (previous, current) => current is! CameraActiveState, - builder: (context, state) => Center( - child: AnimatedSwitcher( - duration: Dimens.durationM, - child: switch (state) { - CameraInitializedState() => CameraView(controller: state.controller), - CameraErrorState() => CameraViewPlaceholder(error: state.error), - _ => const CameraViewPlaceholder(error: null), - }, - ), - ), + return BlocBuilder( + buildWhen: (previous, current) => current is! CameraActiveState, + builder: (context, state) => CameraPreview( + controller: state is CameraInitializedState ? state.controller : null, + error: state is CameraErrorState ? state.error : null, ), ); } @@ -161,7 +151,9 @@ class _CameraControlsBuilder extends StatelessWidget { }, ); } else { - child = const Column(children: [Expanded(child: SizedBox.shrink())],); + child = const Column( + children: [Expanded(child: SizedBox.shrink())], + ); } return AnimatedSwitcher( diff --git a/lib/screens/settings/components/metering/components/metering_screen_layout/components/meterins_screen_layout_features_dialog/widget_dialog_metering_screen_layout_features.dart b/lib/screens/settings/components/metering/components/metering_screen_layout/components/meterins_screen_layout_features_dialog/widget_dialog_metering_screen_layout_features.dart index 654a6c4..d43f011 100644 --- a/lib/screens/settings/components/metering/components/metering_screen_layout/components/meterins_screen_layout_features_dialog/widget_dialog_metering_screen_layout_features.dart +++ b/lib/screens/settings/components/metering/components/metering_screen_layout/components/meterins_screen_layout_features_dialog/widget_dialog_metering_screen_layout_features.dart @@ -71,6 +71,8 @@ class _MeteringScreenLayoutFeaturesDialogState extends State sharedPreferences.setString( UserPreferencesService.meteringScreenLayoutKey, - """{"0":false,"1":true}""", + """{"0":false,"1":true,"2":true}""", ), ).thenAnswer((_) => Future.value(true)); service.meteringScreenLayout = { MeteringScreenLayoutFeature.extremeExposurePairs: false, MeteringScreenLayoutFeature.filmPicker: true, + MeteringScreenLayoutFeature.histogram: true, }; verify( () => sharedPreferences.setString( UserPreferencesService.meteringScreenLayoutKey, - """{"0":false,"1":true}""", + """{"0":false,"1":true,"2":true}""", ), ).called(1); }); From 737a9aa2c252202264118024475cbf82f16a663b Mon Sep 17 00:00:00 2001 From: Vadim <44135514+vodemn@users.noreply.github.com> Date: Mon, 7 Aug 2023 12:56:29 +0200 Subject: [PATCH 08/17] ML-98 Metering top bar cutout doesn't pass through taps (#99) * replaced `OverflowBox` with `Stack` --- .../widget_container_camera.dart | 155 +++++++++--------- 1 file changed, 76 insertions(+), 79 deletions(-) diff --git a/lib/screens/metering/components/camera_container/widget_container_camera.dart b/lib/screens/metering/components/camera_container/widget_container_camera.dart index a8f0a11..e42991c 100644 --- a/lib/screens/metering/components/camera_container/widget_container_camera.dart +++ b/lib/screens/metering/components/camera_container/widget_container_camera.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:lightmeter/data/models/exposure_pair.dart'; @@ -45,59 +47,94 @@ class CameraContainer extends StatelessWidget { @override Widget build(BuildContext context) { - final double cameraViewHeight = - ((MediaQuery.of(context).size.width - Dimens.grid8 - 2 * Dimens.paddingM) / 2) / - PlatformConfig.cameraPreviewAspectRatio; + final double meteringContainerHeight = _meteringContainerHeight(context); + final double cameraPreviewHeight = _cameraPreviewHeight(context); + final double topBarOverflow = meteringContainerHeight - cameraPreviewHeight; - double topBarOverflow = Dimens.readingContainerSingleValueHeight + // ISO & ND - -cameraViewHeight; + return Stack( + children: [ + Positioned( + left: 0, + top: 0, + right: 0, + child: MeteringTopBar( + readingsContainer: ReadingsContainer( + fastest: fastest, + slowest: slowest, + film: film, + iso: iso, + nd: nd, + onFilmChanged: onFilmChanged, + onIsoChanged: onIsoChanged, + onNdChanged: onNdChanged, + ), + appendixHeight: topBarOverflow, + preview: const _CameraViewBuilder(), + ), + ), + SafeArea( + bottom: false, + child: Column( + children: [ + SizedBox( + height: min(meteringContainerHeight, cameraPreviewHeight) + Dimens.paddingM * 2, + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM), + child: Row( + children: [ + Expanded( + child: Padding( + padding: EdgeInsets.only(top: topBarOverflow >= 0 ? topBarOverflow : 0), + child: ExposurePairsList(exposurePairs), + ), + ), + const SizedBox(width: Dimens.grid8), + Expanded( + child: Padding( + padding: EdgeInsets.only(top: topBarOverflow <= 0 ? -topBarOverflow : 0), + child: const _CameraControlsBuilder(), + ), + ), + ], + ), + ), + ), + ], + ), + ) + ], + ); + } + + double _meteringContainerHeight(BuildContext context) { + double enabledFeaturesHeight = 0; if (FeaturesConfig.equipmentProfilesEnabled) { - topBarOverflow += Dimens.readingContainerSingleValueHeight; - topBarOverflow += Dimens.paddingS; + enabledFeaturesHeight += Dimens.readingContainerSingleValueHeight; + enabledFeaturesHeight += Dimens.paddingS; } if (MeteringScreenLayout.featureOf( context, MeteringScreenLayoutFeature.extremeExposurePairs, )) { - topBarOverflow += Dimens.readingContainerDoubleValueHeight; - topBarOverflow += Dimens.paddingS; + enabledFeaturesHeight += Dimens.readingContainerDoubleValueHeight; + enabledFeaturesHeight += Dimens.paddingS; } if (MeteringScreenLayout.featureOf( context, MeteringScreenLayoutFeature.filmPicker, )) { - topBarOverflow += Dimens.readingContainerSingleValueHeight; - topBarOverflow += Dimens.paddingS; + enabledFeaturesHeight += Dimens.readingContainerSingleValueHeight; + enabledFeaturesHeight += Dimens.paddingS; } - return Column( - children: [ - MeteringTopBar( - readingsContainer: ReadingsContainer( - fastest: fastest, - slowest: slowest, - film: film, - iso: iso, - nd: nd, - onFilmChanged: onFilmChanged, - onIsoChanged: onIsoChanged, - onNdChanged: onNdChanged, - ), - appendixHeight: topBarOverflow, - preview: const _CameraViewBuilder(), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM), - child: _MiddleContentWrapper( - topBarOverflow: topBarOverflow, - leftContent: ExposurePairsList(exposurePairs), - rightContent: const _CameraControlsBuilder(), - ), - ), - ), - ], - ); + return enabledFeaturesHeight + Dimens.readingContainerSingleValueHeight; // ISO & ND + } + + double _cameraPreviewHeight(BuildContext context) { + return ((MediaQuery.of(context).size.width - Dimens.grid8 - 2 * Dimens.paddingM) / 2) / + PlatformConfig.cameraPreviewAspectRatio; } } @@ -165,43 +202,3 @@ class _CameraControlsBuilder extends StatelessWidget { ); } } - -class _MiddleContentWrapper extends StatelessWidget { - final double topBarOverflow; - final Widget leftContent; - final Widget rightContent; - - const _MiddleContentWrapper({ - required this.topBarOverflow, - required this.leftContent, - required this.rightContent, - }); - - @override - Widget build(BuildContext context) { - return LayoutBuilder( - builder: (context, constraints) => OverflowBox( - alignment: Alignment.bottomCenter, - maxHeight: constraints.maxHeight + topBarOverflow.abs(), - maxWidth: constraints.maxWidth, - child: Row( - children: [ - Expanded( - child: Padding( - padding: EdgeInsets.only(top: topBarOverflow >= 0 ? topBarOverflow : 0), - child: leftContent, - ), - ), - const SizedBox(width: Dimens.grid8), - Expanded( - child: Padding( - padding: EdgeInsets.only(top: topBarOverflow <= 0 ? -topBarOverflow : 0), - child: rightContent, - ), - ), - ], - ), - ), - ); - } -} From 9c11401175c344d5d69f5adfaf0e7eea4b25b558 Mon Sep 17 00:00:00 2001 From: vodemn Date: Mon, 7 Aug 2023 14:59:47 +0000 Subject: [PATCH 09/17] Version bump --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 315e72f..7a4c862 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: lightmeter description: A new Flutter project. publish_to: "none" -version: 0.12.4+35 +version: 0.13.0+36 environment: sdk: ">=3.0.0 <4.0.0" From d91441bac9ab5bed8abc073521a012b8a95e6ed7 Mon Sep 17 00:00:00 2001 From: Vadim <44135514+vodemn@users.noreply.github.com> Date: Tue, 8 Aug 2023 11:46:43 +0200 Subject: [PATCH 10/17] ML-95 Live histogram (#100) * Removed redundant `stopImageStream()` --- .../camera_preview/components/histogram/widget_histogram.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/screens/metering/components/camera_container/components/camera_preview/components/histogram/widget_histogram.dart b/lib/screens/metering/components/camera_container/components/camera_preview/components/histogram/widget_histogram.dart index 6964e77..05dfa9d 100644 --- a/lib/screens/metering/components/camera_container/components/camera_preview/components/histogram/widget_histogram.dart +++ b/lib/screens/metering/components/camera_container/components/camera_preview/components/histogram/widget_histogram.dart @@ -28,7 +28,9 @@ class _CameraHistogramState extends State { @override void dispose() { - widget.controller.stopImageStream(); + /// There is no need to stop image stream here, + /// because this widget will be disposed when CameraController is disposed + /// widget.controller.stopImageStream(); super.dispose(); } From b9412c7441f8bc68ef02071dc113bb1deaa470a6 Mon Sep 17 00:00:00 2001 From: Vadim <44135514+vodemn@users.noreply.github.com> Date: Wed, 9 Aug 2023 12:24:12 +0200 Subject: [PATCH 11/17] ML-61 Create Google Play release from Github actions (#101) * Delete all artefacts after GP release * Update create_release.yml * Added release notes formatting * Preserve release zip if GP release creation failed * Create whatsnew folder --- .github/workflows/create_release.yml | 118 ++++++++++++++++++++------- 1 file changed, 87 insertions(+), 31 deletions(-) diff --git a/.github/workflows/create_release.yml b/.github/workflows/create_release.yml index 3851170..ca00c25 100644 --- a/.github/workflows/create_release.yml +++ b/.github/workflows/create_release.yml @@ -3,6 +3,11 @@ # separate terms of service, privacy policy, and support # documentation. + +# This workflow uses perl regex. For better syntaxis understading see these docs: +# https://perldoc.perl.org/perlrequick#Search-and-replace +# https://perldoc.perl.org/perlre#Other-Modifiers + name: Create new release run-name: Release v${{ inputs.version }} @@ -14,6 +19,10 @@ on: description: "Version" required: true type: string + release-notes: + description: "Release notes" + required: true + type: string env: BUILD_ARGS: --release --flavor prod --dart-define cameraPreviewAspectRatio=240/320 -t lib/main_prod.dart @@ -23,7 +32,6 @@ jobs: name: Build .apk & .aab runs-on: macos-11 timeout-minutes: 15 - steps: - uses: actions/checkout@v3 with: @@ -95,6 +103,21 @@ jobs: name: m3_lightmeter_bundle path: build/app/outputs/bundle/prodRelease/app-prod-release.aab + generate-release-notes: + name: Generate release notes + runs-on: ubuntu-latest + steps: + - name: Generate release notes + run: | + echo ${{ inputs.release-notes }} > whatsnew-en-US.md + perl -i -pe 's/\s{1}(-{1})/\n$1/g' whatsnew-en-US.md + + - name: Upload merged_native_libs.zip to artifacts + uses: actions/upload-artifact@v3 + with: + name: whatsnew-en-US + path: whatsnew-en-US.md + update-version-in-repo: name: Update repo version needs: [build] @@ -121,22 +144,20 @@ jobs: branch: ${{ github.ref_name }} unprotect_reviews: true - create-release: + create-github-release: name: Create Github release - needs: [build, update-version-in-repo] if: github.ref_name == 'main' + needs: [generate-release-notes, update-version-in-repo] runs-on: ubuntu-latest permissions: contents: write steps: - - uses: actions/checkout@v3 - with: - submodules: recursive - - - name: Download apk + - name: Download apk & release notes uses: actions/download-artifact@v3 with: - name: m3_lightmeter_apk + name: | + m3_lightmeter_apk + whatsnew-en-US - name: Rename apk run: mv app-prod-release.apk m3_lightmeter.apk @@ -146,8 +167,9 @@ jobs: artifacts: "m3_lightmeter.apk" skipIfReleaseExists: true tag: "v${{ github.event.inputs.version }}" + bodyFile: "whatsnew-en-US.md" - - name: Delete no longer used apk artifact + - name: Delete apk artifact uses: geekyeggo/delete-artifact@v2 with: name: m3_lightmeter_apk @@ -157,10 +179,6 @@ jobs: needs: [build] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - with: - submodules: recursive - - name: Download app bundle uses: actions/download-artifact@v3 with: @@ -171,33 +189,71 @@ jobs: unzip app-prod-release.aab (cd base/lib && zip -r "$OLDPWD/merged_native_libs.zip" .) + - name: Upload merged native libs to artifacts + uses: actions/upload-artifact@v3 + with: + name: merged_native_libs + path: merged_native_libs.zip + + create-google-play-release: + name: Create Google Play release + if: github.ref_name == 'main' + needs: [generate-release-notes, extract-merged-native-libs] + runs-on: ubuntu-latest + steps: + - name: Download app bundle & merged native libs + uses: actions/download-artifact@v3 + with: + name: | + m3_lightmeter_bundle + merged_native_libs + whatsnew-en-US + + - name: Move release notes to a folder + run: | + mkdir whatsnew + mv whatsnew-en-US.md whatsnew + + - name: Create Google Play release + id: create-google-play-release-step + uses: r0adkll/upload-google-play@v1.1.1 + with: + serviceAccountJsonPlainText: ${{ secrets.GH_ACTIONS_SERVICE_ACCOUNT_JSON }} + packageName: com.vodemn.lightmeter + releaseFiles: app-prod-release.aab + track: production + status: draft + userFraction: 1.0 + debugSymbols: merged_native_libs.zip + whatsNewDirectory: whatsnew + + # https://docs.github.com/en/actions/learn-github-actions/expressions#failure-with-conditions - name: Zip app bundle and merged_native_libs + if: ${{ failure() && steps.create-google-play-release-step.conclusion == 'failure' }} run: zip m3_lightmeter_release.zip app-prod-release.aab merged_native_libs.zip - - name: Upload merged_native_libs.zip to artifacts + - name: Upload release zip to artifacts + if: ${{ failure() && steps.create-google-play-release-step.conclusion == 'failure' }} uses: actions/upload-artifact@v3 with: name: m3_lightmeter_release path: m3_lightmeter_release.zip - - # TODO: this should be moved to `create-google-play-release` step when it is implemented - - name: Delete no longer used app bundle artifact + + - name: Delete app bundle & merged native libs artifacts + if: ${{ always() }} uses: geekyeggo/delete-artifact@v2 with: - name: m3_lightmeter_bundle + name: | + m3_lightmeter_bundle + merged_native_libs - # TODO: Automate Google Play releases creation - create-google-play-release: - if: false - name: Create Google Play release - needs: [build, extract-merged-native-libs] + cleanup: + name: Cleanup + if: ${{ always() }} + needs: [create-github-release, create-google-play-release] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - name: Delete release notes artifact + uses: geekyeggo/delete-artifact@v2 with: - submodules: recursive - - - name: Download app bundle - uses: actions/download-artifact@v3 - with: - name: m3_lightmeter_bundle + name: whatsnew-en-US From d41fa6fa8494ecd016f511bfbf782e5c345d4c48 Mon Sep 17 00:00:00 2001 From: Vadim <44135514+vodemn@users.noreply.github.com> Date: Wed, 9 Aug 2023 16:39:41 +0200 Subject: [PATCH 12/17] ML-61 Download each artifact separately (#102) * Download each artifact separately * typo * Updated setup-java action * Extract merged native libs directly in GP release job --- .github/workflows/create_release.yml | 45 ++++++++++------------------ 1 file changed, 16 insertions(+), 29 deletions(-) diff --git a/.github/workflows/create_release.yml b/.github/workflows/create_release.yml index ca00c25..71248ac 100644 --- a/.github/workflows/create_release.yml +++ b/.github/workflows/create_release.yml @@ -37,7 +37,7 @@ jobs: with: submodules: recursive - - uses: actions/setup-java@v2 + - uses: actions/setup-java@v3 with: distribution: "zulu" java-version: "11" @@ -152,16 +152,19 @@ jobs: permissions: contents: write steps: - - name: Download apk & release notes + - name: Download apk uses: actions/download-artifact@v3 with: - name: | - m3_lightmeter_apk - whatsnew-en-US + name: m3_lightmeter_apk - name: Rename apk run: mv app-prod-release.apk m3_lightmeter.apk + - name: Download release notes + uses: actions/download-artifact@v3 + with: + name: whatsnew-en-US + - uses: ncipollo/release-action@v1.12.0 with: artifacts: "m3_lightmeter.apk" @@ -174,9 +177,10 @@ jobs: with: name: m3_lightmeter_apk - extract-merged-native-libs: - name: Extract merged native libraries - needs: [build] + create-google-play-release: + name: Create Google Play release + if: github.ref_name == 'main' + needs: [generate-release-notes] runs-on: ubuntu-latest steps: - name: Download app bundle @@ -189,31 +193,16 @@ jobs: unzip app-prod-release.aab (cd base/lib && zip -r "$OLDPWD/merged_native_libs.zip" .) - - name: Upload merged native libs to artifacts - uses: actions/upload-artifact@v3 - with: - name: merged_native_libs - path: merged_native_libs.zip - - create-google-play-release: - name: Create Google Play release - if: github.ref_name == 'main' - needs: [generate-release-notes, extract-merged-native-libs] - runs-on: ubuntu-latest - steps: - - name: Download app bundle & merged native libs + - name: Download release notes uses: actions/download-artifact@v3 with: - name: | - m3_lightmeter_bundle - merged_native_libs - whatsnew-en-US + name: whatsnew-en-US - name: Move release notes to a folder run: | mkdir whatsnew mv whatsnew-en-US.md whatsnew - + - name: Create Google Play release id: create-google-play-release-step uses: r0adkll/upload-google-play@v1.1.1 @@ -243,9 +232,7 @@ jobs: if: ${{ always() }} uses: geekyeggo/delete-artifact@v2 with: - name: | - m3_lightmeter_bundle - merged_native_libs + name: m3_lightmeter_bundle cleanup: name: Cleanup From bdb0442dd104d59c996538e3d3b1fddc9c05e4e5 Mon Sep 17 00:00:00 2001 From: Vadim Date: Wed, 9 Aug 2023 16:49:02 +0200 Subject: [PATCH 13/17] Fixed release jobs dependencies --- .github/workflows/create_release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/create_release.yml b/.github/workflows/create_release.yml index 71248ac..693b3fd 100644 --- a/.github/workflows/create_release.yml +++ b/.github/workflows/create_release.yml @@ -147,7 +147,7 @@ jobs: create-github-release: name: Create Github release if: github.ref_name == 'main' - needs: [generate-release-notes, update-version-in-repo] + needs: [build, generate-release-notes, update-version-in-repo] runs-on: ubuntu-latest permissions: contents: write @@ -180,7 +180,7 @@ jobs: create-google-play-release: name: Create Google Play release if: github.ref_name == 'main' - needs: [generate-release-notes] + needs: [build, generate-release-notes] runs-on: ubuntu-latest steps: - name: Download app bundle From dafbc682886ee8fac129fbdbb568af950f54cce9 Mon Sep 17 00:00:00 2001 From: Vadim Date: Wed, 9 Aug 2023 17:06:47 +0200 Subject: [PATCH 14/17] Increased build job timeout --- .github/workflows/create_release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/create_release.yml b/.github/workflows/create_release.yml index 693b3fd..5f8191e 100644 --- a/.github/workflows/create_release.yml +++ b/.github/workflows/create_release.yml @@ -31,7 +31,7 @@ jobs: build: name: Build .apk & .aab runs-on: macos-11 - timeout-minutes: 15 + timeout-minutes: 30 steps: - uses: actions/checkout@v3 with: From 6e3588a72e9af0e48141f2b80f91285923063b2b Mon Sep 17 00:00:00 2001 From: vodemn Date: Wed, 9 Aug 2023 15:20:00 +0000 Subject: [PATCH 15/17] Version bump --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 7a4c862..7f4fcb5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: lightmeter description: A new Flutter project. publish_to: "none" -version: 0.13.0+36 +version: 0.13.1+37 environment: sdk: ">=3.0.0 <4.0.0" From 4917ee8aefaef71bbfa6a69ab21ebeaa8b47725b Mon Sep 17 00:00:00 2001 From: Vadim <44135514+vodemn@users.noreply.github.com> Date: Thu, 10 Aug 2023 12:42:31 +0200 Subject: [PATCH 16/17] ML-61 Try to automate GP & Github releases via Github Actions (#103) * Added option to create separate releases * Removed branch condition * Added default values to releases checkboxes * Removed user fraction * Remove .md extension for GP release * More refined releases conditions * Parse release name * Create Google Play release name * Checkout first * Update create_release.yml * Increment build number only for GH release * Release with status `complete` * typo --- .github/workflows/create_release.yml | 40 ++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/.github/workflows/create_release.yml b/.github/workflows/create_release.yml index 5f8191e..07f76c8 100644 --- a/.github/workflows/create_release.yml +++ b/.github/workflows/create_release.yml @@ -23,6 +23,14 @@ on: description: "Release notes" required: true type: string + github-release: + type: boolean + description: Create Github release + default: true + google-play-release: + type: boolean + description: Create Google Play release + default: true env: BUILD_ARGS: --release --flavor prod --dart-define cameraPreviewAspectRatio=240/320 -t lib/main_prod.dart @@ -30,6 +38,7 @@ env: jobs: build: name: Build .apk & .aab + if: ${{ inputs.github-release }} || ${{ inputs.google-play-release }} runs-on: macos-11 timeout-minutes: 30 steps: @@ -70,7 +79,10 @@ jobs: echo -n "$FIREBASE_OPTIONS" | base64 --decode --output $FIREBASE_OPTIONS_PATH cp $FIREBASE_OPTIONS_PATH ./lib + # This step makes sense when Github release is enabled because this release increments the build number. + # Therefore here we have to increment it as well to build an apk with the same build number. - name: Increment build number & replace version number + if: ${{ inputs.github-release }} run: perl -i -pe 's/^(version:\s+)(\d+\.\d+\.\d+)(\+)(\d+)$/$1."${{ github.event.inputs.version }}".$3.($4+1)/e' pubspec.yaml - name: Install Flutter @@ -86,18 +98,22 @@ jobs: flutter pub run intl_utils:generate - name: Build apk + if: ${{ inputs.github-release }} run: flutter build apk $BUILD_ARGS - name: Upload apk to artifacts + if: ${{ inputs.github-release }} uses: actions/upload-artifact@v3 with: name: m3_lightmeter_apk path: build/app/outputs/flutter-apk/app-prod-release.apk - name: Build appbundle + if: ${{ inputs.google-play-release }} run: flutter build appbundle $BUILD_ARGS - name: Upload app bundle to artifacts + if: ${{ inputs.google-play-release }} uses: actions/upload-artifact@v3 with: name: m3_lightmeter_bundle @@ -120,6 +136,7 @@ jobs: update-version-in-repo: name: Update repo version + if: ${{ inputs.github-release }} needs: [build] runs-on: ubuntu-latest steps: @@ -146,7 +163,7 @@ jobs: create-github-release: name: Create Github release - if: github.ref_name == 'main' + if: ${{ inputs.github-release }} needs: [build, generate-release-notes, update-version-in-repo] runs-on: ubuntu-latest permissions: @@ -179,10 +196,14 @@ jobs: create-google-play-release: name: Create Google Play release - if: github.ref_name == 'main' + if: ${{ inputs.google-play-release }} needs: [build, generate-release-notes] runs-on: ubuntu-latest steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + - name: Download app bundle uses: actions/download-artifact@v3 with: @@ -200,8 +221,17 @@ jobs: - name: Move release notes to a folder run: | + mv whatsnew-en-US.md whatsnew-en-US mkdir whatsnew - mv whatsnew-en-US.md whatsnew + mv whatsnew-en-US whatsnew + + # https://unix.stackexchange.com/questions/13466/can-grep-output-only-specified-groupings-that-match' + # https://stackoverflow.com/questions/74353311/github-workflow-unable-to-process-file-command-env-successfully + - name: Create Google Play release name + id: release-name + run: | + RELEASE_NAME=$(echo "$(cat pubspec.yaml)" | sed -n -r "s/^version:\s{1}(.*)[+](.*)$/700\2 (\1)/p") + echo "release_name=$RELEASE_NAME" >> $GITHUB_ENV - name: Create Google Play release id: create-google-play-release-step @@ -210,9 +240,9 @@ jobs: serviceAccountJsonPlainText: ${{ secrets.GH_ACTIONS_SERVICE_ACCOUNT_JSON }} packageName: com.vodemn.lightmeter releaseFiles: app-prod-release.aab + releaseName: ${{ env.release_name }} track: production - status: draft - userFraction: 1.0 + status: completed debugSymbols: merged_native_libs.zip whatsNewDirectory: whatsnew From 5adcee00dd8b93af5c5f96315945259bd023dddc Mon Sep 17 00:00:00 2001 From: Vadim <44135514+vodemn@users.noreply.github.com> Date: Mon, 14 Aug 2023 12:25:37 +0200 Subject: [PATCH 17/17] ML-105 Hide providers from the widget tree (#106) * Added `ServiceProviders` widget * Added `EnumProviders` widget for enum values * Moved `ThemeProvider` functionality to `EnumProviders` * Style * `EnumProviders` -> `UserPreferencesProvider` * `ServiceProviders` -> `ServiceProvider` * Moved `MeteringScreenLayoutProvider` functionality to `UserPreferencesProvider` * typo * Removed `InheritedModelAspectListener` * TODO * Removed Inherited Generics * Removed redundant `LightmeterProviders` * Removed redundant methods from `ServicesProvider` * `_inheritFrom` -> `_inheritFromEnumsModel` * Fixed `MeteringScreenLayoutConfig` updates * Separated `_ThemeModel` * typo * `_EnumsModel` -> `_UserPreferencesModel` --- lib/application.dart | 124 +++++--- lib/providers.dart | 77 ----- lib/providers/equipment_profile_provider.dart | 69 +++- lib/providers/ev_source_type_provider.dart | 63 ---- .../metering_screen_layout_provider.dart | 60 ---- lib/providers/services_provider.dart | 36 +++ lib/providers/stop_type_provider.dart | 42 --- lib/providers/supported_locale_provider.dart | 53 ---- lib/providers/theme_provider.dart | 257 --------------- lib/providers/user_preferences_provider.dart | 295 ++++++++++++++++++ lib/res/theme.dart | 97 ++++++ .../widget_bottom_controls.dart | 4 +- .../camera_preview/widget_camera_preview.dart | 4 +- .../provider_container_camera.dart | 5 +- .../widget_container_camera.dart | 6 +- .../provider_container_light_sensor.dart | 5 +- .../widget_container_readings.dart | 19 +- lib/screens/metering/flow_metering.dart | 64 ++-- lib/screens/metering/screen_metering.dart | 25 +- .../listener_metering_layout_feature.dart | 52 +++ .../utils/listsner_equipment_profiles.dart | 30 ++ .../widget_list_tile_report_issue.dart | 5 +- .../widget_list_tile_source_code.dart | 5 +- .../widget_list_tile_write_email.dart | 5 +- .../caffeine/provider_list_tile_caffeine.dart | 5 +- .../haptics/provider_list_tile_haptics.dart | 5 +- .../language/widget_list_tile_language.dart | 9 +- .../provider_list_tile_volume_actions.dart | 5 +- .../provider_dialog_calibration.dart | 5 +- .../widget_dialog_calibration.dart | 5 +- .../widget_list_tile_calibration.dart | 7 +- .../screen_equipment_profile.dart | 19 +- .../widget_list_tile_fractional_stops.dart | 9 +- ...ialog_metering_screen_layout_features.dart | 6 +- .../widget_list_tile_dynamic_color.dart | 7 +- .../widget_dialog_picker_primary_color.dart | 6 +- .../widget_list_tile_primary_color.dart | 7 +- .../widget_list_tile_theme_type.dart | 9 +- .../theme/widget_settings_section_theme.dart | 4 +- lib/screens/settings/flow_settings.dart | 33 +- lib/screens/settings/screen_settings.dart | 7 +- lib/utils/inherited_generics.dart | 171 ---------- test/data/shared_prefs_service_test.dart | 6 +- 43 files changed, 790 insertions(+), 937 deletions(-) delete mode 100644 lib/providers.dart delete mode 100644 lib/providers/ev_source_type_provider.dart delete mode 100644 lib/providers/metering_screen_layout_provider.dart create mode 100644 lib/providers/services_provider.dart delete mode 100644 lib/providers/stop_type_provider.dart delete mode 100644 lib/providers/supported_locale_provider.dart delete mode 100644 lib/providers/theme_provider.dart create mode 100644 lib/providers/user_preferences_provider.dart create mode 100644 lib/res/theme.dart create mode 100644 lib/screens/metering/utils/listener_metering_layout_feature.dart create mode 100644 lib/screens/metering/utils/listsner_equipment_profiles.dart delete mode 100644 lib/utils/inherited_generics.dart diff --git a/lib/application.dart b/lib/application.dart index 3880a08..1249ed9 100644 --- a/lib/application.dart +++ b/lib/application.dart @@ -1,13 +1,22 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:lightmeter/data/caffeine_service.dart'; +import 'package:lightmeter/data/haptics_service.dart'; +import 'package:lightmeter/data/light_sensor_service.dart'; import 'package:lightmeter/data/models/supported_locale.dart'; +import 'package:lightmeter/data/permissions_service.dart'; +import 'package:lightmeter/data/shared_prefs_service.dart'; +import 'package:lightmeter/data/volume_events_service.dart'; import 'package:lightmeter/environment.dart'; import 'package:lightmeter/generated/l10n.dart'; -import 'package:lightmeter/providers.dart'; +import 'package:lightmeter/providers/equipment_profile_provider.dart'; +import 'package:lightmeter/providers/services_provider.dart'; +import 'package:lightmeter/providers/user_preferences_provider.dart'; import 'package:lightmeter/screens/metering/flow_metering.dart'; import 'package:lightmeter/screens/settings/flow_settings.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; +import 'package:platform/platform.dart'; +import 'package:shared_preferences/shared_preferences.dart'; class Application extends StatelessWidget { final Environment env; @@ -16,56 +25,71 @@ class Application extends StatelessWidget { @override Widget build(BuildContext context) { - return LightmeterProviders( - env: env, - builder: (context, ready) => ready - ? _AnnotatedRegionWrapper( - child: MaterialApp( - theme: context.listen(), - locale: Locale(context.listen().intlName), - localizationsDelegates: const [ - S.delegate, - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: S.delegate.supportedLocales, - builder: (context, child) => MediaQuery( - data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0), - child: child!, + return FutureBuilder( + future: Future.wait([ + SharedPreferences.getInstance(), + const LightSensorService(LocalPlatform()).hasSensor(), + ]), + builder: (_, snapshot) { + if (snapshot.data != null) { + return ServicesProvider( + caffeineService: const CaffeineService(), + environment: env.copyWith(hasLightSensor: snapshot.data![1] as bool), + hapticsService: const HapticsService(), + lightSensorService: const LightSensorService(LocalPlatform()), + permissionsService: const PermissionsService(), + userPreferencesService: UserPreferencesService(snapshot.data![0] as SharedPreferences), + volumeEventsService: const VolumeEventsService(LocalPlatform()), + child: UserPreferencesProvider( + child: EquipmentProfileProvider( + child: Builder( + builder: (context) { + final theme = UserPreferencesProvider.themeOf(context); + final systemIconsBrightness = + ThemeData.estimateBrightnessForColor(theme.colorScheme.onSurface); + return AnnotatedRegion( + value: SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarBrightness: systemIconsBrightness == Brightness.light + ? Brightness.dark + : Brightness.light, + statusBarIconBrightness: systemIconsBrightness, + systemNavigationBarColor: Colors.transparent, + systemNavigationBarIconBrightness: systemIconsBrightness, + ), + child: MaterialApp( + theme: theme, + locale: Locale(UserPreferencesProvider.localeOf(context).intlName), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: S.delegate.supportedLocales, + builder: (context, child) => MediaQuery( + data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0), + child: child!, + ), + initialRoute: "metering", + routes: { + "metering": (context) => const MeteringFlow(), + "settings": (context) => const SettingsFlow(), + }, + ), + ); + }, ), - initialRoute: "metering", - routes: { - "metering": (context) => const MeteringFlow(), - "settings": (context) => const SettingsFlow(), - }, ), - ) - : const SizedBox(), - ); - } -} - -class _AnnotatedRegionWrapper extends StatelessWidget { - final Widget child; - - const _AnnotatedRegionWrapper({required this.child}); - - @override - Widget build(BuildContext context) { - final systemIconsBrightness = ThemeData.estimateBrightnessForColor( - context.listen().colorScheme.onSurface, - ); - return AnnotatedRegion( - value: SystemUiOverlayStyle( - statusBarColor: Colors.transparent, - statusBarBrightness: - systemIconsBrightness == Brightness.light ? Brightness.dark : Brightness.light, - statusBarIconBrightness: systemIconsBrightness, - systemNavigationBarColor: Colors.transparent, - systemNavigationBarIconBrightness: systemIconsBrightness, - ), - child: child, + ), + ); + } else if (snapshot.error != null) { + return Center(child: Text(snapshot.error!.toString())); + } + + // TODO(@vodemn): maybe user splashscreen instead + return const SizedBox(); + }, ); } } diff --git a/lib/providers.dart b/lib/providers.dart deleted file mode 100644 index d7907ab..0000000 --- a/lib/providers.dart +++ /dev/null @@ -1,77 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lightmeter/data/caffeine_service.dart'; -import 'package:lightmeter/data/haptics_service.dart'; -import 'package:lightmeter/data/light_sensor_service.dart'; -import 'package:lightmeter/data/permissions_service.dart'; -import 'package:lightmeter/data/shared_prefs_service.dart'; -import 'package:lightmeter/data/volume_events_service.dart'; -import 'package:lightmeter/environment.dart'; -import 'package:lightmeter/providers/equipment_profile_provider.dart'; -import 'package:lightmeter/providers/ev_source_type_provider.dart'; -import 'package:lightmeter/providers/metering_screen_layout_provider.dart'; -import 'package:lightmeter/providers/stop_type_provider.dart'; -import 'package:lightmeter/providers/supported_locale_provider.dart'; -import 'package:lightmeter/providers/theme_provider.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; -import 'package:platform/platform.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -class LightmeterProviders extends StatelessWidget { - final Environment env; - final Widget Function(BuildContext context, bool ready) builder; - - const LightmeterProviders({required this.env, required this.builder, super.key}); - - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: Future.wait([ - SharedPreferences.getInstance(), - const LightSensorService(LocalPlatform()).hasSensor(), - ]), - builder: (_, snapshot) { - if (snapshot.data != null) { - return InheritedWidgetBase( - data: env.copyWith(hasLightSensor: snapshot.data![1] as bool), - child: InheritedWidgetBase( - data: UserPreferencesService(snapshot.data![0] as SharedPreferences), - child: InheritedWidgetBase( - data: const LightSensorService(LocalPlatform()), - child: InheritedWidgetBase( - data: const CaffeineService(), - child: InheritedWidgetBase( - data: const HapticsService(), - child: InheritedWidgetBase( - data: const VolumeEventsService(LocalPlatform()), - child: InheritedWidgetBase( - data: const PermissionsService(), - child: MeteringScreenLayoutProvider( - child: StopTypeProvider( - child: EquipmentProfileProvider( - child: EvSourceTypeProvider( - child: SupportedLocaleProvider( - child: ThemeProvider( - child: Builder( - builder: (context) => builder(context, true), - ), - ), - ), - ), - ), - ), - ), - ), - ), - ), - ), - ), - ), - ); - } else if (snapshot.error != null) { - return Center(child: Text(snapshot.error!.toString())); - } - return builder(context, false); - }, - ); - } -} diff --git a/lib/providers/equipment_profile_provider.dart b/lib/providers/equipment_profile_provider.dart index 85c9377..c0294fa 100644 --- a/lib/providers/equipment_profile_provider.dart +++ b/lib/providers/equipment_profile_provider.dart @@ -1,11 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:lightmeter/data/shared_prefs_service.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; +import 'package:lightmeter/providers/services_provider.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; import 'package:uuid/uuid.dart'; -typedef EquipmentProfiles = List; - +// TODO(@vodemn): This will be removed in #89 class EquipmentProfileProvider extends StatefulWidget { final Widget child; @@ -35,7 +33,8 @@ class EquipmentProfileProviderState extends State { EquipmentProfile get _selectedProfile => _customProfiles.firstWhere( (e) => e.id == _selectedId, orElse: () { - context.get().selectedEquipmentProfileId = _defaultProfile.id; + ServicesProvider.of(context).userPreferencesService.selectedEquipmentProfileId = + _defaultProfile.id; return _defaultProfile; }, ); @@ -43,18 +42,16 @@ class EquipmentProfileProviderState extends State { @override void initState() { super.initState(); - _selectedId = context.get().selectedEquipmentProfileId; - _customProfiles = context.get().equipmentProfiles; + _selectedId = ServicesProvider.of(context).userPreferencesService.selectedEquipmentProfileId; + _customProfiles = ServicesProvider.of(context).userPreferencesService.equipmentProfiles; } @override Widget build(BuildContext context) { - return InheritedWidgetBase>( - data: [_defaultProfile] + _customProfiles, - child: InheritedWidgetBase( - data: _selectedProfile, - child: widget.child, - ), + return EquipmentProfiles( + profiles: [_defaultProfile] + _customProfiles, + selected: _selectedProfile, + child: widget.child, ); } @@ -62,7 +59,8 @@ class EquipmentProfileProviderState extends State { setState(() { _selectedId = data.id; }); - context.get().selectedEquipmentProfileId = _selectedProfile.id; + ServicesProvider.of(context).userPreferencesService.selectedEquipmentProfileId = + _selectedProfile.id; } /// Creates a default equipment profile @@ -94,7 +92,48 @@ class EquipmentProfileProviderState extends State { } void _refreshSavedProfiles() { - context.get().equipmentProfiles = _customProfiles; + ServicesProvider.of(context).userPreferencesService.equipmentProfiles = _customProfiles; setState(() {}); } } + +// Copied from #89 +enum EquipmentProfilesAspect { list, selected } + +class EquipmentProfiles extends InheritedModel { + const EquipmentProfiles({ + super.key, + required this.profiles, + required this.selected, + required super.child, + }); + + final List profiles; + final EquipmentProfile selected; + + static List of(BuildContext context) { + return InheritedModel.inheritFrom( + context, + aspect: EquipmentProfilesAspect.list, + )! + .profiles; + } + + static EquipmentProfile selectedOf(BuildContext context) { + return InheritedModel.inheritFrom( + context, + aspect: EquipmentProfilesAspect.selected, + )! + .selected; + } + + @override + bool updateShouldNotify(EquipmentProfiles oldWidget) => false; + + @override + bool updateShouldNotifyDependent( + EquipmentProfiles oldWidget, + Set dependencies, + ) => + false; +} diff --git a/lib/providers/ev_source_type_provider.dart b/lib/providers/ev_source_type_provider.dart deleted file mode 100644 index aed873b..0000000 --- a/lib/providers/ev_source_type_provider.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lightmeter/data/models/ev_source_type.dart'; -import 'package:lightmeter/data/shared_prefs_service.dart'; -import 'package:lightmeter/environment.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; - -class EvSourceTypeProvider extends StatefulWidget { - final Widget child; - - const EvSourceTypeProvider({required this.child, super.key}); - - static EvSourceTypeProviderState of(BuildContext context) { - return context.findAncestorStateOfType()!; - } - - @override - State createState() => EvSourceTypeProviderState(); -} - -class EvSourceTypeProviderState extends State { - late final ValueNotifier valueListenable; - - @override - void initState() { - super.initState(); - final evSourceType = context.get().evSourceType; - valueListenable = ValueNotifier( - evSourceType == EvSourceType.sensor && !context.get().hasLightSensor - ? EvSourceType.camera - : evSourceType, - ); - } - - @override - void dispose() { - valueListenable.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: valueListenable, - builder: (_, value, child) => InheritedWidgetBase( - data: value, - child: child!, - ), - child: widget.child, - ); - } - - void toggleType() { - switch (valueListenable.value) { - case EvSourceType.camera: - if (context.get().hasLightSensor) { - valueListenable.value = EvSourceType.sensor; - } - case EvSourceType.sensor: - valueListenable.value = EvSourceType.camera; - } - context.get().evSourceType = valueListenable.value; - } -} diff --git a/lib/providers/metering_screen_layout_provider.dart b/lib/providers/metering_screen_layout_provider.dart deleted file mode 100644 index 405c1a7..0000000 --- a/lib/providers/metering_screen_layout_provider.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lightmeter/data/models/metering_screen_layout_config.dart'; -import 'package:lightmeter/data/shared_prefs_service.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; - -class MeteringScreenLayoutProvider extends StatefulWidget { - final Widget child; - - const MeteringScreenLayoutProvider({required this.child, super.key}); - - static MeteringScreenLayoutProviderState of(BuildContext context) { - return context.findAncestorStateOfType()!; - } - - @override - State createState() => MeteringScreenLayoutProviderState(); -} - -class MeteringScreenLayoutProviderState extends State { - late final MeteringScreenLayoutConfig _config = - context.get().meteringScreenLayout; - - @override - Widget build(BuildContext context) { - return InheritedModelBase( - data: MeteringScreenLayoutConfig.from(_config), - child: widget.child, - ); - } - - void updateFeatures(MeteringScreenLayoutConfig config) { - setState(() { - config.forEach((key, value) { - _config.update( - key, - (_) => value, - ifAbsent: () => value, - ); - }); - }); - context.get().meteringScreenLayout = _config; - } -} - -typedef _MeteringScreenLayoutModel = InheritedModelBase; - -extension MeteringScreenLayout on InheritedModelBase { - static MeteringScreenLayoutConfig of(BuildContext context, {bool listen = true}) { - if (listen) { - return context.dependOnInheritedWidgetOfExactType<_MeteringScreenLayoutModel>()!.data; - } else { - return context.findAncestorWidgetOfExactType<_MeteringScreenLayoutModel>()!.data; - } - } - - static bool featureOf(BuildContext context, MeteringScreenLayoutFeature aspect) { - return InheritedModel.inheritFrom<_MeteringScreenLayoutModel>(context, aspect: aspect)! - .data[aspect]!; - } -} diff --git a/lib/providers/services_provider.dart b/lib/providers/services_provider.dart new file mode 100644 index 0000000..c2c548f --- /dev/null +++ b/lib/providers/services_provider.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:lightmeter/data/caffeine_service.dart'; +import 'package:lightmeter/data/haptics_service.dart'; +import 'package:lightmeter/data/light_sensor_service.dart'; +import 'package:lightmeter/data/permissions_service.dart'; +import 'package:lightmeter/data/shared_prefs_service.dart'; +import 'package:lightmeter/data/volume_events_service.dart'; +import 'package:lightmeter/environment.dart'; + +class ServicesProvider extends InheritedWidget { + final CaffeineService caffeineService; + final Environment environment; + final HapticsService hapticsService; + final LightSensorService lightSensorService; + final PermissionsService permissionsService; + final UserPreferencesService userPreferencesService; + final VolumeEventsService volumeEventsService; + + const ServicesProvider({ + required this.caffeineService, + required this.environment, + required this.hapticsService, + required this.lightSensorService, + required this.permissionsService, + required this.userPreferencesService, + required this.volumeEventsService, + required super.child, + }); + + static ServicesProvider of(BuildContext context) { + return context.findAncestorWidgetOfExactType()!; + } + + @override + bool updateShouldNotify(ServicesProvider oldWidget) => false; +} diff --git a/lib/providers/stop_type_provider.dart b/lib/providers/stop_type_provider.dart deleted file mode 100644 index 3b20dd5..0000000 --- a/lib/providers/stop_type_provider.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lightmeter/data/shared_prefs_service.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; -import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; - -class StopTypeProvider extends StatefulWidget { - final Widget child; - - const StopTypeProvider({required this.child, super.key}); - - static StopTypeProviderState of(BuildContext context) { - return context.findAncestorStateOfType()!; - } - - @override - State createState() => StopTypeProviderState(); -} - -class StopTypeProviderState extends State { - late StopType _stopType; - - @override - void initState() { - super.initState(); - _stopType = context.get().stopType; - } - - @override - Widget build(BuildContext context) { - return InheritedWidgetBase( - data: _stopType, - child: widget.child, - ); - } - - void set(StopType type) { - setState(() { - _stopType = type; - }); - context.get().stopType = type; - } -} diff --git a/lib/providers/supported_locale_provider.dart b/lib/providers/supported_locale_provider.dart deleted file mode 100644 index caa7ced..0000000 --- a/lib/providers/supported_locale_provider.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lightmeter/data/models/supported_locale.dart'; -import 'package:lightmeter/data/shared_prefs_service.dart'; -import 'package:lightmeter/generated/l10n.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; - -class SupportedLocaleProvider extends StatefulWidget { - final Widget child; - - const SupportedLocaleProvider({required this.child, super.key}); - - static SupportedLocaleProviderState of(BuildContext context) { - return context.findAncestorStateOfType()!; - } - - @override - State createState() => SupportedLocaleProviderState(); -} - -class SupportedLocaleProviderState extends State { - late final ValueNotifier valueListenable; - - @override - void initState() { - super.initState(); - valueListenable = ValueNotifier(context.get().locale); - } - - @override - void dispose() { - valueListenable.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: valueListenable, - builder: (_, value, child) => InheritedWidgetBase( - data: value, - child: child!, - ), - child: widget.child, - ); - } - - void setLocale(SupportedLocale locale) { - S.load(Locale(locale.intlName)).then((value) { - valueListenable.value = locale; - context.get().locale = locale; - }); - } -} diff --git a/lib/providers/theme_provider.dart b/lib/providers/theme_provider.dart deleted file mode 100644 index 9773df1..0000000 --- a/lib/providers/theme_provider.dart +++ /dev/null @@ -1,257 +0,0 @@ -import 'package:dynamic_color/dynamic_color.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; -import 'package:lightmeter/data/models/dynamic_colors_state.dart'; -import 'package:lightmeter/data/models/theme_type.dart'; -import 'package:lightmeter/data/shared_prefs_service.dart'; -import 'package:lightmeter/res/dimens.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; -import 'package:material_color_utilities/material_color_utilities.dart'; - -class ThemeProvider extends StatefulWidget { - final Widget child; - - const ThemeProvider({ - required this.child, - super.key, - }); - - static ThemeProviderState of(BuildContext context) { - return context.findAncestorStateOfType()!; - } - - static const primaryColorsList = [ - Color(0xfff44336), - Color(0xffe91e63), - Color(0xff9c27b0), - Color(0xff673ab7), - Color(0xff3f51b5), - Color(0xff2196f3), - Color(0xff03a9f4), - Color(0xff00bcd4), - Color(0xff009688), - Color(0xff4caf50), - Color(0xff8bc34a), - Color(0xffcddc39), - Color(0xffffeb3b), - Color(0xffffc107), - Color(0xffff9800), - Color(0xffff5722), - ]; - - @override - State createState() => ThemeProviderState(); -} - -class ThemeProviderState extends State with WidgetsBindingObserver { - UserPreferencesService get _prefs => context.get(); - - late final _themeTypeNotifier = ValueNotifier(_prefs.themeType); - late final _dynamicColorNotifier = ValueNotifier(_prefs.dynamicColor); - late final _primaryColorNotifier = ValueNotifier(_prefs.primaryColor); - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addObserver(this); - } - - @override - void didChangePlatformBrightness() { - super.didChangePlatformBrightness(); - setState(() {}); - } - - @override - void dispose() { - WidgetsBinding.instance.removeObserver(this); - _themeTypeNotifier.dispose(); - _dynamicColorNotifier.dispose(); - _primaryColorNotifier.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: _themeTypeNotifier, - builder: (_, themeType, __) => InheritedWidgetBase( - data: themeType, - child: ValueListenableBuilder( - valueListenable: _dynamicColorNotifier, - builder: (_, useDynamicColor, __) => _DynamicColorProvider( - useDynamicColor: useDynamicColor, - themeBrightness: _themeBrightness, - builder: (_, dynamicPrimaryColor) => ValueListenableBuilder( - valueListenable: _primaryColorNotifier, - builder: (_, primaryColor, __) => _ThemeDataProvider( - primaryColor: dynamicPrimaryColor ?? primaryColor, - brightness: _themeBrightness, - child: widget.child, - ), - ), - ), - ), - ), - ); - } - - void setThemeType(ThemeType themeType) { - _themeTypeNotifier.value = themeType; - _prefs.themeType = themeType; - } - - Brightness get _themeBrightness { - switch (_themeTypeNotifier.value) { - case ThemeType.light: - return Brightness.light; - case ThemeType.dark: - return Brightness.dark; - case ThemeType.systemDefault: - return SchedulerBinding.instance.platformDispatcher.platformBrightness; - } - } - - void setPrimaryColor(Color color) { - _primaryColorNotifier.value = color; - _prefs.primaryColor = color; - } - - void enableDynamicColor(bool enable) { - _dynamicColorNotifier.value = enable; - _prefs.dynamicColor = enable; - } -} - -class _DynamicColorProvider extends StatelessWidget { - final bool useDynamicColor; - final Brightness themeBrightness; - final Widget Function(BuildContext context, Color? primaryColor) builder; - - const _DynamicColorProvider({ - required this.useDynamicColor, - required this.themeBrightness, - required this.builder, - }); - - @override - Widget build(BuildContext context) { - return DynamicColorBuilder( - builder: (lightDynamic, darkDynamic) { - late final DynamicColorState state; - late final Color? dynamicPrimaryColor; - if (lightDynamic != null && darkDynamic != null) { - if (useDynamicColor) { - dynamicPrimaryColor = - (themeBrightness == Brightness.light ? lightDynamic : darkDynamic).primary; - state = DynamicColorState.enabled; - } else { - dynamicPrimaryColor = null; - state = DynamicColorState.disabled; - } - } else { - dynamicPrimaryColor = null; - state = DynamicColorState.unavailable; - } - return InheritedWidgetBase( - data: state, - child: builder(context, dynamicPrimaryColor), - ); - }, - ); - } -} - -class _ThemeDataProvider extends StatelessWidget { - final Color primaryColor; - final Brightness brightness; - final Widget child; - - const _ThemeDataProvider({ - required this.primaryColor, - required this.brightness, - required this.child, - }); - - @override - Widget build(BuildContext context) { - return InheritedWidgetBase( - data: _themeFromColorScheme(_colorSchemeFromColor()), - child: child, - ); - } - - ThemeData _themeFromColorScheme(ColorScheme scheme) { - return ThemeData( - useMaterial3: true, - brightness: scheme.brightness, - primaryColor: primaryColor, - colorScheme: scheme, - appBarTheme: AppBarTheme( - elevation: 4, - color: scheme.surface, - surfaceTintColor: scheme.surfaceTint, - ), - cardTheme: CardTheme( - clipBehavior: Clip.antiAlias, - color: scheme.surface, - elevation: 4, - margin: EdgeInsets.zero, - shadowColor: Colors.transparent, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(Dimens.borderRadiusL)), - surfaceTintColor: scheme.surfaceTint, - ), - dialogBackgroundColor: scheme.surface, - dialogTheme: DialogTheme( - backgroundColor: scheme.surface, - surfaceTintColor: scheme.surfaceTint, - elevation: 6, - ), - dividerColor: scheme.outlineVariant, - dividerTheme: DividerThemeData( - color: scheme.outlineVariant, - space: 0, - ), - listTileTheme: ListTileThemeData( - style: ListTileStyle.list, - iconColor: scheme.onSurface, - textColor: scheme.onSurface, - ), - scaffoldBackgroundColor: scheme.surface, - ); - } - - ColorScheme _colorSchemeFromColor() { - final scheme = brightness == Brightness.light - ? Scheme.light(primaryColor.value) - : Scheme.dark(primaryColor.value); - - return ColorScheme( - brightness: brightness, - background: Color(scheme.background), - error: Color(scheme.error), - errorContainer: Color(scheme.errorContainer), - onBackground: Color(scheme.onBackground), - onError: Color(scheme.onError), - onErrorContainer: Color(scheme.onErrorContainer), - primary: Color(scheme.primary), - onPrimary: Color(scheme.onPrimary), - primaryContainer: Color(scheme.primaryContainer), - onPrimaryContainer: Color(scheme.onPrimaryContainer), - secondary: Color(scheme.secondary), - onSecondary: Color(scheme.onSecondary), - surface: Color.alphaBlend( - Color(scheme.primary).withOpacity(0.05), - Color(scheme.background), - ), - onSurface: Color(scheme.onSurface), - surfaceVariant: Color.alphaBlend( - Color(scheme.primary).withOpacity(0.5), - Color(scheme.background), - ), - onSurfaceVariant: Color(scheme.onSurfaceVariant), - outline: Color(scheme.outline), - outlineVariant: Color(scheme.outlineVariant), - ); - } -} diff --git a/lib/providers/user_preferences_provider.dart b/lib/providers/user_preferences_provider.dart new file mode 100644 index 0000000..af644d1 --- /dev/null +++ b/lib/providers/user_preferences_provider.dart @@ -0,0 +1,295 @@ +import 'package:dynamic_color/dynamic_color.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:lightmeter/data/models/dynamic_colors_state.dart'; +import 'package:lightmeter/data/models/ev_source_type.dart'; +import 'package:lightmeter/data/models/metering_screen_layout_config.dart'; +import 'package:lightmeter/data/models/supported_locale.dart'; +import 'package:lightmeter/data/models/theme_type.dart'; +import 'package:lightmeter/data/shared_prefs_service.dart'; +import 'package:lightmeter/generated/l10n.dart'; +import 'package:lightmeter/providers/services_provider.dart'; +import 'package:lightmeter/res/theme.dart'; +import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; + +class UserPreferencesProvider extends StatefulWidget { + final Widget child; + + const UserPreferencesProvider({required this.child, super.key}); + + static _UserPreferencesProviderState of(BuildContext context) { + return context.findAncestorStateOfType<_UserPreferencesProviderState>()!; + } + + static DynamicColorState dynamicColorStateOf(BuildContext context) { + return _inheritFromEnumsModel(context, _Aspect.dynamicColorState).dynamicColorState; + } + + static EvSourceType evSourceTypeOf(BuildContext context) { + return _inheritFromEnumsModel(context, _Aspect.evSourceType).evSourceType; + } + + static SupportedLocale localeOf(BuildContext context) { + return _inheritFromEnumsModel(context, _Aspect.locale).locale; + } + + static MeteringScreenLayoutConfig meteringScreenConfigOf(BuildContext context) { + return context.findAncestorWidgetOfExactType<_MeteringScreenLayoutModel>()!.data; + } + + static bool meteringScreenFeatureOf(BuildContext context, MeteringScreenLayoutFeature feature) { + return InheritedModel.inheritFrom<_MeteringScreenLayoutModel>(context, aspect: feature)! + .data[feature]!; + } + + static StopType stopTypeOf(BuildContext context) { + return _inheritFromEnumsModel(context, _Aspect.stopType).stopType; + } + + static ThemeData themeOf(BuildContext context) { + return _inheritFromEnumsModel(context, _Aspect.theme).theme; + } + + static ThemeType themeTypeOf(BuildContext context) { + return _inheritFromEnumsModel(context, _Aspect.themeType).themeType; + } + + static _UserPreferencesModel _inheritFromEnumsModel( + BuildContext context, + _Aspect aspect, + ) { + return InheritedModel.inheritFrom<_UserPreferencesModel>(context, aspect: aspect)!; + } + + @override + State createState() => _UserPreferencesProviderState(); +} + +class _UserPreferencesProviderState extends State + with WidgetsBindingObserver { + UserPreferencesService get userPreferencesService => + ServicesProvider.of(context).userPreferencesService; + + late bool dynamicColor = userPreferencesService.dynamicColor; + late EvSourceType evSourceType; + late MeteringScreenLayoutConfig meteringScreenLayout = + userPreferencesService.meteringScreenLayout; + late Color primaryColor = userPreferencesService.primaryColor; + late StopType stopType = userPreferencesService.stopType; + late SupportedLocale locale = userPreferencesService.locale; + late ThemeType themeType = userPreferencesService.themeType; + + @override + void initState() { + super.initState(); + evSourceType = userPreferencesService.evSourceType; + evSourceType = evSourceType == EvSourceType.sensor && + !ServicesProvider.of(context).environment.hasLightSensor + ? EvSourceType.camera + : evSourceType; + WidgetsBinding.instance.addObserver(this); + } + + @override + void didChangePlatformBrightness() { + super.didChangePlatformBrightness(); + setState(() {}); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return DynamicColorBuilder( + builder: (lightDynamic, darkDynamic) { + late final DynamicColorState state; + late final Color? dynamicPrimaryColor; + if (lightDynamic != null && darkDynamic != null) { + if (dynamicColor) { + dynamicPrimaryColor = + (_themeBrightness == Brightness.light ? lightDynamic : darkDynamic).primary; + state = DynamicColorState.enabled; + } else { + dynamicPrimaryColor = null; + state = DynamicColorState.disabled; + } + } else { + dynamicPrimaryColor = null; + state = DynamicColorState.unavailable; + } + return _UserPreferencesModel( + brightness: _themeBrightness, + dynamicColorState: state, + evSourceType: evSourceType, + locale: locale, + primaryColor: dynamicPrimaryColor ?? primaryColor, + stopType: stopType, + themeType: themeType, + child: _MeteringScreenLayoutModel( + data: meteringScreenLayout, + child: widget.child, + ), + ); + }, + ); + } + + void enableDynamicColor(bool enable) { + setState(() { + dynamicColor = enable; + }); + userPreferencesService.dynamicColor = enable; + } + + void toggleEvSourceType() { + if (!ServicesProvider.of(context).environment.hasLightSensor) { + return; + } + setState(() { + switch (evSourceType) { + case EvSourceType.camera: + evSourceType = EvSourceType.sensor; + case EvSourceType.sensor: + evSourceType = EvSourceType.camera; + } + }); + userPreferencesService.evSourceType = evSourceType; + } + + void setLocale(SupportedLocale locale) { + S.load(Locale(locale.intlName)).then((value) { + setState(() { + this.locale = locale; + }); + userPreferencesService.locale = locale; + }); + } + + void setMeteringScreenLayout(MeteringScreenLayoutConfig config) { + setState(() { + meteringScreenLayout = config; + }); + userPreferencesService.meteringScreenLayout = meteringScreenLayout; + } + + void setPrimaryColor(Color primaryColor) { + setState(() { + this.primaryColor = primaryColor; + }); + userPreferencesService.primaryColor = primaryColor; + } + + void setStopType(StopType stopType) { + setState(() { + this.stopType = stopType; + }); + userPreferencesService.stopType = stopType; + } + + void setThemeType(ThemeType themeType) { + setState(() { + this.themeType = themeType; + }); + userPreferencesService.themeType = themeType; + } + + Brightness get _themeBrightness { + switch (themeType) { + case ThemeType.light: + return Brightness.light; + case ThemeType.dark: + return Brightness.dark; + case ThemeType.systemDefault: + return SchedulerBinding.instance.platformDispatcher.platformBrightness; + } + } +} + +enum _Aspect { + dynamicColorState, + evSourceType, + locale, + stopType, + theme, + themeType, +} + +class _UserPreferencesModel extends InheritedModel<_Aspect> { + final DynamicColorState dynamicColorState; + final EvSourceType evSourceType; + final SupportedLocale locale; + final StopType stopType; + final ThemeType themeType; + + final Brightness _brightness; + final Color _primaryColor; + + const _UserPreferencesModel({ + required Brightness brightness, + required this.dynamicColorState, + required this.evSourceType, + required this.locale, + required Color primaryColor, + required this.stopType, + required this.themeType, + required super.child, + }) : _brightness = brightness, + _primaryColor = primaryColor; + + ThemeData get theme => themeFrom(_primaryColor, _brightness); + + @override + bool updateShouldNotify(_UserPreferencesModel oldWidget) { + return _brightness != oldWidget._brightness || + dynamicColorState != oldWidget.dynamicColorState || + evSourceType != oldWidget.evSourceType || + locale != oldWidget.locale || + _primaryColor != oldWidget._primaryColor || + stopType != oldWidget.stopType || + themeType != oldWidget.themeType; + } + + @override + bool updateShouldNotifyDependent( + _UserPreferencesModel oldWidget, + Set<_Aspect> dependencies, + ) { + return (dependencies.contains(_Aspect.dynamicColorState) && + dynamicColorState != oldWidget.dynamicColorState) || + (dependencies.contains(_Aspect.evSourceType) && evSourceType != oldWidget.evSourceType) || + (dependencies.contains(_Aspect.locale) && locale != oldWidget.locale) || + (dependencies.contains(_Aspect.stopType) && stopType != oldWidget.stopType) || + (dependencies.contains(_Aspect.theme) && + (_brightness != oldWidget._brightness || _primaryColor != oldWidget._primaryColor)) || + (dependencies.contains(_Aspect.themeType) && themeType != oldWidget.themeType); + } +} + +class _MeteringScreenLayoutModel extends InheritedModel { + final Map data; + + const _MeteringScreenLayoutModel({ + required this.data, + required super.child, + }); + + @override + bool updateShouldNotify(_MeteringScreenLayoutModel oldWidget) => oldWidget.data != data; + + @override + bool updateShouldNotifyDependent( + _MeteringScreenLayoutModel oldWidget, + Set dependencies, + ) { + for (final dependecy in dependencies) { + if (oldWidget.data[dependecy] != data[dependecy]) { + return true; + } + } + return false; + } +} diff --git a/lib/res/theme.dart b/lib/res/theme.dart new file mode 100644 index 0000000..a6320c1 --- /dev/null +++ b/lib/res/theme.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:lightmeter/res/dimens.dart'; +import 'package:material_color_utilities/material_color_utilities.dart'; + +const primaryColorsList = [ + Color(0xfff44336), + Color(0xffe91e63), + Color(0xff9c27b0), + Color(0xff673ab7), + Color(0xff3f51b5), + Color(0xff2196f3), + Color(0xff03a9f4), + Color(0xff00bcd4), + Color(0xff009688), + Color(0xff4caf50), + Color(0xff8bc34a), + Color(0xffcddc39), + Color(0xffffeb3b), + Color(0xffffc107), + Color(0xffff9800), + Color(0xffff5722), +]; + +ThemeData themeFrom(Color primaryColor, Brightness brightness) { + final scheme = _colorSchemeFromColor(primaryColor, brightness); + return ThemeData( + useMaterial3: true, + brightness: scheme.brightness, + primaryColor: primaryColor, + colorScheme: scheme, + appBarTheme: AppBarTheme( + elevation: 4, + color: scheme.surface, + surfaceTintColor: scheme.surfaceTint, + ), + cardTheme: CardTheme( + clipBehavior: Clip.antiAlias, + color: scheme.surface, + elevation: 4, + margin: EdgeInsets.zero, + shadowColor: Colors.transparent, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(Dimens.borderRadiusL)), + surfaceTintColor: scheme.surfaceTint, + ), + dialogBackgroundColor: scheme.surface, + dialogTheme: DialogTheme( + backgroundColor: scheme.surface, + surfaceTintColor: scheme.surfaceTint, + elevation: 6, + ), + dividerColor: scheme.outlineVariant, + dividerTheme: DividerThemeData( + color: scheme.outlineVariant, + space: 0, + ), + listTileTheme: ListTileThemeData( + style: ListTileStyle.list, + iconColor: scheme.onSurface, + textColor: scheme.onSurface, + ), + scaffoldBackgroundColor: scheme.surface, + ); +} + +ColorScheme _colorSchemeFromColor(Color primaryColor, Brightness brightness) { + final scheme = brightness == Brightness.light + ? Scheme.light(primaryColor.value) + : Scheme.dark(primaryColor.value); + + return ColorScheme( + brightness: brightness, + background: Color(scheme.background), + error: Color(scheme.error), + errorContainer: Color(scheme.errorContainer), + onBackground: Color(scheme.onBackground), + onError: Color(scheme.onError), + onErrorContainer: Color(scheme.onErrorContainer), + primary: Color(scheme.primary), + onPrimary: Color(scheme.onPrimary), + primaryContainer: Color(scheme.primaryContainer), + onPrimaryContainer: Color(scheme.onPrimaryContainer), + secondary: Color(scheme.secondary), + onSecondary: Color(scheme.onSecondary), + surface: Color.alphaBlend( + Color(scheme.primary).withOpacity(0.05), + Color(scheme.background), + ), + onSurface: Color(scheme.onSurface), + surfaceVariant: Color.alphaBlend( + Color(scheme.primary).withOpacity(0.5), + Color(scheme.background), + ), + onSurfaceVariant: Color(scheme.onSurfaceVariant), + outline: Color(scheme.outline), + outlineVariant: Color(scheme.outlineVariant), + ); +} diff --git a/lib/screens/metering/components/bottom_controls/widget_bottom_controls.dart b/lib/screens/metering/components/bottom_controls/widget_bottom_controls.dart index 9989494..54ea810 100644 --- a/lib/screens/metering/components/bottom_controls/widget_bottom_controls.dart +++ b/lib/screens/metering/components/bottom_controls/widget_bottom_controls.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/data/models/ev_source_type.dart'; +import 'package:lightmeter/providers/user_preferences_provider.dart'; import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/screens/metering/components/bottom_controls/components/measure_button/widget_button_measure.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; class MeteringBottomControls extends StatelessWidget { final double? ev; @@ -42,7 +42,7 @@ class MeteringBottomControls extends StatelessWidget { child: IconButton( onPressed: onSwitchEvSourceType, icon: Icon( - context.listen() != EvSourceType.camera + UserPreferencesProvider.evSourceTypeOf(context) != EvSourceType.camera ? Icons.camera_rear : Icons.wb_incandescent, ), diff --git a/lib/screens/metering/components/camera_container/components/camera_preview/widget_camera_preview.dart b/lib/screens/metering/components/camera_container/components/camera_preview/widget_camera_preview.dart index 288c678..ecfcb42 100644 --- a/lib/screens/metering/components/camera_container/components/camera_preview/widget_camera_preview.dart +++ b/lib/screens/metering/components/camera_container/components/camera_preview/widget_camera_preview.dart @@ -2,7 +2,7 @@ import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; import 'package:lightmeter/data/models/metering_screen_layout_config.dart'; import 'package:lightmeter/platform_config.dart'; -import 'package:lightmeter/providers/metering_screen_layout_provider.dart'; +import 'package:lightmeter/providers/user_preferences_provider.dart'; import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/screens/metering/components/camera_container/components/camera_preview/components/camera_view/widget_camera_view.dart'; import 'package:lightmeter/screens/metering/components/camera_container/components/camera_preview/components/camera_view_placeholder/widget_placeholder_camera_view.dart'; @@ -38,7 +38,7 @@ class _CameraPreviewState extends State { alignment: Alignment.bottomCenter, children: [ CameraView(controller: widget.controller!), - if (MeteringScreenLayout.featureOf( + if (UserPreferencesProvider.meteringScreenFeatureOf( context, MeteringScreenLayoutFeature.histogram, )) diff --git a/lib/screens/metering/components/camera_container/provider_container_camera.dart b/lib/screens/metering/components/camera_container/provider_container_camera.dart index 344f573..0e27700 100644 --- a/lib/screens/metering/components/camera_container/provider_container_camera.dart +++ b/lib/screens/metering/components/camera_container/provider_container_camera.dart @@ -2,12 +2,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:lightmeter/data/models/exposure_pair.dart'; import 'package:lightmeter/data/models/film.dart'; -import 'package:lightmeter/interactors/metering_interactor.dart'; import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart'; import 'package:lightmeter/screens/metering/components/camera_container/bloc_container_camera.dart'; import 'package:lightmeter/screens/metering/components/camera_container/event_container_camera.dart'; import 'package:lightmeter/screens/metering/components/camera_container/widget_container_camera.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; +import 'package:lightmeter/screens/metering/flow_metering.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; class CameraContainerProvider extends StatelessWidget { @@ -39,7 +38,7 @@ class CameraContainerProvider extends StatelessWidget { return BlocProvider( lazy: false, create: (context) => CameraContainerBloc( - context.get(), + MeteringInteractorProvider.of(context), context.read(), )..add(const RequestPermissionEvent()), child: CameraContainer( diff --git a/lib/screens/metering/components/camera_container/widget_container_camera.dart b/lib/screens/metering/components/camera_container/widget_container_camera.dart index e42991c..c510bc8 100644 --- a/lib/screens/metering/components/camera_container/widget_container_camera.dart +++ b/lib/screens/metering/components/camera_container/widget_container_camera.dart @@ -7,7 +7,7 @@ import 'package:lightmeter/data/models/film.dart'; import 'package:lightmeter/data/models/metering_screen_layout_config.dart'; import 'package:lightmeter/features.dart'; import 'package:lightmeter/platform_config.dart'; -import 'package:lightmeter/providers/metering_screen_layout_provider.dart'; +import 'package:lightmeter/providers/user_preferences_provider.dart'; import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/screens/metering/components/camera_container/bloc_container_camera.dart'; import 'package:lightmeter/screens/metering/components/camera_container/components/camera_controls/widget_camera_controls.dart'; @@ -114,14 +114,14 @@ class CameraContainer extends StatelessWidget { enabledFeaturesHeight += Dimens.readingContainerSingleValueHeight; enabledFeaturesHeight += Dimens.paddingS; } - if (MeteringScreenLayout.featureOf( + if (UserPreferencesProvider.meteringScreenFeatureOf( context, MeteringScreenLayoutFeature.extremeExposurePairs, )) { enabledFeaturesHeight += Dimens.readingContainerDoubleValueHeight; enabledFeaturesHeight += Dimens.paddingS; } - if (MeteringScreenLayout.featureOf( + if (UserPreferencesProvider.meteringScreenFeatureOf( context, MeteringScreenLayoutFeature.filmPicker, )) { diff --git a/lib/screens/metering/components/light_sensor_container/provider_container_light_sensor.dart b/lib/screens/metering/components/light_sensor_container/provider_container_light_sensor.dart index c7423fc..aa27504 100644 --- a/lib/screens/metering/components/light_sensor_container/provider_container_light_sensor.dart +++ b/lib/screens/metering/components/light_sensor_container/provider_container_light_sensor.dart @@ -2,11 +2,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:lightmeter/data/models/exposure_pair.dart'; import 'package:lightmeter/data/models/film.dart'; -import 'package:lightmeter/interactors/metering_interactor.dart'; import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart'; import 'package:lightmeter/screens/metering/components/light_sensor_container/bloc_container_light_sensor.dart'; import 'package:lightmeter/screens/metering/components/light_sensor_container/widget_container_light_sensor.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; +import 'package:lightmeter/screens/metering/flow_metering.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; class LightSensorContainerProvider extends StatelessWidget { @@ -38,7 +37,7 @@ class LightSensorContainerProvider extends StatelessWidget { return BlocProvider( lazy: false, create: (context) => LightSensorContainerBloc( - context.get(), + MeteringInteractorProvider.of(context), context.read(), ), child: LightSensorContainer( diff --git a/lib/screens/metering/components/shared/readings_container/widget_container_readings.dart b/lib/screens/metering/components/shared/readings_container/widget_container_readings.dart index 0893305..d31380c 100644 --- a/lib/screens/metering/components/shared/readings_container/widget_container_readings.dart +++ b/lib/screens/metering/components/shared/readings_container/widget_container_readings.dart @@ -5,11 +5,10 @@ import 'package:lightmeter/data/models/metering_screen_layout_config.dart'; import 'package:lightmeter/features.dart'; import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/providers/equipment_profile_provider.dart'; -import 'package:lightmeter/providers/metering_screen_layout_provider.dart'; +import 'package:lightmeter/providers/user_preferences_provider.dart'; import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/screens/metering/components/shared/readings_container/components/animated_dialog_picker/widget_picker_dialog_animated.dart'; import 'package:lightmeter/screens/metering/components/shared/readings_container/components/reading_value_container/widget_container_reading_value.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; class ReadingsContainer extends StatelessWidget { @@ -43,7 +42,7 @@ class ReadingsContainer extends StatelessWidget { const _EquipmentProfilePicker(), const _InnerPadding(), ], - if (MeteringScreenLayout.featureOf( + if (UserPreferencesProvider.meteringScreenFeatureOf( context, MeteringScreenLayoutFeature.extremeExposurePairs, )) ...[ @@ -61,7 +60,7 @@ class ReadingsContainer extends StatelessWidget { ), const _InnerPadding(), ], - if (MeteringScreenLayout.featureOf( + if (UserPreferencesProvider.meteringScreenFeatureOf( context, MeteringScreenLayoutFeature.filmPicker, )) ...[ @@ -77,7 +76,7 @@ class ReadingsContainer extends StatelessWidget { Expanded( child: _IsoValuePicker( selectedValue: iso, - values: context.listen().isoValues, + values: EquipmentProfiles.selectedOf(context).isoValues, onChanged: onIsoChanged, ), ), @@ -85,7 +84,7 @@ class ReadingsContainer extends StatelessWidget { Expanded( child: _NdValuePicker( selectedValue: nd, - values: context.listen().ndValues, + values: EquipmentProfiles.selectedOf(context).ndValues, onChanged: onNdChanged, ), ), @@ -108,16 +107,16 @@ class _EquipmentProfilePicker extends StatelessWidget { return AnimatedDialogPicker( icon: Icons.camera, title: S.of(context).equipmentProfile, - selectedValue: context.listen(), - values: context.listen(), + selectedValue: EquipmentProfiles.selectedOf(context), + values: EquipmentProfiles.of(context), itemTitleBuilder: (_, value) => Text(value.id.isEmpty ? S.of(context).none : value.name), onChanged: EquipmentProfileProvider.of(context).setProfile, closedChild: ReadingValueContainer.singleValue( value: ReadingValue( label: S.of(context).equipmentProfile, - value: context.listen().id.isEmpty + value: EquipmentProfiles.selectedOf(context).id.isEmpty ? S.of(context).none - : context.listen().name, + : EquipmentProfiles.selectedOf(context).name, ), ), ); diff --git a/lib/screens/metering/flow_metering.dart b/lib/screens/metering/flow_metering.dart index 1caef02..cca5675 100644 --- a/lib/screens/metering/flow_metering.dart +++ b/lib/screens/metering/flow_metering.dart @@ -1,17 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:lightmeter/data/caffeine_service.dart'; -import 'package:lightmeter/data/haptics_service.dart'; -import 'package:lightmeter/data/light_sensor_service.dart'; -import 'package:lightmeter/data/permissions_service.dart'; -import 'package:lightmeter/data/shared_prefs_service.dart'; -import 'package:lightmeter/data/volume_events_service.dart'; import 'package:lightmeter/interactors/metering_interactor.dart'; +import 'package:lightmeter/providers/services_provider.dart'; import 'package:lightmeter/screens/metering/bloc_metering.dart'; import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart'; import 'package:lightmeter/screens/metering/components/shared/volume_keys_notifier/notifier_volume_keys.dart'; import 'package:lightmeter/screens/metering/screen_metering.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; class MeteringFlow extends StatefulWidget { const MeteringFlow({super.key}); @@ -23,31 +17,45 @@ class MeteringFlow extends StatefulWidget { class _MeteringFlowState extends State { @override Widget build(BuildContext context) { - return InheritedWidgetBase( + return MeteringInteractorProvider( data: MeteringInteractor( - context.get(), - context.get(), - context.get(), - context.get(), - context.get(), - context.get(), + ServicesProvider.of(context).userPreferencesService, + ServicesProvider.of(context).caffeineService, + ServicesProvider.of(context).hapticsService, + ServicesProvider.of(context).permissionsService, + ServicesProvider.of(context).lightSensorService, + ServicesProvider.of(context).volumeEventsService, )..initialize(), - child: InheritedWidgetBase( - data: VolumeKeysNotifier(context.get()), - child: MultiBlocProvider( - providers: [ - BlocProvider(create: (_) => MeteringCommunicationBloc()), - BlocProvider( - create: (context) => MeteringBloc( - context.get(), - context.get(), - context.read(), - ), + child: MultiBlocProvider( + providers: [ + BlocProvider(create: (_) => MeteringCommunicationBloc()), + BlocProvider( + create: (context) => MeteringBloc( + MeteringInteractorProvider.of(context), + VolumeKeysNotifier(ServicesProvider.of(context).volumeEventsService), + context.read(), ), - ], - child: const MeteringScreen(), - ), + ), + ], + child: const MeteringScreen(), ), ); } } + +class MeteringInteractorProvider extends InheritedWidget { + final MeteringInteractor data; + + const MeteringInteractorProvider({ + required this.data, + required super.child, + super.key, + }); + + static MeteringInteractor of(BuildContext context) { + return context.findAncestorWidgetOfExactType()!.data; + } + + @override + bool updateShouldNotify(MeteringInteractorProvider oldWidget) => false; +} diff --git a/lib/screens/metering/screen_metering.dart b/lib/screens/metering/screen_metering.dart index bbfebfc..ba4dbea 100644 --- a/lib/screens/metering/screen_metering.dart +++ b/lib/screens/metering/screen_metering.dart @@ -6,15 +6,17 @@ import 'package:lightmeter/data/models/ev_source_type.dart'; import 'package:lightmeter/data/models/exposure_pair.dart'; import 'package:lightmeter/data/models/film.dart'; import 'package:lightmeter/data/models/metering_screen_layout_config.dart'; -import 'package:lightmeter/environment.dart'; -import 'package:lightmeter/providers/ev_source_type_provider.dart'; +import 'package:lightmeter/providers/equipment_profile_provider.dart'; +import 'package:lightmeter/providers/services_provider.dart'; +import 'package:lightmeter/providers/user_preferences_provider.dart'; import 'package:lightmeter/screens/metering/bloc_metering.dart'; import 'package:lightmeter/screens/metering/components/bottom_controls/provider_bottom_controls.dart'; import 'package:lightmeter/screens/metering/components/camera_container/provider_container_camera.dart'; import 'package:lightmeter/screens/metering/components/light_sensor_container/provider_container_light_sensor.dart'; import 'package:lightmeter/screens/metering/event_metering.dart'; import 'package:lightmeter/screens/metering/state_metering.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; +import 'package:lightmeter/screens/metering/utils/listener_metering_layout_feature.dart'; +import 'package:lightmeter/screens/metering/utils/listsner_equipment_profiles.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; class MeteringScreen extends StatelessWidget { @@ -45,8 +47,8 @@ class MeteringScreen extends StatelessWidget { builder: (context, state) => MeteringBottomControlsProvider( ev: state is MeteringDataState ? state.ev : null, isMetering: state.isMetering, - onSwitchEvSourceType: context.get().hasLightSensor - ? EvSourceTypeProvider.of(context).toggleType + onSwitchEvSourceType: ServicesProvider.of(context).environment.hasLightSensor + ? UserPreferencesProvider.of(context).toggleEvSourceType : null, onMeasure: () => context.read().add(const MeasureEvent()), onSettings: () { @@ -71,12 +73,12 @@ class _InheritedListeners extends StatelessWidget { @override Widget build(BuildContext context) { - return InheritedWidgetListener( + return EquipmentProfileListener( onDidChangeDependencies: (value) { context.read().add(EquipmentProfileChangedEvent(value)); }, - child: InheritedModelAspectListener( - aspect: MeteringScreenLayoutFeature.filmPicker, + child: MeteringScreenLayoutFeatureListener( + feature: MeteringScreenLayoutFeature.filmPicker, onDidChangeDependencies: (value) { if (!value) context.read().add(const FilmChangedEvent(Film.other())); }, @@ -110,7 +112,8 @@ class _MeteringContainerBuidler extends StatelessWidget { final exposurePairs = ev != null ? buildExposureValues(context, ev!, film) : []; final fastest = exposurePairs.isNotEmpty ? exposurePairs.first : null; final slowest = exposurePairs.isNotEmpty ? exposurePairs.last : null; - return context.listen() == EvSourceType.camera + // Doubled build here when switching evSourceType. As new source bloc fires a new state on init + return UserPreferencesProvider.evSourceTypeOf(context) == EvSourceType.camera ? CameraContainerProvider( fastest: fastest, slowest: slowest, @@ -141,10 +144,10 @@ class _MeteringContainerBuidler extends StatelessWidget { } /// Depending on the `stopType` the exposure pairs list length is multiplied by 1,2 or 3 - final StopType stopType = context.listen(); + final StopType stopType = UserPreferencesProvider.stopTypeOf(context); final int evSteps = (ev * (stopType.index + 1)).round(); - final EquipmentProfile equipmentProfile = context.listen(); + final EquipmentProfile equipmentProfile = EquipmentProfiles.selectedOf(context); final List apertureValues = equipmentProfile.apertureValues.whereStopType(stopType); final List shutterSpeedValues = diff --git a/lib/screens/metering/utils/listener_metering_layout_feature.dart b/lib/screens/metering/utils/listener_metering_layout_feature.dart new file mode 100644 index 0000000..c245ec3 --- /dev/null +++ b/lib/screens/metering/utils/listener_metering_layout_feature.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:lightmeter/data/models/metering_screen_layout_config.dart'; +import 'package:lightmeter/providers/user_preferences_provider.dart'; + +/// Listening to multiple dependencies at the same time causes firing an event for all dependencies +/// even though some of them didn't change: +/// ```dart +/// @override +/// void didChangeDependencies() { +/// super.didChangeDependencies(); +/// _bloc.add(EquipmentProfileChangedEvent(EquipmentProfile.of(context))); +/// if (!MeteringScreenLayout.featureStatusOf(context, MeteringScreenLayoutFeature.filmPicker)) { +/// _bloc.add(const FilmChangedEvent(Film.other())); +/// } +/// } +/// ``` +/// To overcome this issue I've decided to create a generic listener, +/// that will listen to each dependency separately. +class MeteringScreenLayoutFeatureListener extends StatefulWidget { + final MeteringScreenLayoutFeature feature; + final ValueChanged onDidChangeDependencies; + final Widget child; + + const MeteringScreenLayoutFeatureListener({ + required this.feature, + required this.onDidChangeDependencies, + required this.child, + super.key, + }); + + @override + State createState() => + _MeteringScreenLayoutFeatureListenerState(); +} + +class _MeteringScreenLayoutFeatureListenerState extends State { + @override + void didChangeDependencies() { + super.didChangeDependencies(); + widget.onDidChangeDependencies( + UserPreferencesProvider.meteringScreenFeatureOf( + context, + widget.feature, + ), + ); + } + + @override + Widget build(BuildContext context) { + return widget.child; + } +} diff --git a/lib/screens/metering/utils/listsner_equipment_profiles.dart b/lib/screens/metering/utils/listsner_equipment_profiles.dart new file mode 100644 index 0000000..ec604ce --- /dev/null +++ b/lib/screens/metering/utils/listsner_equipment_profiles.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'package:lightmeter/providers/equipment_profile_provider.dart'; +import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; + +class EquipmentProfileListener extends StatefulWidget { + final ValueChanged onDidChangeDependencies; + final Widget child; + + const EquipmentProfileListener({ + required this.onDidChangeDependencies, + required this.child, + super.key, + }); + + @override + State createState() => _EquipmentProfileListenerState(); +} + +class _EquipmentProfileListenerState extends State { + @override + void didChangeDependencies() { + super.didChangeDependencies(); + widget.onDidChangeDependencies(EquipmentProfiles.selectedOf(context)); + } + + @override + Widget build(BuildContext context) { + return widget.child; + } +} diff --git a/lib/screens/settings/components/about/components/report_issue/widget_list_tile_report_issue.dart b/lib/screens/settings/components/about/components/report_issue/widget_list_tile_report_issue.dart index 4c477f8..72bc1b5 100644 --- a/lib/screens/settings/components/about/components/report_issue/widget_list_tile_report_issue.dart +++ b/lib/screens/settings/components/about/components/report_issue/widget_list_tile_report_issue.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:lightmeter/environment.dart'; import 'package:lightmeter/generated/l10n.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; +import 'package:lightmeter/providers/services_provider.dart'; import 'package:url_launcher/url_launcher.dart'; class ReportIssueListTile extends StatelessWidget { @@ -14,7 +13,7 @@ class ReportIssueListTile extends StatelessWidget { title: Text(S.of(context).reportIssue), onTap: () { launchUrl( - Uri.parse(context.get().issuesReportUrl), + Uri.parse(ServicesProvider.of(context).environment.issuesReportUrl), mode: LaunchMode.externalApplication, ); }, diff --git a/lib/screens/settings/components/about/components/source_code/widget_list_tile_source_code.dart b/lib/screens/settings/components/about/components/source_code/widget_list_tile_source_code.dart index 42a73e8..4327332 100644 --- a/lib/screens/settings/components/about/components/source_code/widget_list_tile_source_code.dart +++ b/lib/screens/settings/components/about/components/source_code/widget_list_tile_source_code.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:lightmeter/environment.dart'; import 'package:lightmeter/generated/l10n.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; +import 'package:lightmeter/providers/services_provider.dart'; import 'package:url_launcher/url_launcher.dart'; class SourceCodeListTile extends StatelessWidget { @@ -14,7 +13,7 @@ class SourceCodeListTile extends StatelessWidget { title: Text(S.of(context).sourceCode), onTap: () { launchUrl( - Uri.parse(context.get().sourceCodeUrl), + Uri.parse(ServicesProvider.of(context).environment.sourceCodeUrl), mode: LaunchMode.externalApplication, ); }, diff --git a/lib/screens/settings/components/about/components/write_email/widget_list_tile_write_email.dart b/lib/screens/settings/components/about/components/write_email/widget_list_tile_write_email.dart index b12d0d4..b0a4391 100644 --- a/lib/screens/settings/components/about/components/write_email/widget_list_tile_write_email.dart +++ b/lib/screens/settings/components/about/components/write_email/widget_list_tile_write_email.dart @@ -1,8 +1,7 @@ import 'package:clipboard/clipboard.dart'; import 'package:flutter/material.dart'; -import 'package:lightmeter/environment.dart'; import 'package:lightmeter/generated/l10n.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; +import 'package:lightmeter/providers/services_provider.dart'; import 'package:url_launcher/url_launcher.dart'; class WriteEmailListTile extends StatelessWidget { @@ -14,7 +13,7 @@ class WriteEmailListTile extends StatelessWidget { leading: const Icon(Icons.email), title: Text(S.of(context).writeEmail), onTap: () { - final email = context.get().contactEmail; + final email = ServicesProvider.of(context).environment.contactEmail; final mailToUrl = Uri.parse('mailto:$email?subject=M3 Lightmeter'); canLaunchUrl(mailToUrl).then((canLaunch) { if (canLaunch) { diff --git a/lib/screens/settings/components/general/components/caffeine/provider_list_tile_caffeine.dart b/lib/screens/settings/components/general/components/caffeine/provider_list_tile_caffeine.dart index 4509d20..8d9bcfb 100644 --- a/lib/screens/settings/components/general/components/caffeine/provider_list_tile_caffeine.dart +++ b/lib/screens/settings/components/general/components/caffeine/provider_list_tile_caffeine.dart @@ -1,10 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:lightmeter/interactors/settings_interactor.dart'; import 'package:lightmeter/screens/settings/components/general/components/caffeine/bloc_list_tile_caffeine.dart'; import 'package:lightmeter/screens/settings/components/general/components/caffeine/widget_list_tile_caffeine.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; +import 'package:lightmeter/screens/settings/flow_settings.dart'; class CaffeineListTileProvider extends StatelessWidget { const CaffeineListTileProvider({super.key}); @@ -12,7 +11,7 @@ class CaffeineListTileProvider extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => CaffeineListTileBloc(context.get()), + create: (context) => CaffeineListTileBloc(SettingsInteractorProvider.of(context)), child: const CaffeineListTile(), ); } diff --git a/lib/screens/settings/components/general/components/haptics/provider_list_tile_haptics.dart b/lib/screens/settings/components/general/components/haptics/provider_list_tile_haptics.dart index beb0462..34cdeab 100644 --- a/lib/screens/settings/components/general/components/haptics/provider_list_tile_haptics.dart +++ b/lib/screens/settings/components/general/components/haptics/provider_list_tile_haptics.dart @@ -1,10 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:lightmeter/interactors/settings_interactor.dart'; import 'package:lightmeter/screens/settings/components/general/components/haptics/bloc_list_tile_haptics.dart'; import 'package:lightmeter/screens/settings/components/general/components/haptics/widget_list_tile_haptics.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; +import 'package:lightmeter/screens/settings/flow_settings.dart'; class HapticsListTileProvider extends StatelessWidget { const HapticsListTileProvider({super.key}); @@ -12,7 +11,7 @@ class HapticsListTileProvider extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => HapticsListTileBloc(context.get()), + create: (context) => HapticsListTileBloc(SettingsInteractorProvider.of(context)), child: const HapticsListTile(), ); } diff --git a/lib/screens/settings/components/general/components/language/widget_list_tile_language.dart b/lib/screens/settings/components/general/components/language/widget_list_tile_language.dart index 381f286..663c634 100644 --- a/lib/screens/settings/components/general/components/language/widget_list_tile_language.dart +++ b/lib/screens/settings/components/general/components/language/widget_list_tile_language.dart @@ -1,9 +1,8 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/data/models/supported_locale.dart'; import 'package:lightmeter/generated/l10n.dart'; -import 'package:lightmeter/providers/supported_locale_provider.dart'; +import 'package:lightmeter/providers/user_preferences_provider.dart'; import 'package:lightmeter/screens/settings/components/shared/dialog_picker.dart/widget_dialog_picker.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; class LanguageListTile extends StatelessWidget { const LanguageListTile({super.key}); @@ -13,20 +12,20 @@ class LanguageListTile extends StatelessWidget { return ListTile( leading: const Icon(Icons.language), title: Text(S.of(context).language), - trailing: Text(context.listen().localizedName), + trailing: Text(UserPreferencesProvider.localeOf(context).localizedName), onTap: () { showDialog( context: context, builder: (_) => DialogPicker( icon: Icons.language, title: S.of(context).chooseLanguage, - selectedValue: context.get(), + selectedValue: UserPreferencesProvider.localeOf(context), values: SupportedLocale.values, titleAdapter: (context, value) => value.localizedName, ), ).then((value) { if (value != null) { - SupportedLocaleProvider.of(context).setLocale(value); + UserPreferencesProvider.of(context).setLocale(value); } }); }, diff --git a/lib/screens/settings/components/general/components/volume_actions/provider_list_tile_volume_actions.dart b/lib/screens/settings/components/general/components/volume_actions/provider_list_tile_volume_actions.dart index 790ad4f..f99505f 100644 --- a/lib/screens/settings/components/general/components/volume_actions/provider_list_tile_volume_actions.dart +++ b/lib/screens/settings/components/general/components/volume_actions/provider_list_tile_volume_actions.dart @@ -1,10 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:lightmeter/interactors/settings_interactor.dart'; import 'package:lightmeter/screens/settings/components/general/components/volume_actions/bloc_list_tile_volume_actions.dart'; import 'package:lightmeter/screens/settings/components/general/components/volume_actions/widget_list_tile_volume_actions.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; +import 'package:lightmeter/screens/settings/flow_settings.dart'; class VolumeActionsListTileProvider extends StatelessWidget { const VolumeActionsListTileProvider({super.key}); @@ -12,7 +11,7 @@ class VolumeActionsListTileProvider extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => VolumeActionsListTileBloc(context.get()), + create: (context) => VolumeActionsListTileBloc(SettingsInteractorProvider.of(context)), child: const VolumeActionsListTile(), ); } diff --git a/lib/screens/settings/components/metering/components/calibration/components/calibration_dialog/provider_dialog_calibration.dart b/lib/screens/settings/components/metering/components/calibration/components/calibration_dialog/provider_dialog_calibration.dart index a95ec1b..a26a6cf 100644 --- a/lib/screens/settings/components/metering/components/calibration/components/calibration_dialog/provider_dialog_calibration.dart +++ b/lib/screens/settings/components/metering/components/calibration/components/calibration_dialog/provider_dialog_calibration.dart @@ -1,10 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:lightmeter/interactors/settings_interactor.dart'; import 'package:lightmeter/screens/settings/components/metering/components/calibration/components/calibration_dialog/bloc_dialog_calibration.dart'; import 'package:lightmeter/screens/settings/components/metering/components/calibration/components/calibration_dialog/widget_dialog_calibration.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; +import 'package:lightmeter/screens/settings/flow_settings.dart'; class CalibrationDialogProvider extends StatelessWidget { const CalibrationDialogProvider({super.key}); @@ -12,7 +11,7 @@ class CalibrationDialogProvider extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => CalibrationDialogBloc(context.get()), + create: (context) => CalibrationDialogBloc(SettingsInteractorProvider.of(context)), child: const CalibrationDialog(), ); } diff --git a/lib/screens/settings/components/metering/components/calibration/components/calibration_dialog/widget_dialog_calibration.dart b/lib/screens/settings/components/metering/components/calibration/components/calibration_dialog/widget_dialog_calibration.dart index 4c91c87..69a4f00 100644 --- a/lib/screens/settings/components/metering/components/calibration/components/calibration_dialog/widget_dialog_calibration.dart +++ b/lib/screens/settings/components/metering/components/calibration/components/calibration_dialog/widget_dialog_calibration.dart @@ -1,13 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:lightmeter/environment.dart'; import 'package:lightmeter/generated/l10n.dart'; +import 'package:lightmeter/providers/services_provider.dart'; import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/screens/settings/components/metering/components/calibration/components/calibration_dialog/bloc_dialog_calibration.dart'; import 'package:lightmeter/screens/settings/components/metering/components/calibration/components/calibration_dialog/event_dialog_calibration.dart'; import 'package:lightmeter/screens/settings/components/metering/components/calibration/components/calibration_dialog/state_dialog_calibration.dart'; import 'package:lightmeter/screens/shared/centered_slider/widget_slider_centered.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; import 'package:lightmeter/utils/to_string_signed.dart'; class CalibrationDialog extends StatelessWidget { @@ -15,7 +14,7 @@ class CalibrationDialog extends StatelessWidget { @override Widget build(BuildContext context) { - final bool hasLightSensor = context.get().hasLightSensor; + final bool hasLightSensor = ServicesProvider.of(context).environment.hasLightSensor; return AlertDialog( icon: const Icon(Icons.settings_brightness), titlePadding: Dimens.dialogIconTitlePadding, diff --git a/lib/screens/settings/components/metering/components/calibration/widget_list_tile_calibration.dart b/lib/screens/settings/components/metering/components/calibration/widget_list_tile_calibration.dart index e591e4b..61690c4 100644 --- a/lib/screens/settings/components/metering/components/calibration/widget_list_tile_calibration.dart +++ b/lib/screens/settings/components/metering/components/calibration/widget_list_tile_calibration.dart @@ -1,8 +1,7 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/generated/l10n.dart'; -import 'package:lightmeter/interactors/settings_interactor.dart'; import 'package:lightmeter/screens/settings/components/metering/components/calibration/components/calibration_dialog/provider_dialog_calibration.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; +import 'package:lightmeter/screens/settings/flow_settings.dart'; class CalibrationListTile extends StatelessWidget { const CalibrationListTile({super.key}); @@ -15,8 +14,8 @@ class CalibrationListTile extends StatelessWidget { onTap: () { showDialog( context: context, - builder: (_) => InheritedWidgetBase( - data: context.get(), + builder: (_) => SettingsInteractorProvider( + data: SettingsInteractorProvider.of(context), child: const CalibrationDialogProvider(), ), ); diff --git a/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart b/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart index 4872d79..3120b38 100644 --- a/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart +++ b/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/providers/equipment_profile_provider.dart'; + import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart'; import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_name_dialog/widget_dialog_equipment_profile_name.dart'; import 'package:lightmeter/screens/shared/sliver_screen/screen_sliver.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; class EquipmentProfilesScreen extends StatefulWidget { @@ -19,13 +19,12 @@ class _EquipmentProfilesScreenState extends State { static const maxProfiles = 5 + 1; // replace with a constant from iap late List> profileContainersKeys = []; - int get profilesCount => context.listen().length; + int get profilesCount => EquipmentProfiles.of(context).length; @override - void initState() { - super.initState(); - profileContainersKeys = context - .get() + void didChangeDependencies() { + super.didChangeDependencies(); + profileContainersKeys = EquipmentProfiles.of(context) .map((e) => GlobalKey(debugLabel: e.id)) .toList(); } @@ -58,14 +57,14 @@ class _EquipmentProfilesScreenState extends State { ), child: EquipmentProfileContainer( key: profileContainersKeys[index], - data: context.listen()[index], + data: EquipmentProfiles.of(context)[index], onExpand: () => _keepExpandedAt(index), onUpdate: (profileData) => _updateProfileAt(profileData, index), onDelete: () => _removeProfileAt(index), ), ) : const SizedBox.shrink(), - childCount: profileContainersKeys.length, + childCount: profilesCount, ), ), ], @@ -79,7 +78,6 @@ class _EquipmentProfilesScreenState extends State { ).then((value) { if (value != null) { EquipmentProfileProvider.of(context).addProfile(value); - profileContainersKeys.add(GlobalKey()); } }); } @@ -89,8 +87,7 @@ class _EquipmentProfilesScreenState extends State { } void _removeProfileAt(int index) { - EquipmentProfileProvider.of(context).deleteProfile(context.listen()[index]); - profileContainersKeys.removeAt(index); + EquipmentProfileProvider.of(context).deleteProfile(EquipmentProfiles.of(context)[index]); } void _keepExpandedAt(int index) { diff --git a/lib/screens/settings/components/metering/components/fractional_stops/widget_list_tile_fractional_stops.dart b/lib/screens/settings/components/metering/components/fractional_stops/widget_list_tile_fractional_stops.dart index 2a27a56..1bcf6bc 100644 --- a/lib/screens/settings/components/metering/components/fractional_stops/widget_list_tile_fractional_stops.dart +++ b/lib/screens/settings/components/metering/components/fractional_stops/widget_list_tile_fractional_stops.dart @@ -1,8 +1,7 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/generated/l10n.dart'; -import 'package:lightmeter/providers/stop_type_provider.dart'; +import 'package:lightmeter/providers/user_preferences_provider.dart'; import 'package:lightmeter/screens/settings/components/shared/dialog_picker.dart/widget_dialog_picker.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; class StopTypeListTile extends StatelessWidget { @@ -13,20 +12,20 @@ class StopTypeListTile extends StatelessWidget { return ListTile( leading: const Icon(Icons.straighten), title: Text(S.of(context).fractionalStops), - trailing: Text(_typeToString(context, context.listen())), + trailing: Text(_typeToString(context, UserPreferencesProvider.stopTypeOf(context))), onTap: () { showDialog( context: context, builder: (_) => DialogPicker( icon: Icons.straighten, title: S.of(context).showFractionalStops, - selectedValue: context.get(), + selectedValue: UserPreferencesProvider.stopTypeOf(context), values: StopType.values, titleAdapter: _typeToString, ), ).then((value) { if (value != null) { - StopTypeProvider.of(context).set(value); + UserPreferencesProvider.of(context).setStopType(value); } }); }, diff --git a/lib/screens/settings/components/metering/components/metering_screen_layout/components/meterins_screen_layout_features_dialog/widget_dialog_metering_screen_layout_features.dart b/lib/screens/settings/components/metering/components/metering_screen_layout/components/meterins_screen_layout_features_dialog/widget_dialog_metering_screen_layout_features.dart index d43f011..c60abab 100644 --- a/lib/screens/settings/components/metering/components/metering_screen_layout/components/meterins_screen_layout_features_dialog/widget_dialog_metering_screen_layout_features.dart +++ b/lib/screens/settings/components/metering/components/metering_screen_layout/components/meterins_screen_layout_features_dialog/widget_dialog_metering_screen_layout_features.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/data/models/metering_screen_layout_config.dart'; import 'package:lightmeter/generated/l10n.dart'; -import 'package:lightmeter/providers/metering_screen_layout_provider.dart'; +import 'package:lightmeter/providers/user_preferences_provider.dart'; import 'package:lightmeter/res/dimens.dart'; class MeteringScreenLayoutFeaturesDialog extends StatefulWidget { @@ -14,7 +14,7 @@ class MeteringScreenLayoutFeaturesDialog extends StatefulWidget { class _MeteringScreenLayoutFeaturesDialogState extends State { late final _features = - MeteringScreenLayoutConfig.from(MeteringScreenLayout.of(context, listen: false)); + MeteringScreenLayoutConfig.from(UserPreferencesProvider.meteringScreenConfigOf(context)); @override Widget build(BuildContext context) { @@ -56,7 +56,7 @@ class _MeteringScreenLayoutFeaturesDialogState extends State() == DynamicColorState.enabled, - onChanged: ThemeProvider.of(context).enableDynamicColor, + value: UserPreferencesProvider.dynamicColorStateOf(context) == DynamicColorState.enabled, + onChanged: UserPreferencesProvider.of(context).enableDynamicColor, contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM), ); } diff --git a/lib/screens/settings/components/theme/components/primary_color/components/primary_color_picker_dialog/widget_dialog_picker_primary_color.dart b/lib/screens/settings/components/theme/components/primary_color/components/primary_color_picker_dialog/widget_dialog_picker_primary_color.dart index 434336a..380faa1 100644 --- a/lib/screens/settings/components/theme/components/primary_color/components/primary_color_picker_dialog/widget_dialog_picker_primary_color.dart +++ b/lib/screens/settings/components/theme/components/primary_color/components/primary_color_picker_dialog/widget_dialog_picker_primary_color.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/generated/l10n.dart'; -import 'package:lightmeter/providers/theme_provider.dart'; import 'package:lightmeter/res/dimens.dart'; +import 'package:lightmeter/res/theme.dart'; import 'package:lightmeter/screens/shared/filled_circle/widget_circle_filled.dart'; class PrimaryColorDialogPicker extends StatefulWidget { @@ -38,9 +38,9 @@ class _PrimaryColorDialogPickerState extends State { padding: EdgeInsets.zero, child: Row( children: List.generate( - ThemeProvider.primaryColorsList.length, + primaryColorsList.length, (index) { - final color = ThemeProvider.primaryColorsList[index]; + final color = primaryColorsList[index]; return Padding( padding: EdgeInsets.only(left: index == 0 ? 0 : Dimens.paddingS), child: _SelectableColorItem( diff --git a/lib/screens/settings/components/theme/components/primary_color/widget_list_tile_primary_color.dart b/lib/screens/settings/components/theme/components/primary_color/widget_list_tile_primary_color.dart index afc1a7b..44de22e 100644 --- a/lib/screens/settings/components/theme/components/primary_color/widget_list_tile_primary_color.dart +++ b/lib/screens/settings/components/theme/components/primary_color/widget_list_tile_primary_color.dart @@ -1,17 +1,16 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/data/models/dynamic_colors_state.dart'; import 'package:lightmeter/generated/l10n.dart'; -import 'package:lightmeter/providers/theme_provider.dart'; +import 'package:lightmeter/providers/user_preferences_provider.dart'; import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/screens/settings/components/theme/components/primary_color/components/primary_color_picker_dialog/widget_dialog_picker_primary_color.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; class PrimaryColorListTile extends StatelessWidget { const PrimaryColorListTile({super.key}); @override Widget build(BuildContext context) { - if (context.listen() == DynamicColorState.enabled) { + if (UserPreferencesProvider.dynamicColorStateOf(context) == DynamicColorState.enabled) { return Opacity( opacity: Dimens.disabledOpacity, child: IgnorePointer( @@ -31,7 +30,7 @@ class PrimaryColorListTile extends StatelessWidget { builder: (_) => const PrimaryColorDialogPicker(), ).then((value) { if (value != null) { - ThemeProvider.of(context).setPrimaryColor(value); + UserPreferencesProvider.of(context).setPrimaryColor(value); } }); }, diff --git a/lib/screens/settings/components/theme/components/theme_type/widget_list_tile_theme_type.dart b/lib/screens/settings/components/theme/components/theme_type/widget_list_tile_theme_type.dart index 46e1b86..8a95161 100644 --- a/lib/screens/settings/components/theme/components/theme_type/widget_list_tile_theme_type.dart +++ b/lib/screens/settings/components/theme/components/theme_type/widget_list_tile_theme_type.dart @@ -1,9 +1,8 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/data/models/theme_type.dart'; import 'package:lightmeter/generated/l10n.dart'; -import 'package:lightmeter/providers/theme_provider.dart'; +import 'package:lightmeter/providers/user_preferences_provider.dart'; import 'package:lightmeter/screens/settings/components/shared/dialog_picker.dart/widget_dialog_picker.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; class ThemeTypeListTile extends StatelessWidget { const ThemeTypeListTile({super.key}); @@ -13,20 +12,20 @@ class ThemeTypeListTile extends StatelessWidget { return ListTile( leading: const Icon(Icons.brightness_6), title: Text(S.of(context).theme), - trailing: Text(_typeToString(context, context.listen())), + trailing: Text(_typeToString(context, UserPreferencesProvider.themeTypeOf(context))), onTap: () { showDialog( context: context, builder: (_) => DialogPicker( icon: Icons.brightness_6, title: S.of(context).chooseTheme, - selectedValue: context.get(), + selectedValue: UserPreferencesProvider.themeTypeOf(context), values: ThemeType.values, titleAdapter: _typeToString, ), ).then((value) { if (value != null) { - ThemeProvider.of(context).setThemeType(value); + UserPreferencesProvider.of(context).setThemeType(value); } }); }, diff --git a/lib/screens/settings/components/theme/widget_settings_section_theme.dart b/lib/screens/settings/components/theme/widget_settings_section_theme.dart index d137469..ee03c54 100644 --- a/lib/screens/settings/components/theme/widget_settings_section_theme.dart +++ b/lib/screens/settings/components/theme/widget_settings_section_theme.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/data/models/dynamic_colors_state.dart'; import 'package:lightmeter/generated/l10n.dart'; +import 'package:lightmeter/providers/user_preferences_provider.dart'; import 'package:lightmeter/screens/settings/components/shared/settings_section/widget_settings_section.dart'; import 'package:lightmeter/screens/settings/components/theme/components/dynamic_color/widget_list_tile_dynamic_color.dart'; import 'package:lightmeter/screens/settings/components/theme/components/primary_color/widget_list_tile_primary_color.dart'; import 'package:lightmeter/screens/settings/components/theme/components/theme_type/widget_list_tile_theme_type.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; class ThemeSettingsSection extends StatelessWidget { const ThemeSettingsSection({super.key}); @@ -17,7 +17,7 @@ class ThemeSettingsSection extends StatelessWidget { children: [ const ThemeTypeListTile(), const PrimaryColorListTile(), - if (context.get() != DynamicColorState.unavailable) + if (UserPreferencesProvider.dynamicColorStateOf(context) != DynamicColorState.unavailable) const DynamicColorListTile(), ], ); diff --git a/lib/screens/settings/flow_settings.dart b/lib/screens/settings/flow_settings.dart index 3195c25..f3c8156 100644 --- a/lib/screens/settings/flow_settings.dart +++ b/lib/screens/settings/flow_settings.dart @@ -1,25 +1,38 @@ import 'package:flutter/material.dart'; -import 'package:lightmeter/data/caffeine_service.dart'; -import 'package:lightmeter/data/haptics_service.dart'; -import 'package:lightmeter/data/shared_prefs_service.dart'; -import 'package:lightmeter/data/volume_events_service.dart'; import 'package:lightmeter/interactors/settings_interactor.dart'; +import 'package:lightmeter/providers/services_provider.dart'; import 'package:lightmeter/screens/settings/screen_settings.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; class SettingsFlow extends StatelessWidget { const SettingsFlow({super.key}); @override Widget build(BuildContext context) { - return InheritedWidgetBase( + return SettingsInteractorProvider( data: SettingsInteractor( - context.get(), - context.get(), - context.get(), - context.get(), + ServicesProvider.of(context).userPreferencesService, + ServicesProvider.of(context).caffeineService, + ServicesProvider.of(context).hapticsService, + ServicesProvider.of(context).volumeEventsService, ), child: const SettingsScreen(), ); } } + +class SettingsInteractorProvider extends InheritedWidget { + final SettingsInteractor data; + + const SettingsInteractorProvider({ + required this.data, + required super.child, + super.key, + }); + + static SettingsInteractor of(BuildContext context) { + return context.findAncestorWidgetOfExactType()!.data; + } + + @override + bool updateShouldNotify(SettingsInteractorProvider oldWidget) => false; +} diff --git a/lib/screens/settings/screen_settings.dart b/lib/screens/settings/screen_settings.dart index 3c745bd..9332d3f 100644 --- a/lib/screens/settings/screen_settings.dart +++ b/lib/screens/settings/screen_settings.dart @@ -1,12 +1,11 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/generated/l10n.dart'; -import 'package:lightmeter/interactors/settings_interactor.dart'; import 'package:lightmeter/screens/settings/components/about/widget_settings_section_about.dart'; import 'package:lightmeter/screens/settings/components/general/widget_settings_section_general.dart'; import 'package:lightmeter/screens/settings/components/metering/widget_settings_section_metering.dart'; import 'package:lightmeter/screens/settings/components/theme/widget_settings_section_theme.dart'; +import 'package:lightmeter/screens/settings/flow_settings.dart'; import 'package:lightmeter/screens/shared/sliver_screen/screen_sliver.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; class SettingsScreen extends StatefulWidget { const SettingsScreen({super.key}); @@ -19,12 +18,12 @@ class _SettingsScreenState extends State { @override void didChangeDependencies() { super.didChangeDependencies(); - context.get().disableVolumeHandling(); + SettingsInteractorProvider.of(context).disableVolumeHandling(); } @override void deactivate() { - context.get().restoreVolumeHandling(); + SettingsInteractorProvider.of(context).restoreVolumeHandling(); super.deactivate(); } diff --git a/lib/utils/inherited_generics.dart b/lib/utils/inherited_generics.dart deleted file mode 100644 index 5f71ec6..0000000 --- a/lib/utils/inherited_generics.dart +++ /dev/null @@ -1,171 +0,0 @@ -import 'package:flutter/widgets.dart'; - -/// Listening to multiple dependencies at the same time causes firing an event for all dependencies -/// even though some of them didn't change: -/// ```dart -/// @override -/// void didChangeDependencies() { -/// super.didChangeDependencies(); -/// _bloc.add(EquipmentProfileChangedEvent(EquipmentProfile.of(context))); -/// if (!MeteringScreenLayout.featureStatusOf(context, MeteringScreenLayoutFeature.filmPicker)) { -/// _bloc.add(const FilmChangedEvent(Film.other())); -/// } -/// } -/// ``` -/// To overcome this issue I've decided to create a generic listener, -/// that will listen to each dependency separately. -class InheritedWidgetListener extends StatefulWidget { - final ValueChanged onDidChangeDependencies; - final Widget child; - - const InheritedWidgetListener({ - required this.onDidChangeDependencies, - required this.child, - super.key, - }); - - @override - State> createState() => _InheritedWidgetListenerState(); -} - -class _InheritedWidgetListenerState extends State> { - @override - void didChangeDependencies() { - super.didChangeDependencies(); - widget.onDidChangeDependencies(context.listen()); - } - - @override - Widget build(BuildContext context) { - return widget.child; - } -} - -class InheritedWidgetBase extends InheritedWidget { - final T data; - - const InheritedWidgetBase({ - required this.data, - required super.child, - super.key, - }); - - static T of(BuildContext context, {bool listen = true}) { - if (listen) { - return context.dependOnInheritedWidgetOfExactType>()!.data; - } else { - return context.findAncestorWidgetOfExactType>()!.data; - } - } - - @override - bool updateShouldNotify(InheritedWidgetBase oldWidget) => true; -} - -extension InheritedWidgetBaseContext on BuildContext { - T get() { - return InheritedWidgetBase.of(this, listen: false); - } - - T listen() { - return InheritedWidgetBase.of(this); - } -} - -/// Listening to multiple dependencies at the same time causes firing an event for all dependencies -/// even though some of them didn't change: -/// ```dart -/// @override -/// void didChangeDependencies() { -/// super.didChangeDependencies(); -/// _bloc.add(EquipmentProfileChangedEvent(EquipmentProfile.of(context))); -/// if (!MeteringScreenLayout.featureStatusOf(context, MeteringScreenLayoutFeature.filmPicker)) { -/// _bloc.add(const FilmChangedEvent(Film.other())); -/// } -/// } -/// ``` -/// To overcome this issue I've decided to create a generic listener, -/// that will listen to each dependency separately. -class InheritedModelAspectListener extends StatefulWidget { - final A aspect; - final ValueChanged onDidChangeDependencies; - final Widget child; - - const InheritedModelAspectListener({ - required this.aspect, - required this.onDidChangeDependencies, - required this.child, - super.key, - }); - - @override - State> createState() => - _InheritedModelAspectListenerState(); -} - -class _InheritedModelAspectListenerState - extends State> { - @override - void didChangeDependencies() { - super.didChangeDependencies(); - widget.onDidChangeDependencies(context.listenModelFeature(widget.aspect)); - } - - @override - Widget build(BuildContext context) { - return widget.child; - } -} - -class InheritedModelBase extends InheritedModel { - final Map data; - - const InheritedModelBase({ - required this.data, - required super.child, - super.key, - }); - - static Map of(BuildContext context, {bool listen = true}) { - if (listen) { - return context.dependOnInheritedWidgetOfExactType>()!.data; - } else { - return context.findAncestorWidgetOfExactType>()!.data; - } - } - - static T featureOf(BuildContext context, A aspect) { - return InheritedModel.inheritFrom>(context, aspect: aspect)! - .data[aspect]!; - } - - @override - bool updateShouldNotify(InheritedModelBase oldWidget) => true; - - @override - bool updateShouldNotifyDependent( - InheritedModelBase oldWidget, - Set dependencies, - ) { - for (final dependecy in dependencies) { - if (oldWidget.data[dependecy] != data[dependecy]) { - return true; - } - } - return false; - } -} - -extension InheritedModelBaseContext on BuildContext { - Map getModel() { - return InheritedModelBase.of(this, listen: false); - } - - Map listenModel() { - return InheritedModelBase.of(this); - } - - T listenModelFeature(A aspect) { - return InheritedModelBase.featureOf(this, aspect); - } -} diff --git a/test/data/shared_prefs_service_test.dart b/test/data/shared_prefs_service_test.dart index 514629f..692cb81 100644 --- a/test/data/shared_prefs_service_test.dart +++ b/test/data/shared_prefs_service_test.dart @@ -6,7 +6,7 @@ import 'package:lightmeter/data/models/metering_screen_layout_config.dart'; import 'package:lightmeter/data/models/supported_locale.dart'; import 'package:lightmeter/data/models/theme_type.dart'; import 'package:lightmeter/data/shared_prefs_service.dart'; -import 'package:lightmeter/providers/theme_provider.dart'; +import 'package:lightmeter/res/theme.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; import 'package:mocktail/mocktail.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -348,13 +348,13 @@ void main() { group('primaryColor', () { test('get default', () { when(() => sharedPreferences.getInt(UserPreferencesService.primaryColorKey)).thenReturn(null); - expect(service.primaryColor, ThemeProvider.primaryColorsList[5]); + expect(service.primaryColor, primaryColorsList[5]); }); test('get', () { when(() => sharedPreferences.getInt(UserPreferencesService.primaryColorKey)) .thenReturn(0xff9c27b0); - expect(service.primaryColor, ThemeProvider.primaryColorsList[2]); + expect(service.primaryColor, primaryColorsList[2]); }); test('set', () {