From 4bb080a144e9233bd40ac4a5eeac6d3126256ae4 Mon Sep 17 00:00:00 2001 From: Vadim <44135514+vodemn@users.noreply.github.com> Date: Sat, 2 Sep 2023 10:32:08 +0200 Subject: [PATCH] Implemented IAP & Equipment profiles (#89) * added equipment profiles to layout config * calculate layout height based on `MeteringScreenLayoutFeature` * Update cd_dev.yml * Fixed equipment profile tile padding * import * `webfactory/ssh-agent` * Update pubspec.yaml * fixed `MeteringScreenLayoutConfigJson` tests * fixed `UserPreferencesService` tests * reset selected equipment profile when layout feature is disabled * `IAPProductType.equipment` -> `IAPProductType.paidFeatures` * updated packages versions * Update shared_prefs_service.dart * Fixed & tested exposure pairs list builder * typo * typo * added iap repo stub * Renamed `EquipmentProfileData` ->`EquipmentProfile` * Moved `EquipmentProfileProvider` to iap repo * Update README.md * Fixed `EquipmentProfileListener` * Improved `EquipmentProfilesListTile` statuses visualization * Update README.md * Update ci.yml * Post-merge fixes * typo * Added workflow checks * more sophisticated iap icons * Include IAP by default * added loader for `IAPProductStatus.pending` * typo * Added equipment profiles list placeholder * typo * separated `IconPlaceholder` * improved `buildExposureValues` testing * cleanup --- .github/workflows/build_apk.yml | 16 +- .github/workflows/create_release.yml | 17 +- .github/workflows/pr_check.yml | 20 +- .vscode/launch.json | 2 + README.md | 19 +- iap/.gitignore | 36 + iap/.metadata | 10 + iap/LICENSE | 1 + iap/analysis_options.yaml | 4 + iap/lib/m3_lightmeter_iap.dart | 30 + iap/lib/src/data/models/iap_product.dart | 5 + .../providers/equipment_profile_provider.dart | 79 ++ .../src/providers/iap_products_provider.dart | 47 + iap/pubspec.yaml | 25 + ios/Runner.xcodeproj/project.pbxproj | 16 +- lib/application.dart | 24 +- lib/data/models/exposure_pair.dart | 12 + lib/data/models/film.dart | 2 + .../models/metering_screen_layout_config.dart | 9 +- lib/data/shared_prefs_service.dart | 7 +- lib/features.dart | 3 - lib/l10n/intl_en.arb | 3 +- lib/l10n/intl_fr.arb | 3 +- lib/l10n/intl_ru.arb | 3 +- lib/l10n/intl_zh.arb | 1 + lib/providers/equipment_profile_provider.dart | 139 --- lib/res/dimens.dart | 3 +- .../bloc_container_camera.dart | 2 +- .../widget_container_camera.dart | 8 +- .../bloc_container_light_sensor.dart | 2 +- .../widget_list_exposure_pairs.dart | 8 +- .../widget_container_readings.dart | 8 +- lib/screens/metering/screen_metering.dart | 94 +- .../utils/equipment_profile_listener.dart | 30 + .../utils/listsner_equipment_profiles.dart | 2 +- .../widget_container_equipment_profile.dart | 3 +- .../screen_equipment_profile.dart | 88 +- .../widget_list_tile_equipment_profiles.dart | 30 +- ...ialog_metering_screen_layout_features.dart | 31 +- .../widget_settings_section_metering.dart | 3 +- lib/screens/settings/screen_settings.dart | 1 + .../widget_icon_placeholder.dart} | 18 +- .../shared/sliver_screen/screen_sliver.dart | 3 +- lib/utils/log_2.dart | 3 - pubspec.yaml | 24 +- .../metering_screen_layout_config_test.dart | 35 +- test/data/shared_prefs_service_test.dart | 7 +- .../metering/screen_metering_test.dart | 966 ++++++++++++++++++ 48 files changed, 1608 insertions(+), 294 deletions(-) create mode 100644 iap/.gitignore create mode 100644 iap/.metadata create mode 100644 iap/LICENSE create mode 100644 iap/analysis_options.yaml create mode 100644 iap/lib/m3_lightmeter_iap.dart create mode 100644 iap/lib/src/data/models/iap_product.dart create mode 100644 iap/lib/src/providers/equipment_profile_provider.dart create mode 100644 iap/lib/src/providers/iap_products_provider.dart create mode 100644 iap/pubspec.yaml delete mode 100644 lib/features.dart delete mode 100644 lib/providers/equipment_profile_provider.dart create mode 100644 lib/screens/metering/utils/equipment_profile_listener.dart rename lib/screens/{metering/components/shared/exposure_pairs_list/components/empty_exposure_pairs_list/widget_list_exposure_pairs_empty.dart => shared/icon_placeholder/widget_icon_placeholder.dart} (68%) delete mode 100644 lib/utils/log_2.dart create mode 100644 test/screens/metering/screen_metering_test.dart diff --git a/.github/workflows/build_apk.yml b/.github/workflows/build_apk.yml index 9157f90..e61b38b 100644 --- a/.github/workflows/build_apk.yml +++ b/.github/workflows/build_apk.yml @@ -16,14 +16,28 @@ on: - dev - prod default: 'dev' + include-iap: + type: boolean + description: Include IAP package + default: true jobs: build: name: Build .apk runs-on: macos-11 timeout-minutes: 15 - steps: + - name: Connect private iap package + uses: webfactory/ssh-agent@v0.8.0 + if: ${{ inputs.include-iap }} + with: + ssh-private-key: ${{ secrets.M3_LIGHTMETER_IAP_KEY }} + + - name: Override iap package with stub + if: ${{ !inputs.include-iap }} + run: | + echo "\ndependency_overrides:\n m3_lightmeter_iap:\n path: iap" >> pubspec.yaml + - uses: actions/checkout@v3 with: submodules: recursive diff --git a/.github/workflows/create_release.yml b/.github/workflows/create_release.yml index 3d238b4..e0281e3 100644 --- a/.github/workflows/create_release.yml +++ b/.github/workflows/create_release.yml @@ -31,6 +31,10 @@ on: type: boolean description: Create Google Play release default: true + include-iap: + type: boolean + description: Include IAP package + default: true env: BUILD_ARGS: --release --flavor prod --dart-define cameraPreviewAspectRatio=240/320 -t lib/main_prod.dart @@ -38,10 +42,21 @@ env: jobs: build: name: Build .apk & .aab - if: ${{ inputs.github-release }} || ${{ inputs.google-play-release }} + if: ${{ inputs.github-release || inputs.google-play-release }} runs-on: macos-11 timeout-minutes: 30 steps: + - name: Connect private iap package + uses: webfactory/ssh-agent@v0.8.0 + if: ${{ inputs.include-iap }} + with: + ssh-private-key: ${{ secrets.M3_LIGHTMETER_IAP_KEY }} + + - name: Override iap package with stub + if: ${{ !inputs.include-iap }} + run: | + echo "\ndependency_overrides:\n m3_lightmeter_iap:\n path: iap" >> pubspec.yaml + - uses: actions/checkout@v3 with: submodules: recursive diff --git a/.github/workflows/pr_check.yml b/.github/workflows/pr_check.yml index 9022170..e6f0294 100644 --- a/.github/workflows/pr_check.yml +++ b/.github/workflows/pr_check.yml @@ -11,13 +11,27 @@ on: pull_request: branches: ["main"] +env: + # Stub iap package if this worlflow is running from the PR from a fork + STUB_IAP: ${{ github.event.pull_request.head.repo.full_name != github.repository }} + jobs: analyze_and_test: name: Analyze & test runs-on: macos-11 timeout-minutes: 10 - steps: + - name: Connect private iap package + uses: webfactory/ssh-agent@v0.8.0 + if: !env.STUB_IAP + with: + ssh-private-key: ${{ secrets.M3_LIGHTMETER_IAP_KEY }} + + - name: Override iap package with stub + if: env.STUB_IAP + run: | + echo "\ndependency_overrides:\n m3_lightmeter_iap:\n path: iap" >> pubspec.yaml + - uses: actions/checkout@v3 with: submodules: recursive @@ -25,7 +39,7 @@ jobs: - uses: subosito/flutter-action@v2 with: channel: "stable" - flutter-version: '3.10.0' + flutter-version: "3.10.0" - name: Prepare flutter project run: | @@ -37,4 +51,4 @@ jobs: run: flutter analyze lib --fatal-infos - name: Run tests - run: flutter test \ No newline at end of file + run: flutter test diff --git a/.vscode/launch.json b/.vscode/launch.json index 822d34d..6cbc2bc 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -31,6 +31,7 @@ { "name": "prod (android)", "request": "launch", + //"flutterMode": "release", "type": "dart", "args": [ "--flavor", @@ -43,6 +44,7 @@ { "name": "prod (ios)", "request": "launch", + //"flutterMode": "release", "type": "dart", "args": [ "--flavor", diff --git a/README.md b/README.md index f629aa2..c4af94a 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,22 @@ Out of the box Firebase Crashlytics won't work. If you want to add Crashlytics t ### 3. Get packages -Fetch all the neccessary dependencies and generate translation files by running the following commands: +As part of the app's functionallity is in the private repo, you have to replace these lines in _pubspec.yaml_: + +```yaml +m3_lightmeter_iap: + git: + url: "https://github.com/vodemn/m3_lightmeter_iap" + ref: main +``` +with these: +```yaml +m3_lightmeter_iap: + path: iap +``` +and run `flutter pub get` from the _iap/_ folder. + +Then you can fetch all the neccessary dependencies and generate translation files by running the following commands: ```console flutter pub get flutter pub run intl_utils:generate @@ -69,4 +84,4 @@ Apple does not provide API for reading Lux stream form the ambient light sensor. ## Volume buttons action -This can be [implemented](https://stackoverflow.com/questions/70161271/ios-override-hardware-volume-buttons-same-as-zello) but the app will be rejected due to [2.5.9](https://developer.apple.com/app-store/review/guidelines/#software-requirements) \ No newline at end of file +This can be [implemented](https://stackoverflow.com/questions/70161271/ios-override-hardware-volume-buttons-same-as-zello) but the app will be rejected due to [2.5.9](https://developer.apple.com/app-store/review/guidelines/#software-requirements) diff --git a/iap/.gitignore b/iap/.gitignore new file mode 100644 index 0000000..205884d --- /dev/null +++ b/iap/.gitignore @@ -0,0 +1,36 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ + +.fvm/ +*.properties +ios/Flutter/ +.flutter-plugins +.flutter-plugins-dependencies \ No newline at end of file diff --git a/iap/.metadata b/iap/.metadata new file mode 100644 index 0000000..acbef51 --- /dev/null +++ b/iap/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 9944297138845a94256f1cf37beb88ff9a8e811a + channel: stable + +project_type: package diff --git a/iap/LICENSE b/iap/LICENSE new file mode 100644 index 0000000..ba75c69 --- /dev/null +++ b/iap/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/iap/analysis_options.yaml b/iap/analysis_options.yaml new file mode 100644 index 0000000..a5744c1 --- /dev/null +++ b/iap/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/iap/lib/m3_lightmeter_iap.dart b/iap/lib/m3_lightmeter_iap.dart new file mode 100644 index 0000000..8fe8aa5 --- /dev/null +++ b/iap/lib/m3_lightmeter_iap.dart @@ -0,0 +1,30 @@ +library m3_lightmeter_iap; + +import 'package:flutter/material.dart'; +import 'package:m3_lightmeter_iap/src/providers/equipment_profile_provider.dart'; +import 'package:m3_lightmeter_iap/src/providers/iap_products_provider.dart'; + +export 'src/data/models/iap_product.dart'; + +export 'src/providers/equipment_profile_provider.dart' hide EquipmentProfilesAspect; +export 'src/providers/iap_products_provider.dart'; + +class IAPProviders extends StatelessWidget { + final Object sharedPreferences; + final Widget child; + + const IAPProviders({ + required this.sharedPreferences, + required this.child, + super.key, + }); + + @override + Widget build(BuildContext context) { + return IAPProductsProvider( + child: EquipmentProfileProvider( + child: child, + ), + ); + } +} diff --git a/iap/lib/src/data/models/iap_product.dart b/iap/lib/src/data/models/iap_product.dart new file mode 100644 index 0000000..706e3f1 --- /dev/null +++ b/iap/lib/src/data/models/iap_product.dart @@ -0,0 +1,5 @@ +enum IAPProductType { paidFeatures } + +class IAPProduct { + IAPProduct(); +} diff --git a/iap/lib/src/providers/equipment_profile_provider.dart b/iap/lib/src/providers/equipment_profile_provider.dart new file mode 100644 index 0000000..4f7aa0f --- /dev/null +++ b/iap/lib/src/providers/equipment_profile_provider.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; + +class EquipmentProfileProvider extends StatefulWidget { + final Widget child; + + const EquipmentProfileProvider({required this.child, super.key}); + + static EquipmentProfileProviderState of(BuildContext context) { + return context.findAncestorStateOfType()!; + } + + @override + State createState() => EquipmentProfileProviderState(); +} + +class EquipmentProfileProviderState extends State { + static const EquipmentProfile _defaultProfile = EquipmentProfile( + id: '', + name: '', + apertureValues: ApertureValue.values, + ndValues: NdValue.values, + shutterSpeedValues: ShutterSpeedValue.values, + isoValues: IsoValue.values, + ); + + @override + Widget build(BuildContext context) { + return EquipmentProfiles( + profiles: const [_defaultProfile], + selected: _defaultProfile, + child: widget.child, + ); + } + + void setProfile(EquipmentProfile data) {} + + void addProfile(String name) {} + + void updateProdile(EquipmentProfile data) {} + + void deleteProfile(EquipmentProfile data) {} +} + +enum EquipmentProfilesAspect { list, selected } + +class EquipmentProfiles extends InheritedModel { + const EquipmentProfiles({ + super.key, + required this.profiles, + required this.selected, + required super.child, + }); + + final List profiles; + final EquipmentProfile selected; + + static List of(BuildContext context) { + return InheritedModel.inheritFrom( + context, + aspect: EquipmentProfilesAspect.list, + )! + .profiles; + } + + static EquipmentProfile selectedOf(BuildContext context) { + return InheritedModel.inheritFrom( + context, + aspect: EquipmentProfilesAspect.selected, + )! + .selected; + } + + @override + bool updateShouldNotify(EquipmentProfiles oldWidget) => false; + + @override + bool updateShouldNotifyDependent(EquipmentProfiles oldWidget, Set dependencies) => false; +} diff --git a/iap/lib/src/providers/iap_products_provider.dart b/iap/lib/src/providers/iap_products_provider.dart new file mode 100644 index 0000000..4ea3c98 --- /dev/null +++ b/iap/lib/src/providers/iap_products_provider.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:m3_lightmeter_iap/src/data/models/iap_product.dart'; + +class IAPProductsProvider extends StatefulWidget { + final Widget child; + + const IAPProductsProvider({required this.child, super.key}); + + static IAPProductsProviderState of(BuildContext context) { + return context.findAncestorStateOfType()!; + } + + @override + State createState() => IAPProductsProviderState(); +} + +class IAPProductsProviderState extends State { + @override + Widget build(BuildContext context) { + return IAPProducts( + products: const [], + child: widget.child, + ); + } + + Future buy(IAPProductType type) async {} +} + +class IAPProducts extends InheritedModel { + final List products; + + const IAPProducts({ + required this.products, + required super.child, + super.key, + }); + + static IAPProduct? of(BuildContext context, IAPProductType type) => null; + + static bool isPurchased(BuildContext context, IAPProductType type) => false; + + @override + bool updateShouldNotify(IAPProducts oldWidget) => false; + + @override + bool updateShouldNotifyDependent(covariant IAPProducts oldWidget, Set dependencies) => false; +} diff --git a/iap/pubspec.yaml b/iap/pubspec.yaml new file mode 100644 index 0000000..6aed37e --- /dev/null +++ b/iap/pubspec.yaml @@ -0,0 +1,25 @@ +name: m3_lightmeter_iap +description: IAP stubs for the M3 Lightmeter app. +version: 0.2.0 +publish_to: 'none' + +environment: + sdk: '>=2.19.2 <3.0.0' + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + m3_lightmeter_resources: + git: + url: "https://github.com/vodemn/m3_lightmeter_resources" + ref: main + shared_preferences: 2.2.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + +flutter: + uses-material-design: true diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index a858962..73ce339 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -237,6 +237,7 @@ }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -268,6 +269,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -371,7 +373,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 74JQ9DBXY6; + DEVELOPMENT_TEAM = 489Z6UQMGN; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -500,7 +502,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 74JQ9DBXY6; + DEVELOPMENT_TEAM = 489Z6UQMGN; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -523,7 +525,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 74JQ9DBXY6; + DEVELOPMENT_TEAM = 489Z6UQMGN; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -600,7 +602,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 74JQ9DBXY6; + DEVELOPMENT_TEAM = 489Z6UQMGN; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -675,7 +677,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 74JQ9DBXY6; + DEVELOPMENT_TEAM = 489Z6UQMGN; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -747,7 +749,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = 74JQ9DBXY6; + DEVELOPMENT_TEAM = 489Z6UQMGN; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/lib/application.dart b/lib/application.dart index 1249ed9..38ef0bf 100644 --- a/lib/application.dart +++ b/lib/application.dart @@ -10,11 +10,11 @@ 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/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:m3_lightmeter_iap/m3_lightmeter_iap.dart'; import 'package:platform/platform.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -32,16 +32,18 @@ class Application extends StatelessWidget { ]), 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( + return IAPProviders( + sharedPreferences: snapshot.data![0] as SharedPreferences, + child: 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: Builder( builder: (context) { final theme = UserPreferencesProvider.themeOf(context); diff --git a/lib/data/models/exposure_pair.dart b/lib/data/models/exposure_pair.dart index 9df3ada..d3aa823 100644 --- a/lib/data/models/exposure_pair.dart +++ b/lib/data/models/exposure_pair.dart @@ -8,4 +8,16 @@ class ExposurePair { @override String toString() => '$aperture - $shutterSpeed'; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other.runtimeType != runtimeType) return false; + return other is ExposurePair && + other.aperture == aperture && + other.shutterSpeed == shutterSpeed; + } + + @override + int get hashCode => Object.hash(aperture, shutterSpeed, runtimeType); } diff --git a/lib/data/models/film.dart b/lib/data/models/film.dart index 2da1c9b..ab651e8 100644 --- a/lib/data/models/film.dart +++ b/lib/data/models/film.dart @@ -13,6 +13,8 @@ double log10polynomian( ) => a * pow(log10(x), 2) + b * log10(x) + c; +typedef ReciprocityFailureBuilder = ShutterSpeedValue Function(ShutterSpeedValue shutterSpeed); + /// Only Ilford films have reciprocity formulas provided by the manufacturer: /// https://www.ilfordphoto.com/wp/wp-content/uploads/2017/06/Reciprocity-Failure-Compensation.pdf /// diff --git a/lib/data/models/metering_screen_layout_config.dart b/lib/data/models/metering_screen_layout_config.dart index 0aae055..7802195 100644 --- a/lib/data/models/metering_screen_layout_config.dart +++ b/lib/data/models/metering_screen_layout_config.dart @@ -1,9 +1,14 @@ -enum MeteringScreenLayoutFeature { extremeExposurePairs, filmPicker, histogram } +enum MeteringScreenLayoutFeature { + extremeExposurePairs, + filmPicker, + histogram, + equipmentProfiles, +} typedef MeteringScreenLayoutConfig = Map; extension MeteringScreenLayoutConfigJson on MeteringScreenLayoutConfig { - static MeteringScreenLayoutConfig fromJson(Map data) => + static MeteringScreenLayoutConfig fromJson(Map data) => { for (final f in MeteringScreenLayoutFeature.values) f: data[f.index.toString()] as bool? ?? true diff --git a/lib/data/shared_prefs_service.dart b/lib/data/shared_prefs_service.dart index 8ae5487..812f0e6 100644 --- a/lib/data/shared_prefs_service.dart +++ b/lib/data/shared_prefs_service.dart @@ -95,6 +95,7 @@ class UserPreferencesService { ); } else { return { + MeteringScreenLayoutFeature.equipmentProfiles: true, MeteringScreenLayoutFeature.extremeExposurePairs: true, MeteringScreenLayoutFeature.filmPicker: true, MeteringScreenLayoutFeature.histogram: true, @@ -147,10 +148,4 @@ class UserPreferencesService { orElse: () => Film.values.first, ); set film(Film value) => _sharedPreferences.setString(filmKey, value.name); - - String get selectedEquipmentProfileId => ''; // coverage:ignore-line - set selectedEquipmentProfileId(String id) {} // coverage:ignore-line - - List get equipmentProfiles => []; // coverage:ignore-line - set equipmentProfiles(List profiles) {} // coverage:ignore-line } diff --git a/lib/features.dart b/lib/features.dart deleted file mode 100644 index deede30..0000000 --- a/lib/features.dart +++ /dev/null @@ -1,3 +0,0 @@ -class FeaturesConfig { - static const bool equipmentProfilesEnabled = false; -} diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 1bfe8ed..925d280 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -54,6 +54,7 @@ "isoValuesFilterDescription": "Select the ISO values to display. These may be your most commonly used values or those supported by your camera.", "equipmentProfile": "Equipment profile", "equipmentProfiles": "Equipment profiles", + "tapToAdd": "Tap to add", "general": "General", "keepScreenOn": "Keep screen on", "haptics": "Haptics", @@ -86,4 +87,4 @@ } } } -} \ No newline at end of file +} diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index f75d76b..c199d1e 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -42,6 +42,7 @@ "film": "Pellicule", "equipment": "Équipement", "equipmentProfileName": "Nom du profil de l'équipement", + "tapToAdd": "Appuie pour ajouter", "equipmentProfileNameHint": "Praktica MTL5B", "equipmentProfileAllValues": "Tout", "apertureValues": "Valeurs Aperture", @@ -86,4 +87,4 @@ } } } -} \ No newline at end of file +} diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index 3a67fc2..d20c4bd 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -54,6 +54,7 @@ "isoValuesFilterDescription": "Выберите значения ISO для отображения. Это может быть наиболее часто используемые значения или значения, поддерживаемые вашей камерой.", "equipmentProfile": "Оборудование", "equipmentProfiles": "Профили оборудования", + "tapToAdd": "Нажмите, чтобы добавить", "general": "Общие", "keepScreenOn": "Запрет блокировки", "haptics": "Вибрация", @@ -86,4 +87,4 @@ } } } -} \ No newline at end of file +} diff --git a/lib/l10n/intl_zh.arb b/lib/l10n/intl_zh.arb index 5d020cf..02bdf6c 100644 --- a/lib/l10n/intl_zh.arb +++ b/lib/l10n/intl_zh.arb @@ -54,6 +54,7 @@ "isoValuesFilterDescription": "选择要显示的 ISO。这些值可能是您最常用的值,也可能是相机支持的值。", "equipmentProfile": "设备配置", "equipmentProfiles": "设备配置", + "tapToAdd": "點擊添加", "general": "通用", "keepScreenOn": "保持屏幕常亮", "haptics": "震动", diff --git a/lib/providers/equipment_profile_provider.dart b/lib/providers/equipment_profile_provider.dart deleted file mode 100644 index c0294fa..0000000 --- a/lib/providers/equipment_profile_provider.dart +++ /dev/null @@ -1,139 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lightmeter/providers/services_provider.dart'; -import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; -import 'package:uuid/uuid.dart'; - -// TODO(@vodemn): This will be removed in #89 -class EquipmentProfileProvider extends StatefulWidget { - final Widget child; - - const EquipmentProfileProvider({required this.child, super.key}); - - static EquipmentProfileProviderState of(BuildContext context) { - return context.findAncestorStateOfType()!; - } - - @override - State createState() => EquipmentProfileProviderState(); -} - -class EquipmentProfileProviderState extends State { - static const EquipmentProfile _defaultProfile = EquipmentProfile( - id: '', - name: '', - apertureValues: ApertureValue.values, - ndValues: NdValue.values, - shutterSpeedValues: ShutterSpeedValue.values, - isoValues: IsoValue.values, - ); - - List _customProfiles = []; - String _selectedId = ''; - - EquipmentProfile get _selectedProfile => _customProfiles.firstWhere( - (e) => e.id == _selectedId, - orElse: () { - ServicesProvider.of(context).userPreferencesService.selectedEquipmentProfileId = - _defaultProfile.id; - return _defaultProfile; - }, - ); - - @override - void initState() { - super.initState(); - _selectedId = ServicesProvider.of(context).userPreferencesService.selectedEquipmentProfileId; - _customProfiles = ServicesProvider.of(context).userPreferencesService.equipmentProfiles; - } - - @override - Widget build(BuildContext context) { - return EquipmentProfiles( - profiles: [_defaultProfile] + _customProfiles, - selected: _selectedProfile, - child: widget.child, - ); - } - - void setProfile(EquipmentProfile data) { - setState(() { - _selectedId = data.id; - }); - ServicesProvider.of(context).userPreferencesService.selectedEquipmentProfileId = - _selectedProfile.id; - } - - /// Creates a default equipment profile - void addProfile(String name) { - _customProfiles.add( - EquipmentProfile( - id: const Uuid().v1(), - name: name, - apertureValues: ApertureValue.values, - ndValues: NdValue.values, - shutterSpeedValues: ShutterSpeedValue.values, - isoValues: IsoValue.values, - ), - ); - _refreshSavedProfiles(); - } - - void updateProdile(EquipmentProfile data) { - final indexToUpdate = _customProfiles.indexWhere((element) => element.id == data.id); - if (indexToUpdate >= 0) { - _customProfiles[indexToUpdate] = data; - _refreshSavedProfiles(); - } - } - - void deleteProfile(EquipmentProfile data) { - _customProfiles.remove(data); - _refreshSavedProfiles(); - } - - void _refreshSavedProfiles() { - ServicesProvider.of(context).userPreferencesService.equipmentProfiles = _customProfiles; - setState(() {}); - } -} - -// Copied from #89 -enum EquipmentProfilesAspect { list, selected } - -class EquipmentProfiles extends InheritedModel { - const EquipmentProfiles({ - super.key, - required this.profiles, - required this.selected, - required super.child, - }); - - final List profiles; - final EquipmentProfile selected; - - static List of(BuildContext context) { - return InheritedModel.inheritFrom( - context, - aspect: EquipmentProfilesAspect.list, - )! - .profiles; - } - - static EquipmentProfile selectedOf(BuildContext context) { - return InheritedModel.inheritFrom( - context, - aspect: EquipmentProfilesAspect.selected, - )! - .selected; - } - - @override - bool updateShouldNotify(EquipmentProfiles oldWidget) => false; - - @override - bool updateShouldNotifyDependent( - EquipmentProfiles oldWidget, - Set dependencies, - ) => - false; -} diff --git a/lib/res/dimens.dart b/lib/res/dimens.dart index a1bb780..e3eed95 100644 --- a/lib/res/dimens.dart +++ b/lib/res/dimens.dart @@ -14,7 +14,6 @@ class Dimens { static const double grid48 = 48; static const double grid56 = 56; static const double grid72 = 72; - static const double grid168 = 168; static const double paddingS = 8; static const double paddingM = 16; @@ -30,6 +29,8 @@ class Dimens { static const double enabledOpacity = 1.0; static const double disabledOpacity = 0.38; + static const double sliverAppBarExpandedHeight = 168; + // TopBar static const double readingContainerDoubleValueHeight = 128; static const double readingContainerSingleValueHeight = 76; 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 991bdf6..0313391 100644 --- a/lib/screens/metering/components/camera_container/bloc_container_camera.dart +++ b/lib/screens/metering/components/camera_container/bloc_container_camera.dart @@ -18,7 +18,7 @@ import 'package:lightmeter/screens/metering/components/camera_container/event_co 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'; import 'package:lightmeter/screens/metering/components/shared/ev_source_base/bloc_base_ev_source.dart'; -import 'package:lightmeter/utils/log_2.dart'; +import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; class CameraContainerBloc extends EvSourceBlocBase { final MeteringInteractor _meteringInteractor; 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 c510bc8..0514d92 100644 --- a/lib/screens/metering/components/camera_container/widget_container_camera.dart +++ b/lib/screens/metering/components/camera_container/widget_container_camera.dart @@ -5,7 +5,6 @@ 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/features.dart'; import 'package:lightmeter/platform_config.dart'; import 'package:lightmeter/providers/user_preferences_provider.dart'; import 'package:lightmeter/res/dimens.dart'; @@ -110,7 +109,10 @@ class CameraContainer extends StatelessWidget { double _meteringContainerHeight(BuildContext context) { double enabledFeaturesHeight = 0; - if (FeaturesConfig.equipmentProfilesEnabled) { + if (UserPreferencesProvider.meteringScreenFeatureOf( + context, + MeteringScreenLayoutFeature.equipmentProfiles, + )) { enabledFeaturesHeight += Dimens.readingContainerSingleValueHeight; enabledFeaturesHeight += Dimens.paddingS; } @@ -133,7 +135,7 @@ class CameraContainer extends StatelessWidget { } double _cameraPreviewHeight(BuildContext context) { - return ((MediaQuery.of(context).size.width - Dimens.grid8 - 2 * Dimens.paddingM) / 2) / + return ((MediaQuery.sizeOf(context).width - Dimens.grid8 - 2 * Dimens.paddingM) / 2) / PlatformConfig.cameraPreviewAspectRatio; } } diff --git a/lib/screens/metering/components/light_sensor_container/bloc_container_light_sensor.dart b/lib/screens/metering/components/light_sensor_container/bloc_container_light_sensor.dart index 23cd796..c76727b 100644 --- a/lib/screens/metering/components/light_sensor_container/bloc_container_light_sensor.dart +++ b/lib/screens/metering/components/light_sensor_container/bloc_container_light_sensor.dart @@ -10,7 +10,7 @@ import 'package:lightmeter/screens/metering/communication/state_communication_me import 'package:lightmeter/screens/metering/components/light_sensor_container/event_container_light_sensor.dart'; import 'package:lightmeter/screens/metering/components/light_sensor_container/state_container_light_sensor.dart'; import 'package:lightmeter/screens/metering/components/shared/ev_source_base/bloc_base_ev_source.dart'; -import 'package:lightmeter/utils/log_2.dart'; +import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; class LightSensorContainerBloc extends EvSourceBlocBase { 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 71b293e..3f33d03 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 @@ -1,9 +1,10 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/data/models/exposure_pair.dart'; +import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/res/dimens.dart'; -import 'package:lightmeter/screens/metering/components/shared/exposure_pairs_list/components/empty_exposure_pairs_list/widget_list_exposure_pairs_empty.dart'; import 'package:lightmeter/screens/metering/components/shared/exposure_pairs_list/components/exposure_pairs_list_item/widget_item_list_exposure_pairs.dart'; +import 'package:lightmeter/screens/shared/icon_placeholder/widget_icon_placeholder.dart'; class ExposurePairsList extends StatelessWidget { final List exposurePairs; @@ -15,7 +16,10 @@ class ExposurePairsList extends StatelessWidget { return AnimatedSwitcher( duration: Dimens.switchDuration, child: exposurePairs.isEmpty - ? const EmptyExposurePairsList() + ? IconPlaceholder( + icon: Icons.not_interested, + text: S.of(context).noExposurePairs, + ) : Stack( alignment: Alignment.center, children: [ 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 d31380c..c9f6f54 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 @@ -2,13 +2,12 @@ import 'package:flutter/material.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/features.dart'; import 'package:lightmeter/generated/l10n.dart'; -import 'package:lightmeter/providers/equipment_profile_provider.dart'; import 'package:lightmeter/providers/user_preferences_provider.dart'; import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/screens/metering/components/shared/readings_container/components/animated_dialog_picker/widget_picker_dialog_animated.dart'; import 'package:lightmeter/screens/metering/components/shared/readings_container/components/reading_value_container/widget_container_reading_value.dart'; +import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; class ReadingsContainer extends StatelessWidget { @@ -38,7 +37,10 @@ class ReadingsContainer extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - if (FeaturesConfig.equipmentProfilesEnabled) ...[ + if (UserPreferencesProvider.meteringScreenFeatureOf( + context, + MeteringScreenLayoutFeature.equipmentProfiles, + )) ...[ const _EquipmentProfilePicker(), const _InnerPadding(), ], diff --git a/lib/screens/metering/screen_metering.dart b/lib/screens/metering/screen_metering.dart index ba4dbea..e9a77b5 100644 --- a/lib/screens/metering/screen_metering.dart +++ b/lib/screens/metering/screen_metering.dart @@ -6,7 +6,6 @@ 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/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'; @@ -17,6 +16,7 @@ import 'package:lightmeter/screens/metering/event_metering.dart'; import 'package:lightmeter/screens/metering/state_metering.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_iap/m3_lightmeter_iap.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; class MeteringScreen extends StatelessWidget { @@ -31,7 +31,7 @@ class MeteringScreen extends StatelessWidget { children: [ Expanded( child: BlocBuilder( - builder: (_, state) => _MeteringContainerBuidler( + builder: (_, state) => MeteringContainerBuidler( ev: state is MeteringDataState ? state.ev : null, film: state.film, iso: state.iso, @@ -80,15 +80,25 @@ class _InheritedListeners extends StatelessWidget { child: MeteringScreenLayoutFeatureListener( feature: MeteringScreenLayoutFeature.filmPicker, onDidChangeDependencies: (value) { - if (!value) context.read().add(const FilmChangedEvent(Film.other())); + if (!value) { + context.read().add(const FilmChangedEvent(Film.other())); + } }, - child: child, + child: MeteringScreenLayoutFeatureListener( + feature: MeteringScreenLayoutFeature.equipmentProfiles, + onDidChangeDependencies: (value) { + if (!value) { + EquipmentProfileProvider.of(context).setProfile(EquipmentProfiles.of(context).first); + } + }, + child: child, + ), ), ); } } -class _MeteringContainerBuidler extends StatelessWidget { +class MeteringContainerBuidler extends StatelessWidget { final double? ev; final Film film; final IsoValue iso; @@ -97,7 +107,7 @@ class _MeteringContainerBuidler extends StatelessWidget { final ValueChanged onIsoChanged; final ValueChanged onNdChanged; - const _MeteringContainerBuidler({ + const MeteringContainerBuidler({ required this.ev, required this.film, required this.iso, @@ -109,7 +119,14 @@ class _MeteringContainerBuidler extends StatelessWidget { @override Widget build(BuildContext context) { - final exposurePairs = ev != null ? buildExposureValues(context, ev!, film) : []; + final exposurePairs = ev != null + ? buildExposureValues( + ev!, + UserPreferencesProvider.stopTypeOf(context), + EquipmentProfiles.selectedOf(context), + film, + ) + : []; final fastest = exposurePairs.isNotEmpty ? exposurePairs.first : null; final slowest = exposurePairs.isNotEmpty ? exposurePairs.last : null; // Doubled build here when switching evSourceType. As new source bloc fires a new state on init @@ -138,39 +155,28 @@ class _MeteringContainerBuidler extends StatelessWidget { ); } - List buildExposureValues(BuildContext context, double ev, Film film) { + @visibleForTesting + static List buildExposureValues( + double ev, + StopType stopType, + EquipmentProfile equipmentProfile, + Film film, + ) { if (ev.isNaN || ev.isInfinite) { return List.empty(); } /// Depending on the `stopType` the exposure pairs list length is multiplied by 1,2 or 3 - final StopType stopType = UserPreferencesProvider.stopTypeOf(context); final int evSteps = (ev * (stopType.index + 1)).round(); - final EquipmentProfile equipmentProfile = EquipmentProfiles.selectedOf(context); - final List apertureValues = - equipmentProfile.apertureValues.whereStopType(stopType); - final List shutterSpeedValues = - equipmentProfile.shutterSpeedValues.whereStopType(stopType); + final apertureValues = ApertureValue.values.whereStopType(stopType); + final shutterSpeedValues = ShutterSpeedValue.values.whereStopType(stopType); /// Basically we use 1" shutter speed as an anchor point for building the exposure pairs list. /// But user can exclude this value from the list using custom equipment profile. /// So we have to restore the index of the anchor value. - const ShutterSpeedValue anchorShutterSpeed = ShutterSpeedValue(1, false, StopType.full); - int anchorIndex = shutterSpeedValues.indexOf(anchorShutterSpeed); - if (anchorIndex < 0) { - final filteredFullList = ShutterSpeedValue.values.whereStopType(stopType); - final customListStartIndex = filteredFullList.indexOf(shutterSpeedValues.first); - final fullListAnchor = filteredFullList.indexOf(anchorShutterSpeed); - if (customListStartIndex < fullListAnchor) { - /// This means, that user excluded anchor value at the end, - /// i.e. all shutter speed values are shorter than 1". - anchorIndex = fullListAnchor - customListStartIndex; - } else { - /// In case user excludes anchor value at the start, - /// we can do no adjustment. - } - } + const anchorShutterSpeed = ShutterSpeedValue(1, false, StopType.full); + final int anchorIndex = shutterSpeedValues.indexOf(anchorShutterSpeed); final int evOffset = anchorIndex - evSteps; late final int apertureOffset; @@ -189,10 +195,11 @@ class _MeteringContainerBuidler extends StatelessWidget { ) - max(apertureOffset, shutterSpeedOffset); - if (itemsCount < 0) { + if (itemsCount <= 0) { return List.empty(); } - return List.generate( + + final exposurePairs = List.generate( itemsCount, (index) => ExposurePair( apertureValues[index + apertureOffset], @@ -200,5 +207,30 @@ class _MeteringContainerBuidler extends StatelessWidget { ), growable: false, ); + + /// Full equipment profile, nothing to cut + if (equipmentProfile.id == "") { + return exposurePairs; + } + + final equipmentApertureValues = equipmentProfile.apertureValues.whereStopType(stopType); + final equipmentShutterSpeedValues = equipmentProfile.shutterSpeedValues.whereStopType(stopType); + + final startCutEV = max( + exposurePairs.first.aperture.difference(equipmentApertureValues.first), + exposurePairs.first.shutterSpeed.difference(equipmentShutterSpeedValues.first), + ); + final endCutEV = max( + equipmentApertureValues.last.difference(exposurePairs.last.aperture), + equipmentShutterSpeedValues.last.difference(exposurePairs.last.shutterSpeed), + ); + + final startCut = (startCutEV * (stopType.index + 1)).round().clamp(0, itemsCount); + final endCut = (endCutEV * (stopType.index + 1)).round().clamp(0, itemsCount); + + if (startCut > itemsCount - endCut) { + return const []; + } + return exposurePairs.sublist(startCut, itemsCount - endCut); } } diff --git a/lib/screens/metering/utils/equipment_profile_listener.dart b/lib/screens/metering/utils/equipment_profile_listener.dart new file mode 100644 index 0000000..68d03dc --- /dev/null +++ b/lib/screens/metering/utils/equipment_profile_listener.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'package:m3_lightmeter_iap/m3_lightmeter_iap.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/metering/utils/listsner_equipment_profiles.dart b/lib/screens/metering/utils/listsner_equipment_profiles.dart index ec604ce..68d03dc 100644 --- a/lib/screens/metering/utils/listsner_equipment_profiles.dart +++ b/lib/screens/metering/utils/listsner_equipment_profiles.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:lightmeter/providers/equipment_profile_provider.dart'; +import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; class EquipmentProfileListener extends StatefulWidget { diff --git a/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart b/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart index 1d9b7bf..e4a9e95 100644 --- a/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart +++ b/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart @@ -71,6 +71,7 @@ class EquipmentProfileContainerState extends State mainAxisSize: MainAxisSize.min, children: [ ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM), title: Row( children: [ _AnimatedNameLeading(controller: _controller), @@ -163,7 +164,7 @@ class _AnimatedNameLeading extends AnimatedWidget { @override Widget build(BuildContext context) { return Padding( - padding: EdgeInsets.only(right: _progress.value * Dimens.grid24), + padding: EdgeInsets.only(right: _progress.value * Dimens.grid8), child: Icon( Icons.edit, size: _progress.value * Dimens.grid24, 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 3120b38..0168608 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,12 @@ 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/icon_placeholder/widget_icon_placeholder.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 { @@ -44,30 +45,38 @@ class _EquipmentProfilesScreenState extends State { icon: const Icon(Icons.close), ), ], - slivers: [ - SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) => index > 0 - ? Padding( - padding: EdgeInsets.fromLTRB( - Dimens.paddingM, - index == 0 ? Dimens.paddingM : 0, - Dimens.paddingM, - Dimens.paddingM, - ), - child: EquipmentProfileContainer( - key: profileContainersKeys[index], - data: EquipmentProfiles.of(context)[index], - onExpand: () => _keepExpandedAt(index), - onUpdate: (profileData) => _updateProfileAt(profileData, index), - onDelete: () => _removeProfileAt(index), - ), - ) - : const SizedBox.shrink(), - childCount: profilesCount, - ), - ), - ], + slivers: profilesCount == 1 + ? [ + SliverFillRemaining( + hasScrollBody: false, + child: _EquipmentProfilesListPlaceholder(onTap: _addProfile), + ) + ] + : [ + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) => index > 0 // skip default + ? Padding( + padding: EdgeInsets.fromLTRB( + Dimens.paddingM, + index == 0 ? Dimens.paddingM : 0, + Dimens.paddingM, + Dimens.paddingM, + ), + child: EquipmentProfileContainer( + key: profileContainersKeys[index], + data: EquipmentProfiles.of(context)[index], + onExpand: () => _keepExpandedAt(index), + onUpdate: (profileData) => _updateProfileAt(profileData, index), + onDelete: () => _removeProfileAt(index), + ), + ) + : const SizedBox.shrink(), + childCount: profilesCount, + ), + ), + SliverToBoxAdapter(child: SizedBox(height: MediaQuery.paddingOf(context).bottom)), + ], ); } @@ -99,3 +108,32 @@ class _EquipmentProfilesScreenState extends State { }); } } + +class _EquipmentProfilesListPlaceholder extends StatelessWidget { + final VoidCallback onTap; + + const _EquipmentProfilesListPlaceholder({required this.onTap}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: Dimens.sliverAppBarExpandedHeight), + child: FractionallySizedBox( + widthFactor: 1 / 1.618, + child: Center( + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: onTap, + child: Padding( + padding: const EdgeInsets.all(Dimens.paddingL), + child: IconPlaceholder( + icon: Icons.add, + text: S.of(context).tapToAdd, + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/screens/settings/components/metering/components/equipment_profiles/widget_list_tile_equipment_profiles.dart b/lib/screens/settings/components/metering/components/equipment_profiles/widget_list_tile_equipment_profiles.dart index d1a6ef3..774e215 100644 --- a/lib/screens/settings/components/metering/components/equipment_profiles/widget_list_tile_equipment_profiles.dart +++ b/lib/screens/settings/components/metering/components/equipment_profiles/widget_list_tile_equipment_profiles.dart @@ -1,6 +1,10 @@ +import 'dart:developer'; + import 'package:flutter/material.dart'; import 'package:lightmeter/generated/l10n.dart'; +import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart'; +import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; class EquipmentProfilesListTile extends StatelessWidget { @@ -8,13 +12,31 @@ class EquipmentProfilesListTile extends StatelessWidget { @override Widget build(BuildContext context) { + final paidStatus = IAPProducts.productOf(context, IAPProductType.paidFeatures)?.status ?? + IAPProductStatus.pending; + log(paidStatus.toString()); return ListTile( leading: const Icon(Icons.camera), title: Text(S.of(context).equipmentProfiles), - onTap: () { - Navigator.of(context).push( - MaterialPageRoute(builder: (_) => const EquipmentProfilesScreen()), - ); + onTap: switch (paidStatus) { + IAPProductStatus.purchased => () { + Navigator.of(context).push( + MaterialPageRoute(builder: (_) => const EquipmentProfilesScreen()), + ); + }, + IAPProductStatus.pending => null, + _ => () { + IAPProductsProvider.of(context).buy(IAPProductType.paidFeatures); + }, + }, + trailing: switch (paidStatus) { + IAPProductStatus.purchasable => const Icon(Icons.lock), + IAPProductStatus.pending => const SizedBox( + height: Dimens.grid24, + width: Dimens.grid24, + child: CircularProgressIndicator(), + ), + _ => null, }, ); } 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 c60abab..a043a12 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 @@ -33,18 +33,10 @@ class _MeteringScreenLayoutFeaturesDialogState extends State SwitchListTile( - contentPadding: EdgeInsets.symmetric(horizontal: Dimens.dialogTitlePadding.left), - title: Text(_toStringLocalized(context, f)), - value: _features[f]!, - onChanged: (value) { - setState(() { - _features.update(f, (_) => value); - }); - }, - ), - ), + _featureListTile(MeteringScreenLayoutFeature.equipmentProfiles), + _featureListTile(MeteringScreenLayoutFeature.extremeExposurePairs), + _featureListTile(MeteringScreenLayoutFeature.filmPicker), + _featureListTile(MeteringScreenLayoutFeature.histogram), ], ), ), @@ -65,8 +57,23 @@ class _MeteringScreenLayoutFeaturesDialogState extends State value); + }); + }, + ); + } + String _toStringLocalized(BuildContext context, MeteringScreenLayoutFeature feature) { switch (feature) { + case MeteringScreenLayoutFeature.equipmentProfiles: + return S.of(context).equipmentProfiles; case MeteringScreenLayoutFeature.extremeExposurePairs: return S.of(context).meteringScreenFeatureExtremeExposurePairs; case MeteringScreenLayoutFeature.filmPicker: diff --git a/lib/screens/settings/components/metering/widget_settings_section_metering.dart b/lib/screens/settings/components/metering/widget_settings_section_metering.dart index a033cbd..9f86709 100644 --- a/lib/screens/settings/components/metering/widget_settings_section_metering.dart +++ b/lib/screens/settings/components/metering/widget_settings_section_metering.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:lightmeter/features.dart'; import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/screens/settings/components/metering/components/calibration/widget_list_tile_calibration.dart'; import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/widget_list_tile_equipment_profiles.dart'; @@ -18,7 +17,7 @@ class MeteringSettingsSection extends StatelessWidget { StopTypeListTile(), CalibrationListTile(), MeteringScreenLayoutListTile(), - if (FeaturesConfig.equipmentProfilesEnabled) EquipmentProfilesListTile(), + EquipmentProfilesListTile(), ], ); } diff --git a/lib/screens/settings/screen_settings.dart b/lib/screens/settings/screen_settings.dart index 9332d3f..38256e8 100644 --- a/lib/screens/settings/screen_settings.dart +++ b/lib/screens/settings/screen_settings.dart @@ -50,6 +50,7 @@ class _SettingsScreenState extends State { ], ), ), + SliverToBoxAdapter(child: SizedBox(height: MediaQuery.paddingOf(context).bottom)), ], ), ); diff --git a/lib/screens/metering/components/shared/exposure_pairs_list/components/empty_exposure_pairs_list/widget_list_exposure_pairs_empty.dart b/lib/screens/shared/icon_placeholder/widget_icon_placeholder.dart similarity index 68% rename from lib/screens/metering/components/shared/exposure_pairs_list/components/empty_exposure_pairs_list/widget_list_exposure_pairs_empty.dart rename to lib/screens/shared/icon_placeholder/widget_icon_placeholder.dart index ea6728f..f11128a 100644 --- a/lib/screens/metering/components/shared/exposure_pairs_list/components/empty_exposure_pairs_list/widget_list_exposure_pairs_empty.dart +++ b/lib/screens/shared/icon_placeholder/widget_icon_placeholder.dart @@ -1,24 +1,30 @@ import 'package:flutter/material.dart'; -import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/res/dimens.dart'; -class EmptyExposurePairsList extends StatelessWidget { - const EmptyExposurePairsList({super.key}); +class IconPlaceholder extends StatelessWidget { + final IconData icon; + final String text; + + const IconPlaceholder({ + required this.icon, + required this.text, + super.key, + }); @override Widget build(BuildContext context) { return ConstrainedBox( - constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width / 2), + constraints: BoxConstraints(maxWidth: MediaQuery.sizeOf(context).width / 2), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( - Icons.not_interested, + icon, color: Theme.of(context).colorScheme.onBackground, ), const SizedBox(height: Dimens.grid8), Text( - S.of(context).noExposurePairs, + text, style: Theme.of(context) .textTheme .bodyMedium diff --git a/lib/screens/shared/sliver_screen/screen_sliver.dart b/lib/screens/shared/sliver_screen/screen_sliver.dart index d4542b1..20d2e66 100644 --- a/lib/screens/shared/sliver_screen/screen_sliver.dart +++ b/lib/screens/shared/sliver_screen/screen_sliver.dart @@ -24,7 +24,7 @@ class SliverScreen extends StatelessWidget { SliverAppBar( pinned: true, automaticallyImplyLeading: false, - expandedHeight: Dimens.grid168, + expandedHeight: Dimens.sliverAppBarExpandedHeight, flexibleSpace: FlexibleSpaceBar( centerTitle: false, titlePadding: const EdgeInsets.all(Dimens.paddingM), @@ -39,7 +39,6 @@ class SliverScreen extends StatelessWidget { actions: appBarActions, ), ...slivers, - SliverToBoxAdapter(child: SizedBox(height: MediaQuery.of(context).padding.bottom)), ], ), ), diff --git a/lib/utils/log_2.dart b/lib/utils/log_2.dart deleted file mode 100644 index 37bcc92..0000000 --- a/lib/utils/log_2.dart +++ /dev/null @@ -1,3 +0,0 @@ -import 'dart:math'; - -double log2(num x) => log(x) / log(2); diff --git a/pubspec.yaml b/pubspec.yaml index f8ba217..0a2bfb6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: lightmeter -description: A new Flutter project. +description: Lightmeter app inspired by Material 3 design system. publish_to: "none" version: 0.13.2+38 @@ -11,10 +11,10 @@ dependencies: bloc_concurrency: 0.2.2 camera: 0.10.5+2 clipboard: 0.1.3 - dynamic_color: 1.6.5 + dynamic_color: 1.6.6 exif: 3.1.4 - firebase_core: 2.13.0 - firebase_crashlytics: 3.3.1 + firebase_core: 2.14.0 + firebase_crashlytics: 3.3.3 flutter: sdk: flutter flutter_bloc: 8.1.3 @@ -23,22 +23,26 @@ dependencies: intl: 0.18.0 intl_utils: 2.8.2 light_sensor: 2.0.2 + m3_lightmeter_iap: + git: + url: "https://github.com/vodemn/m3_lightmeter_iap" + ref: main m3_lightmeter_resources: git: url: "https://github.com/vodemn/m3_lightmeter_resources" ref: main material_color_utilities: 0.2.0 - package_info_plus: 4.0.1 - permission_handler: 10.2.0 + package_info_plus: 4.0.2 + permission_handler: 10.4.3 platform: 3.1.0 - shared_preferences: 2.1.1 - url_launcher: 6.1.11 + shared_preferences: 2.2.0 + url_launcher: 6.1.12 uuid: 3.0.7 - vibration: 1.7.7 + vibration: 1.8.1 dev_dependencies: bloc_test: 9.1.3 - build_runner: ^2.1.7 + build_runner: 2.4.6 flutter_launcher_icons: 0.11.0 flutter_native_splash: 2.2.16 flutter_test: diff --git a/test/data/models/metering_screen_layout_config_test.dart b/test/data/models/metering_screen_layout_config_test.dart index d763b08..9e9393e 100644 --- a/test/data/models/metering_screen_layout_config_test.dart +++ b/test/data/models/metering_screen_layout_config_test.dart @@ -12,12 +12,31 @@ 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,28 +45,32 @@ void main() { expect( MeteringScreenLayoutConfigJson.fromJson( { - '0': true, - '1': true, + '0': false, + '1': false, + '2': false, }, ), { - MeteringScreenLayoutFeature.extremeExposurePairs: true, - MeteringScreenLayoutFeature.filmPicker: true, - MeteringScreenLayoutFeature.histogram: true, + MeteringScreenLayoutFeature.extremeExposurePairs: false, + MeteringScreenLayoutFeature.filmPicker: false, + MeteringScreenLayoutFeature.histogram: false, + MeteringScreenLayoutFeature.equipmentProfiles: true, }, ); }); }, ); - test('toJson', () { + test('toJson()', () { expect( { + MeteringScreenLayoutFeature.equipmentProfiles: true, MeteringScreenLayoutFeature.extremeExposurePairs: true, MeteringScreenLayoutFeature.filmPicker: true, MeteringScreenLayoutFeature.histogram: true, }.toJson(), { + '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 692cb81..2a63fe5 100644 --- a/test/data/shared_prefs_service_test.dart +++ b/test/data/shared_prefs_service_test.dart @@ -193,6 +193,7 @@ void main() { { MeteringScreenLayoutFeature.extremeExposurePairs: true, MeteringScreenLayoutFeature.filmPicker: true, + MeteringScreenLayoutFeature.equipmentProfiles: true, MeteringScreenLayoutFeature.histogram: true, }, ); @@ -207,6 +208,7 @@ void main() { { MeteringScreenLayoutFeature.extremeExposurePairs: false, MeteringScreenLayoutFeature.filmPicker: true, + MeteringScreenLayoutFeature.equipmentProfiles: true, MeteringScreenLayoutFeature.histogram: true, }, ); @@ -216,18 +218,19 @@ void main() { when( () => sharedPreferences.setString( UserPreferencesService.meteringScreenLayoutKey, - """{"0":false,"1":true,"2":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,"2":true}""", + """{"0":false,"1":true,"2":true,"3":true}""", ), ).called(1); }); diff --git a/test/screens/metering/screen_metering_test.dart b/test/screens/metering/screen_metering_test.dart new file mode 100644 index 0000000..bb50168 --- /dev/null +++ b/test/screens/metering/screen_metering_test.dart @@ -0,0 +1,966 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:lightmeter/data/models/exposure_pair.dart'; +import 'package:lightmeter/data/models/film.dart'; +import 'package:lightmeter/screens/metering/screen_metering.dart'; +import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; + +void main() { + const defaultEquipmentProfile = EquipmentProfile( + id: "", + name: 'Default', + apertureValues: ApertureValue.values, + ndValues: NdValue.values, + shutterSpeedValues: ShutterSpeedValue.values, + isoValues: IsoValue.values, + ); + + group('Empty list', () { + List exposurePairsFull(double ev) => MeteringContainerBuidler.buildExposureValues( + ev, + StopType.full, + defaultEquipmentProfile, + const Film.other(), + ); + + test('isNan', () { + expect(exposurePairsFull(double.nan), const []); + }); + + test('isInifinity', () { + expect(exposurePairsFull(double.infinity), const []); + }); + + test('Big ass number', () { + expect(exposurePairsFull(23), const []); + }); + }); + + group('Default equipment profile', () { + group("StopType.full", () { + List exposurePairsFull(double ev) => + MeteringContainerBuidler.buildExposureValues( + ev, + StopType.full, + defaultEquipmentProfile, + const Film.other(), + ); + + test('EV 1', () { + final exposurePairs = exposurePairsFull(1); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1, StopType.full), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(5.6, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 1.3', () { + final exposurePairs = exposurePairsFull(1.3); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1, StopType.full), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(5.6, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 1.5', () { + final exposurePairs = exposurePairsFull(1.5); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1, StopType.full), + ShutterSpeedValue(4, true, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(8, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 1.7', () { + final exposurePairs = exposurePairsFull(1.7); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1, StopType.full), + ShutterSpeedValue(4, true, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(8, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 2', () { + final exposurePairs = exposurePairsFull(2); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1, StopType.full), + ShutterSpeedValue(4, true, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(8, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + }); + + group("StopType.half", () { + List exposurePairsFull(double ev) => + MeteringContainerBuidler.buildExposureValues( + ev, + StopType.half, + defaultEquipmentProfile, + const Film.other(), + ); + + test('EV 1', () { + final exposurePairs = exposurePairsFull(1); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1, StopType.full), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(5.6, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 1.3', () { + final exposurePairs = exposurePairsFull(1.3); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1, StopType.full), + ShutterSpeedValue(3, true, StopType.half), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(6.7, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 1.5', () { + final exposurePairs = exposurePairsFull(1.5); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1, StopType.full), + ShutterSpeedValue(3, true, StopType.half), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(6.7, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 1.7', () { + final exposurePairs = exposurePairsFull(1.7); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1, StopType.full), + ShutterSpeedValue(3, true, StopType.half), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(6.7, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 2', () { + final exposurePairs = exposurePairsFull(2); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1, StopType.full), + ShutterSpeedValue(4, true, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(8, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + }); + + group("StopType.third", () { + List exposurePairsFull(double ev) => + MeteringContainerBuidler.buildExposureValues( + ev, + StopType.third, + defaultEquipmentProfile, + const Film.other(), + ); + + test('EV 1', () { + final exposurePairs = exposurePairsFull(1); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1, StopType.full), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(5.6, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 1.3', () { + final exposurePairs = exposurePairsFull(1.3); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1, StopType.full), + ShutterSpeedValue(2.5, true, StopType.third), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(6.3, StopType.third), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 1.5', () { + final exposurePairs = exposurePairsFull(1.5); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1, StopType.full), + ShutterSpeedValue(3, true, StopType.third), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(7.1, StopType.third), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 1.7', () { + final exposurePairs = exposurePairsFull(1.7); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1, StopType.full), + ShutterSpeedValue(3, true, StopType.third), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(7.1, StopType.third), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 2', () { + final exposurePairs = exposurePairsFull(2); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1, StopType.full), + ShutterSpeedValue(4, true, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(8, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + }); + }); + + group('Shutter speed 1/1000-1/2"', () { + final equipmentProfile = EquipmentProfile( + id: "1", + name: 'Test1', + apertureValues: ApertureValue.values, + ndValues: NdValue.values, + shutterSpeedValues: ShutterSpeedValue.values.sublist( + ShutterSpeedValue.values.indexOf(const ShutterSpeedValue(1000, true, StopType.full)), + ShutterSpeedValue.values.indexOf(const ShutterSpeedValue(2, true, StopType.full)) + 1, + ), + isoValues: IsoValue.values, + ); + + group("StopType.full", () { + List exposurePairsFull(double ev) => + MeteringContainerBuidler.buildExposureValues( + ev, + StopType.full, + equipmentProfile, + const Film.other(), + ); + + test('EV 1', () { + final exposurePairs = exposurePairsFull(1); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1.0, StopType.full), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(1.0, StopType.full), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + }); + + test('EV 1.3', () { + final exposurePairs = exposurePairsFull(1.3); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1.0, StopType.full), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(1.0, StopType.full), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + }); + + test('EV 1.5', () { + final exposurePairs = exposurePairsFull(1.5); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1.0, StopType.full), + ShutterSpeedValue(4, true, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(1.4, StopType.full), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + }); + + test('EV 1.7', () { + final exposurePairs = exposurePairsFull(1.7); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1.0, StopType.full), + ShutterSpeedValue(4, true, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(1.4, StopType.full), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + }); + + test('EV 2', () { + final exposurePairs = exposurePairsFull(2); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1.0, StopType.full), + ShutterSpeedValue(4, true, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(1.4, StopType.full), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + }); + }); + + group("StopType.half", () { + List exposurePairsFull(double ev) => + MeteringContainerBuidler.buildExposureValues( + ev, + StopType.half, + equipmentProfile, + const Film.other(), + ); + + test('EV 1', () { + final exposurePairs = exposurePairsFull(1); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1.0, StopType.full), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(1.0, StopType.full), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + }); + + test('EV 1.3', () { + final exposurePairs = exposurePairsFull(1.3); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1.0, StopType.full), + ShutterSpeedValue(3, true, StopType.half), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(1.2, StopType.half), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + }); + + test('EV 1.5', () { + final exposurePairs = exposurePairsFull(1.5); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1.0, StopType.full), + ShutterSpeedValue(3, true, StopType.half), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(1.2, StopType.half), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + }); + + test('EV 1.7', () { + final exposurePairs = exposurePairsFull(1.7); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1.0, StopType.full), + ShutterSpeedValue(3, true, StopType.half), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(1.2, StopType.half), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + }); + + test('EV 2', () { + final exposurePairs = exposurePairsFull(2); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1.0, StopType.full), + ShutterSpeedValue(4, true, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(1.4, StopType.full), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + }); + }); + + group("StopType.third", () { + List exposurePairsFull(double ev) => + MeteringContainerBuidler.buildExposureValues( + ev, + StopType.third, + equipmentProfile, + const Film.other(), + ); + + test('EV 1', () { + final exposurePairs = exposurePairsFull(1); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1.0, StopType.full), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(1.0, StopType.full), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + }); + + test('EV 1.3', () { + final exposurePairs = exposurePairsFull(1.3); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1.0, StopType.full), + ShutterSpeedValue(2.5, true, StopType.third), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(1.1, StopType.third), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + }); + + test('EV 1.5', () { + final exposurePairs = exposurePairsFull(1.5); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1.0, StopType.full), + ShutterSpeedValue(3, true, StopType.third), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(1.2, StopType.third), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + }); + + test('EV 1.7', () { + final exposurePairs = exposurePairsFull(1.7); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1.0, StopType.full), + ShutterSpeedValue(3, true, StopType.third), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(1.2, StopType.third), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + }); + + test('EV 2', () { + final exposurePairs = exposurePairsFull(2); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(1.0, StopType.full), + ShutterSpeedValue(4, true, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(1.4, StopType.full), + ShutterSpeedValue(2, true, StopType.full), + ), + ); + }); + }); + }); + + group('Shutter speed 2"-16"', () { + final equipmentProfile = EquipmentProfile( + id: "1", + name: 'Test1', + apertureValues: ApertureValue.values.sublist(4), + ndValues: NdValue.values, + shutterSpeedValues: ShutterSpeedValue.values.sublist( + ShutterSpeedValue.values.indexOf(const ShutterSpeedValue(2, false, StopType.full)), + ), + isoValues: IsoValue.values, + ); + + group("StopType.full", () { + List exposurePairsFull(double ev) => + MeteringContainerBuidler.buildExposureValues( + ev, + StopType.full, + equipmentProfile, + const Film.other(), + ); + + test('EV 1', () { + final exposurePairs = exposurePairsFull(1); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(2.0, StopType.full), + ShutterSpeedValue(2, false, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(5.6, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 1.3', () { + final exposurePairs = exposurePairsFull(1.3); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(2.0, StopType.full), + ShutterSpeedValue(2, false, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(5.6, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 1.5', () { + final exposurePairs = exposurePairsFull(1.5); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(2.8, StopType.full), + ShutterSpeedValue(2, false, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(8, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 1.7', () { + final exposurePairs = exposurePairsFull(1.7); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(2.8, StopType.full), + ShutterSpeedValue(2, false, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(8, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 2', () { + final exposurePairs = exposurePairsFull(2); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(2.8, StopType.full), + ShutterSpeedValue(2, false, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(8, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + }); + + group("StopType.half", () { + List exposurePairsFull(double ev) => + MeteringContainerBuidler.buildExposureValues( + ev, + StopType.half, + equipmentProfile, + const Film.other(), + ); + + test('EV 1', () { + final exposurePairs = exposurePairsFull(1); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(2.0, StopType.full), + ShutterSpeedValue(2, false, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(5.6, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 1.3', () { + final exposurePairs = exposurePairsFull(1.3); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(2.4, StopType.half), + ShutterSpeedValue(2, false, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(6.7, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 1.5', () { + final exposurePairs = exposurePairsFull(1.5); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(2.4, StopType.half), + ShutterSpeedValue(2, false, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(6.7, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 1.7', () { + final exposurePairs = exposurePairsFull(1.7); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(2.4, StopType.half), + ShutterSpeedValue(2, false, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(6.7, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 2', () { + final exposurePairs = exposurePairsFull(2); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(2.8, StopType.full), + ShutterSpeedValue(2, false, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(8, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + }); + + group("StopType.third", () { + List exposurePairsFull(double ev) => + MeteringContainerBuidler.buildExposureValues( + ev, + StopType.third, + equipmentProfile, + const Film.other(), + ); + + test('EV 1', () { + final exposurePairs = exposurePairsFull(1); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(2.0, StopType.full), + ShutterSpeedValue(2, false, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(5.6, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 1.3', () { + final exposurePairs = exposurePairsFull(1.3); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(2.2, StopType.full), + ShutterSpeedValue(2, false, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(6.3, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 1.5', () { + final exposurePairs = exposurePairsFull(1.5); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(2.4, StopType.full), + ShutterSpeedValue(2, false, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(7.1, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 1.7', () { + final exposurePairs = exposurePairsFull(1.7); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(2.4, StopType.full), + ShutterSpeedValue(2, false, StopType.third), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(7.1, StopType.third), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + + test('EV 2', () { + final exposurePairs = exposurePairsFull(2); + expect( + exposurePairs.first, + const ExposurePair( + ApertureValue(2.8, StopType.full), + ShutterSpeedValue(2, false, StopType.full), + ), + ); + expect( + exposurePairs.last, + const ExposurePair( + ApertureValue(8, StopType.full), + ShutterSpeedValue(16, false, StopType.full), + ), + ); + }); + }); + }); +}