diff --git a/.github/workflows/cd_dev.yml b/.github/workflows/build_apk.yml similarity index 97% rename from .github/workflows/cd_dev.yml rename to .github/workflows/build_apk.yml index 66695b1..12749a9 100644 --- a/.github/workflows/cd_dev.yml +++ b/.github/workflows/build_apk.yml @@ -19,8 +19,9 @@ on: jobs: build: + name: Build .apk runs-on: macos-11 - timeout-minutes: 30 + timeout-minutes: 15 steps: - uses: webfactory/ssh-agent@v0.8.0 @@ -79,7 +80,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/cd_prod.yml b/.github/workflows/cd_prod.yml deleted file mode 100644 index 832dcd4..0000000 --- a/.github/workflows/cd_prod.yml +++ /dev/null @@ -1,156 +0,0 @@ -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - -name: Build prod .aab & .apk - -on: - workflow_dispatch: - inputs: - version: - description: "Version" - required: true - type: string - -env: - BUILD_ARGS: --release --flavor prod --dart-define cameraPreviewAspectRatio=2/3 -t lib/main_prod.dart - -jobs: - build: - name: Build .apk & .aab - runs-on: macos-11 - timeout-minutes: 30 - steps: - - uses: webfactory/ssh-agent@v0.8.0 - with: - ssh-private-key: ${{ secrets.M3_LIGHTMETER_IAP_KEY }} - - - uses: actions/checkout@v3 - with: - submodules: recursive - - - uses: actions/setup-java@v2 - with: - distribution: "zulu" - java-version: "11" - - - name: Restore Android keystore .jsk and .properties files - env: - KEYSTORE: ${{ secrets.KEYSTORE }} - KEYSTORE_PROPERTIES: ${{ secrets.KEYSTORE_PROPERTIES }} - run: | - KEYSTORE_PATH=$RUNNER_TEMP/keystore.jks - echo -n "$KEYSTORE" | base64 --decode --output $KEYSTORE_PATH - cp $KEYSTORE_PATH ./android/app - KEYSTORE_PROPERTIES_PATH=$RUNNER_TEMP/key.properties - echo -n "$KEYSTORE_PROPERTIES" | base64 --decode --output $KEYSTORE_PROPERTIES_PATH - cp $KEYSTORE_PROPERTIES_PATH ./android - - - name: Restore android/app/google-services.json - env: - GOOGLE_SERVICES_JSON_ANDROID: ${{ secrets.GOOGLE_SERVICES_JSON_ANDROID }} - run: | - GOOGLE_SERVICES_JSON_ANDROID_PATH=$RUNNER_TEMP/google-services.json - echo -n "$GOOGLE_SERVICES_JSON_ANDROID" | base64 --decode --output $GOOGLE_SERVICES_JSON_ANDROID_PATH - cp $GOOGLE_SERVICES_JSON_ANDROID_PATH ./android/app - - - name: Restore firebase_options.dart - env: - FIREBASE_OPTIONS: ${{ secrets.FIREBASE_OPTIONS }} - run: | - FIREBASE_OPTIONS_PATH=$RUNNER_TEMP/firebase_options.dart - echo -n "$FIREBASE_OPTIONS" | base64 --decode --output $FIREBASE_OPTIONS_PATH - cp $FIREBASE_OPTIONS_PATH ./lib - - - name: Increment build number & replace version number - 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 - uses: subosito/flutter-action@v2 - with: - channel: "stable" - flutter-version: '3.10.0' - - - name: Prepare flutter project - run: | - flutter --version - flutter pub get - flutter pub run intl_utils:generate - - - name: Build apk - run: flutter build apk $BUILD_ARGS - - - name: Upload apk to artifacts - uses: actions/upload-artifact@v3 - with: - name: m3_lightmeter_apk - path: build/app/outputs/flutter-apk/app-prod-release.apk - - - name: Build appbundle - run: flutter build appbundle $BUILD_ARGS - - - name: Upload app bundle to artifacts - uses: actions/upload-artifact@v3 - with: - name: m3_lightmeter_bundle - path: build/app/outputs/bundle/prodRelease/app-prod-release.aab - - update-version-in-repo: - name: Update repo version - needs: [build] - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - submodules: recursive - - - name: Increment build number & replace version number - run: perl -i -pe 's/^(version:\s+)(\d+\.\d+\.\d+)(\+)(\d+)$/$1."${{ github.event.inputs.version }}".$3.($4+1)/e' pubspec.yaml - - - name: Commit changes - run: | - git config --global user.name "vodemn" - git config --global user.email "vadim.turko@gmail.com" - git add -A - git commit -m "Version bump" - - - name: Push to main - uses: CasperWA/push-protected@v2 - with: - token: ${{ secrets.PUSH_TO_MAIN_TOKEN }} - branch: ${{ github.ref_name }} - unprotect_reviews: true - - create-release: - name: Create Github release - needs: [build, update-version-in-repo] - if: github.ref_name == 'main' - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/checkout@v3 - with: - submodules: recursive - - - name: Download apk - uses: actions/download-artifact@v3 - with: - name: m3_lightmeter_apk - - - name: Download app bundle - uses: actions/download-artifact@v3 - with: - name: m3_lightmeter_bundle - - - name: Rename artifacts - run: | - mv app-prod-release.apk m3_lightmeter.apk - mv app-prod-release.aab m3_lightmeter.aab - - - uses: ncipollo/release-action@v1.12.0 - with: - artifacts: "m3_lightmeter.apk, m3_lightmeter.aab" - skipIfReleaseExists: true - tag: "v${{ github.event.inputs.version }}" diff --git a/.github/workflows/create_release.yml b/.github/workflows/create_release.yml new file mode 100644 index 0000000..07f76c8 --- /dev/null +++ b/.github/workflows/create_release.yml @@ -0,0 +1,276 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# 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 }} + +on: + workflow_dispatch: + inputs: + version: + description: "Version" + required: true + type: string + release-notes: + 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 + +jobs: + build: + name: Build .apk & .aab + if: ${{ inputs.github-release }} || ${{ inputs.google-play-release }} + runs-on: macos-11 + timeout-minutes: 30 + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - uses: actions/setup-java@v3 + with: + distribution: "zulu" + java-version: "11" + + - name: Restore Android keystore .jsk and .properties files + env: + KEYSTORE: ${{ secrets.KEYSTORE }} + KEYSTORE_PROPERTIES: ${{ secrets.KEYSTORE_PROPERTIES }} + run: | + KEYSTORE_PATH=$RUNNER_TEMP/keystore.jks + echo -n "$KEYSTORE" | base64 --decode --output $KEYSTORE_PATH + cp $KEYSTORE_PATH ./android/app + KEYSTORE_PROPERTIES_PATH=$RUNNER_TEMP/key.properties + echo -n "$KEYSTORE_PROPERTIES" | base64 --decode --output $KEYSTORE_PROPERTIES_PATH + cp $KEYSTORE_PROPERTIES_PATH ./android + + - name: Restore android/app/google-services.json + env: + GOOGLE_SERVICES_JSON_ANDROID: ${{ secrets.GOOGLE_SERVICES_JSON_ANDROID }} + run: | + GOOGLE_SERVICES_JSON_ANDROID_PATH=$RUNNER_TEMP/google-services.json + echo -n "$GOOGLE_SERVICES_JSON_ANDROID" | base64 --decode --output $GOOGLE_SERVICES_JSON_ANDROID_PATH + cp $GOOGLE_SERVICES_JSON_ANDROID_PATH ./android/app + + - name: Restore firebase_options.dart + env: + FIREBASE_OPTIONS: ${{ secrets.FIREBASE_OPTIONS }} + run: | + FIREBASE_OPTIONS_PATH=$RUNNER_TEMP/firebase_options.dart + 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 + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: '3.10.0' + + - name: Prepare flutter project + run: | + flutter --version + flutter pub get + 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 + 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 + if: ${{ inputs.github-release }} + needs: [build] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Increment build number & replace version number + run: perl -i -pe 's/^(version:\s+)(\d+\.\d+\.\d+)(\+)(\d+)$/$1."${{ github.event.inputs.version }}".$3.($4+1)/e' pubspec.yaml + + - name: Commit changes + run: | + git config --global user.name "vodemn" + git config --global user.email "vadim.turko@gmail.com" + git add -A + git commit -m "Version bump" + + - name: Push to main + uses: CasperWA/push-protected@v2 + with: + token: ${{ secrets.PUSH_TO_MAIN_TOKEN }} + branch: ${{ github.ref_name }} + unprotect_reviews: true + + create-github-release: + name: Create Github release + if: ${{ inputs.github-release }} + needs: [build, generate-release-notes, update-version-in-repo] + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Download apk + uses: actions/download-artifact@v3 + with: + 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" + skipIfReleaseExists: true + tag: "v${{ github.event.inputs.version }}" + bodyFile: "whatsnew-en-US.md" + + - name: Delete apk artifact + uses: geekyeggo/delete-artifact@v2 + with: + name: m3_lightmeter_apk + + create-google-play-release: + name: Create Google Play release + 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: + name: m3_lightmeter_bundle + + - name: Extract & zip merged_native_libs + run: | + unzip app-prod-release.aab + (cd base/lib && zip -r "$OLDPWD/merged_native_libs.zip" .) + + - name: Download release notes + uses: actions/download-artifact@v3 + with: + name: whatsnew-en-US + + - name: Move release notes to a folder + run: | + mv whatsnew-en-US.md whatsnew-en-US + mkdir 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 + 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 + releaseName: ${{ env.release_name }} + track: production + status: completed + 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 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 + + - name: Delete app bundle & merged native libs artifacts + if: ${{ always() }} + uses: geekyeggo/delete-artifact@v2 + with: + name: m3_lightmeter_bundle + + cleanup: + name: Cleanup + if: ${{ always() }} + needs: [create-github-release, create-google-play-release] + runs-on: ubuntu-latest + steps: + - name: Delete release notes artifact + uses: geekyeggo/delete-artifact@v2 + with: + name: whatsnew-en-US 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 diff --git a/.vscode/launch.json b/.vscode/launch.json index 9984519..6cbc2bc 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", }, @@ -37,7 +37,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 6ef1627..c4af94a 100644 --- a/README.md +++ b/README.md @@ -60,11 +60,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/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/data/models/metering_screen_layout_config.dart b/lib/data/models/metering_screen_layout_config.dart index 375d12f..7802195 100644 --- a/lib/data/models/metering_screen_layout_config.dart +++ b/lib/data/models/metering_screen_layout_config.dart @@ -1,6 +1,7 @@ enum MeteringScreenLayoutFeature { extremeExposurePairs, filmPicker, + histogram, equipmentProfiles, } diff --git a/lib/data/shared_prefs_service.dart b/lib/data/shared_prefs_service.dart index 020e0b1..812f0e6 100644 --- a/lib/data/shared_prefs_service.dart +++ b/lib/data/shared_prefs_service.dart @@ -98,6 +98,7 @@ class UserPreferencesService { MeteringScreenLayoutFeature.equipmentProfiles: true, 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/providers.dart b/lib/providers.dart deleted file mode 100644 index 1f00c67..0000000 --- a/lib/providers.dart +++ /dev/null @@ -1,80 +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/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:m3_lightmeter_iap/m3_lightmeter_iap.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) { - final sharedPrefs = snapshot.data![0] as SharedPreferences; - return IAPProviders( - sharedPreferences: sharedPrefs, - child: InheritedWidgetBase( - data: env.copyWith(hasLightSensor: snapshot.data![1] as bool), - child: InheritedWidgetBase( - data: UserPreferencesService(sharedPrefs), - 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: 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/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/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/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/bloc_container_camera.dart b/lib/screens/metering/components/camera_container/bloc_container_camera.dart index bfc12ac..0313391 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, ); @@ -205,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); 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 79% 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 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_preview/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/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..05dfa9d --- /dev/null +++ b/lib/screens/metering/components/camera_container/components/camera_preview/components/histogram/widget_histogram.dart @@ -0,0 +1,134 @@ +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() { + /// There is no need to stop image stream here, + /// because this widget will be disposed when CameraController is disposed + /// 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..ecfcb42 --- /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/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'; +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 (UserPreferencesProvider.meteringScreenFeatureOf( + 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/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 260eae4..beadb34 100644 --- a/lib/screens/metering/components/camera_container/widget_container_camera.dart +++ b/lib/screens/metering/components/camera_container/widget_container_camera.dart @@ -1,16 +1,17 @@ +import 'dart:math'; + 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/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/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'; @@ -45,63 +46,97 @@ 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(), + ), + ), + ], + ), + ), + ), + ], + ), + ) + ], + ); + } - if (MeteringScreenLayout.featureOf( + double _meteringContainerHeight(BuildContext context) { + double enabledFeaturesHeight = 0; + if (UserPreferencesProvider.meteringScreenFeatureOf( context, MeteringScreenLayoutFeature.equipmentProfiles, )) { - topBarOverflow += Dimens.readingContainerSingleValueHeight; - topBarOverflow += Dimens.paddingS; + enabledFeaturesHeight += Dimens.readingContainerSingleValueHeight; + enabledFeaturesHeight += Dimens.paddingS; } - if (MeteringScreenLayout.featureOf( + if (UserPreferencesProvider.meteringScreenFeatureOf( context, MeteringScreenLayoutFeature.extremeExposurePairs, )) { - topBarOverflow += Dimens.readingContainerDoubleValueHeight; - topBarOverflow += Dimens.paddingS; + enabledFeaturesHeight += Dimens.readingContainerDoubleValueHeight; + enabledFeaturesHeight += Dimens.paddingS; } - if (MeteringScreenLayout.featureOf( + if (UserPreferencesProvider.meteringScreenFeatureOf( 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; } } @@ -110,20 +145,11 @@ class _CameraViewBuilder extends StatelessWidget { @override Widget build(BuildContext context) { - 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); - } - }, + 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, ), ); } @@ -164,11 +190,13 @@ 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, ); }, @@ -176,43 +204,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, - ), - ), - ], - ), - ), - ); - } -} 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/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, + ), ) ], ); 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 df0fd30..78f5688 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 @@ -3,8 +3,7 @@ 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/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'; 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'; @@ -38,14 +37,14 @@ class ReadingsContainer extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - if (MeteringScreenLayout.featureOf( + if (UserPreferencesProvider.meteringScreenFeatureOf( context, - MeteringScreenLayoutFeature.equipmentProfiles, + MeteringScreenLayoutFeature.extremeExposurePairs, )) ...[ const _EquipmentProfilePicker(), const _InnerPadding(), ], - if (MeteringScreenLayout.featureOf( + if (UserPreferencesProvider.meteringScreenFeatureOf( context, MeteringScreenLayoutFeature.extremeExposurePairs, )) ...[ @@ -63,7 +62,7 @@ class ReadingsContainer extends StatelessWidget { ), const _InnerPadding(), ], - if (MeteringScreenLayout.featureOf( + if (UserPreferencesProvider.meteringScreenFeatureOf( context, MeteringScreenLayoutFeature.filmPicker, )) ...[ 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 bca7271..8c99e07 100644 --- a/lib/screens/metering/screen_metering.dart +++ b/lib/screens/metering/screen_metering.dart @@ -6,17 +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/screens/metering/utils/equipment_profile_listener.dart'; -import 'package:lightmeter/utils/inherited_generics.dart'; -import 'package:m3_lightmeter_iap/m3_lightmeter_iap.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 { @@ -47,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: () { @@ -77,15 +77,15 @@ class _InheritedListeners extends StatelessWidget { 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())); } }, - child: InheritedModelAspectListener( - aspect: MeteringScreenLayoutFeature.equipmentProfiles, + child: MeteringScreenLayoutFeatureListener( + feature: MeteringScreenLayoutFeature.equipmentProfiles, onDidChangeDependencies: (value) { if (!value) { EquipmentProfileProvider.of(context).setProfile(EquipmentProfiles.of(context).first); @@ -122,14 +122,15 @@ class MeteringContainerBuidler extends StatelessWidget { final exposurePairs = ev != null ? buildExposureValues( ev!, - context.listen(), + UserPreferencesProvider.stopTypeOf(context), EquipmentProfiles.selectedOf(context), 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, 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 1819e7f..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:m3_lightmeter_iap/m3_lightmeter_iap.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; class EquipmentProfilesScreen extends StatefulWidget { 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 a02471e..d8dbe5d 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) { @@ -47,7 +47,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/pubspec.yaml b/pubspec.yaml index 2beafe9..80baa21 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: lightmeter description: Lightmeter app inspired by Material 3 design system. publish_to: "none" -version: 0.12.2+33 +version: 0.13.1+37 environment: sdk: ">=3.0.0 <4.0.0" diff --git a/test/data/models/metering_screen_layout_config_test.dart b/test/data/models/metering_screen_layout_config_test.dart index 2dd31aa..9e9393e 100644 --- a/test/data/models/metering_screen_layout_config_test.dart +++ b/test/data/models/metering_screen_layout_config_test.dart @@ -12,11 +12,30 @@ void main() { '0': true, '1': true, '2': true, + '3': true, }, ), { MeteringScreenLayoutFeature.extremeExposurePairs: true, MeteringScreenLayoutFeature.filmPicker: true, + MeteringScreenLayoutFeature.histogram: true, + MeteringScreenLayoutFeature.equipmentProfiles: true, + }, + ); + }); + + test('Legacy (no histogram & equipment profiles)', () { + expect( + MeteringScreenLayoutConfigJson.fromJson( + { + '0': false, + '1': false, + }, + ), + { + MeteringScreenLayoutFeature.extremeExposurePairs: false, + MeteringScreenLayoutFeature.filmPicker: false, + MeteringScreenLayoutFeature.histogram: true, MeteringScreenLayoutFeature.equipmentProfiles: true, }, ); @@ -26,13 +45,15 @@ void main() { expect( MeteringScreenLayoutConfigJson.fromJson( { - '0': true, - '1': true, + '0': false, + '1': false, + '2': false, }, ), { - MeteringScreenLayoutFeature.extremeExposurePairs: true, - MeteringScreenLayoutFeature.filmPicker: true, + MeteringScreenLayoutFeature.extremeExposurePairs: false, + MeteringScreenLayoutFeature.filmPicker: false, + MeteringScreenLayoutFeature.histogram: false, MeteringScreenLayoutFeature.equipmentProfiles: true, }, ); @@ -46,11 +67,13 @@ void main() { MeteringScreenLayoutFeature.equipmentProfiles: true, MeteringScreenLayoutFeature.extremeExposurePairs: true, MeteringScreenLayoutFeature.filmPicker: true, + MeteringScreenLayoutFeature.histogram: true, }.toJson(), { - '2': true, + '3': true, '0': true, '1': true, + '2': true, }, ); }); diff --git a/test/data/shared_prefs_service_test.dart b/test/data/shared_prefs_service_test.dart index 485d6d2..2a63fe5 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'; @@ -194,6 +194,7 @@ void main() { MeteringScreenLayoutFeature.extremeExposurePairs: true, MeteringScreenLayoutFeature.filmPicker: true, MeteringScreenLayoutFeature.equipmentProfiles: true, + MeteringScreenLayoutFeature.histogram: true, }, ); }); @@ -208,6 +209,7 @@ void main() { MeteringScreenLayoutFeature.extremeExposurePairs: false, MeteringScreenLayoutFeature.filmPicker: true, MeteringScreenLayoutFeature.equipmentProfiles: true, + MeteringScreenLayoutFeature.histogram: true, }, ); }); @@ -216,17 +218,19 @@ void main() { when( () => sharedPreferences.setString( UserPreferencesService.meteringScreenLayoutKey, - """{"0":false,"1":true}""", + """{"0":false,"1":true,"2":true,"3":true}""", ), ).thenAnswer((_) => Future.value(true)); service.meteringScreenLayout = { MeteringScreenLayoutFeature.extremeExposurePairs: false, MeteringScreenLayoutFeature.filmPicker: true, + MeteringScreenLayoutFeature.histogram: true, + MeteringScreenLayoutFeature.equipmentProfiles: true, }; verify( () => sharedPreferences.setString( UserPreferencesService.meteringScreenLayoutKey, - """{"0":false,"1":true}""", + """{"0":false,"1":true,"2":true,"3":true}""", ), ).called(1); }); @@ -347,13 +351,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', () {