diff --git a/.vscode/launch.json b/.vscode/launch.json index 40dbfac..a222516 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -82,5 +82,20 @@ ], "program": "${workspaceFolder}/lib/main_dev.dart", }, + { + "name": "dev-simulator", + "request": "launch", + "type": "dart", + "flutterMode": "debug", + "args": [ + "--flavor", + "dev", + "--dart-define", + "cameraPreviewAspectRatio=240/320", + "--dart-define", + "cameraStubImage=assets/camera_stub_image.jpg" + ], + "program": "${workspaceFolder}/lib/main_dev.dart", + }, ], } \ No newline at end of file diff --git a/PRIVACY_POLICY.md b/PRIVACY_POLICY.md index 05710bd..d9b9cad 100644 --- a/PRIVACY_POLICY.md +++ b/PRIVACY_POLICY.md @@ -1,6 +1,6 @@ **Privacy Policy** -I, Vodemn, built the Material Lightmeter app as a Free app. This app is provided at no cost and is intended for use as is. +I, Vadim Turko, built the Material Lightmeter app as a Free app. This app is provided at no cost and is intended for use as is. **Information Collection and Use** @@ -20,7 +20,7 @@ This app contains links to other sites. If you click on a third-party link, you I may update our Privacy Policy from time to time. Thus, you are advised to review this page periodically for any changes. I will notify you of any changes by posting the new Privacy Policy on this page. -This policy is effective as of 2023-02-24 +This policy is effective as of 2024-01-04 **Contact Us** diff --git a/README.md b/README.md index ef38b93..b5ef345 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ - [Backstory](#backstory) - [Screenshots](#screenshots) - [Development](#development) -- [Contribution](#contribution) +- [Support](#support) - [iOS Limitations](#ios-limitations) # Backstory @@ -36,7 +36,7 @@ Without further delay behold my new Lightmeter app inspired by Material You (a.k To build this app you need to install Flutter 3.10.0 stable. [How to install](https://docs.flutter.dev/get-started/install). -### 3. Project setup +### 2. Project setup As part of the app's functionallity is in the private repo, you have to replace these lines in _pubspec.yaml_: @@ -69,11 +69,11 @@ flutter pub get flutter pub run intl_utils:generate ``` -### 4. (Optional) Install Firebase +### 3. (Optional) Install Firebase Out of the box Firebase Crashlytics won't work. If you want to add Crashlytics to your local build please follow [this guide](https://firebase.google.com/docs/flutter/setup). -### 5. Build +### 4. Build #### Android @@ -87,11 +87,11 @@ flutter build apk --release --flavor dev --dart-define cameraPreviewAspectRatio= TBD -# Contribution +# Support -To report a bug or suggest a new feature open a new [issue](https://github.com/vodemn/m3_lightmeter/issues). +To report a bug or suggest a new feature open a new [issue](https://github.com/vodemn/m3_lightmeter/issues). To contribute to the project feel free to open a Pull Request, but you need to follow this [style guide](doc/style_guide.md). -In case you want to help develop this project feel free to open a Pull Request, but you need to follow this [style guide](doc/style_guide.md). +In case you have any other questions please contact me via [email](mailto:contact.vodemn@gmail.com?subject="Lightmeter"). # iOS Limitations diff --git a/android/app/build.gradle b/android/app/build.gradle index 5ccd388..be85756 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -75,12 +75,13 @@ android { flavorDimensions "app" productFlavors { dev { - applicationId "com.vodemn.lightmeter.dev" + resValue "string", "app_name", "Lightmeter (DEV)" dimension "app" signingConfig signingConfigs.release + applicationIdSuffix ".dev" } prod { - applicationId "com.vodemn.lightmeter" + resValue "string", "app_name", "Lightmeter" dimension "app" signingConfig signingConfigs.release } diff --git a/android/app/src/dev/ic_launcher-playstore.png b/android/app/src/dev/ic_launcher-playstore.png new file mode 100644 index 0000000..00039fd Binary files /dev/null and b/android/app/src/dev/ic_launcher-playstore.png differ diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/dev/res/mipmap-anydpi-v26/ic_launcher.xml similarity index 100% rename from android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml rename to android/app/src/dev/res/mipmap-anydpi-v26/ic_launcher.xml diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/app/src/dev/res/mipmap-anydpi-v26/ic_launcher_round.xml similarity index 100% rename from android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml rename to android/app/src/dev/res/mipmap-anydpi-v26/ic_launcher_round.xml diff --git a/android/app/src/dev/res/mipmap-hdpi/ic_launcher.png b/android/app/src/dev/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..6dec4cd Binary files /dev/null and b/android/app/src/dev/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/dev/res/mipmap-hdpi/ic_launcher_foreground.png b/android/app/src/dev/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..7fd4342 Binary files /dev/null and b/android/app/src/dev/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/dev/res/mipmap-hdpi/ic_launcher_round.png b/android/app/src/dev/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..dd78094 Binary files /dev/null and b/android/app/src/dev/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/android/app/src/dev/res/mipmap-mdpi/ic_launcher.png b/android/app/src/dev/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..5169915 Binary files /dev/null and b/android/app/src/dev/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/dev/res/mipmap-mdpi/ic_launcher_foreground.png b/android/app/src/dev/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..c7d106b Binary files /dev/null and b/android/app/src/dev/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/dev/res/mipmap-mdpi/ic_launcher_round.png b/android/app/src/dev/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..89c0f9f Binary files /dev/null and b/android/app/src/dev/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/android/app/src/dev/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/dev/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..1da9378 Binary files /dev/null and b/android/app/src/dev/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/dev/res/mipmap-xhdpi/ic_launcher_foreground.png b/android/app/src/dev/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..79cc56f Binary files /dev/null and b/android/app/src/dev/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/dev/res/mipmap-xhdpi/ic_launcher_round.png b/android/app/src/dev/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..61ea400 Binary files /dev/null and b/android/app/src/dev/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/android/app/src/dev/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/dev/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..f1c2f64 Binary files /dev/null and b/android/app/src/dev/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/dev/res/mipmap-xxhdpi/ic_launcher_foreground.png b/android/app/src/dev/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..f73d67b Binary files /dev/null and b/android/app/src/dev/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/dev/res/mipmap-xxhdpi/ic_launcher_round.png b/android/app/src/dev/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..672e3e1 Binary files /dev/null and b/android/app/src/dev/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/android/app/src/dev/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/dev/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..a2e0a13 Binary files /dev/null and b/android/app/src/dev/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/dev/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/android/app/src/dev/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..9203230 Binary files /dev/null and b/android/app/src/dev/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/dev/res/mipmap-xxxhdpi/ic_launcher_round.png b/android/app/src/dev/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..5711a66 Binary files /dev/null and b/android/app/src/dev/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/android/app/src/dev/res/values/ic_launcher_background.xml b/android/app/src/dev/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..58043be --- /dev/null +++ b/android/app/src/dev/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #212121 + \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 6dba1a5..a7d420e 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -3,7 +3,7 @@ package="com.vodemn.lightmeter"> - - - - diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index de34bbe..0000000 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png deleted file mode 100644 index ae5b426..0000000 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png deleted file mode 100644 index 5ab8dd2..0000000 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index d49e5da..0000000 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png deleted file mode 100644 index b57a18c..0000000 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png deleted file mode 100644 index f068022..0000000 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index afaeb3c..0000000 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png deleted file mode 100644 index f75d3c2..0000000 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png deleted file mode 100644 index d7dd8be..0000000 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index 20eff01..0000000 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png deleted file mode 100644 index 3dfccd0..0000000 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png deleted file mode 100644 index 36e8d33..0000000 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 0110c49..0000000 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png deleted file mode 100644 index ac6e5bc..0000000 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png deleted file mode 100644 index a2162bf..0000000 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/android/app/src/prod/ic_launcher-playstore.png b/android/app/src/prod/ic_launcher-playstore.png new file mode 100644 index 0000000..4f082f8 Binary files /dev/null and b/android/app/src/prod/ic_launcher-playstore.png differ diff --git a/android/app/src/prod/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/prod/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/android/app/src/prod/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/prod/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/app/src/prod/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/android/app/src/prod/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/prod/res/mipmap-hdpi/ic_launcher.png b/android/app/src/prod/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..b86b175 Binary files /dev/null and b/android/app/src/prod/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/prod/res/mipmap-hdpi/ic_launcher_foreground.png b/android/app/src/prod/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..1392a3b Binary files /dev/null and b/android/app/src/prod/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/prod/res/mipmap-hdpi/ic_launcher_round.png b/android/app/src/prod/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..e791add Binary files /dev/null and b/android/app/src/prod/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/android/app/src/prod/res/mipmap-mdpi/ic_launcher.png b/android/app/src/prod/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..31b5f50 Binary files /dev/null and b/android/app/src/prod/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/prod/res/mipmap-mdpi/ic_launcher_foreground.png b/android/app/src/prod/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..3399a7d Binary files /dev/null and b/android/app/src/prod/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/prod/res/mipmap-mdpi/ic_launcher_round.png b/android/app/src/prod/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..e22fb78 Binary files /dev/null and b/android/app/src/prod/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/android/app/src/prod/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/prod/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..a2ace48 Binary files /dev/null and b/android/app/src/prod/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/prod/res/mipmap-xhdpi/ic_launcher_foreground.png b/android/app/src/prod/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..c6e5d43 Binary files /dev/null and b/android/app/src/prod/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/prod/res/mipmap-xhdpi/ic_launcher_round.png b/android/app/src/prod/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..db39634 Binary files /dev/null and b/android/app/src/prod/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/android/app/src/prod/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/prod/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..2eb0bfb Binary files /dev/null and b/android/app/src/prod/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/prod/res/mipmap-xxhdpi/ic_launcher_foreground.png b/android/app/src/prod/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..9a5a827 Binary files /dev/null and b/android/app/src/prod/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/prod/res/mipmap-xxhdpi/ic_launcher_round.png b/android/app/src/prod/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..fdd534d Binary files /dev/null and b/android/app/src/prod/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/android/app/src/prod/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/prod/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..9d3e572 Binary files /dev/null and b/android/app/src/prod/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/prod/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/android/app/src/prod/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..fe2a50c Binary files /dev/null and b/android/app/src/prod/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/prod/res/mipmap-xxxhdpi/ic_launcher_round.png b/android/app/src/prod/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..74f9d28 Binary files /dev/null and b/android/app/src/prod/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/android/app/src/prod/res/values/ic_launcher_background.xml b/android/app/src/prod/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..58043be --- /dev/null +++ b/android/app/src/prod/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #212121 + \ No newline at end of file diff --git a/assets/README.md b/assets/README.md new file mode 100644 index 0000000..decb62c --- /dev/null +++ b/assets/README.md @@ -0,0 +1,11 @@ +# Assets + +## Launcher icons + +### Android + +Resources for Android are generated in Android Studio from the 512x512 source image as described in this [guide](https://developer.android.com/studio/write/create-app-icons). + +### iOS + +Resources for iOS are generated in XCode from the 1024x1024 source image as described in this [guide](https://developer.apple.com/documentation/xcode/configuring-your-app-icon). diff --git a/assets/launcher_icon_circle.png b/assets/launcher_icon_circle.png deleted file mode 100644 index ab23e26..0000000 Binary files a/assets/launcher_icon_circle.png and /dev/null differ diff --git a/assets/launcher_icon_dev_1024.png b/assets/launcher_icon_dev_1024.png new file mode 100644 index 0000000..0923f36 Binary files /dev/null and b/assets/launcher_icon_dev_1024.png differ diff --git a/assets/launcher_icon_dev_512.png b/assets/launcher_icon_dev_512.png new file mode 100644 index 0000000..1c234a0 Binary files /dev/null and b/assets/launcher_icon_dev_512.png differ diff --git a/assets/launcher_icon_prod_1024.png b/assets/launcher_icon_prod_1024.png new file mode 100755 index 0000000..a54026f Binary files /dev/null and b/assets/launcher_icon_prod_1024.png differ diff --git a/assets/launcher_icon_prod_512.png b/assets/launcher_icon_prod_512.png new file mode 100644 index 0000000..706987b Binary files /dev/null and b/assets/launcher_icon_prod_512.png differ diff --git a/assets/launcher_icon_square.png b/assets/launcher_icon_square.png deleted file mode 100644 index e8ac5f8..0000000 Binary files a/assets/launcher_icon_square.png and /dev/null differ diff --git a/iap/lib/src/providers/iap_products_provider.dart b/iap/lib/src/providers/iap_products_provider.dart index 4895fdf..9d381ae 100644 --- a/iap/lib/src/providers/iap_products_provider.dart +++ b/iap/lib/src/providers/iap_products_provider.dart @@ -6,8 +6,10 @@ class IAPProductsProvider extends StatefulWidget { const IAPProductsProvider({required this.child, super.key}); - static IAPProductsProviderState of(BuildContext context) { - return context.findAncestorStateOfType()!; + static IAPProductsProviderState of(BuildContext context) => IAPProductsProvider.maybeOf(context)!; + + static IAPProductsProviderState? maybeOf(BuildContext context) { + return context.findAncestorStateOfType(); } @override @@ -54,8 +56,7 @@ class IAPProducts extends InheritedModel { bool updateShouldNotify(IAPProducts oldWidget) => false; @override - bool updateShouldNotifyDependent(IAPProducts oldWidget, Set dependencies) => - false; + bool updateShouldNotifyDependent(IAPProducts oldWidget, Set dependencies) => false; IAPProduct? _findProduct(IAPProductType type) { try { diff --git a/ios/Podfile b/ios/Podfile index e9b0728..462df98 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -40,6 +40,12 @@ post_install do |installer| # Start of the permission_handler configuration target.build_configurations.each do |config| + # https://github.com/CocoaPods/CocoaPods/issues/12012 + xcconfig_path = config.base_configuration_reference.real_path + xcconfig = File.read(xcconfig_path) + xcconfig_mod = xcconfig.gsub(/DT_TOOLCHAIN_DIR/, "TOOLCHAIN_DIR") + File.open(xcconfig_path, "w") { |file| file << xcconfig_mod } + # Preprocessor definitions can be found in: https://github.com/Baseflow/flutter-permission-handler/blob/master/permission_handler_apple/ios/Classes/PermissionHandlerEnums.h config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ '$(inherited)', diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 73ce339..0b4c62e 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -44,7 +44,7 @@ 8C539F8FF42AB22E298D5A5E /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146EE1CF9000F007C117D /* Lightmeter.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Lightmeter.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; @@ -95,7 +95,7 @@ 97C146EF1CF9000F007C117D /* Products */ = { isa = PBXGroup; children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, + 97C146EE1CF9000F007C117D /* Lightmeter.app */, ); name = Products; sourceTree = ""; @@ -154,6 +154,7 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 45F53C083F2EA48EF231DA16 /* [CP] Embed Pods Frameworks */, + FF00F85CE432774850A0EDB7 /* [firebase_crashlytics] Crashlytics Upload Symbols */, ); buildRules = ( ); @@ -161,7 +162,7 @@ ); name = Runner; productName = Runner; - productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productReference = 97C146EE1CF9000F007C117D /* Lightmeter.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ @@ -242,6 +243,7 @@ files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -282,6 +284,29 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + FF00F85CE432774850A0EDB7 /* [firebase_crashlytics] Crashlytics Upload Symbols */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "\"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}\"", + "\"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/\"", + "\"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist\"", + "\"$(TARGET_BUILD_DIR)/$(EXECUTABLE_PATH)\"", + "\"$(PROJECT_DIR)/firebase_app_id_file.json\"", + ); + name = "[firebase_crashlytics] Crashlytics Upload Symbols"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$PODS_ROOT/FirebaseCrashlytics/upload-symbols\" --flutter-project \"$PROJECT_DIR/firebase_app_id_file.json\" "; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -370,18 +395,23 @@ isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-prod"; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = 489Z6UQMGN; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = Lightmeter; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.vodemn.lightmeter; - PRODUCT_NAME = "$(TARGET_NAME)"; + PRODUCT_NAME = Lightmeter; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -499,18 +529,23 @@ isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-prod"; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = 489Z6UQMGN; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = Lightmeter; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.vodemn.lightmeter; - PRODUCT_NAME = "$(TARGET_NAME)"; + PRODUCT_NAME = Lightmeter; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -522,18 +557,23 @@ isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-prod"; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = 489Z6UQMGN; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = Lightmeter; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.vodemn.lightmeter; - PRODUCT_NAME = "$(TARGET_NAME)"; + PRODUCT_NAME = Lightmeter; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -599,18 +639,23 @@ isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-dev"; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = 489Z6UQMGN; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Lightmeter (DEV)"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.vodemn.lightmeter.dev; - PRODUCT_NAME = "$(TARGET_NAME)"; + PRODUCT_NAME = "Lightmeter (DEV)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -674,18 +719,23 @@ isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-dev"; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = 489Z6UQMGN; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Lightmeter (DEV)"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.vodemn.lightmeter.dev; - PRODUCT_NAME = "$(TARGET_NAME)"; + PRODUCT_NAME = "Lightmeter (DEV)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -746,18 +796,23 @@ isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-dev"; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = 489Z6UQMGN; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "Lightmeter (DEV)"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.vodemn.lightmeter.dev; - PRODUCT_NAME = "$(TARGET_NAME)"; + PRODUCT_NAME = "Lightmeter (DEV)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; diff --git a/ios/Runner/Assets.xcassets/AppIcon-dev.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon-dev.appiconset/Contents.json new file mode 100644 index 0000000..c89c0fc --- /dev/null +++ b/ios/Runner/Assets.xcassets/AppIcon-dev.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "Icon square (dev).png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon-dev.appiconset/Icon square (dev).png b/ios/Runner/Assets.xcassets/AppIcon-dev.appiconset/Icon square (dev).png new file mode 100644 index 0000000..0923f36 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon-dev.appiconset/Icon square (dev).png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon-prod.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon-prod.appiconset/Contents.json new file mode 100644 index 0000000..e85412b --- /dev/null +++ b/ios/Runner/Assets.xcassets/AppIcon-prod.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "Icon square.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon-prod.appiconset/Icon square.png b/ios/Runner/Assets.xcassets/AppIcon-prod.appiconset/Icon square.png new file mode 100644 index 0000000..a54026f Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon-prod.appiconset/Icon square.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index d36b1fa..0000000 --- a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" - }, - { - "size" : "1024x1024", - "idiom" : "ios-marketing", - "filename" : "Icon-App-1024x1024@1x.png", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png deleted file mode 100644 index 21fba1c..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png deleted file mode 100644 index 8836ffd..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png deleted file mode 100644 index 826dc2b..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100644 index ddb081d..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png deleted file mode 100644 index e9dcbdb..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100644 index d8669e5..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png deleted file mode 100644 index 6178967..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png deleted file mode 100644 index 826dc2b..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100644 index d865bad..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png deleted file mode 100644 index 7863936..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png deleted file mode 100644 index 37d3359..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png deleted file mode 100644 index cd9bbfe..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png deleted file mode 100644 index 687010b..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png deleted file mode 100644 index 284f7ce..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100644 index 7863936..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100644 index 2ea76d6..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png deleted file mode 100644 index e5c29c0..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png deleted file mode 100644 index b895962..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png deleted file mode 100644 index 16a1ae1..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png deleted file mode 100644 index 9debf8c..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png deleted file mode 100644 index e32e837..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/Contents.json b/ios/Runner/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/ios/Runner/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 9b13e6e..f2e1bd9 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -1,51 +1,51 @@ - - CADisableMinimumFrameDurationOnPhone - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - Lightmeter - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - lightmeter - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleSignature - ???? - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSRequiresIPhoneOS - - UIApplicationSupportsIndirectInputEvents - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - NSCameraUsageDescription - Provide camera permissions in order to make measurements - - \ No newline at end of file + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + $(PRODUCT_NAME) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + lightmeter + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + NSCameraUsageDescription + Provide camera permissions in order to make measurements + + diff --git a/lib/application.dart b/lib/application.dart index 68dd44a..15eba3a 100644 --- a/lib/application.dart +++ b/lib/application.dart @@ -3,6 +3,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:lightmeter/data/models/supported_locale.dart'; import 'package:lightmeter/generated/l10n.dart'; +import 'package:lightmeter/platform_config.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'; @@ -17,13 +18,13 @@ class Application extends StatelessWidget { return AnnotatedRegion( value: SystemUiOverlayStyle( statusBarColor: Colors.transparent, - statusBarBrightness: - systemIconsBrightness == Brightness.light ? Brightness.dark : Brightness.light, + statusBarBrightness: systemIconsBrightness == Brightness.light ? Brightness.dark : Brightness.light, statusBarIconBrightness: systemIconsBrightness, systemNavigationBarColor: Colors.transparent, systemNavigationBarIconBrightness: systemIconsBrightness, ), child: MaterialApp( + debugShowCheckedModeBanner: !PlatformConfig.isTest, theme: theme, locale: Locale(UserPreferencesProvider.localeOf(context).intlName), localizationsDelegates: const [ diff --git a/lib/application_wrapper.dart b/lib/application_wrapper.dart index d28fbcf..ae7d1ae 100644 --- a/lib/application_wrapper.dart +++ b/lib/application_wrapper.dart @@ -30,7 +30,7 @@ class ApplicationWrapper extends StatelessWidget { future: Future.wait([ SharedPreferences.getInstance(), const LightSensorService(LocalPlatform()).hasSensor(), - const RemoteConfigService().activeAndFetchFeatures(), + if (env.buildType != BuildType.dev) const RemoteConfigService().activeAndFetchFeatures(), ]), builder: (_, snapshot) { if (snapshot.data != null) { @@ -47,7 +47,8 @@ class ApplicationWrapper extends StatelessWidget { userPreferencesService: userPreferencesService, volumeEventsService: const VolumeEventsService(LocalPlatform()), child: RemoteConfigProvider( - remoteConfigService: const RemoteConfigService(), + remoteConfigService: + env.buildType != BuildType.dev ? const RemoteConfigService() : const MockRemoteConfigService(), child: EquipmentProfileProvider( storageService: iapService, child: FilmsProvider( diff --git a/lib/data/models/camera_feature.dart b/lib/data/models/camera_feature.dart new file mode 100644 index 0000000..20193bf --- /dev/null +++ b/lib/data/models/camera_feature.dart @@ -0,0 +1,13 @@ +enum CameraFeature { + spotMetering, + histogram, +} + +typedef CameraFeaturesConfig = Map; + +extension CameraFeaturesConfigJson on CameraFeaturesConfig { + static CameraFeaturesConfig fromJson(Map data) => + {for (final f in CameraFeature.values) f: data[f.name] as bool? ?? false}; + + Map toJson() => map((key, value) => MapEntry(key.name, value)); +} diff --git a/lib/data/models/feature.dart b/lib/data/models/feature.dart index e4dc30e..b022db7 100644 --- a/lib/data/models/feature.dart +++ b/lib/data/models/feature.dart @@ -1,5 +1,5 @@ enum Feature { unlockProFeaturesText } const featuresDefaultValues = { - Feature.unlockProFeaturesText: false, + Feature.unlockProFeaturesText: true, }; diff --git a/lib/data/models/metering_screen_layout_config.dart b/lib/data/models/metering_screen_layout_config.dart index 7802195..7d550e4 100644 --- a/lib/data/models/metering_screen_layout_config.dart +++ b/lib/data/models/metering_screen_layout_config.dart @@ -1,18 +1,31 @@ enum MeteringScreenLayoutFeature { - extremeExposurePairs, - filmPicker, - histogram, - equipmentProfiles, + extremeExposurePairs, // 0 + filmPicker, // 1 + equipmentProfiles, // 3 } typedef MeteringScreenLayoutConfig = Map; extension MeteringScreenLayoutConfigJson on MeteringScreenLayoutConfig { - static MeteringScreenLayoutConfig fromJson(Map data) => - { - for (final f in MeteringScreenLayoutFeature.values) - f: data[f.index.toString()] as bool? ?? true - }; + static MeteringScreenLayoutConfig fromJson(Map data) { + int? migratedIndex(MeteringScreenLayoutFeature feature) { + switch (feature) { + case MeteringScreenLayoutFeature.extremeExposurePairs: + return 0; + case MeteringScreenLayoutFeature.filmPicker: + return 1; + case MeteringScreenLayoutFeature.equipmentProfiles: + return 3; + default: + return null; + } + } - Map toJson() => map((key, value) => MapEntry(key.index.toString(), value)); + return { + for (final f in MeteringScreenLayoutFeature.values) + f: (data[migratedIndex(f).toString()] ?? data[f.name]) as bool? ?? true + }; + } + + Map toJson() => map((key, value) => MapEntry(key.name, value)); } diff --git a/lib/data/remote_config_service.dart b/lib/data/remote_config_service.dart index 9fc83fc..f8c123b 100644 --- a/lib/data/remote_config_service.dart +++ b/lib/data/remote_config_service.dart @@ -7,9 +7,26 @@ import 'package:firebase_remote_config/firebase_remote_config.dart'; import 'package:flutter/foundation.dart'; import 'package:lightmeter/data/models/feature.dart'; -class RemoteConfigService { +abstract class IRemoteConfigService { + const IRemoteConfigService(); + + Future activeAndFetchFeatures(); + + Future fetchConfig(); + + dynamic getValue(Feature feature); + + Map getAll(); + + Stream> onConfigUpdated(); + + bool isEnabled(Feature feature); +} + +class RemoteConfigService implements IRemoteConfigService { const RemoteConfigService(); + @override Future activeAndFetchFeatures() async { final FirebaseRemoteConfig remoteConfig = FirebaseRemoteConfig.instance; const cacheStaleDuration = kDebugMode ? Duration(minutes: 1) : Duration(hours: 12); @@ -24,18 +41,26 @@ class RemoteConfigService { await remoteConfig.setDefaults(featuresDefaultValues.map((key, value) => MapEntry(key.name, value))); await remoteConfig.activate(); await remoteConfig.ensureInitialized(); - unawaited(remoteConfig.fetch()); log('Firebase remote config initialized successfully'); } on FirebaseException catch (e) { _logError('Firebase exception during Firebase Remote Config initialization: $e'); - } on Exception catch (e) { + } catch (e) { _logError('Error during Firebase Remote Config initialization: $e'); } } + @override + Future fetchConfig() async { + // https://github.com/firebase/flutterfire/issues/6196#issuecomment-927751667 + await Future.delayed(const Duration(seconds: 1)); + await FirebaseRemoteConfig.instance.fetch(); + } + + @override dynamic getValue(Feature feature) => FirebaseRemoteConfig.instance.getValue(feature.name).toValue(feature); + @override Map getAll() { final Map result = {}; for (final value in FirebaseRemoteConfig.instance.getAll().entries) { @@ -49,6 +74,7 @@ class RemoteConfigService { return result; } + @override Stream> onConfigUpdated() => FirebaseRemoteConfig.instance.onConfigUpdated.asyncMap( (event) async { await FirebaseRemoteConfig.instance.activate(); @@ -64,6 +90,7 @@ class RemoteConfigService { }, ); + @override bool isEnabled(Feature feature) => FirebaseRemoteConfig.instance.getBool(feature.name); void _logError(dynamic throwable, {StackTrace? stackTrace}) { @@ -71,6 +98,29 @@ class RemoteConfigService { } } +class MockRemoteConfigService implements IRemoteConfigService { + const MockRemoteConfigService(); + + @override + Future activeAndFetchFeatures() async {} + + @override + Future fetchConfig() async {} + + @override + Map getAll() => featuresDefaultValues; + + @override + dynamic getValue(Feature feature) => featuresDefaultValues[feature]; + + @override + // ignore: cast_nullable_to_non_nullable + bool isEnabled(Feature feature) => featuresDefaultValues[feature] as bool; + + @override + Stream> onConfigUpdated() => const Stream.empty(); +} + extension on RemoteConfigValue { dynamic toValue(Feature feature) { switch (feature) { diff --git a/lib/data/shared_prefs_service.dart b/lib/data/shared_prefs_service.dart index 96d7d9d..3bb6dfb 100644 --- a/lib/data/shared_prefs_service.dart +++ b/lib/data/shared_prefs_service.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:lightmeter/data/models/camera_feature.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'; @@ -15,9 +16,11 @@ class UserPreferencesService { static const evSourceTypeKey = "evSourceType"; static const stopTypeKey = "stopType"; + static const showEv100Key = "showEv100"; static const cameraEvCalibrationKey = "cameraEvCalibration"; static const lightSensorEvCalibrationKey = "lightSensorEvCalibration"; static const meteringScreenLayoutKey = "meteringScreenLayout"; + static const cameraFeaturesKey = "cameraFeatures"; static const caffeineKey = "caffeine"; static const hapticsKey = "haptics"; @@ -70,21 +73,21 @@ class UserPreferencesService { } } - IsoValue get iso => - IsoValue.values.firstWhere((v) => v.value == (_sharedPreferences.getInt(isoKey) ?? 100)); + IsoValue get iso => IsoValue.values.firstWhere((v) => v.value == (_sharedPreferences.getInt(isoKey) ?? 100)); set iso(IsoValue value) => _sharedPreferences.setInt(isoKey, value.value); - NdValue get ndFilter => - NdValue.values.firstWhere((v) => v.value == (_sharedPreferences.getInt(ndFilterKey) ?? 0)); + NdValue get ndFilter => NdValue.values.firstWhere((v) => v.value == (_sharedPreferences.getInt(ndFilterKey) ?? 0)); set ndFilter(NdValue value) => _sharedPreferences.setInt(ndFilterKey, value.value); - EvSourceType get evSourceType => - EvSourceType.values[_sharedPreferences.getInt(evSourceTypeKey) ?? 0]; + EvSourceType get evSourceType => EvSourceType.values[_sharedPreferences.getInt(evSourceTypeKey) ?? 0]; set evSourceType(EvSourceType value) => _sharedPreferences.setInt(evSourceTypeKey, value.index); StopType get stopType => StopType.values[_sharedPreferences.getInt(stopTypeKey) ?? 2]; set stopType(StopType value) => _sharedPreferences.setInt(stopTypeKey, value.index); + bool get showEv100 => _sharedPreferences.getBool(showEv100Key) ?? false; + set showEv100(bool value) => _sharedPreferences.setBool(showEv100Key, value); + MeteringScreenLayoutConfig get meteringScreenLayout { final configJson = _sharedPreferences.getString(meteringScreenLayoutKey); if (configJson != null) { @@ -96,7 +99,6 @@ class UserPreferencesService { MeteringScreenLayoutFeature.equipmentProfiles: true, MeteringScreenLayoutFeature.extremeExposurePairs: true, MeteringScreenLayoutFeature.filmPicker: true, - MeteringScreenLayoutFeature.histogram: true, }; } } @@ -104,6 +106,21 @@ class UserPreferencesService { set meteringScreenLayout(MeteringScreenLayoutConfig value) => _sharedPreferences.setString(meteringScreenLayoutKey, json.encode(value.toJson())); + CameraFeaturesConfig get cameraFeatures { + final configJson = _sharedPreferences.getString(cameraFeaturesKey); + if (configJson != null) { + return CameraFeaturesConfigJson.fromJson(json.decode(configJson) as Map); + } else { + return { + CameraFeature.spotMetering: false, + CameraFeature.histogram: false, + }; + } + } + + set cameraFeatures(CameraFeaturesConfig value) => + _sharedPreferences.setString(cameraFeaturesKey, json.encode(value.toJson())); + bool get caffeine => _sharedPreferences.getBool(caffeineKey) ?? false; set caffeine(bool value) => _sharedPreferences.setBool(caffeineKey, value); @@ -114,8 +131,7 @@ class UserPreferencesService { (e) => e.toString() == _sharedPreferences.getString(volumeActionKey), orElse: () => VolumeAction.shutter, ); - set volumeAction(VolumeAction value) => - _sharedPreferences.setString(volumeActionKey, value.toString()); + set volumeAction(VolumeAction value) => _sharedPreferences.setString(volumeActionKey, value.toString()); SupportedLocale get locale => SupportedLocale.values.firstWhere( (e) => e.toString() == _sharedPreferences.getString(localeKey), @@ -124,13 +140,10 @@ class UserPreferencesService { set locale(SupportedLocale value) => _sharedPreferences.setString(localeKey, value.toString()); double get cameraEvCalibration => _sharedPreferences.getDouble(cameraEvCalibrationKey) ?? 0.0; - set cameraEvCalibration(double value) => - _sharedPreferences.setDouble(cameraEvCalibrationKey, value); + set cameraEvCalibration(double value) => _sharedPreferences.setDouble(cameraEvCalibrationKey, value); - double get lightSensorEvCalibration => - _sharedPreferences.getDouble(lightSensorEvCalibrationKey) ?? 0.0; - set lightSensorEvCalibration(double value) => - _sharedPreferences.setDouble(lightSensorEvCalibrationKey, value); + double get lightSensorEvCalibration => _sharedPreferences.getDouble(lightSensorEvCalibrationKey) ?? 0.0; + set lightSensorEvCalibration(double value) => _sharedPreferences.setDouble(lightSensorEvCalibrationKey, value); ThemeType get themeType => ThemeType.values[_sharedPreferences.getInt(themeTypeKey) ?? 0]; set themeType(ThemeType value) => _sharedPreferences.setInt(themeTypeKey, value.index); diff --git a/lib/data/volume_events_service.dart b/lib/data/volume_events_service.dart index d57936a..360de75 100644 --- a/lib/data/volume_events_service.dart +++ b/lib/data/volume_events_service.dart @@ -3,7 +3,7 @@ import 'package:flutter/services.dart'; import 'package:platform/platform.dart'; class VolumeEventsService { - final LocalPlatform localPlatform; + final LocalPlatform _localPlatform; @visibleForTesting static const volumeHandlingChannel = MethodChannel("com.vodemn.lightmeter/volumeHandling"); @@ -11,12 +11,12 @@ class VolumeEventsService { @visibleForTesting static const volumeEventsChannel = EventChannel("com.vodemn.lightmeter/volumeEvents"); - const VolumeEventsService(this.localPlatform); + const VolumeEventsService(this._localPlatform); /// If set to `false` we allow system to handle key events. /// Returns current status of volume handling. Future setVolumeHandling(bool enableHandling) async { - if (!localPlatform.isAndroid) { + if (!_localPlatform.isAndroid) { return false; } return volumeHandlingChannel @@ -29,7 +29,7 @@ class VolumeEventsService { /// KEYCODE_VOLUME_DOWN = 25; /// pressed Stream volumeButtonsEventStream() { - if (!localPlatform.isAndroid) { + if (!_localPlatform.isAndroid) { return const Stream.empty(); } return volumeEventsChannel diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 57e91d2..02dbb3d 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -34,16 +34,21 @@ "calibrationMessageCameraOnly": "The accuracy of the readings measured by this application depends entirely on the rear camera of the device. Therefore, consider testing this application and setting up an EV calibration value that will give you the desired measurement results.", "camera": "Camera", "lightSensor": "Light sensor", + "showEv100": "Show EV\u2081\u2080\u2080", "meteringScreenLayout": "Metering screen layout", "meteringScreenLayoutHint": "Hide elements on the metering screen that you don't need so that they don't waste exposure pairs list space.", "meteringScreenLayoutHintEquipmentProfiles": "Equipment profile picker", "meteringScreenFeatureExtremeExposurePairs": "Fastest & shortest exposure pairs", "meteringScreenFeatureFilmPicker": "Film picker", - "meteringScreenFeatureHistogram": "Histogram", + "cameraFeatures": "Camera features", + "cameraFeatureSpotMetering": "Spot metering", + "cameraFeatureSpotMeteringHint": "Long press the camera view to remove metering spot", + "cameraFeatureHistogram": "Histogram", + "cameraFeatureHistogramHint": "Enabling histogram can encrease battery drain", "film": "Film", "filmPush": "Film (push)", "filmPull": "Film (pull)", - "filmReciprocityHint": "Applies correction for shutter speeds grater than 1 second", + "filmReciprocityHint": "Applies correction for shutter speeds greater than 1 second", "equipmentProfileName": "Equipment profile name", "equipmentProfileNameHint": "Praktica MTL5B", "equipmentProfileAllValues": "All", @@ -92,13 +97,9 @@ } } }, - "lightmeterPro": "Lightmeter Pro", - "buyLightmeterPro": "Buy Lightmeter Pro", - "lightmeterProDescription": "Unlocks extra features, such as equipment profiles containing filters for aperture, shutter speed, and more; and a list of films with compensation for what's known as reciprocity failure.\n\nThe source code of Lightmeter is available on GitHub. You are welcome to compile it yourself. However, if you want to support the development and receive new features and updates, consider purchasing Lightmeter Pro.", - "buy": "Buy", "proFeatures": "Pro features", "unlockProFeatures": "Unlock Pro features", - "unlockProFeaturesDescription": "Unlock professional features, such as equipment profiles containing filters for aperture, shutter speed, and more; and a list of films with compensation for what's known as reciprocity failure.\n\nBy unlocking Pro features you support the development and make it possible to add new features to the app.", + "unlockProFeaturesDescription": "Unlock professional features:\n \u2022 Equipment profiles containing filters for aperture, shutter speed, and more\n \u2022 List of films with compensation for what's known as reciprocity failure\n \u2022 Spot metering\n \u2022 Histogram\n\nBy unlocking Pro features you support the development and make it possible to add new features to the app.", "unlock": "Unlock", "tooltipAdd": "Add", "tooltipClose": "Close", @@ -112,4 +113,4 @@ "tooltipUseLightSensor": "Use lightsensor", "tooltipUseCamera": "Use camera", "tooltipOpenSettings": "Open settings" -} \ No newline at end of file +} diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index b1cd7fb..590976d 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -34,12 +34,17 @@ "calibrationMessageCameraOnly": "La précision des lectures mesurées par cette application dépend entièrement de la caméra arrière de l'appareil. Par conséquent, envisagez de tester cette application et de configurer une valeur d'étalonnage EV qui vous donnera les résultats de mesure souhaités.", "camera": "Caméra", "lightSensor": "Capteur de lumière", + "showEv100": "Montrer EV\u2081\u2080\u2080", "meteringScreenLayout": "Disposition de l'écran de mesure", "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.", "meteringScreenLayoutHintEquipmentProfiles": "Sélecteur de profil de l'équipement", "meteringScreenFeatureExtremeExposurePairs": "Paires d'exposition les plus rapides et les plus courtes", "meteringScreenFeatureFilmPicker": "Sélecteur de film", - "meteringScreenFeatureHistogram": "Histogramme", + "cameraFeatures": "Fonctionnalités de la caméra", + "cameraFeatureSpotMetering": "Mesure spot", + "cameraFeatureSpotMeteringHint": "Appuyez longuement sur la vue de l'appareil photo pour supprimer le spot de mesure", + "cameraFeatureHistogram": "Histogramme", + "cameraFeatureHistogramHint": "L'activation de l'histogramme peut augmenter la consommation de la batterie", "film": "Pellicule", "filmPush": "Pellicule (push)", "filmPull": "Pellicule (pull)", @@ -92,13 +97,9 @@ } } }, - "buyLightmeterPro": "Acheter Lightmeter Pro", - "lightmeterPro": "Lightmeter Pro", - "lightmeterProDescription": "Déverrouille des fonctionnalités supplémentaires, telles que des profils d'équipement contenant des filtres pour l'ouverture, la vitesse d'obturation et plus encore, ainsi qu'une liste de films avec une compensation pour ce que l'on appelle l'échec de réciprocité.\n\nLe code source du Lightmeter est disponible sur GitHub. Vous pouvez le compiler vous-même. Cependant, si vous souhaitez soutenir le développement et recevoir de nouvelles fonctionnalités et mises à jour, envisagez d'acheter Lightmeter Pro.", - "buy": "Acheter", "proFeatures": "Fonctionnalités professionnelles", "unlockProFeatures": "Déverrouiller les fonctionnalités professionnelles", - "unlockProFeaturesDescription": "Déverrouillez des fonctions professionnelles, telles que des profils d'équipement contenant des filtres pour l'ouverture, la vitesse d'obturation et plus encore, ainsi qu'une liste de films avec compensation pour ce que l'on appelle l'échec de réciprocité.\n\nEn débloquant les fonctionnalités Pro, vous soutenez le développement et permettez d'ajouter de nouvelles fonctionnalités à l'application.", + "unlockProFeaturesDescription": "Déverrouillez des fonctions professionnelles:\n \u2022 Profils d'équipement contenant des filtres pour l'ouverture, la vitesse d'obturation et plus encore, ainsi qu'une liste de films avec compensation pour ce que l'on appelle l'échec de réciprocité\n \u2022 Mesure spot\n \u2022 Histogramme\n\nEn débloquant les fonctionnalités Pro, vous soutenez le développement et permettez d'ajouter de nouvelles fonctionnalités à l'application.", "unlock": "Déverrouiller", "tooltipAdd": "Ajouter", "tooltipClose": "Fermer", diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index a18cc9c..e7f0743 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -34,12 +34,17 @@ "calibrationMessageCameraOnly": "Точность измерений данного приложения полностью зависит от точности камеры вашего устройства. Поэтому рекомендуется самостоятельно подобрать калибровочное значение, которое даст желаемый результат измерений.", "camera": "Камера", "lightSensor": "Датчик освещённости", + "showEv100": "Показывать EV\u2081\u2080\u2080", "meteringScreenLayout": "Элементы главного экрана", "meteringScreenLayoutHint": "Здесь вы можете скрыть некоторые ненужные или неиспользуемые элементы с главного экрана.", "meteringScreenLayoutHintEquipmentProfiles": "Выбор профиля оборудования", "meteringScreenFeatureExtremeExposurePairs": "Длинная и короткая выдержки", "meteringScreenFeatureFilmPicker": "Выбор пленки", - "meteringScreenFeatureHistogram": "Гистограмма", + "cameraFeatures": "Возможности камеры", + "cameraFeatureSpotMetering": "Точечный замер", + "cameraFeatureSpotMeteringHint": "Используйте долгое нажатие, чтобы удалить точку замера", + "cameraFeatureHistogram": "Гистограмма", + "cameraFeatureHistogramHint": "Использование гистограммы может увеличить расход аккумулятора", "film": "Пленка", "filmPush": "Пленка (push)", "filmPull": "Пленка (pull)", @@ -92,13 +97,9 @@ } } }, - "buyLightmeterPro": "Купить Lightmeter Pro", - "lightmeterPro": "Lightmeter Pro", - "lightmeterProDescription": "Даёт доступ к таким функциям как профили оборудования, содержащие фильтры для диафрагмы, выдержки и других значений, а также набору пленок с компенсацией эффекта Шварцшильда.\n\nИсходный код Lightmeter доступен на GitHub. Вы можете собрать его самостоятельно. Однако если вы хотите поддержать разработку и получать новые функции и обновления, то приобретите Lightmeter Pro.", - "buy": "Купить", "proFeatures": "Профессиональные настройки", "unlockProFeatures": "Разблокировать профессиональные настройки", - "unlockProFeaturesDescription": "Вы можете разблокировать профессиональные настройки, такие как профили оборудования, содержащие фильтры для диафрагмы, выдержки и других значений, а также набору пленок с компенсацией эффекта Шварцшильда.\n\nПолучая доступ к профессиональным настройкам, вы поддерживаете разработку и делаете возможным появление новых функций в приложении.", + "unlockProFeaturesDescription": "Вы можете разблокировать профессиональные настройки:\n \u2022 Профили оборудования, содержащие фильтры для диафрагмы, выдержки и других значений\n \u2022 Список пленок с компенсацией эффекта Шварцшильда\n \u2022 Точечный замер\n \u2022 Гистограмма\n\nПолучая доступ к профессиональным настройкам, вы поддерживаете разработку и делаете возможным появление новых функций в приложении.", "unlock": "Разблокировать", "tooltipAdd": "Добавить", "tooltipClose": "Закрыть", diff --git a/lib/l10n/intl_zh.arb b/lib/l10n/intl_zh.arb index 5788ea9..71fe757 100644 --- a/lib/l10n/intl_zh.arb +++ b/lib/l10n/intl_zh.arb @@ -34,12 +34,17 @@ "calibrationMessageCameraOnly": "此应用程序测量读数的准确s性完全取决于设备的后置摄像头。因此,请考虑测试此应用并手动设置 EV 校准,以获得准确的测量结果。", "camera": "摄像头", "lightSensor": "光传感器", + "showEv100": "显示 EV\u2081\u2080\u2080", "meteringScreenLayout": "布局", "meteringScreenLayoutHint": "隐藏不需要的元素,以免浪费曝光列表空间", "meteringScreenLayoutHintEquipmentProfiles": "设备配置选择", "meteringScreenFeatureExtremeExposurePairs": "最快 & 最慢曝光组合", "meteringScreenFeatureFilmPicker": "胶片选择", - "meteringScreenFeatureHistogram": "直方图", + "cameraFeatures": "相机功能", + "cameraFeatureSpotMetering": "点测光", + "cameraFeatureSpotMeteringHint": "长按相机视图可移除测光点", + "cameraFeatureHistogram": "直方图", + "cameraFeatureHistogramHint": "启用直方图会增加电池消耗", "film": "胶片", "filmPush": "胶片 (push)", "filmPull": "胶片 (pull)", @@ -92,13 +97,9 @@ } } }, - "buyLightmeterPro": "购买 Lightmeter Pro", - "lightmeterPro": "Lightmeter Pro", - "lightmeterProDescription": "购买以解锁额外功能。例如包含光圈、快门速度等参数的配置文件;以及一个胶卷预设列表来提供倒易率失效时的曝光补偿。\n\n您可以在 GitHub 上获取 Lightmeter 的源代码,欢迎自行编译。不过,如果您想支持开发并获得新功能和更新,请考虑购买 Lightmeter Pro。", - "buy": "购买", "proFeatures": "专业功能", "unlockProFeatures": "解锁专业功能", - "unlockProFeaturesDescription": "解锁专业功能。例如包含光圈、快门速度等参数的配置文件;以及一个胶卷预设列表来提供倒易率失效时的曝光补偿。\n\n通过解锁专业版功能,您可以支持开发工作,帮助为应用程序添加新功能。", + "unlockProFeaturesDescription": "\n \u2022 配置文件,其中包含光圈、快门速度等参数\n \u2022 胶卷列表,对胶片倒易率失效进行曝光补偿\n \u2022 点测光\n \u2022 直方图\n\n通过解锁专业版功能,您可以支持开发工作,帮助为应用程序添加新功能。", "unlock": "解锁", "tooltipAdd": "添加", "tooltipClose": "关闭", diff --git a/lib/platform_config.dart b/lib/platform_config.dart index def5a80..d06223e 100644 --- a/lib/platform_config.dart +++ b/lib/platform_config.dart @@ -7,4 +7,6 @@ class PlatformConfig { } static String get cameraStubImage => const String.fromEnvironment('cameraStubImage'); + + static bool get isTest => cameraStubImage.isNotEmpty; } diff --git a/lib/providers/equipment_profile_provider.dart b/lib/providers/equipment_profile_provider.dart index a5e0999..74397a5 100644 --- a/lib/providers/equipment_profile_provider.dart +++ b/lib/providers/equipment_profile_provider.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:lightmeter/utils/context_utils.dart'; import 'package:lightmeter/utils/selectable_provider.dart'; import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; @@ -52,11 +53,9 @@ class EquipmentProfileProviderState extends State { return EquipmentProfiles( values: [ _defaultProfile, - if (IAPProducts.isPurchased(context, IAPProductType.paidFeatures)) ..._customProfiles, + if (context.isPro) ..._customProfiles, ], - selected: IAPProducts.isPurchased(context, IAPProductType.paidFeatures) - ? _selectedProfile - : _defaultProfile, + selected: context.isPro ? _selectedProfile : _defaultProfile, child: widget.child, ); } @@ -85,7 +84,7 @@ class EquipmentProfileProviderState extends State { _refreshSavedProfiles(); } - void updateProdile(EquipmentProfile data) { + void updateProfile(EquipmentProfile data) { final indexToUpdate = _customProfiles.indexWhere((element) => element.id == data.id); if (indexToUpdate >= 0) { _customProfiles[indexToUpdate] = data; @@ -118,13 +117,14 @@ class EquipmentProfiles extends SelectableInheritedModel { /// [_defaultProfile] + profiles created by the user static List of(BuildContext context) { - return InheritedModel.inheritFrom(context, aspect: SelectableAspect.list)! - .values; + return InheritedModel.inheritFrom(context, aspect: SelectableAspect.list)!.values; } static EquipmentProfile selectedOf(BuildContext context) { - return InheritedModel.inheritFrom(context, - aspect: SelectableAspect.selected,)! + return InheritedModel.inheritFrom( + context, + aspect: SelectableAspect.selected, + )! .selected; } } diff --git a/lib/providers/films_provider.dart b/lib/providers/films_provider.dart index aff6d01..3e9d02d 100644 --- a/lib/providers/films_provider.dart +++ b/lib/providers/films_provider.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:lightmeter/utils/context_utils.dart'; import 'package:lightmeter/utils/selectable_provider.dart'; import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; @@ -44,11 +45,9 @@ class FilmsProviderState extends State { ], filmsInUse: [ const Film.other(), - if (IAPProducts.isPurchased(context, IAPProductType.paidFeatures)) ..._filmsInUse, + if (context.isPro) ..._filmsInUse, ], - selected: IAPProducts.isPurchased(context, IAPProductType.paidFeatures) - ? _selected - : const Film.other(), + selected: context.isPro ? _selected : const Film.other(), child: widget.child, ); } diff --git a/lib/providers/remote_config_provider.dart b/lib/providers/remote_config_provider.dart index 9736e1d..4557ed6 100644 --- a/lib/providers/remote_config_provider.dart +++ b/lib/providers/remote_config_provider.dart @@ -1,11 +1,12 @@ import 'dart:async'; +import 'dart:developer'; import 'package:flutter/material.dart'; import 'package:lightmeter/data/models/feature.dart'; import 'package:lightmeter/data/remote_config_service.dart'; class RemoteConfigProvider extends StatefulWidget { - final RemoteConfigService remoteConfigService; + final IRemoteConfigService remoteConfigService; final Widget child; const RemoteConfigProvider({ @@ -25,7 +26,11 @@ class RemoteConfigProviderState extends State { @override void initState() { super.initState(); - _updatesSubscription = widget.remoteConfigService.onConfigUpdated().listen(_updateFeatures); + widget.remoteConfigService.fetchConfig(); + _updatesSubscription = widget.remoteConfigService.onConfigUpdated().listen( + _updateFeatures, + onError: (e) => log(e.toString()), + ); } @override diff --git a/lib/providers/user_preferences_provider.dart b/lib/providers/user_preferences_provider.dart index 3a4111c..e2eecd7 100644 --- a/lib/providers/user_preferences_provider.dart +++ b/lib/providers/user_preferences_provider.dart @@ -1,6 +1,7 @@ import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; +import 'package:lightmeter/data/models/camera_feature.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'; @@ -9,6 +10,7 @@ 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/res/theme.dart'; +import 'package:lightmeter/utils/map_model.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; class UserPreferencesProvider extends StatefulWidget { @@ -51,6 +53,18 @@ class UserPreferencesProvider extends StatefulWidget { return _inheritFromEnumsModel(context, _Aspect.stopType).stopType; } + static bool showEv100Of(BuildContext context) { + return _inheritFromEnumsModel(context, _Aspect.showEv100).showEv100; + } + + static CameraFeaturesConfig cameraConfigOf(BuildContext context) { + return context.findAncestorWidgetOfExactType<_CameraFeaturesModel>()!.data; + } + + static bool cameraFeatureOf(BuildContext context, CameraFeature feature) { + return InheritedModel.inheritFrom<_CameraFeaturesModel>(context, aspect: feature)!.data[feature]!; + } + static ThemeData themeOf(BuildContext context) { return _inheritFromEnumsModel(context, _Aspect.theme).theme; } @@ -73,7 +87,9 @@ class UserPreferencesProvider extends StatefulWidget { class _UserPreferencesProviderState extends State with WidgetsBindingObserver { late EvSourceType _evSourceType; late StopType _stopType = widget.userPreferencesService.stopType; + late bool _showEv100 = widget.userPreferencesService.showEv100; late MeteringScreenLayoutConfig _meteringScreenLayout = widget.userPreferencesService.meteringScreenLayout; + late CameraFeaturesConfig _cameraFeatures = widget.userPreferencesService.cameraFeatures; late SupportedLocale _locale = widget.userPreferencesService.locale; late ThemeType _themeType = widget.userPreferencesService.themeType; late Color _primaryColor = widget.userPreferencesService.primaryColor; @@ -83,7 +99,8 @@ class _UserPreferencesProviderState extends State with void initState() { super.initState(); _evSourceType = widget.userPreferencesService.evSourceType; - _evSourceType = _evSourceType == EvSourceType.sensor && !widget.hasLightSensor ? EvSourceType.camera : _evSourceType; + _evSourceType = + _evSourceType == EvSourceType.sensor && !widget.hasLightSensor ? EvSourceType.camera : _evSourceType; WidgetsBinding.instance.addObserver(this); } @@ -123,11 +140,15 @@ class _UserPreferencesProviderState extends State with evSourceType: _evSourceType, locale: _locale, primaryColor: dynamicPrimaryColor ?? _primaryColor, + showEv100: _showEv100, stopType: _stopType, themeType: _themeType, child: _MeteringScreenLayoutModel( data: _meteringScreenLayout, - child: widget.child, + child: _CameraFeaturesModel( + data: _cameraFeatures, + child: widget.child, + ), ), ); }, @@ -172,6 +193,13 @@ class _UserPreferencesProviderState extends State with widget.userPreferencesService.meteringScreenLayout = _meteringScreenLayout; } + void setCameraFeature(CameraFeaturesConfig config) { + setState(() { + _cameraFeatures = config; + }); + widget.userPreferencesService.cameraFeatures = _cameraFeatures; + } + void setPrimaryColor(Color primaryColor) { setState(() { _primaryColor = primaryColor; @@ -179,6 +207,13 @@ class _UserPreferencesProviderState extends State with widget.userPreferencesService.primaryColor = primaryColor; } + void toggleShowEv100() { + setState(() { + _showEv100 = !_showEv100; + }); + widget.userPreferencesService.showEv100 = _showEv100; + } + void setStopType(StopType stopType) { setState(() { _stopType = stopType; @@ -209,6 +244,7 @@ enum _Aspect { dynamicColorState, evSourceType, locale, + showEv100, stopType, theme, themeType, @@ -218,6 +254,7 @@ class _UserPreferencesModel extends InheritedModel<_Aspect> { final DynamicColorState dynamicColorState; final EvSourceType evSourceType; final SupportedLocale locale; + final bool showEv100; final StopType stopType; final ThemeType themeType; @@ -230,6 +267,7 @@ class _UserPreferencesModel extends InheritedModel<_Aspect> { required this.evSourceType, required this.locale, required Color primaryColor, + required this.showEv100, required this.stopType, required this.themeType, required super.child, @@ -245,6 +283,7 @@ class _UserPreferencesModel extends InheritedModel<_Aspect> { evSourceType != oldWidget.evSourceType || locale != oldWidget.locale || _primaryColor != oldWidget._primaryColor || + showEv100 != oldWidget.showEv100 || stopType != oldWidget.stopType || themeType != oldWidget.themeType; } @@ -257,6 +296,7 @@ class _UserPreferencesModel extends InheritedModel<_Aspect> { 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.showEv100) && showEv100 != oldWidget.showEv100) || (dependencies.contains(_Aspect.stopType) && stopType != oldWidget.stopType) || (dependencies.contains(_Aspect.theme) && (_brightness != oldWidget._brightness || _primaryColor != oldWidget._primaryColor)) || @@ -264,27 +304,16 @@ class _UserPreferencesModel extends InheritedModel<_Aspect> { } } -class _MeteringScreenLayoutModel extends InheritedModel { - final Map data; - +class _MeteringScreenLayoutModel extends MapModel { const _MeteringScreenLayoutModel({ - required this.data, + required super.data, + required super.child, + }); +} + +class _CameraFeaturesModel extends MapModel { + const _CameraFeaturesModel({ + required super.data, required super.child, }); - - @override - bool updateShouldNotify(_MeteringScreenLayoutModel oldWidget) => oldWidget.data != data; - - @override - bool updateShouldNotifyDependent( - _MeteringScreenLayoutModel oldWidget, - Set dependencies, - ) { - for (final dependecy in dependencies) { - if (oldWidget.data[dependecy] != data[dependecy]) { - return true; - } - } - return false; - } } diff --git a/lib/res/theme.dart b/lib/res/theme.dart index a6320c1..52e0e92 100644 --- a/lib/res/theme.dart +++ b/lib/res/theme.dart @@ -23,7 +23,7 @@ const primaryColorsList = [ ThemeData themeFrom(Color primaryColor, Brightness brightness) { final scheme = _colorSchemeFromColor(primaryColor, brightness); - return ThemeData( + final theme = ThemeData( useMaterial3: true, brightness: scheme.brightness, primaryColor: primaryColor, @@ -60,12 +60,18 @@ ThemeData themeFrom(Color primaryColor, Brightness brightness) { ), scaffoldBackgroundColor: scheme.surface, ); + return theme.copyWith( + listTileTheme: ListTileThemeData( + style: ListTileStyle.list, + iconColor: scheme.onSurface, + textColor: scheme.onSurface, + subtitleTextStyle: theme.textTheme.bodyMedium!.copyWith(color: scheme.onSurfaceVariant), + ), + ); } ColorScheme _colorSchemeFromColor(Color primaryColor, Brightness brightness) { - final scheme = brightness == Brightness.light - ? Scheme.light(primaryColor.value) - : Scheme.dark(primaryColor.value); + final scheme = brightness == Brightness.light ? Scheme.light(primaryColor.value) : Scheme.dark(primaryColor.value); return ColorScheme( brightness: brightness, diff --git a/lib/screens/metering/bloc_metering.dart b/lib/screens/metering/bloc_metering.dart index cbb0356..753a7e7 100644 --- a/lib/screens/metering/bloc_metering.dart +++ b/lib/screens/metering/bloc_metering.dart @@ -10,9 +10,9 @@ import 'package:lightmeter/screens/metering/communication/event_communication_me as communication_events; import 'package:lightmeter/screens/metering/communication/state_communication_metering.dart' as communication_states; -import 'package:lightmeter/screens/metering/components/shared/volume_keys_notifier/notifier_volume_keys.dart'; import 'package:lightmeter/screens/metering/event_metering.dart'; import 'package:lightmeter/screens/metering/state_metering.dart'; +import 'package:lightmeter/screens/metering/utils/notifier_volume_keys.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; class MeteringBloc extends Bloc { diff --git a/lib/screens/metering/components/bottom_controls/components/measure_button/widget_button_measure.dart b/lib/screens/metering/components/bottom_controls/components/measure_button/widget_button_measure.dart index 99bd2ae..e918557 100644 --- a/lib/screens/metering/components/bottom_controls/components/measure_button/widget_button_measure.dart +++ b/lib/screens/metering/components/bottom_controls/components/measure_button/widget_button_measure.dart @@ -1,15 +1,21 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/generated/l10n.dart'; +import 'package:lightmeter/providers/user_preferences_provider.dart'; import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/screens/shared/filled_circle/widget_circle_filled.dart'; +import 'package:lightmeter/utils/context_utils.dart'; + +const String _subscript100 = '\u2081\u2080\u2080'; class MeteringMeasureButton extends StatefulWidget { final double? ev; + final double? ev100; final bool isMetering; final VoidCallback onTap; const MeteringMeasureButton({ required this.ev, + required this.ev100, required this.isMetering, required this.onTap, super.key, @@ -61,7 +67,7 @@ class _MeteringMeasureButtonState extends State { color: Theme.of(context).colorScheme.onSurface, size: Dimens.grid72 - Dimens.grid8, child: Center( - child: widget.ev != null ? _EvValueText(ev: widget.ev!) : null, + child: widget.ev != null ? _EvValueText(ev: widget.ev!, ev100: widget.ev100!) : null, ), ), ), @@ -83,16 +89,32 @@ class _MeteringMeasureButtonState extends State { class _EvValueText extends StatelessWidget { final double ev; + final double ev100; - const _EvValueText({required this.ev}); + const _EvValueText({ + required this.ev, + required this.ev100, + }); @override Widget build(BuildContext context) { final theme = Theme.of(context); return Text( - '${ev.toStringAsFixed(1)}\n${S.of(context).ev}', + _text(context), style: theme.textTheme.bodyMedium?.copyWith(color: theme.colorScheme.surface), textAlign: TextAlign.center, ); } + + String _text(BuildContext context) { + final bool showEv100 = context.isPro && UserPreferencesProvider.showEv100Of(context); + final StringBuffer buffer = StringBuffer() + ..writeAll([ + (showEv100 ? ev100 : ev).toStringAsFixed(1), + '\n', + S.of(context).ev, + if (showEv100) _subscript100, + ]); + return buffer.toString(); + } } diff --git a/lib/screens/metering/components/bottom_controls/provider_bottom_controls.dart b/lib/screens/metering/components/bottom_controls/provider_bottom_controls.dart index dd4a9be..f9d6d47 100644 --- a/lib/screens/metering/components/bottom_controls/provider_bottom_controls.dart +++ b/lib/screens/metering/components/bottom_controls/provider_bottom_controls.dart @@ -5,6 +5,7 @@ import 'package:lightmeter/screens/metering/components/bottom_controls/widget_bo class MeteringBottomControlsProvider extends StatelessWidget { final double? ev; + final double? ev100; final bool isMetering; final VoidCallback? onSwitchEvSourceType; final VoidCallback onMeasure; @@ -12,6 +13,7 @@ class MeteringBottomControlsProvider extends StatelessWidget { const MeteringBottomControlsProvider({ required this.ev, + required this.ev100, required this.isMetering, required this.onSwitchEvSourceType, required this.onMeasure, @@ -35,6 +37,7 @@ class MeteringBottomControlsProvider extends StatelessWidget { ), child: MeteringBottomControls( ev: ev, + ev100: ev100, isMetering: isMetering, onSwitchEvSourceType: onSwitchEvSourceType, onMeasure: onMeasure, 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 31ecac4..0eef568 100644 --- a/lib/screens/metering/components/bottom_controls/widget_bottom_controls.dart +++ b/lib/screens/metering/components/bottom_controls/widget_bottom_controls.dart @@ -7,6 +7,7 @@ import 'package:lightmeter/screens/metering/components/bottom_controls/component class MeteringBottomControls extends StatelessWidget { final double? ev; + final double? ev100; final bool isMetering; final VoidCallback? onSwitchEvSourceType; final VoidCallback onMeasure; @@ -14,6 +15,7 @@ class MeteringBottomControls extends StatelessWidget { const MeteringBottomControls({ required this.ev, + required this.ev100, required this.isMetering, required this.onSwitchEvSourceType, required this.onMeasure, @@ -58,6 +60,7 @@ class MeteringBottomControls extends StatelessWidget { const Spacer(), MeteringMeasureButton( ev: ev, + ev100: ev100, isMetering: isMetering, onTap: onMeasure, ), 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 1d7d4b5..3b5596b 100644 --- a/lib/screens/metering/components/camera_container/bloc_container_camera.dart +++ b/lib/screens/metering/components/camera_container/bloc_container_camera.dart @@ -4,22 +4,21 @@ import 'dart:io'; import 'dart:math' as math; import 'package:camera/camera.dart'; -import 'package:exif/exif.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:lightmeter/interactors/metering_interactor.dart'; import 'package:lightmeter/platform_config.dart'; import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart'; -import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart' - as communication_event; -import 'package:lightmeter/screens/metering/communication/state_communication_metering.dart' - as communication_states; +import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart' as communication_event; +import 'package:lightmeter/screens/metering/communication/state_communication_metering.dart' as communication_states; 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'; import 'package:lightmeter/screens/metering/components/shared/ev_source_base/bloc_base_ev_source.dart'; -import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; +import 'package:lightmeter/utils/ev_from_bytes.dart'; + +part 'mock_bloc_container_camera.dart'; class CameraContainerBloc extends EvSourceBlocBase { final MeteringInteractor _meteringInteractor; @@ -57,6 +56,7 @@ class CameraContainerBloc extends EvSourceBlocBase(_onZoomChanged); on(_onExposureOffsetChanged); on(_onExposureOffsetResetEvent); + on(_onExposureSpotChangedEvent); } @override @@ -166,9 +166,7 @@ class CameraContainerBloc extends EvSourceBlocBase _onZoomChanged(ZoomChangedEvent event, Emitter emit) async { - if (_cameraController != null && - event.value >= _zoomRange!.start && - event.value <= _zoomRange!.end) { + if (_cameraController != null && event.value >= _zoomRange!.start && event.value <= _zoomRange!.end) { _cameraController!.setZoomLevel(event.value); _currentZoom = event.value; _emitActiveState(emit); @@ -188,6 +186,13 @@ class CameraContainerBloc extends EvSourceBlocBase _onExposureSpotChangedEvent(ExposureSpotChangedEvent event, Emitter emit) async { + if (_cameraController != null) { + _cameraController!.setExposurePoint(event.offset); + _cameraController!.setFocusPoint(event.offset); + } + } + void _emitActiveState(Emitter emit) { emit( CameraActiveState( @@ -209,33 +214,15 @@ 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 bytes = await file.readAsBytes(); + Directory(file.path).deleteSync(recursive: true); - late final Uint8List bytes; - if (PlatformConfig.cameraStubImage.isNotEmpty) { - bytes = (await rootBundle.load(PlatformConfig.cameraStubImage)).buffer.asUint8List(); - } else { - 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); - bytes = await file.readAsBytes(); - Directory(file.path).deleteSync(recursive: true); - } - - final tags = await readExifFromBytes(bytes); - final iso = double.tryParse("${tags["EXIF ISOSpeedRatings"]}"); - final apertureValueRatio = (tags["EXIF FNumber"]?.values as IfdRatios?)?.ratios.first; - final speedValueRatio = (tags["EXIF ExposureTime"]?.values as IfdRatios?)?.ratios.first; - if (iso == null || apertureValueRatio == null || speedValueRatio == null) { - log('Error parsing EXIF: ${tags.keys}'); - return null; - } - - final aperture = apertureValueRatio.numerator / apertureValueRatio.denominator; - final speed = speedValueRatio.numerator / speedValueRatio.denominator; - - return log2(math.pow(aperture, 2)) - log2(speed) - log2(iso / 100); + return await evFromImage(bytes); } catch (e) { log(e.toString()); return null; diff --git a/lib/screens/metering/components/camera_container/components/camera_controls/components/exposure_offset_slider/widget_slider_exposure_offset.dart b/lib/screens/metering/components/camera_container/components/camera_controls/components/exposure_offset_slider/widget_slider_exposure_offset.dart index 484ee4b..40ec1f0 100644 --- a/lib/screens/metering/components/camera_container/components/camera_controls/components/exposure_offset_slider/widget_slider_exposure_offset.dart +++ b/lib/screens/metering/components/camera_container/components/camera_controls/components/exposure_offset_slider/widget_slider_exposure_offset.dart @@ -72,7 +72,7 @@ class _Ruler extends StatelessWidget { children: [ if (showValue) Text( - (index + min).toStringSigned(), + (index + min).toStringSignedAsFixed(0), style: Theme.of(context).textTheme.bodyLarge, ), const SizedBox(width: Dimens.grid8), diff --git a/lib/screens/metering/components/camera_container/components/camera_preview/components/camera_spot_detector/widget_camera_spot_detector.dart b/lib/screens/metering/components/camera_container/components/camera_preview/components/camera_spot_detector/widget_camera_spot_detector.dart new file mode 100644 index 0000000..0ec7f17 --- /dev/null +++ b/lib/screens/metering/components/camera_container/components/camera_preview/components/camera_spot_detector/widget_camera_spot_detector.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:lightmeter/res/dimens.dart'; + +class CameraSpotDetector extends StatefulWidget { + final ValueChanged onSpotTap; + + const CameraSpotDetector({ + required this.onSpotTap, + super.key, + }); + + @override + State createState() => _CameraSpotDetectorState(); +} + +class _CameraSpotDetectorState extends State { + Offset? spot; + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (_, constraints) => GestureDetector( + behavior: HitTestBehavior.opaque, + onTapDown: (TapDownDetails details) => onViewFinderTap(details, constraints), + onLongPress: () => onViewFinderTap(null, constraints), + child: Stack( + children: [ + if (spot != null) + AnimatedPositioned( + duration: Dimens.durationS, + left: spot!.dx - Dimens.grid16 / 2, + top: spot!.dy - Dimens.grid16 / 2, + height: Dimens.grid16, + width: Dimens.grid16, + child: const _Spot(), + ), + ], + ), + ), + ); + } + + void onViewFinderTap(TapDownDetails? details, BoxConstraints constraints) { + setState(() { + spot = details?.localPosition; + }); + + widget.onSpotTap( + details != null + ? Offset( + details.localPosition.dx / constraints.maxWidth, + details.localPosition.dy / constraints.maxHeight, + ) + : null, + ); + } +} + +class _Spot extends StatelessWidget { + const _Spot(); + + @override + Widget build(BuildContext context) { + return const DecoratedBox( + decoration: BoxDecoration( + color: Colors.white70, + shape: BoxShape.circle, + ), + ); + } +} diff --git a/lib/screens/metering/components/camera_container/components/camera_preview/components/camera_view/widget_camera_view.dart b/lib/screens/metering/components/camera_container/components/camera_preview/components/camera_view/widget_camera_view.dart index 7c06062..c054f3d 100644 --- a/lib/screens/metering/components/camera_container/components/camera_preview/components/camera_view/widget_camera_view.dart +++ b/lib/screens/metering/components/camera_container/components/camera_preview/components/camera_view/widget_camera_view.dart @@ -12,14 +12,17 @@ class CameraView extends StatelessWidget { final value = controller.value; return ValueListenableBuilder( valueListenable: controller, - builder: (_, __, ___) => AspectRatio( + builder: (_, __, Widget? child) => AspectRatio( aspectRatio: _isLandscape(value) ? value.aspectRatio : (1 / value.aspectRatio), - child: value.isInitialized - ? RotatedBox( - quarterTurns: _getQuarterTurns(value), - child: controller.buildPreview(), - ) - : const SizedBox.shrink(), + child: Stack( + children: [ + RotatedBox( + quarterTurns: _getQuarterTurns(value), + child: controller.buildPreview(), + ), + child ?? const SizedBox(), + ], + ), ), ); } @@ -42,8 +45,6 @@ class CameraView extends StatelessWidget { DeviceOrientation _getApplicableOrientation(CameraValue value) { return value.isRecordingVideo ? value.recordingOrientation! - : (value.previewPauseOrientation ?? - value.lockedCaptureOrientation ?? - value.deviceOrientation); + : (value.previewPauseOrientation ?? value.lockedCaptureOrientation ?? value.deviceOrientation); } } diff --git a/lib/screens/metering/components/camera_container/components/camera_preview/widget_camera_preview.dart b/lib/screens/metering/components/camera_container/components/camera_preview/widget_camera_preview.dart index 3e9538f..464228f 100644 --- a/lib/screens/metering/components/camera_container/components/camera_preview/widget_camera_preview.dart +++ b/lib/screens/metering/components/camera_container/components/camera_preview/widget_camera_preview.dart @@ -1,19 +1,27 @@ import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; -import 'package:lightmeter/data/models/metering_screen_layout_config.dart'; +import 'package:lightmeter/data/models/camera_feature.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_spot_detector/widget_camera_spot_detector.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'; +import 'package:lightmeter/utils/context_utils.dart'; class CameraPreview extends StatefulWidget { final CameraController? controller; final CameraErrorType? error; + final ValueChanged onSpotTap; - const CameraPreview({this.controller, this.error, super.key}); + const CameraPreview({ + this.controller, + this.error, + required this.onSpotTap, + super.key, + }); @override State createState() => _CameraPreviewState(); @@ -31,7 +39,10 @@ class _CameraPreviewState extends State { AnimatedSwitcher( duration: Dimens.switchDuration, child: widget.controller != null - ? _CameraPreviewBuilder(controller: widget.controller!) + ? _CameraPreviewBuilder( + controller: widget.controller!, + onSpotTap: widget.onSpotTap, + ) : CameraViewPlaceholder(error: widget.error), ), ], @@ -43,16 +54,19 @@ class _CameraPreviewState extends State { class _CameraPreviewBuilder extends StatefulWidget { final CameraController controller; + final ValueChanged onSpotTap; - const _CameraPreviewBuilder({required this.controller}); + const _CameraPreviewBuilder({ + required this.controller, + required this.onSpotTap, + }); @override State<_CameraPreviewBuilder> createState() => _CameraPreviewBuilderState(); } class _CameraPreviewBuilderState extends State<_CameraPreviewBuilder> { - late final ValueNotifier _initializedNotifier = - ValueNotifier(widget.controller.value.isInitialized); + late final ValueNotifier _initializedNotifier = ValueNotifier(widget.controller.value.isInitialized); @override void initState() { @@ -79,16 +93,23 @@ class _CameraPreviewBuilderState extends State<_CameraPreviewBuilder> { 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), - ), + if (context.isPro) ...[ + if (UserPreferencesProvider.cameraFeatureOf( + context, + CameraFeature.histogram, + )) + Positioned( + left: Dimens.grid8, + right: Dimens.grid8, + bottom: Dimens.grid16, + child: CameraHistogram(controller: widget.controller), + ), + if (UserPreferencesProvider.cameraFeatureOf( + context, + CameraFeature.spotMetering, + )) + CameraSpotDetector(onSpotTap: widget.onSpotTap) + ], ], ) : const SizedBox.shrink(), diff --git a/lib/screens/metering/components/camera_container/event_container_camera.dart b/lib/screens/metering/components/camera_container/event_container_camera.dart index d3e5995..fe0713d 100644 --- a/lib/screens/metering/components/camera_container/event_container_camera.dart +++ b/lib/screens/metering/components/camera_container/event_container_camera.dart @@ -1,3 +1,5 @@ +import 'package:flutter/gestures.dart'; + abstract class CameraContainerEvent { const CameraContainerEvent(); } @@ -53,3 +55,19 @@ class ExposureOffsetChangedEvent extends CameraContainerEvent { class ExposureOffsetResetEvent extends CameraContainerEvent { const ExposureOffsetResetEvent(); } + +class ExposureSpotChangedEvent extends CameraContainerEvent { + final Offset? offset; + + const ExposureSpotChangedEvent(this.offset); + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other.runtimeType != runtimeType) return false; + return other is ExposureSpotChangedEvent && other.offset == offset; + } + + @override + int get hashCode => Object.hash(offset, runtimeType); +} diff --git a/lib/screens/metering/components/camera_container/mock_bloc_container_camera.dart b/lib/screens/metering/components/camera_container/mock_bloc_container_camera.dart new file mode 100644 index 0000000..f915a52 --- /dev/null +++ b/lib/screens/metering/components/camera_container/mock_bloc_container_camera.dart @@ -0,0 +1,80 @@ +part of 'bloc_container_camera.dart'; + +class MockCameraContainerBloc extends CameraContainerBloc { + MockCameraContainerBloc( + super._meteringInteractor, + super.communicationBloc, + ); + + @override + Future _onRequestPermission(_, Emitter emit) async { + add(const InitializeEvent()); + } + + @override + Future _onOpenAppSettings(_, Emitter emit) async { + _meteringInteractor.openAppSettings(); + } + + @override + Future _onInitialize(_, Emitter emit) async { + emit(const CameraLoadingState()); + try { + _cameraController = CameraController( + const CameraDescription(name: '0', lensDirection: CameraLensDirection.back, sensorOrientation: 0), + ResolutionPreset.low, + enableAudio: false, + ); + + _zoomRange = const RangeValues(1, 6); + _currentZoom = _zoomRange!.start; + + _exposureOffsetRange = const RangeValues(-4, 4); + _exposureStep = 0.1; + _currentExposureOffset = 0.0; + + emit(CameraInitializedState(_cameraController!)); + + _emitActiveState(emit); + } catch (e) { + emit(const CameraErrorState(CameraErrorType.other)); + } + } + + @override + Future _onZoomChanged(ZoomChangedEvent event, Emitter emit) async { + if (event.value >= _zoomRange!.start && event.value <= _zoomRange!.end) { + _currentZoom = event.value; + _emitActiveState(emit); + } + } + + @override + Future _onExposureOffsetChanged(ExposureOffsetChangedEvent event, Emitter emit) async { + _currentExposureOffset = event.value; + _emitActiveState(emit); + } + + @override + Future _onExposureOffsetResetEvent(ExposureOffsetResetEvent event, Emitter emit) async { + _meteringInteractor.quickVibration(); + add(const ExposureOffsetChangedEvent(0)); + } + + @override + Future _onExposureSpotChangedEvent(ExposureSpotChangedEvent event, Emitter emit) async {} + + @override + bool get _canTakePhoto => PlatformConfig.cameraStubImage.isNotEmpty; + + @override + Future _takePhoto() async { + try { + final bytes = (await rootBundle.load(PlatformConfig.cameraStubImage)).buffer.asUint8List(); + return await evFromImage(bytes); + } catch (e) { + log(e.toString()); + return null; + } + } +} 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 1d6d8c0..80be814 100644 --- a/lib/screens/metering/components/camera_container/provider_container_camera.dart +++ b/lib/screens/metering/components/camera_container/provider_container_camera.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:lightmeter/data/models/exposure_pair.dart'; +import 'package:lightmeter/platform_config.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'; @@ -30,12 +31,18 @@ class CameraContainerProvider extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocProvider( + return BlocProvider( lazy: false, - create: (context) => CameraContainerBloc( - MeteringInteractorProvider.of(context), - context.read(), - )..add(const RequestPermissionEvent()), + create: (context) => (PlatformConfig.cameraStubImage.isNotEmpty + ? MockCameraContainerBloc( + MeteringInteractorProvider.of(context), + context.read(), + ) + : CameraContainerBloc( + MeteringInteractorProvider.of(context), + context.read(), + )) + ..add(const RequestPermissionEvent()), child: CameraContainer( fastest: fastest, slowest: slowest, 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 944aa34..5ed2b43 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/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/bloc_container_camera.dart'; import 'package:lightmeter/screens/metering/components/camera_container/components/camera_controls/widget_camera_controls.dart'; @@ -17,6 +16,7 @@ import 'package:lightmeter/screens/metering/components/camera_container/state_co import 'package:lightmeter/screens/metering/components/shared/exposure_pairs_list/widget_list_exposure_pairs.dart'; import 'package:lightmeter/screens/metering/components/shared/metering_top_bar/widget_top_bar_metering.dart'; import 'package:lightmeter/screens/metering/components/shared/readings_container/widget_container_readings.dart'; +import 'package:lightmeter/utils/context_utils.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; class CameraContainer extends StatelessWidget { @@ -102,27 +102,23 @@ class CameraContainer extends StatelessWidget { double _meteringContainerHeight(BuildContext context) { double enabledFeaturesHeight = 0; - if (UserPreferencesProvider.meteringScreenFeatureOf( - context, - MeteringScreenLayoutFeature.equipmentProfiles, - )) { + if (!context.isPro) { enabledFeaturesHeight += Dimens.readingContainerSingleValueHeight; enabledFeaturesHeight += Dimens.paddingS; + } else { + if (context.meteringFeature(MeteringScreenLayoutFeature.equipmentProfiles)) { + enabledFeaturesHeight += Dimens.readingContainerSingleValueHeight; + enabledFeaturesHeight += Dimens.paddingS; + } + if (context.meteringFeature(MeteringScreenLayoutFeature.filmPicker)) { + enabledFeaturesHeight += Dimens.readingContainerSingleValueHeight; + enabledFeaturesHeight += Dimens.paddingS; + } } - if (UserPreferencesProvider.meteringScreenFeatureOf( - context, - MeteringScreenLayoutFeature.extremeExposurePairs, - )) { + if (context.meteringFeature(MeteringScreenLayoutFeature.extremeExposurePairs)) { enabledFeaturesHeight += Dimens.readingContainerDoubleValueHeight; enabledFeaturesHeight += Dimens.paddingS; } - if (UserPreferencesProvider.meteringScreenFeatureOf( - context, - MeteringScreenLayoutFeature.filmPicker, - )) { - enabledFeaturesHeight += Dimens.readingContainerSingleValueHeight; - enabledFeaturesHeight += Dimens.paddingS; - } return enabledFeaturesHeight + Dimens.readingContainerSingleValueHeight; // ISO & ND } @@ -143,6 +139,9 @@ class _CameraViewBuilder extends StatelessWidget { builder: (context, state) => CameraPreview( controller: state is CameraInitializedState ? state.controller : null, error: state is CameraErrorState ? state.error : null, + onSpotTap: (value) { + context.read().add(ExposureSpotChangedEvent(value)); + }, ), ); } diff --git a/lib/screens/metering/components/shared/readings_container/components/lightmeter_pro/widget_lightmeter_pro.dart b/lib/screens/metering/components/shared/readings_container/components/lightmeter_pro/widget_lightmeter_pro.dart new file mode 100644 index 0000000..9d5218a --- /dev/null +++ b/lib/screens/metering/components/shared/readings_container/components/lightmeter_pro/widget_lightmeter_pro.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:lightmeter/generated/l10n.dart'; +import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/components/animated_dialog/widget_dialog_animated.dart'; +import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/reading_value_container/widget_container_reading_value.dart'; +import 'package:lightmeter/screens/shared/pro_features_dialog/widget_dialog_pro_features.dart'; + +class LightmeterProAnimatedDialog extends StatelessWidget { + const LightmeterProAnimatedDialog({super.key}); + + @override + Widget build(BuildContext context) { + return AnimatedDialog( + closedChild: ReadingValueContainer( + color: Theme.of(context).colorScheme.errorContainer, + textColor: Theme.of(context).colorScheme.onErrorContainer, + values: [ + ReadingValue( + label: S.of(context).proFeatures, + value: S.of(context).unlock, + ), + ], + ), + openedChild: const ProFeaturesDialog(), + openedSize: Size.fromHeight(const ProFeaturesDialog().height(context)), + ); + } +} diff --git a/lib/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/components/animated_dialog/widget_dialog_animated.dart b/lib/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/components/animated_dialog/widget_dialog_animated.dart index 72e770b..63bd1e6 100644 --- a/lib/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/components/animated_dialog/widget_dialog_animated.dart +++ b/lib/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/components/animated_dialog/widget_dialog_animated.dart @@ -1,9 +1,15 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:lightmeter/res/dimens.dart'; +mixin AnimatedDialogClosedChild on Widget { + Color backgroundColor(BuildContext context); +} + class AnimatedDialog extends StatefulWidget { final Size? openedSize; - final Widget? closedChild; + final AnimatedDialogClosedChild? closedChild; final Widget? openedChild; final Widget? child; @@ -15,6 +21,9 @@ class AnimatedDialog extends StatefulWidget { super.key, }); + static Future? maybeClose(BuildContext context) => + context.findAncestorWidgetOfExactType<_AnimatedOverlay>()?.onDismiss(); + @override State createState() => AnimatedDialogState(); } @@ -95,7 +104,7 @@ class AnimatedDialogState extends State with SingleTickerProvide void didChangeDependencies() { super.didChangeDependencies(); _foregroundColorAnimation = ColorTween( - begin: Theme.of(context).colorScheme.primaryContainer, + begin: widget.closedChild?.backgroundColor(context) ?? Theme.of(context).colorScheme.primaryContainer, end: Theme.of(context).colorScheme.surface, ).animate(_defaultCurvedAnimation); @@ -135,14 +144,15 @@ class AnimatedDialogState extends State with SingleTickerProvide if (renderBox != null) { final size = MediaQuery.sizeOf(context); final padding = MediaQuery.paddingOf(context); + final maxWidth = size.width - padding.horizontal - Dimens.dialogMargin.horizontal; + final maxHeight = size.height - padding.vertical - Dimens.dialogMargin.vertical; _closedSize = _key.currentContext!.size!; _sizeTween = SizeTween( begin: _closedSize, - end: widget.openedSize ?? - Size( - size.width - padding.horizontal - Dimens.dialogMargin.horizontal, - size.height - padding.vertical - Dimens.dialogMargin.vertical, - ), + end: Size( + min(widget.openedSize?.width ?? double.maxFinite, maxWidth), + min(widget.openedSize?.height ?? double.maxFinite, maxHeight), + ), ); _sizeAnimation = _sizeTween.animate(_defaultCurvedAnimation); @@ -181,7 +191,6 @@ class AnimatedDialogState extends State with SingleTickerProvide onDismiss: close, builder: widget.closedChild != null && widget.openedChild != null ? (_) => _AnimatedSwitcher( - sizeAnimation: _sizeAnimation, closedOpacityAnimation: _closedOpacityAnimation, openedOpacityAnimation: _openedOpacityAnimation, closedSize: _sizeTween.begin!, @@ -223,7 +232,7 @@ class _AnimatedOverlay extends StatelessWidget { final Animation borderRadiusAnimation; final Animation foregroundColorAnimation; final Animation elevationAnimation; - final VoidCallback onDismiss; + final Future Function() onDismiss; final Widget? child; final Widget Function(BuildContext context)? builder; @@ -281,7 +290,6 @@ class _AnimatedOverlay extends StatelessWidget { } class _AnimatedSwitcher extends StatelessWidget { - final Animation sizeAnimation; final Animation closedOpacityAnimation; final Animation openedOpacityAnimation; final Size closedSize; @@ -290,7 +298,6 @@ class _AnimatedSwitcher extends StatelessWidget { final Widget openedChild; const _AnimatedSwitcher({ - required this.sizeAnimation, required this.closedOpacityAnimation, required this.openedOpacityAnimation, required this.closedSize, @@ -306,17 +313,21 @@ class _AnimatedSwitcher extends StatelessWidget { children: [ Opacity( opacity: closedOpacityAnimation.value, - child: Transform.scale( - scale: sizeAnimation.value!.width / closedSize.width, - child: SizedBox( - width: closedSize.width, + child: FittedBox( + child: SizedBox.fromSize( + size: closedSize, child: closedChild, ), ), ), Opacity( opacity: openedOpacityAnimation.value, - child: openedChild, + child: FittedBox( + child: SizedBox.fromSize( + size: openedSize, + child: openedChild, + ), + ), ), ], ); diff --git a/lib/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/components/dialog_picker/widget_picker_dialog.dart b/lib/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/components/dialog_picker/widget_picker_dialog.dart index 3c253b1..dfbf1ce 100644 --- a/lib/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/components/dialog_picker/widget_picker_dialog.dart +++ b/lib/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/components/dialog_picker/widget_picker_dialog.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/res/dimens.dart'; +import 'package:lightmeter/screens/shared/transparent_dialog/widget_dialog_transparent.dart'; typedef DialogPickerItemTitleBuilder = Widget Function(BuildContext context, T value); typedef DialogPickerItemTrailingBuilder = Widget? Function(T selected, T value); @@ -29,6 +30,14 @@ class DialogPicker extends StatefulWidget { super.key, }); + double height(BuildContext context) => TransparentDialog.height( + context, + title: title, + subtitle: subtitle, + scrollableContent: true, + contextHeight: Dimens.grid56 * values.length, + ); + @override State> createState() => _DialogPickerState(); } @@ -46,83 +55,43 @@ class _DialogPickerState extends State> { @override Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: Dimens.dialogTitlePadding, - child: Icon(widget.icon), - ), - Padding( - padding: Dimens.dialogIconTitlePadding, - child: Text( - widget.title, - style: Theme.of(context).textTheme.headlineSmall, - textAlign: TextAlign.center, - ), - ), - if (widget.subtitle != null) - Padding( - padding: const EdgeInsets.fromLTRB( - Dimens.paddingL, - 0, - Dimens.paddingL, - Dimens.paddingM, - ), - child: Text( - widget.subtitle!, - style: Theme.of(context).textTheme.bodyMedium, - textAlign: TextAlign.center, - ), - ), - ], - ), - const Divider(), - Expanded( - child: ListView.builder( - controller: _scrollController, - padding: EdgeInsets.zero, - itemCount: widget.values.length, - itemExtent: Dimens.grid56, - itemBuilder: (context, index) => RadioListTile( - value: widget.values[index], - groupValue: _selectedValue, - title: DefaultTextStyle( - style: Theme.of(context).textTheme.bodyLarge!, - child: widget.itemTitleBuilder(context, widget.values[index]), - ), - secondary: widget.itemTrailingBuilder?.call(_selectedValue, widget.values[index]), - onChanged: (value) { - if (value != null) { - setState(() { - _selectedValue = value; - }); - } - }, + return TransparentDialog( + icon: widget.icon, + title: widget.title, + subtitle: widget.subtitle, + content: Expanded( + child: ListView.builder( + controller: _scrollController, + padding: EdgeInsets.zero, + itemCount: widget.values.length, + itemExtent: Dimens.grid56, + itemBuilder: (context, index) => RadioListTile( + value: widget.values[index], + groupValue: _selectedValue, + title: DefaultTextStyle( + style: Theme.of(context).textTheme.bodyLarge!, + child: widget.itemTitleBuilder(context, widget.values[index]), ), + secondary: widget.itemTrailingBuilder?.call(_selectedValue, widget.values[index]), + onChanged: (value) { + if (value != null) { + setState(() { + _selectedValue = value; + }); + } + }, ), ), - const Divider(), - Padding( - padding: Dimens.dialogActionsPadding, - child: Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - const Spacer(), - TextButton( - onPressed: widget.onCancel, - child: Text(S.of(context).cancel), - ), - const SizedBox(width: Dimens.grid16), - TextButton( - onPressed: () => widget.onSelect(_selectedValue), - child: Text(S.of(context).select), - ), - ], - ), + ), + scrollableContent: true, + actions: [ + TextButton( + onPressed: widget.onCancel, + child: Text(S.of(context).cancel), + ), + TextButton( + onPressed: () => widget.onSelect(_selectedValue), + child: Text(S.of(context).select), ), ], ); diff --git a/lib/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/widget_picker_dialog_animated.dart b/lib/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/widget_picker_dialog_animated.dart index eeeec52..efa07c7 100644 --- a/lib/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/widget_picker_dialog_animated.dart +++ b/lib/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/widget_picker_dialog_animated.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/components/animated_dialog/widget_dialog_animated.dart'; import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/components/dialog_picker/widget_picker_dialog.dart'; -// Has to be stateful, so that [GlobalKey] is not recreated. +// Has to be stateful, so that [GlobalKey] is not recreated. // Otherwise use will no be able to close the dialog after EV value has changed. class AnimatedDialogPicker extends StatefulWidget { final IconData icon; @@ -13,7 +13,7 @@ class AnimatedDialogPicker extends StatefulWidget { final DialogPickerItemTitleBuilder itemTitleBuilder; final DialogPickerItemTrailingBuilder? itemTrailingBuilder; final ValueChanged onChanged; - final Widget closedChild; + final AnimatedDialogClosedChild closedChild; const AnimatedDialogPicker({ required this.icon, @@ -37,24 +37,26 @@ class _AnimatedDialogPickerState extends State> { @override Widget build(BuildContext context) { + final dialogPicker = DialogPicker( + icon: widget.icon, + title: widget.title, + subtitle: widget.subtitle, + initialValue: widget.selectedValue, + values: widget.values, + itemTitleBuilder: widget.itemTitleBuilder, + itemTrailingBuilder: widget.itemTrailingBuilder, + onCancel: () { + _key.currentState?.close(); + }, + onSelect: (value) { + _key.currentState?.close().then((_) => widget.onChanged(value)); + }, + ); return AnimatedDialog( key: _key, closedChild: widget.closedChild, - openedChild: DialogPicker( - icon: widget.icon, - title: widget.title, - subtitle: widget.subtitle, - initialValue: widget.selectedValue, - values: widget.values, - itemTitleBuilder: widget.itemTitleBuilder, - itemTrailingBuilder: widget.itemTrailingBuilder, - onCancel: () { - _key.currentState?.close(); - }, - onSelect: (value) { - _key.currentState?.close().then((_) => widget.onChanged(value)); - }, - ), + openedChild: dialogPicker, + openedSize: Size.fromHeight(dialogPicker.height(context)), ); } } diff --git a/lib/screens/metering/components/shared/readings_container/components/shared/reading_value_container/widget_container_reading_value.dart b/lib/screens/metering/components/shared/readings_container/components/shared/reading_value_container/widget_container_reading_value.dart index 3254456..6968db6 100644 --- a/lib/screens/metering/components/shared/readings_container/components/shared/reading_value_container/widget_container_reading_value.dart +++ b/lib/screens/metering/components/shared/readings_container/components/shared/reading_value_container/widget_container_reading_value.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/res/dimens.dart'; +import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/components/animated_dialog/widget_dialog_animated.dart'; class ReadingValue { final String label; @@ -11,11 +12,15 @@ class ReadingValue { }); } -class ReadingValueContainer extends StatelessWidget { +class ReadingValueContainer extends StatelessWidget implements AnimatedDialogClosedChild { late final List _items; + final Color? color; + final Color? textColor; ReadingValueContainer({ required List values, + this.color, + this.textColor, super.key, }) { _items = []; @@ -23,21 +28,26 @@ class ReadingValueContainer extends StatelessWidget { if (i > 0) { _items.add(const SizedBox(height: Dimens.grid8)); } - _items.add(_ReadingValueBuilder(values[i])); + _items.add(_ReadingValueBuilder(values[i], textColor: textColor)); } } ReadingValueContainer.singleValue({ required ReadingValue value, + this.color, + this.textColor, super.key, - }) : _items = [_ReadingValueBuilder(value)]; + }) : _items = [_ReadingValueBuilder(value, textColor: textColor)]; + + @override + Color backgroundColor(BuildContext context) => color ?? Theme.of(context).colorScheme.primaryContainer; @override Widget build(BuildContext context) { return ClipRRect( borderRadius: BorderRadius.circular(Dimens.borderRadiusM), child: ColoredBox( - color: Theme.of(context).colorScheme.primaryContainer, + color: backgroundColor(context), child: Padding( padding: const EdgeInsets.all(Dimens.paddingM), child: Column( @@ -53,20 +63,21 @@ class ReadingValueContainer extends StatelessWidget { class _ReadingValueBuilder extends StatelessWidget { final ReadingValue reading; + final Color? textColor; - const _ReadingValueBuilder(this.reading); + const _ReadingValueBuilder(this.reading, {this.textColor}); @override Widget build(BuildContext context) { final textTheme = Theme.of(context).textTheme; - final textColor = Theme.of(context).colorScheme.onPrimaryContainer; + final color = textColor ?? Theme.of(context).colorScheme.onPrimaryContainer; return Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( reading.label, - style: textTheme.labelMedium?.copyWith(color: textColor), + style: textTheme.labelMedium?.copyWith(color: color), maxLines: 1, overflow: TextOverflow.visible, softWrap: false, @@ -76,7 +87,7 @@ class _ReadingValueBuilder extends StatelessWidget { duration: Dimens.switchDuration, child: Text( reading.value, - style: textTheme.titleMedium?.copyWith(color: textColor), + style: textTheme.titleMedium?.copyWith(color: color), 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 cb8af05..ce373a8 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,14 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/data/models/exposure_pair.dart'; import 'package:lightmeter/data/models/metering_screen_layout_config.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/equipment_profile_picker/widget_picker_equipment_profiles.dart'; import 'package:lightmeter/screens/metering/components/shared/readings_container/components/extreme_exposure_pairs_container/widget_container_extreme_exposure_pairs.dart'; import 'package:lightmeter/screens/metering/components/shared/readings_container/components/film_picker/widget_picker_film.dart'; import 'package:lightmeter/screens/metering/components/shared/readings_container/components/iso_picker/widget_picker_iso.dart'; +import 'package:lightmeter/screens/metering/components/shared/readings_container/components/lightmeter_pro/widget_lightmeter_pro.dart'; import 'package:lightmeter/screens/metering/components/shared/readings_container/components/nd_picker/widget_picker_nd.dart'; +import 'package:lightmeter/utils/context_utils.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; class ReadingsContainer extends StatelessWidget { @@ -34,27 +35,22 @@ class ReadingsContainer extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - if (UserPreferencesProvider.meteringScreenFeatureOf( - context, - MeteringScreenLayoutFeature.equipmentProfiles, - )) ...[ + if (!context.isPro) ...[ + const LightmeterProAnimatedDialog(), + const _InnerPadding(), + ], + if (context.isPro && context.meteringFeature(MeteringScreenLayoutFeature.equipmentProfiles)) ...[ const EquipmentProfilePicker(), const _InnerPadding(), ], - if (UserPreferencesProvider.meteringScreenFeatureOf( - context, - MeteringScreenLayoutFeature.extremeExposurePairs, - )) ...[ + if (context.meteringFeature(MeteringScreenLayoutFeature.extremeExposurePairs)) ...[ ExtremeExposurePairsContainer( fastest: fastest, slowest: slowest, ), const _InnerPadding(), ], - if (UserPreferencesProvider.meteringScreenFeatureOf( - context, - MeteringScreenLayoutFeature.filmPicker, - )) ...[ + if (context.isPro && context.meteringFeature(MeteringScreenLayoutFeature.filmPicker)) ...[ FilmPicker(selectedIso: iso), const _InnerPadding(), ], diff --git a/lib/screens/metering/flow_metering.dart b/lib/screens/metering/flow_metering.dart index cca5675..f78482d 100644 --- a/lib/screens/metering/flow_metering.dart +++ b/lib/screens/metering/flow_metering.dart @@ -4,8 +4,8 @@ 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/screens/metering/utils/notifier_volume_keys.dart'; class MeteringFlow extends StatefulWidget { const MeteringFlow({super.key}); diff --git a/lib/screens/metering/screen_metering.dart b/lib/screens/metering/screen_metering.dart index 6900159..836e6d0 100644 --- a/lib/screens/metering/screen_metering.dart +++ b/lib/screens/metering/screen_metering.dart @@ -13,7 +13,7 @@ import 'package:lightmeter/screens/metering/components/camera_container/provider 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/listsner_equipment_profiles.dart'; +import 'package:lightmeter/screens/metering/utils/listener_equipment_profiles.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; class MeteringScreen extends StatelessWidget { @@ -40,6 +40,7 @@ class MeteringScreen extends StatelessWidget { BlocBuilder( builder: (context, state) => MeteringBottomControlsProvider( ev: state is MeteringDataState ? state.ev : null, + ev100: state is MeteringDataState ? state.ev100 : null, isMetering: state.isMetering, onSwitchEvSourceType: ServicesProvider.of(context).environment.hasLightSensor ? UserPreferencesProvider.of(context).toggleEvSourceType diff --git a/lib/screens/metering/utils/listsner_equipment_profiles.dart b/lib/screens/metering/utils/listener_equipment_profiles.dart similarity index 100% rename from lib/screens/metering/utils/listsner_equipment_profiles.dart rename to lib/screens/metering/utils/listener_equipment_profiles.dart diff --git a/lib/screens/metering/components/shared/volume_keys_notifier/notifier_volume_keys.dart b/lib/screens/metering/utils/notifier_volume_keys.dart similarity index 82% rename from lib/screens/metering/components/shared/volume_keys_notifier/notifier_volume_keys.dart rename to lib/screens/metering/utils/notifier_volume_keys.dart index df64fdf..0c19955 100644 --- a/lib/screens/metering/components/shared/volume_keys_notifier/notifier_volume_keys.dart +++ b/lib/screens/metering/utils/notifier_volume_keys.dart @@ -5,12 +5,12 @@ import 'package:lightmeter/data/models/volume_action.dart'; import 'package:lightmeter/data/volume_events_service.dart'; class VolumeKeysNotifier extends ChangeNotifier with RouteAware { - final VolumeEventsService volumeEventsService; + final VolumeEventsService _volumeEventsService; late final StreamSubscription _volumeKeysSubscription; VolumeKey _value = VolumeKey.up; - VolumeKeysNotifier(this.volumeEventsService) { - _volumeKeysSubscription = volumeEventsService + VolumeKeysNotifier(this._volumeEventsService) { + _volumeKeysSubscription = _volumeEventsService .volumeButtonsEventStream() .map((event) => event == 24 ? VolumeKey.up : VolumeKey.down) .listen((event) { @@ -19,6 +19,8 @@ class VolumeKeysNotifier extends ChangeNotifier with RouteAware { } VolumeKey get value => _value; + + @protected set value(VolumeKey newValue) { _value = newValue; notifyListeners(); diff --git a/lib/screens/settings/components/lightmeter_pro/components/buy_pro/widget_list_tile_buy_pro.dart b/lib/screens/settings/components/lightmeter_pro/components/buy_pro/widget_list_tile_buy_pro.dart index 5f8adcd..4bb401d 100644 --- a/lib/screens/settings/components/lightmeter_pro/components/buy_pro/widget_list_tile_buy_pro.dart +++ b/lib/screens/settings/components/lightmeter_pro/components/buy_pro/widget_list_tile_buy_pro.dart @@ -1,10 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:lightmeter/data/models/feature.dart'; import 'package:lightmeter/generated/l10n.dart'; -import 'package:lightmeter/providers/remote_config_provider.dart'; -import 'package:lightmeter/providers/services_provider.dart'; import 'package:lightmeter/res/dimens.dart'; -import 'package:lightmeter/screens/settings/components/utils/show_buy_pro_dialog.dart'; +import 'package:lightmeter/screens/shared/pro_features_dialog/widget_dialog_pro_features.dart'; import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; class BuyProListTile extends StatelessWidget { @@ -12,18 +9,19 @@ class BuyProListTile extends StatelessWidget { @override Widget build(BuildContext context) { - final unlockFeaturesEnabled = RemoteConfig.isEnabled(context, Feature.unlockProFeaturesText); final status = IAPProducts.productOf(context, IAPProductType.paidFeatures)?.status; final isPending = status == IAPProductStatus.purchased || status == null; return ListTile( leading: const Icon(Icons.star), - title: Text(unlockFeaturesEnabled ? S.of(context).unlockProFeatures : S.of(context).buyLightmeterPro), - onTap: () { - showBuyProDialog(context); - ServicesProvider.of(context) - .analytics - .logUnlockProFeatures(unlockFeaturesEnabled ? 'Unlock Pro features' : 'Buy Lightmeter Pro'); - }, + title: Text(S.of(context).unlockProFeatures), + onTap: !isPending + ? () { + showDialog( + context: context, + builder: (_) => const Dialog(child: ProFeaturesDialog()), + ); + } + : null, trailing: isPending ? const SizedBox( height: Dimens.grid24, diff --git a/lib/screens/settings/components/lightmeter_pro/widget_settings_section_lightmeter_pro.dart b/lib/screens/settings/components/lightmeter_pro/widget_settings_section_lightmeter_pro.dart index 7050ae2..57d5d14 100644 --- a/lib/screens/settings/components/lightmeter_pro/widget_settings_section_lightmeter_pro.dart +++ b/lib/screens/settings/components/lightmeter_pro/widget_settings_section_lightmeter_pro.dart @@ -1,7 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:lightmeter/data/models/feature.dart'; import 'package:lightmeter/generated/l10n.dart'; -import 'package:lightmeter/providers/remote_config_provider.dart'; import 'package:lightmeter/screens/settings/components/lightmeter_pro/components/buy_pro/widget_list_tile_buy_pro.dart'; import 'package:lightmeter/screens/settings/components/shared/settings_section/widget_settings_section.dart'; @@ -11,9 +9,7 @@ class LightmeterProSettingsSection extends StatelessWidget { @override Widget build(BuildContext context) { return SettingsSection( - title: RemoteConfig.isEnabled(context, Feature.unlockProFeaturesText) - ? S.of(context).proFeatures - : S.of(context).lightmeterPro, + title: S.of(context).proFeatures, children: const [BuyProListTile()], ); } diff --git a/lib/screens/settings/components/metering/components/camera_features/widget_list_tile_camera_features.dart b/lib/screens/settings/components/metering/components/camera_features/widget_list_tile_camera_features.dart new file mode 100644 index 0000000..1446be3 --- /dev/null +++ b/lib/screens/settings/components/metering/components/camera_features/widget_list_tile_camera_features.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:lightmeter/data/models/camera_feature.dart'; +import 'package:lightmeter/generated/l10n.dart'; +import 'package:lightmeter/providers/user_preferences_provider.dart'; +import 'package:lightmeter/screens/settings/components/shared/dialog_switch/widget_dialog_switch.dart'; +import 'package:lightmeter/screens/settings/components/shared/iap_list_tile/widget_list_tile_iap.dart'; + +class CameraFeaturesListTile extends StatelessWidget { + const CameraFeaturesListTile({super.key}); + + @override + Widget build(BuildContext context) { + return IAPListTile( + leading: const Icon(Icons.camera_alt), + title: Text(S.of(context).cameraFeatures), + onTap: () { + showDialog( + context: context, + builder: (_) => DialogSwitch( + icon: Icons.layers_outlined, + title: S.of(context).cameraFeatures, + values: UserPreferencesProvider.cameraConfigOf(context), + titleAdapter: (context, feature) { + switch (feature) { + case CameraFeature.spotMetering: + return S.of(context).cameraFeatureSpotMetering; + case CameraFeature.histogram: + return S.of(context).cameraFeatureHistogram; + } + }, + subtitleAdapter: (context, feature) { + switch (feature) { + case CameraFeature.spotMetering: + return S.of(context).cameraFeatureSpotMeteringHint; + case CameraFeature.histogram: + return S.of(context).cameraFeatureHistogramHint; + } + }, + onSave: UserPreferencesProvider.of(context).setCameraFeature, + ), + ); + }, + ); + } +} 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 b10190f..6879a2c 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 @@ -90,7 +90,7 @@ class _EquipmentProfilesScreenState extends State { } void _updateProfileAt(EquipmentProfile data) { - EquipmentProfileProvider.of(context).updateProdile(data); + EquipmentProfileProvider.of(context).updateProfile(data); } void _removeProfileAt(EquipmentProfile data) { 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 deleted file mode 100644 index 2529e2e..0000000 --- a/lib/screens/settings/components/metering/components/metering_screen_layout/components/meterins_screen_layout_features_dialog/widget_dialog_metering_screen_layout_features.dart +++ /dev/null @@ -1,97 +0,0 @@ -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/equipment_profile_provider.dart'; -import 'package:lightmeter/providers/films_provider.dart'; -import 'package:lightmeter/providers/user_preferences_provider.dart'; -import 'package:lightmeter/res/dimens.dart'; -import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; - -class MeteringScreenLayoutFeaturesDialog extends StatefulWidget { - const MeteringScreenLayoutFeaturesDialog({super.key}); - - @override - State createState() => _MeteringScreenLayoutFeaturesDialogState(); -} - -class _MeteringScreenLayoutFeaturesDialogState extends State { - late final _features = MeteringScreenLayoutConfig.from(UserPreferencesProvider.meteringScreenConfigOf(context)); - - @override - Widget build(BuildContext context) { - return AlertDialog( - icon: const Icon(Icons.layers_outlined), - titlePadding: Dimens.dialogIconTitlePadding, - title: Text(S.of(context).meteringScreenLayout), - contentPadding: EdgeInsets.zero, - content: SizedBox( - width: double.maxFinite, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: Dimens.paddingL), - child: Text(S.of(context).meteringScreenLayoutHint), - ), - const SizedBox(height: Dimens.grid16), - ListView( - shrinkWrap: true, - children: [ - _featureListTile(MeteringScreenLayoutFeature.equipmentProfiles), - _featureListTile(MeteringScreenLayoutFeature.extremeExposurePairs), - _featureListTile(MeteringScreenLayoutFeature.filmPicker), - _featureListTile(MeteringScreenLayoutFeature.histogram), - ], - ), - ], - ), - ), - actionsPadding: Dimens.dialogActionsPadding, - actions: [ - TextButton( - onPressed: Navigator.of(context).pop, - child: Text(S.of(context).cancel), - ), - TextButton( - onPressed: () { - if (!_features[MeteringScreenLayoutFeature.equipmentProfiles]!) { - EquipmentProfileProvider.of(context).setProfile(EquipmentProfiles.of(context).first); - } - if (!_features[MeteringScreenLayoutFeature.filmPicker]!) { - FilmsProvider.of(context).setFilm(const Film.other()); - } - UserPreferencesProvider.of(context).setMeteringScreenLayout(_features); - Navigator.of(context).pop(); - }, - child: Text(S.of(context).save), - ), - ], - ); - } - - Widget _featureListTile(MeteringScreenLayoutFeature f) { - return SwitchListTile( - contentPadding: EdgeInsets.symmetric(horizontal: Dimens.dialogTitlePadding.left), - title: Text(_toStringLocalized(context, f)), - value: _features[f]!, - onChanged: (value) { - setState(() { - _features.update(f, (_) => value); - }); - }, - ); - } - - String _toStringLocalized(BuildContext context, MeteringScreenLayoutFeature feature) { - switch (feature) { - case MeteringScreenLayoutFeature.equipmentProfiles: - return S.of(context).meteringScreenLayoutHintEquipmentProfiles; - case MeteringScreenLayoutFeature.extremeExposurePairs: - return S.of(context).meteringScreenFeatureExtremeExposurePairs; - case MeteringScreenLayoutFeature.filmPicker: - return S.of(context).meteringScreenFeatureFilmPicker; - case MeteringScreenLayoutFeature.histogram: - return S.of(context).meteringScreenFeatureHistogram; - } - } -} diff --git a/lib/screens/settings/components/metering/components/metering_screen_layout/widget_list_tile_metering_screen_layout.dart b/lib/screens/settings/components/metering/components/metering_screen_layout/widget_list_tile_metering_screen_layout.dart index a540926..9dfb026 100644 --- a/lib/screens/settings/components/metering/components/metering_screen_layout/widget_list_tile_metering_screen_layout.dart +++ b/lib/screens/settings/components/metering/components/metering_screen_layout/widget_list_tile_metering_screen_layout.dart @@ -1,7 +1,12 @@ import 'package:flutter/material.dart'; +import 'package:lightmeter/data/models/metering_screen_layout_config.dart'; import 'package:lightmeter/generated/l10n.dart'; - -import 'package:lightmeter/screens/settings/components/metering/components/metering_screen_layout/components/meterins_screen_layout_features_dialog/widget_dialog_metering_screen_layout_features.dart'; +import 'package:lightmeter/providers/equipment_profile_provider.dart'; +import 'package:lightmeter/providers/films_provider.dart'; +import 'package:lightmeter/providers/user_preferences_provider.dart'; +import 'package:lightmeter/screens/settings/components/shared/dialog_switch/widget_dialog_switch.dart'; +import 'package:lightmeter/utils/context_utils.dart'; +import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; class MeteringScreenLayoutListTile extends StatelessWidget { const MeteringScreenLayoutListTile({super.key}); @@ -14,9 +19,44 @@ class MeteringScreenLayoutListTile extends StatelessWidget { onTap: () { showDialog( context: context, - builder: (_) => const MeteringScreenLayoutFeaturesDialog(), + builder: (_) => DialogSwitch( + icon: Icons.layers_outlined, + title: S.of(context).meteringScreenLayout, + description: S.of(context).meteringScreenLayoutHint, + values: UserPreferencesProvider.meteringScreenConfigOf(context), + titleAdapter: _toStringLocalized, + enabledAdapter: (value) { + switch (value) { + case MeteringScreenLayoutFeature.equipmentProfiles: + case MeteringScreenLayoutFeature.filmPicker: + return context.isPro; + default: + return true; + } + }, + onSave: (value) { + if (!value[MeteringScreenLayoutFeature.equipmentProfiles]!) { + EquipmentProfileProvider.of(context).setProfile(EquipmentProfiles.of(context).first); + } + if (!value[MeteringScreenLayoutFeature.filmPicker]!) { + FilmsProvider.of(context).setFilm(const Film.other()); + } + UserPreferencesProvider.of(context).setMeteringScreenLayout(value); + }, + ), ); }, ); } + + String _toStringLocalized(BuildContext context, MeteringScreenLayoutFeature feature) { + switch (feature) { + case MeteringScreenLayoutFeature.equipmentProfiles: + return S.of(context).meteringScreenLayoutHintEquipmentProfiles; + case MeteringScreenLayoutFeature.extremeExposurePairs: + return S.of(context).meteringScreenFeatureExtremeExposurePairs; + case MeteringScreenLayoutFeature.filmPicker: + return S.of(context).meteringScreenFeatureFilmPicker; + } + } } diff --git a/lib/screens/settings/components/metering/components/show_ev_100/widget_list_tile_show_ev_100.dart b/lib/screens/settings/components/metering/components/show_ev_100/widget_list_tile_show_ev_100.dart new file mode 100644 index 0000000..a92623e --- /dev/null +++ b/lib/screens/settings/components/metering/components/show_ev_100/widget_list_tile_show_ev_100.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:lightmeter/generated/l10n.dart'; +import 'package:lightmeter/providers/user_preferences_provider.dart'; +import 'package:lightmeter/res/dimens.dart'; +import 'package:lightmeter/screens/settings/components/shared/disable/widget_disable.dart'; +import 'package:lightmeter/utils/context_utils.dart'; + +class ShowEv100ListTile extends StatelessWidget { + const ShowEv100ListTile({super.key}); + + @override + Widget build(BuildContext context) { + return Disable( + disable: !context.isPro, + child: SwitchListTile( + secondary: const Icon(Icons.adjust), + title: Text(S.of(context).showEv100), + value: context.isPro && UserPreferencesProvider.showEv100Of(context), + onChanged: (_) => UserPreferencesProvider.of(context).toggleShowEv100(), + contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM), + ), + ); + } +} 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 90de68d..becf531 100644 --- a/lib/screens/settings/components/metering/widget_settings_section_metering.dart +++ b/lib/screens/settings/components/metering/widget_settings_section_metering.dart @@ -1,10 +1,12 @@ import 'package:flutter/material.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/camera_features/widget_list_tile_camera_features.dart'; import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/widget_list_tile_equipment_profiles.dart'; import 'package:lightmeter/screens/settings/components/metering/components/films/widget_list_tile_films.dart'; import 'package:lightmeter/screens/settings/components/metering/components/fractional_stops/widget_list_tile_fractional_stops.dart'; import 'package:lightmeter/screens/settings/components/metering/components/metering_screen_layout/widget_list_tile_metering_screen_layout.dart'; +import 'package:lightmeter/screens/settings/components/metering/components/show_ev_100/widget_list_tile_show_ev_100.dart'; import 'package:lightmeter/screens/settings/components/shared/settings_section/widget_settings_section.dart'; class MeteringSettingsSection extends StatelessWidget { @@ -17,9 +19,11 @@ class MeteringSettingsSection extends StatelessWidget { children: const [ StopTypeListTile(), CalibrationListTile(), + ShowEv100ListTile(), MeteringScreenLayoutListTile(), EquipmentProfilesListTile(), FilmsListTile(), + CameraFeaturesListTile(), ], ); } diff --git a/lib/screens/settings/components/shared/dialog_filter/widget_dialog_filter.dart b/lib/screens/settings/components/shared/dialog_filter/widget_dialog_filter.dart index 76cd729..eb03e23 100644 --- a/lib/screens/settings/components/shared/dialog_filter/widget_dialog_filter.dart +++ b/lib/screens/settings/components/shared/dialog_filter/widget_dialog_filter.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/res/dimens.dart'; @@ -34,18 +35,20 @@ class _DialogFilterState extends State> { bool get _hasAnySelected => checkboxValues.contains(true); bool get _hasAnyUnselected => checkboxValues.contains(false); - late final ScrollController _scrollController; + final ScrollController _scrollController = ScrollController(); @override void initState() { super.initState(); - int i = 0; - for (; i < checkboxValues.length; i++) { - if (checkboxValues[i]) { - break; + SchedulerBinding.instance.addPostFrameCallback((_) { + int i = 0; + for (; i < checkboxValues.length; i++) { + if (checkboxValues[i]) { + break; + } } - } - _scrollController = ScrollController(initialScrollOffset: Dimens.grid56 * i); + _scrollController.jumpTo((Dimens.grid56 * i).clamp(0, _scrollController.position.maxScrollExtent)); + }); } @override @@ -61,79 +64,80 @@ class _DialogFilterState extends State> { titlePadding: Dimens.dialogIconTitlePadding, title: Text(widget.title), contentPadding: EdgeInsets.zero, - content: Column( - children: [ - Padding( - padding: Dimens.dialogIconTitlePadding, - child: Text(widget.description), - ), - const Divider(), - Expanded( - child: SingleChildScrollView( - controller: _scrollController, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - mainAxisSize: MainAxisSize.min, - children: List.generate( - widget.values.length, - (index) => CheckboxListTile( - value: checkboxValues[index], - controlAffinity: ListTileControlAffinity.leading, - title: Text( - widget.titleAdapter(context, widget.values[index]), - style: Theme.of(context).textTheme.bodyLarge, + content: SizedBox( + width: double.maxFinite, + child: Column( + children: [ + Padding( + padding: Dimens.dialogIconTitlePadding, + child: Text(widget.description), + ), + const Divider(), + Expanded( + child: SingleChildScrollView( + controller: _scrollController, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: List.generate( + widget.values.length, + (index) => CheckboxListTile( + value: checkboxValues[index], + controlAffinity: ListTileControlAffinity.leading, + title: Text( + widget.titleAdapter(context, widget.values[index]), + style: Theme.of(context).textTheme.bodyLarge, + ), + onChanged: (value) { + if (value != null) { + setState(() { + checkboxValues[index] = value; + }); + } + }, ), - onChanged: (value) { - if (value != null) { - setState(() { - checkboxValues[index] = value; - }); - } - }, ), ), ), ), - ), - const Divider(), - Padding( - padding: Dimens.dialogActionsPadding, - child: Row( - children: [ - SizedBox( - width: 40, - child: IconButton( - padding: EdgeInsets.zero, - icon: Icon(_hasAnyUnselected ? Icons.select_all : Icons.deselect), - onPressed: _toggleAll, - tooltip: _hasAnyUnselected - ? S.of(context).tooltipSelectAll - : S.of(context).tooltipDesecelectAll, + const Divider(), + Padding( + padding: Dimens.dialogActionsPadding, + child: Row( + children: [ + SizedBox( + width: 40, + child: IconButton( + padding: EdgeInsets.zero, + icon: Icon(_hasAnyUnselected ? Icons.select_all : Icons.deselect), + onPressed: _toggleAll, + tooltip: _hasAnyUnselected ? S.of(context).tooltipSelectAll : S.of(context).tooltipDesecelectAll, + ), ), - ), - const Spacer(), - TextButton( - onPressed: Navigator.of(context).pop, - child: Text(S.of(context).cancel), - ), - TextButton( - onPressed: _hasAnySelected - ? () { - final List selectedValues = []; - for (int i = 0; i < widget.values.length; i++) { - if (checkboxValues[i]) { - selectedValues.add(widget.values[i]); + const Spacer(), + TextButton( + onPressed: Navigator.of(context).pop, + child: Text(S.of(context).cancel), + ), + TextButton( + onPressed: _hasAnySelected + ? () { + final List selectedValues = []; + for (int i = 0; i < widget.values.length; i++) { + if (checkboxValues[i]) { + selectedValues.add(widget.values[i]); + } } + Navigator.of(context).pop(selectedValues); } - Navigator.of(context).pop(selectedValues); - } - : null, - child: Text(S.of(context).save), - ), - ], - ), - ) - ], + : null, + child: Text(S.of(context).save), + ), + ], + ), + ) + ], + ), ), ); } diff --git a/lib/screens/settings/components/shared/dialog_picker/widget_dialog_picker.dart b/lib/screens/settings/components/shared/dialog_picker/widget_dialog_picker.dart index c893027..dc31b5c 100644 --- a/lib/screens/settings/components/shared/dialog_picker/widget_dialog_picker.dart +++ b/lib/screens/settings/components/shared/dialog_picker/widget_dialog_picker.dart @@ -32,24 +32,28 @@ class _DialogPickerState extends State> { titlePadding: Dimens.dialogIconTitlePadding, title: Text(widget.title), contentPadding: EdgeInsets.zero, - content: Column( - mainAxisSize: MainAxisSize.min, - children: widget.values - .map( - (e) => RadioListTile( - value: e, - groupValue: _selected, - title: Text(widget.titleAdapter(context, e)), - onChanged: (T? value) { - if (value != null) { - setState(() { - _selected = value; - }); - } - }, - ), - ) - .toList(), + content: SizedBox( + width: double.maxFinite, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: widget.values + .map( + (e) => RadioListTile( + value: e, + groupValue: _selected, + title: Text(widget.titleAdapter(context, e)), + onChanged: (T? value) { + if (value != null) { + setState(() { + _selected = value; + }); + } + }, + ), + ) + .toList(), + ), ), actionsPadding: Dimens.dialogActionsPadding, actions: [ diff --git a/lib/screens/settings/components/shared/dialog_range_picker/widget_dialog_picker_range.dart b/lib/screens/settings/components/shared/dialog_range_picker/widget_dialog_picker_range.dart index cc3f4ca..56f7967 100644 --- a/lib/screens/settings/components/shared/dialog_range_picker/widget_dialog_picker_range.dart +++ b/lib/screens/settings/components/shared/dialog_range_picker/widget_dialog_picker_range.dart @@ -36,47 +36,50 @@ class _DialogRangePickerState extends State = String Function(BuildContext context, T value); + +class DialogSwitch extends StatefulWidget { + final IconData icon; + final String title; + final String? description; + final Map values; + final StringAdapter titleAdapter; + final StringAdapter? subtitleAdapter; + final bool Function(T value)? enabledAdapter; + final ValueChanged> onSave; + + const DialogSwitch({ + required this.icon, + required this.title, + this.description, + required this.values, + required this.titleAdapter, + this.subtitleAdapter, + this.enabledAdapter, + required this.onSave, + super.key, + }); + + @override + State> createState() => _DialogSwitchState(); +} + +class _DialogSwitchState extends State> { + late final Map _features = Map.from(widget.values); + + @override + Widget build(BuildContext context) { + return AlertDialog( + icon: Icon(widget.icon), + titlePadding: Dimens.dialogIconTitlePadding, + title: Text(widget.title), + contentPadding: EdgeInsets.zero, + content: SizedBox( + width: double.maxFinite, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (widget.description != null) ...[ + Padding( + padding: const EdgeInsets.symmetric(horizontal: Dimens.paddingL), + child: Text(widget.description!), + ), + const SizedBox(height: Dimens.grid16) + ], + ListView( + shrinkWrap: true, + children: _features.entries.map( + (entry) { + final isEnabled = widget.enabledAdapter?.call(entry.key) ?? true; + return Disable( + disable: !isEnabled, + child: SwitchListTile( + contentPadding: EdgeInsets.symmetric(horizontal: Dimens.dialogTitlePadding.left), + title: Text(widget.titleAdapter(context, entry.key)), + subtitle: widget.subtitleAdapter != null + ? Text( + widget.subtitleAdapter!.call(context, entry.key), + style: Theme.of(context).listTileTheme.subtitleTextStyle, + ) + : null, + value: isEnabled && _features[entry.key]!, + onChanged: (value) { + setState(() { + _features.update(entry.key, (_) => value); + }); + }, + ), + ); + }, + ).toList(), + ), + ], + ), + ), + actionsPadding: Dimens.dialogActionsPadding, + actions: [ + TextButton( + onPressed: Navigator.of(context).pop, + child: Text(S.of(context).cancel), + ), + TextButton( + onPressed: () { + widget.onSave(_features); + Navigator.of(context).pop(); + }, + child: Text(S.of(context).save), + ), + ], + ); + } +} diff --git a/lib/screens/settings/components/shared/disable/widget_disable.dart b/lib/screens/settings/components/shared/disable/widget_disable.dart new file mode 100644 index 0000000..3b1d276 --- /dev/null +++ b/lib/screens/settings/components/shared/disable/widget_disable.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:lightmeter/res/dimens.dart'; + +class Disable extends StatelessWidget { + final bool disable; + final Widget? child; + + const Disable({ + this.disable = true, + this.child, + super.key, + }); + + @override + Widget build(BuildContext context) { + return Opacity( + opacity: disable ? Dimens.disabledOpacity : Dimens.enabledOpacity, + child: IgnorePointer( + ignoring: disable, + child: child, + ), + ); + } +} diff --git a/lib/screens/settings/components/shared/iap_list_tile/widget_list_tile_iap.dart b/lib/screens/settings/components/shared/iap_list_tile/widget_list_tile_iap.dart index a2b980f..8cc522a 100644 --- a/lib/screens/settings/components/shared/iap_list_tile/widget_list_tile_iap.dart +++ b/lib/screens/settings/components/shared/iap_list_tile/widget_list_tile_iap.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:lightmeter/res/dimens.dart'; +import 'package:lightmeter/screens/settings/components/shared/disable/widget_disable.dart'; +import 'package:lightmeter/utils/context_utils.dart'; import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; /// Depends on the product status and replaces [onTap] with purchase callback @@ -22,13 +23,12 @@ class IAPListTile extends StatelessWidget { @override Widget build(BuildContext context) { - final isPurchased = IAPProducts.isPurchased(context, IAPProductType.paidFeatures); - return Opacity( - opacity: isPurchased ? Dimens.enabledOpacity : Dimens.disabledOpacity, + return Disable( + disable: !context.isPro, child: ListTile( leading: leading, title: title, - onTap: isPurchased ? onTap : null, + onTap: onTap, ), ); } 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 44de22e..1cf8762 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 @@ -2,7 +2,7 @@ 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/res/dimens.dart'; +import 'package:lightmeter/screens/settings/components/shared/disable/widget_disable.dart'; import 'package:lightmeter/screens/settings/components/theme/components/primary_color/components/primary_color_picker_dialog/widget_dialog_picker_primary_color.dart'; class PrimaryColorListTile extends StatelessWidget { @@ -11,13 +11,10 @@ class PrimaryColorListTile extends StatelessWidget { @override Widget build(BuildContext context) { if (UserPreferencesProvider.dynamicColorStateOf(context) == DynamicColorState.enabled) { - return Opacity( - opacity: Dimens.disabledOpacity, - child: IgnorePointer( - child: ListTile( - leading: const Icon(Icons.palette), - title: Text(S.of(context).primaryColor), - ), + return Disable( + child: ListTile( + leading: const Icon(Icons.palette), + title: Text(S.of(context).primaryColor), ), ); } diff --git a/lib/screens/settings/components/utils/show_buy_pro_dialog.dart b/lib/screens/settings/components/utils/show_buy_pro_dialog.dart deleted file mode 100644 index 50edbd1..0000000 --- a/lib/screens/settings/components/utils/show_buy_pro_dialog.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:lightmeter/data/models/feature.dart'; -import 'package:lightmeter/generated/l10n.dart'; -import 'package:lightmeter/providers/remote_config_provider.dart'; -import 'package:lightmeter/res/dimens.dart'; -import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; - -Future showBuyProDialog(BuildContext context) { - final unlockFeaturesEnabled = RemoteConfig.isEnabled(context, Feature.unlockProFeaturesText); - return showDialog( - context: context, - builder: (_) => AlertDialog( - icon: const Icon(Icons.star), - titlePadding: Dimens.dialogIconTitlePadding, - title: Text(unlockFeaturesEnabled ? S.of(context).proFeatures : S.of(context).lightmeterPro), - contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingL), - content: SingleChildScrollView( - child: Text( - unlockFeaturesEnabled ? S.of(context).unlockProFeaturesDescription : S.of(context).lightmeterProDescription, - ), - ), - actionsPadding: Dimens.dialogActionsPadding, - actions: [ - TextButton( - onPressed: Navigator.of(context).pop, - child: Text(S.of(context).cancel), - ), - FilledButton( - onPressed: () { - Navigator.of(context).pop(); - IAPProductsProvider.of(context).buy(IAPProductType.paidFeatures); - }, - child: Text(unlockFeaturesEnabled ? S.of(context).unlock : S.of(context).buy), - ), - ], - ), - ); -} diff --git a/lib/screens/settings/screen_settings.dart b/lib/screens/settings/screen_settings.dart index ea8e1c5..295e01c 100644 --- a/lib/screens/settings/screen_settings.dart +++ b/lib/screens/settings/screen_settings.dart @@ -7,7 +7,7 @@ import 'package:lightmeter/screens/settings/components/metering/widget_settings_ 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:m3_lightmeter_iap/m3_lightmeter_iap.dart'; +import 'package:lightmeter/utils/context_utils.dart'; class SettingsScreen extends StatefulWidget { const SettingsScreen({super.key}); @@ -38,8 +38,7 @@ class _SettingsScreenState extends State { SliverList( delegate: SliverChildListDelegate( [ - if (!IAPProducts.isPurchased(context, IAPProductType.paidFeatures)) - const LightmeterProSettingsSection(), + if (!context.isPro) const LightmeterProSettingsSection(), const MeteringSettingsSection(), const GeneralSettingsSection(), const ThemeSettingsSection(), diff --git a/lib/screens/shared/pro_features_dialog/widget_dialog_pro_features.dart b/lib/screens/shared/pro_features_dialog/widget_dialog_pro_features.dart new file mode 100644 index 0000000..c58e94d --- /dev/null +++ b/lib/screens/shared/pro_features_dialog/widget_dialog_pro_features.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:lightmeter/generated/l10n.dart'; +import 'package:lightmeter/res/dimens.dart'; +import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/components/animated_dialog/widget_dialog_animated.dart'; +import 'package:lightmeter/screens/shared/transparent_dialog/widget_dialog_transparent.dart'; +import 'package:lightmeter/utils/text_height.dart'; +import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; + +class ProFeaturesDialog extends StatelessWidget { + const ProFeaturesDialog({super.key}); + + double height(BuildContext context) => TransparentDialog.height( + context, + title: S.of(context).proFeatures, + contextHeight: dialogTextHeight( + context, + S.of(context).unlockProFeaturesDescription, + Theme.of(context).textTheme.bodyMedium, + Dimens.paddingL * 2, + ), + ); + + @override + Widget build(BuildContext context) { + return TransparentDialog( + icon: Icons.star, + title: S.of(context).proFeatures, + scrollableContent: false, + content: Flexible( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: Dimens.paddingL), + child: Text( + S.of(context).unlockProFeaturesDescription, + style: Theme.of(context).textTheme.bodyMedium, + ), + ), + ), + ), + actions: [ + TextButton( + onPressed: () => _close(context), + child: Text(S.of(context).cancel), + ), + FilledButton( + onPressed: () { + _close(context).then((_) => IAPProductsProvider.maybeOf(context)?.buy(IAPProductType.paidFeatures)); + }, + child: Text(S.of(context).unlock), + ), + ], + ); + } + + Future _close(BuildContext context) async => AnimatedDialog.maybeClose(context) ?? Navigator.of(context).pop(); +} diff --git a/lib/screens/shared/transparent_dialog/widget_dialog_transparent.dart b/lib/screens/shared/transparent_dialog/widget_dialog_transparent.dart new file mode 100644 index 0000000..ee3870e --- /dev/null +++ b/lib/screens/shared/transparent_dialog/widget_dialog_transparent.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; +import 'package:lightmeter/res/dimens.dart'; +import 'package:lightmeter/utils/text_height.dart'; + +class TransparentDialog extends StatelessWidget { + final IconData icon; + final String title; + final String? subtitle; + final Widget content; + final bool scrollableContent; + final List actions; + + const TransparentDialog({ + required this.icon, + required this.title, + this.subtitle, + required this.content, + required this.scrollableContent, + this.actions = const [], + super.key, + }); + + static double height( + BuildContext context, { + required String title, + String? subtitle, + required double contextHeight, + bool scrollableContent = false, + }) { + double height = IconTheme.of(context).size! + Dimens.dialogTitlePadding.vertical; + height += dialogTextHeight( + context, + title, + Theme.of(context).textTheme.headlineSmall, + Dimens.dialogIconTitlePadding.horizontal, + ) + + Dimens.dialogIconTitlePadding.vertical; + if (subtitle != null) { + height += dialogTextHeight( + context, + subtitle, + Theme.of(context).textTheme.bodyMedium, + Dimens.dialogIconTitlePadding.horizontal, + ) + + Dimens.dialogIconTitlePadding.vertical; + } + height += contextHeight; + if (scrollableContent) height += 1; + return height += 48 + Dimens.dialogActionsPadding.vertical; + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: Dimens.dialogTitlePadding, + child: Icon(icon), + ), + Padding( + padding: Dimens.dialogIconTitlePadding, + child: Text( + title, + style: Theme.of(context).textTheme.headlineSmall, + textAlign: TextAlign.center, + ), + ), + if (subtitle != null) + Padding( + padding: Dimens.dialogIconTitlePadding, + child: Text( + subtitle!, + style: Theme.of(context).textTheme.bodyMedium, + textAlign: TextAlign.center, + ), + ), + ], + ), + if (scrollableContent) const Divider(), + content, + if (scrollableContent) const Divider(), + Padding( + padding: Dimens.dialogActionsPadding, + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: _actions().toList(), + ), + ), + ], + ); + } + + Iterable _actions() sync* { + for (int i = 0; i < actions.length; i++) { + yield i == 0 ? const Spacer() : const SizedBox(width: Dimens.grid16); + yield actions[i]; + } + } +} diff --git a/lib/utils/context_utils.dart b/lib/utils/context_utils.dart new file mode 100644 index 0000000..f64ea76 --- /dev/null +++ b/lib/utils/context_utils.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; +import 'package:lightmeter/data/models/metering_screen_layout_config.dart'; +import 'package:lightmeter/providers/user_preferences_provider.dart'; +import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; + +extension BuildContextUtils on BuildContext { + bool meteringFeature(MeteringScreenLayoutFeature feature) { + return UserPreferencesProvider.meteringScreenFeatureOf(this, feature); + } + + bool get isPro => IAPProducts.isPurchased(this, IAPProductType.paidFeatures); +} diff --git a/lib/utils/ev_from_bytes.dart b/lib/utils/ev_from_bytes.dart new file mode 100644 index 0000000..c7d12ea --- /dev/null +++ b/lib/utils/ev_from_bytes.dart @@ -0,0 +1,27 @@ +import 'dart:developer'; +import 'dart:math' as math; +import 'dart:typed_data'; + +import 'package:exif/exif.dart'; +import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; + +Future evFromImage(Uint8List bytes) async { + try { + final tags = await readExifFromBytes(bytes); + final iso = double.tryParse("${tags["EXIF ISOSpeedRatings"]}"); + final apertureValueRatio = (tags["EXIF FNumber"]?.values as IfdRatios?)?.ratios.first; + final speedValueRatio = (tags["EXIF ExposureTime"]?.values as IfdRatios?)?.ratios.first; + if (iso == null || apertureValueRatio == null || speedValueRatio == null) { + log('Error parsing EXIF: ${tags.keys}'); + return null; + } + + final aperture = apertureValueRatio.numerator / apertureValueRatio.denominator; + final speed = speedValueRatio.numerator / speedValueRatio.denominator; + + return log2(math.pow(aperture, 2)) - log2(speed) - log2(iso / 100); + } catch (e) { + log(e.toString()); + return null; + } +} diff --git a/lib/utils/map_model.dart b/lib/utils/map_model.dart new file mode 100644 index 0000000..1f5ea09 --- /dev/null +++ b/lib/utils/map_model.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; + +class MapModel extends InheritedModel { + final Map data; + + const MapModel({ + required this.data, + required super.child, + }); + + @override + bool updateShouldNotify(MapModel oldWidget) => oldWidget.data != data; + + @override + bool updateShouldNotifyDependent( + MapModel oldWidget, + Set dependencies, + ) { + for (final dependecy in dependencies) { + if (oldWidget.data[dependecy] != data[dependecy]) { + return true; + } + } + return false; + } +} diff --git a/lib/utils/text_height.dart b/lib/utils/text_height.dart new file mode 100644 index 0000000..f5a0fe1 --- /dev/null +++ b/lib/utils/text_height.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:lightmeter/res/dimens.dart'; + +double dialogTextHeight( + BuildContext context, + String text, + TextStyle? style, + double textPadding, +) => + textHeight( + text, + style, + MediaQuery.sizeOf(context).width - Dimens.dialogMargin.horizontal - textPadding, + ); + +double textHeight( + String text, + TextStyle? style, + double maxWidth, +) { + final TextPainter titlePainter = TextPainter( + text: TextSpan( + text: text, + style: style, + ), + textDirection: TextDirection.ltr, + )..layout(maxWidth: maxWidth); + return titlePainter.height; +} diff --git a/lib/utils/text_line_height.dart b/lib/utils/text_line_height.dart deleted file mode 100644 index a531074..0000000 --- a/lib/utils/text_line_height.dart +++ /dev/null @@ -1,5 +0,0 @@ -import 'package:flutter/widgets.dart'; - -extension TextLineHeight on TextStyle { - double get lineHeight => fontSize! * height!; -} diff --git a/lib/utils/to_string_signed.dart b/lib/utils/to_string_signed.dart index 4122893..fcced8d 100644 --- a/lib/utils/to_string_signed.dart +++ b/lib/utils/to_string_signed.dart @@ -1,13 +1,4 @@ -extension SignedString on num { - String toStringSigned() { - if (this > 0) { - return "+${toString()}"; - } else { - return toString(); - } - } -} - +/// Returns value in form -1 or + 1. The only exception - 0. extension SignedStringDouble on double { String toStringSignedAsFixed(int fractionDigits) { if (this > 0) { diff --git a/pubspec.yaml b/pubspec.yaml index a7eb3a2..93635ef 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.15.1+42 +version: 0.16.0+45 environment: sdk: ">=3.0.0 <4.0.0" @@ -28,7 +28,7 @@ dependencies: m3_lightmeter_iap: git: url: "https://github.com/vodemn/m3_lightmeter_iap" - ref: v0.6.2 + ref: v0.7.1 m3_lightmeter_resources: git: url: "https://github.com/vodemn/m3_lightmeter_resources" @@ -45,8 +45,7 @@ dependencies: dev_dependencies: bloc_test: 9.1.3 build_runner: 2.4.6 - flutter_launcher_icons: 0.11.0 - flutter_native_splash: 2.2.16 + flutter_native_splash: 2.3.5 flutter_test: sdk: flutter google_fonts: 3.0.1 diff --git a/screenshots/generate_screenshots.dart b/screenshots/generate_screenshots.dart index 8c8e7e9..814cfd1 100644 --- a/screenshots/generate_screenshots.dart +++ b/screenshots/generate_screenshots.dart @@ -46,7 +46,6 @@ void main() { MeteringScreenLayoutFeature.equipmentProfiles: true, MeteringScreenLayoutFeature.extremeExposurePairs: true, MeteringScreenLayoutFeature.filmPicker: true, - MeteringScreenLayoutFeature.histogram: false, }.toJson(), ), diff --git a/test/application_mock.dart b/test/application_mock.dart index dbcf260..a5aecb3 100644 --- a/test/application_mock.dart +++ b/test/application_mock.dart @@ -2,12 +2,18 @@ import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/res/theme.dart'; +import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; /// Provides [MaterialApp] with default theme and "en" localization class WidgetTestApplicationMock extends StatelessWidget { + final IAPProductStatus productStatus; final Widget child; - const WidgetTestApplicationMock({required this.child, super.key}); + const WidgetTestApplicationMock({ + this.productStatus = IAPProductStatus.purchased, + required this.child, + super.key, + }); @override Widget build(BuildContext context) { diff --git a/test/data/models/camera_features_config_test.dart b/test/data/models/camera_features_config_test.dart new file mode 100644 index 0000000..d7261a8 --- /dev/null +++ b/test/data/models/camera_features_config_test.dart @@ -0,0 +1,47 @@ +import 'package:lightmeter/data/models/camera_feature.dart'; +import 'package:test/test.dart'; + +void main() { + group( + 'fromJson()', + () { + test('All keys', () { + expect( + CameraFeaturesConfigJson.fromJson( + { + 'spotMetering': true, + 'histogram': true, + }, + ), + { + CameraFeature.spotMetering: true, + CameraFeature.histogram: true, + }, + ); + }); + + test('Legacy (no spotMetering & histogram)', () { + expect( + CameraFeaturesConfigJson.fromJson({}), + { + CameraFeature.spotMetering: false, + CameraFeature.histogram: false, + }, + ); + }); + }, + ); + + test('toJson()', () { + expect( + { + CameraFeature.spotMetering: true, + CameraFeature.histogram: true, + }.toJson(), + { + 'spotMetering': true, + 'histogram': true, + }, + ); + }); +} diff --git a/test/data/models/metering_screen_layout_config_test.dart b/test/data/models/metering_screen_layout_config_test.dart index 9e9393e..f3216ba 100644 --- a/test/data/models/metering_screen_layout_config_test.dart +++ b/test/data/models/metering_screen_layout_config_test.dart @@ -18,7 +18,6 @@ void main() { { MeteringScreenLayoutFeature.extremeExposurePairs: true, MeteringScreenLayoutFeature.filmPicker: true, - MeteringScreenLayoutFeature.histogram: true, MeteringScreenLayoutFeature.equipmentProfiles: true, }, ); @@ -35,7 +34,6 @@ void main() { { MeteringScreenLayoutFeature.extremeExposurePairs: false, MeteringScreenLayoutFeature.filmPicker: false, - MeteringScreenLayoutFeature.histogram: true, MeteringScreenLayoutFeature.equipmentProfiles: true, }, ); @@ -53,7 +51,6 @@ void main() { { MeteringScreenLayoutFeature.extremeExposurePairs: false, MeteringScreenLayoutFeature.filmPicker: false, - MeteringScreenLayoutFeature.histogram: false, MeteringScreenLayoutFeature.equipmentProfiles: true, }, ); @@ -67,13 +64,11 @@ void main() { MeteringScreenLayoutFeature.equipmentProfiles: true, MeteringScreenLayoutFeature.extremeExposurePairs: true, MeteringScreenLayoutFeature.filmPicker: true, - MeteringScreenLayoutFeature.histogram: true, }.toJson(), { - '3': true, - '0': true, - '1': true, - '2': true, + 'equipmentProfiles': true, + 'extremeExposurePairs': true, + 'filmPicker': true, }, ); }); diff --git a/test/data/shared_prefs_service_test.dart b/test/data/shared_prefs_service_test.dart index 8ec42e1..269d002 100644 --- a/test/data/shared_prefs_service_test.dart +++ b/test/data/shared_prefs_service_test.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:lightmeter/data/models/camera_feature.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'; @@ -180,6 +181,25 @@ void main() { }); }); + group('showEv100', () { + test('get default', () { + when(() => sharedPreferences.getBool(UserPreferencesService.showEv100Key)).thenReturn(null); + expect(service.showEv100, false); + }); + + test('get', () { + when(() => sharedPreferences.getBool(UserPreferencesService.showEv100Key)).thenReturn(true); + expect(service.showEv100, true); + }); + + test('set', () { + when(() => sharedPreferences.setBool(UserPreferencesService.showEv100Key, false)) + .thenAnswer((_) => Future.value(true)); + service.showEv100 = false; + verify(() => sharedPreferences.setBool(UserPreferencesService.showEv100Key, false)).called(1); + }); + }); + group('meteringScreenLayout', () { test('get default', () { when( @@ -191,12 +211,11 @@ void main() { MeteringScreenLayoutFeature.extremeExposurePairs: true, MeteringScreenLayoutFeature.filmPicker: true, MeteringScreenLayoutFeature.equipmentProfiles: true, - MeteringScreenLayoutFeature.histogram: true, }, ); }); - test('get', () { + test('get (legacy)', () { when( () => sharedPreferences.getString(UserPreferencesService.meteringScreenLayoutKey), ).thenReturn("""{"0":false,"1":true}"""); @@ -206,7 +225,20 @@ void main() { MeteringScreenLayoutFeature.extremeExposurePairs: false, MeteringScreenLayoutFeature.filmPicker: true, MeteringScreenLayoutFeature.equipmentProfiles: true, - MeteringScreenLayoutFeature.histogram: true, + }, + ); + }); + + test('get', () { + when( + () => sharedPreferences.getString(UserPreferencesService.meteringScreenLayoutKey), + ).thenReturn("""{"extremeExposurePairs":false,"filmPicker":true}"""); + expect( + service.meteringScreenLayout, + { + MeteringScreenLayoutFeature.extremeExposurePairs: false, + MeteringScreenLayoutFeature.filmPicker: true, + MeteringScreenLayoutFeature.equipmentProfiles: true, }, ); }); @@ -215,19 +247,62 @@ void main() { when( () => sharedPreferences.setString( UserPreferencesService.meteringScreenLayoutKey, - """{"0":false,"1":true,"2":true,"3":true}""", + """{"extremeExposurePairs":false,"filmPicker":true,"equipmentProfiles":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,"3":true}""", + """{"extremeExposurePairs":false,"filmPicker":true,"equipmentProfiles":true}""", + ), + ).called(1); + }); + }); + + group('cameraFeatures', () { + test('get default', () { + when(() => sharedPreferences.getString(UserPreferencesService.cameraFeaturesKey)).thenReturn(null); + expect( + service.cameraFeatures, + { + CameraFeature.spotMetering: false, + CameraFeature.histogram: false, + }, + ); + }); + + test('get', () { + when(() => sharedPreferences.getString(UserPreferencesService.cameraFeaturesKey)) + .thenReturn("""{"spotMetering":false,"histogram":true}"""); + expect( + service.cameraFeatures, + { + CameraFeature.spotMetering: false, + CameraFeature.histogram: true, + }, + ); + }); + + test('set', () { + when( + () => sharedPreferences.setString( + UserPreferencesService.cameraFeaturesKey, + """{"spotMetering":false,"histogram":true}""", + ), + ).thenAnswer((_) => Future.value(true)); + service.cameraFeatures = { + CameraFeature.spotMetering: false, + CameraFeature.histogram: true, + }; + verify( + () => sharedPreferences.setString( + UserPreferencesService.cameraFeaturesKey, + """{"spotMetering":false,"histogram":true}""", ), ).called(1); }); diff --git a/test/function_mock.dart b/test/function_mock.dart new file mode 100644 index 0000000..8a4abfe --- /dev/null +++ b/test/function_mock.dart @@ -0,0 +1,7 @@ +import 'package:mocktail/mocktail.dart'; + +class _ValueChanged { + void onChanged(T value) {} +} + +class MockValueChanged extends Mock implements _ValueChanged {} diff --git a/test/interactors/metering_interactor_test.dart b/test/interactors/metering_interactor_test.dart index 1f0b05a..083a116 100644 --- a/test/interactors/metering_interactor_test.dart +++ b/test/interactors/metering_interactor_test.dart @@ -236,7 +236,7 @@ void main() { ); group( - 'Haptics', + 'Light sensor', () { test('hasAmbientLightSensor() - true', () async { when(() => mockLightSensorService.hasSensor()).thenAnswer((_) async => true); diff --git a/test/providers/equipment_profile_provider_test.dart b/test/providers/equipment_profile_provider_test.dart index 83f04f7..fb329cd 100644 --- a/test/providers/equipment_profile_provider_test.dart +++ b/test/providers/equipment_profile_provider_test.dart @@ -260,7 +260,7 @@ class _Application extends StatelessWidget { ElevatedButton( key: updateProfileButtonKey(profile.id), onPressed: () { - EquipmentProfileProvider.of(context).updateProdile( + EquipmentProfileProvider.of(context).updateProfile( profile.copyWith( name: '${profile.name} updated', isoValues: _customProfiles.first.isoValues, diff --git a/test/providers/remote_config_provider_test.dart b/test/providers/remote_config_provider_test.dart index aa6815d..a215cbe 100644 --- a/test/providers/remote_config_provider_test.dart +++ b/test/providers/remote_config_provider_test.dart @@ -18,6 +18,7 @@ void main() { }); setUp(() { + when(() => mockRemoteConfigService.fetchConfig()).thenAnswer((_) async {}); when(() => mockRemoteConfigService.getValue(Feature.unlockProFeaturesText)).thenReturn(false); when(() => mockRemoteConfigService.getAll()).thenReturn({Feature.unlockProFeaturesText: false}); }); diff --git a/test/providers/user_preferences_provider_test.dart b/test/providers/user_preferences_provider_test.dart index ffa7993..1a91419 100644 --- a/test/providers/user_preferences_provider_test.dart +++ b/test/providers/user_preferences_provider_test.dart @@ -1,6 +1,7 @@ import 'package:dynamic_color/test_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:lightmeter/data/models/camera_feature.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'; @@ -26,12 +27,16 @@ void main() { setUp(() { when(() => mockUserPreferencesService.evSourceType).thenReturn(EvSourceType.camera); + when(() => mockUserPreferencesService.showEv100).thenReturn(false); when(() => mockUserPreferencesService.stopType).thenReturn(StopType.third); when(() => mockUserPreferencesService.meteringScreenLayout).thenReturn({ MeteringScreenLayoutFeature.extremeExposurePairs: true, MeteringScreenLayoutFeature.filmPicker: true, MeteringScreenLayoutFeature.equipmentProfiles: true, - MeteringScreenLayoutFeature.histogram: true, + }); + when(() => mockUserPreferencesService.cameraFeatures).thenReturn({ + CameraFeature.spotMetering: true, + CameraFeature.histogram: true, }); when(() => mockUserPreferencesService.locale).thenReturn(SupportedLocale.en); when(() => mockUserPreferencesService.themeType).thenReturn(ThemeType.light); @@ -160,6 +165,27 @@ void main() { }, ); + testWidgets( + 'Toggle Ev100', + (tester) async { + when(() => mockUserPreferencesService.showEv100).thenReturn(false); + await pumpTestWidget( + tester, + builder: (context) => ElevatedButton( + onPressed: () => UserPreferencesProvider.of(context).toggleShowEv100(), + child: Text('${UserPreferencesProvider.showEv100Of(context)}'), + ), + ); + await tester.pumpAndSettle(); + expect(find.text("${false}"), findsOneWidget); + + await tester.tap(find.text("${false}")); + await tester.pumpAndSettle(); + expect(find.text("${true}"), findsOneWidget); + verify(() => mockUserPreferencesService.showEv100 = true).called(1); + }, + ); + testWidgets( 'Set metering screen layout config', (tester) async { @@ -184,7 +210,6 @@ void main() { MeteringScreenLayoutFeature.equipmentProfiles: true, MeteringScreenLayoutFeature.extremeExposurePairs: false, MeteringScreenLayoutFeature.filmPicker: false, - MeteringScreenLayoutFeature.histogram: true, }), child: const Text(''), ), @@ -196,20 +221,64 @@ void main() { expect(find.text("${MeteringScreenLayoutFeature.equipmentProfiles}: true"), findsNWidgets(2)); expect(find.text("${MeteringScreenLayoutFeature.extremeExposurePairs}: true"), findsNWidgets(2)); expect(find.text("${MeteringScreenLayoutFeature.filmPicker}: true"), findsNWidgets(2)); - expect(find.text("${MeteringScreenLayoutFeature.histogram}: true"), findsNWidgets(2)); await tester.tap(find.byType(ElevatedButton)); await tester.pumpAndSettle(); expect(find.text("${MeteringScreenLayoutFeature.equipmentProfiles}: true"), findsNWidgets(2)); expect(find.text("${MeteringScreenLayoutFeature.extremeExposurePairs}: false"), findsNWidgets(2)); expect(find.text("${MeteringScreenLayoutFeature.filmPicker}: false"), findsNWidgets(2)); - expect(find.text("${MeteringScreenLayoutFeature.histogram}: true"), findsNWidgets(2)); verify( () => mockUserPreferencesService.meteringScreenLayout = { MeteringScreenLayoutFeature.extremeExposurePairs: false, MeteringScreenLayoutFeature.filmPicker: false, MeteringScreenLayoutFeature.equipmentProfiles: true, - MeteringScreenLayoutFeature.histogram: true, + }, + ).called(1); + }, + ); + + testWidgets( + 'Set camera features config', + (tester) async { + await pumpTestWidget( + tester, + builder: (context) { + final config = UserPreferencesProvider.cameraConfigOf(context); + return Column( + children: [ + ...List.generate( + config.length, + (index) => Text('${config.keys.toList()[index]}: ${config.values.toList()[index]}'), + ), + ...List.generate( + CameraFeature.values.length, + (index) => Text( + '${CameraFeature.values[index]}: ${UserPreferencesProvider.cameraFeatureOf(context, CameraFeature.values[index])}', + ), + ), + ElevatedButton( + onPressed: () => UserPreferencesProvider.of(context).setCameraFeature({ + CameraFeature.spotMetering: true, + CameraFeature.histogram: false, + }), + child: const Text(''), + ), + ], + ); + }, + ); + // Match `findsNWidgets(2)` to verify that `cameraFeatureOf` specific results are the same as the whole config + expect(find.text("${CameraFeature.spotMetering}: true"), findsNWidgets(2)); + expect(find.text("${CameraFeature.histogram}: true"), findsNWidgets(2)); + + await tester.tap(find.byType(ElevatedButton)); + await tester.pumpAndSettle(); + expect(find.text("${CameraFeature.spotMetering}: true"), findsNWidgets(2)); + expect(find.text("${CameraFeature.histogram}: false"), findsNWidgets(2)); + verify( + () => mockUserPreferencesService.cameraFeatures = { + CameraFeature.spotMetering: true, + CameraFeature.histogram: false, }, ).called(1); }, diff --git a/test/screens/metering/bloc_metering_test.dart b/test/screens/metering/bloc_metering_test.dart index fbffbbc..f07d35c 100644 --- a/test/screens/metering/bloc_metering_test.dart +++ b/test/screens/metering/bloc_metering_test.dart @@ -7,9 +7,9 @@ import 'package:lightmeter/screens/metering/communication/event_communication_me as communication_events; import 'package:lightmeter/screens/metering/communication/state_communication_metering.dart' as communication_states; -import 'package:lightmeter/screens/metering/components/shared/volume_keys_notifier/notifier_volume_keys.dart'; import 'package:lightmeter/screens/metering/event_metering.dart'; import 'package:lightmeter/screens/metering/state_metering.dart'; +import 'package:lightmeter/screens/metering/utils/notifier_volume_keys.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; import 'package:mocktail/mocktail.dart'; import 'package:test/test.dart'; diff --git a/test/screens/metering/components/camera/bloc_container_camera_test.dart b/test/screens/metering/components/camera/bloc_container_camera_test.dart index d54c0a5..52c57c8 100644 --- a/test/screens/metering/components/camera/bloc_container_camera_test.dart +++ b/test/screens/metering/components/camera/bloc_container_camera_test.dart @@ -4,10 +4,8 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:lightmeter/interactors/metering_interactor.dart'; import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart'; -import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart' - as communication_events; -import 'package:lightmeter/screens/metering/communication/state_communication_metering.dart' - as communication_states; +import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart' as communication_events; +import 'package:lightmeter/screens/metering/communication/state_communication_metering.dart' as communication_states; 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/models/camera_error_type.dart'; @@ -16,9 +14,9 @@ import 'package:mocktail/mocktail.dart'; class _MockMeteringInteractor extends Mock implements MeteringInteractor {} -class _MockMeteringCommunicationBloc extends MockBloc< - communication_events.MeteringCommunicationEvent, - communication_states.MeteringCommunicationState> implements MeteringCommunicationBloc {} +class _MockMeteringCommunicationBloc + extends MockBloc + implements MeteringCommunicationBloc {} void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -147,8 +145,7 @@ void main() { verify(() => meteringInteractor.requestCameraPermission()).called(1); }, expect: () => [ - isA() - .having((state) => state.error, "error", CameraErrorType.permissionNotGranted), + isA().having((state) => state.error, "error", CameraErrorType.permissionNotGranted), ], ); @@ -166,8 +163,7 @@ void main() { }, expect: () => [ isA(), - isA() - .having((state) => state.error, "error", CameraErrorType.permissionNotGranted), + isA().having((state) => state.error, "error", CameraErrorType.permissionNotGranted), ], ); @@ -215,8 +211,7 @@ void main() { 'No cameras detected error', setUp: () { when(() => meteringInteractor.checkCameraPermission()).thenAnswer((_) async => true); - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler( + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( cameraMethodChannel, (methodCall) async => cameraMethodCallSuccessHandler(methodCall, cameras: const []), ); @@ -232,8 +227,7 @@ void main() { }, expect: () => [ isA(), - isA() - .having((state) => state.error, "error", CameraErrorType.noCamerasDetected), + isA().having((state) => state.error, "error", CameraErrorType.noCamerasDetected), ], ); @@ -241,8 +235,7 @@ void main() { 'No back facing cameras available', setUp: () { when(() => meteringInteractor.checkCameraPermission()).thenAnswer((_) async => true); - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler( + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( cameraMethodChannel, (methodCall) async => cameraMethodCallSuccessHandler(methodCall, cameras: frontCameras), ); @@ -263,8 +256,7 @@ void main() { 'Catch other initialization errors', setUp: () { when(() => meteringInteractor.checkCameraPermission()).thenAnswer((_) async => true); - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler( + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( cameraMethodChannel, (methodCall) async { switch (methodCall.method) { @@ -300,10 +292,8 @@ void main() { act: (bloc) async { bloc.add(const InitializeEvent()); await Future.delayed(Duration.zero); - TestWidgetsFlutterBinding.instance - .handleAppLifecycleStateChanged(AppLifecycleState.detached); - TestWidgetsFlutterBinding.instance - .handleAppLifecycleStateChanged(AppLifecycleState.resumed); + TestWidgetsFlutterBinding.instance.handleAppLifecycleStateChanged(AppLifecycleState.detached); + TestWidgetsFlutterBinding.instance.handleAppLifecycleStateChanged(AppLifecycleState.resumed); }, verify: (_) { verify(() => meteringInteractor.checkCameraPermission()).called(2); @@ -500,6 +490,29 @@ void main() { ); }, ); + + group( + '`ExposureSpotChangedEvent`', + () { + blocTest( + 'Set exposure spot multiple times', + setUp: () { + when(() => meteringInteractor.checkCameraPermission()).thenAnswer((_) async => true); + }, + build: () => bloc, + act: (bloc) async { + bloc.add(const InitializeEvent()); + await Future.delayed(Duration.zero); + bloc.add(const ExposureSpotChangedEvent(Offset(0.1, 0.1))); + bloc.add(const ExposureSpotChangedEvent(Offset(1.0, 0.5))); + }, + verify: (_) { + verify(() => meteringInteractor.checkCameraPermission()).called(1); + }, + expect: () => [...initializedStateSequence], + ); + }, + ); } extension _MethodChannelMock on MethodChannel { diff --git a/test/screens/metering/components/camera/event_container_camera_test.dart b/test/screens/metering/components/camera/event_container_camera_test.dart index f136b5f..3751f10 100644 --- a/test/screens/metering/components/camera/event_container_camera_test.dart +++ b/test/screens/metering/components/camera/event_container_camera_test.dart @@ -1,5 +1,7 @@ // ignore_for_file: prefer_const_constructors +import 'dart:ui'; + import 'package:lightmeter/screens/metering/components/camera_container/event_container_camera.dart'; import 'package:test/test.dart'; @@ -41,4 +43,23 @@ void main() { }); }, ); + + group( + '`ExposureSpotChangedEvent`', + () { + final a = ExposureSpotChangedEvent(Offset(0.0, 0.0)); + final b = ExposureSpotChangedEvent(Offset(0.0, 0.0)); + final c = ExposureSpotChangedEvent(Offset(2.0, 2.0)); + test('==', () { + expect(a == b && b == a, true); + expect(a != c && c != a, true); + expect(b != c && c != b, true); + }); + test('hashCode', () { + expect(a.hashCode == b.hashCode, true); + expect(a.hashCode != c.hashCode, true); + expect(b.hashCode != c.hashCode, true); + }); + }, + ); } diff --git a/test/screens/metering/components/shared/readings_container/shared/dialog_picker_test.dart b/test/screens/metering/components/shared/readings_container/shared/dialog_picker_test.dart index 39c38ba..bd037af 100644 --- a/test/screens/metering/components/shared/readings_container/shared/dialog_picker_test.dart +++ b/test/screens/metering/components/shared/readings_container/shared/dialog_picker_test.dart @@ -7,16 +7,11 @@ import 'package:lightmeter/screens/metering/components/shared/readings_container import 'package:mocktail/mocktail.dart'; import '../../../../../../application_mock.dart'; +import '../../../../../../function_mock.dart'; import '../utils.dart'; -class _ValueChanged { - void onChanged(T value) {} -} - -class _MockValueChanged extends Mock implements _ValueChanged {} - void main() { - final functions = _MockValueChanged(); + final functions = MockValueChanged(); group( 'onChanged', diff --git a/test/screens/metering/utils/listener_equipment_profiles_test.dart b/test/screens/metering/utils/listener_equipment_profiles_test.dart new file mode 100644 index 0000000..ae6cb87 --- /dev/null +++ b/test/screens/metering/utils/listener_equipment_profiles_test.dart @@ -0,0 +1,138 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:lightmeter/providers/equipment_profile_provider.dart'; +import 'package:lightmeter/screens/metering/utils/listener_equipment_profiles.dart'; +import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; +import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../../function_mock.dart'; + +class _MockIAPStorageService extends Mock implements IAPStorageService {} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final storageService = _MockIAPStorageService(); + final equipmentProfileProviderKey = GlobalKey(); + final onDidChangeDependencies = MockValueChanged(); + + tearDown(() { + reset(onDidChangeDependencies); + reset(storageService); + }); + + Future pumpTestWidget(WidgetTester tester) async { + await tester.pumpWidget( + IAPProducts( + products: [ + IAPProduct( + storeId: IAPProductType.paidFeatures.storeId, + status: IAPProductStatus.purchased, + ), + ], + child: EquipmentProfileProvider( + key: equipmentProfileProviderKey, + storageService: storageService, + child: MaterialApp( + home: EquipmentProfileListener( + onDidChangeDependencies: onDidChangeDependencies.onChanged, + child: Builder(builder: (context) => Text(EquipmentProfiles.selectedOf(context).name)), + ), + ), + ), + ), + ); + } + + testWidgets( + 'Trigger `onDidChangeDependencies` by selecting a new profile', + (tester) async { + when(() => storageService.equipmentProfiles).thenReturn(List.from(_customProfiles)); + when(() => storageService.selectedEquipmentProfileId).thenReturn(''); + await pumpTestWidget(tester); + + equipmentProfileProviderKey.currentState!.setProfile(_customProfiles[0]); + await tester.pump(); + verify(() => onDidChangeDependencies.onChanged(_customProfiles[0])).called(1); + }, + ); + + testWidgets( + 'Trigger `onDidChangeDependencies` by updating the selected profile', + (tester) async { + when(() => storageService.equipmentProfiles).thenReturn(List.from(_customProfiles)); + when(() => storageService.selectedEquipmentProfileId).thenReturn(_customProfiles[0].id); + await pumpTestWidget(tester); + + final updatedProfile1 = _customProfiles[0].copyWith(name: 'Test 1 updated'); + equipmentProfileProviderKey.currentState!.updateProfile(updatedProfile1); + await tester.pump(); + verify(() => onDidChangeDependencies.onChanged(updatedProfile1)).called(1); + + /// Verify that updating the not selected profile doesn't trigger the callback + final updatedProfile2 = _customProfiles[1].copyWith(name: 'Test 2 updated'); + equipmentProfileProviderKey.currentState!.updateProfile(updatedProfile2); + await tester.pump(); + verifyNever(() => onDidChangeDependencies.onChanged(updatedProfile2)); + }, + ); + + testWidgets( + "Don't trigger `onDidChangeDependencies` by updating the unselected profile", + (tester) async { + when(() => storageService.equipmentProfiles).thenReturn(List.from(_customProfiles)); + when(() => storageService.selectedEquipmentProfileId).thenReturn(_customProfiles[0].id); + await pumpTestWidget(tester); + + final updatedProfile2 = _customProfiles[1].copyWith(name: 'Test 2 updated'); + equipmentProfileProviderKey.currentState!.updateProfile(updatedProfile2); + await tester.pump(); + verifyNever(() => onDidChangeDependencies.onChanged(updatedProfile2)); + }, + ); +} + +final List _customProfiles = [ + const EquipmentProfile( + id: '1', + name: 'Test 1', + apertureValues: [ + ApertureValue(4.0, StopType.full), + ApertureValue(4.5, StopType.third), + ApertureValue(4.8, StopType.half), + ApertureValue(5.0, StopType.third), + ApertureValue(5.6, StopType.full), + ApertureValue(6.3, StopType.third), + ApertureValue(6.7, StopType.half), + ApertureValue(7.1, StopType.third), + ApertureValue(8, StopType.full), + ], + ndValues: [ + NdValue(0), + NdValue(2), + NdValue(4), + NdValue(8), + NdValue(16), + NdValue(32), + NdValue(64), + ], + shutterSpeedValues: ShutterSpeedValue.values, + isoValues: [ + IsoValue(100, StopType.full), + IsoValue(125, StopType.third), + IsoValue(160, StopType.third), + IsoValue(200, StopType.full), + IsoValue(250, StopType.third), + IsoValue(320, StopType.third), + IsoValue(400, StopType.full), + ], + ), + const EquipmentProfile( + id: '2', + name: 'Test 2', + apertureValues: ApertureValue.values, + ndValues: NdValue.values, + shutterSpeedValues: ShutterSpeedValue.values, + isoValues: IsoValue.values, + ), +]; diff --git a/test/screens/metering/utils/notifier_volume_keys_test.dart b/test/screens/metering/utils/notifier_volume_keys_test.dart new file mode 100644 index 0000000..7bb6458 --- /dev/null +++ b/test/screens/metering/utils/notifier_volume_keys_test.dart @@ -0,0 +1,46 @@ +import 'dart:async'; + +import 'package:lightmeter/data/models/volume_action.dart'; +import 'package:lightmeter/data/volume_events_service.dart'; +import 'package:lightmeter/screens/metering/utils/notifier_volume_keys.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; + +import '../../../function_mock.dart'; + +class _MockVolumeEventsService extends Mock implements VolumeEventsService {} + +void main() { + late _MockVolumeEventsService mockVolumeEventsService; + + setUp(() { + mockVolumeEventsService = _MockVolumeEventsService(); + }); + + test( + 'Listen to `volumeButtonsEventStream()`', + () async { + final StreamController volumeButtonsEvents = StreamController(); + when(() => mockVolumeEventsService.volumeButtonsEventStream()).thenAnswer((_) => volumeButtonsEvents.stream); + + final volumeKeysNotifier = VolumeKeysNotifier(mockVolumeEventsService); + final functions = MockValueChanged(); + volumeKeysNotifier.addListener(() => functions.onChanged(volumeKeysNotifier.value)); + expect(volumeKeysNotifier.value, VolumeKey.up); + + volumeButtonsEvents.add(25); + volumeButtonsEvents.add(25); + volumeButtonsEvents.add(25); + volumeButtonsEvents.add(24); + volumeButtonsEvents.add(24); + volumeButtonsEvents.add(25); + await Future.delayed(Duration.zero); + verify(() => functions.onChanged(VolumeKey.up)).called(2); + verify(() => functions.onChanged(VolumeKey.down)).called(4); + + volumeKeysNotifier.removeListener(() => functions.onChanged(volumeKeysNotifier.value)); + await volumeKeysNotifier.dispose(); + await volumeButtonsEvents.close(); + }, + ); +} diff --git a/test/screens/settings/components/widget_settings_section_lightmeter_pro_test.dart b/test/screens/settings/components/widget_settings_section_lightmeter_pro_test.dart new file mode 100644 index 0000000..1ae99f4 --- /dev/null +++ b/test/screens/settings/components/widget_settings_section_lightmeter_pro_test.dart @@ -0,0 +1,60 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:lightmeter/generated/l10n.dart'; +import 'package:lightmeter/screens/settings/components/lightmeter_pro/components/buy_pro/widget_list_tile_buy_pro.dart'; +import 'package:lightmeter/screens/settings/components/lightmeter_pro/widget_settings_section_lightmeter_pro.dart'; +import 'package:lightmeter/screens/shared/transparent_dialog/widget_dialog_transparent.dart'; +import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; + +import '../../../application_mock.dart'; + +void main() { + Future pumpApplication(WidgetTester tester) async { + await tester.pumpWidget( + IAPProducts( + products: [ + IAPProduct( + storeId: IAPProductType.paidFeatures.storeId, + ), + ], + child: const WidgetTestApplicationMock( + child: LightmeterProSettingsSection(), + ), + ), + ); + await tester.pumpAndSettle(); + } + + testWidgets( + '`showBuyProDialog` and buy', + (tester) async { + await pumpApplication(tester); + await tester.tap(find.byType(BuyProListTile)); + await tester.pumpAndSettle(); + expect(find.byType(TransparentDialog), findsOneWidget); + expect(find.text(S.current.proFeatures), findsNWidgets(2)); + expect(find.text(S.current.cancel), findsOneWidget); + expect(find.text(S.current.unlock), findsOneWidget); + + await tester.tap(find.text(S.current.unlock)); + await tester.pumpAndSettle(); + expect(find.byType(TransparentDialog), findsNothing); + }, + ); + + testWidgets( + '`showBuyProDialog` and cancel', + (tester) async { + await pumpApplication(tester); + await tester.tap(find.byType(BuyProListTile)); + await tester.pumpAndSettle(); + expect(find.byType(TransparentDialog), findsOneWidget); + expect(find.text(S.current.proFeatures), findsNWidgets(2)); + expect(find.text(S.current.cancel), findsOneWidget); + expect(find.text(S.current.unlock), findsOneWidget); + + await tester.tap(find.text(S.current.cancel)); + await tester.pumpAndSettle(); + expect(find.byType(TransparentDialog), findsNothing); + }, + ); +} diff --git a/test/utils/ev_from_bytes_test.dart b/test/utils/ev_from_bytes_test.dart new file mode 100644 index 0000000..f73a257 --- /dev/null +++ b/test/utils/ev_from_bytes_test.dart @@ -0,0 +1,24 @@ +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:lightmeter/utils/ev_from_bytes.dart'; + +void main() { + group('evFromImage', () { + test( + 'camera_stub_image.jpg', + () { + final bytes = File('assets/camera_stub_image.jpg').readAsBytesSync(); + expectLater(evFromImage(bytes), completion(8.25230310752341)); + }, + ); + + test( + 'no EXIF', + () { + final bytes = File('assets/launcher_icon_dev_512.png').readAsBytesSync(); + expectLater(evFromImage(bytes), completion(null)); + }, + ); + }); +} diff --git a/test/utils/selectable_provider_test.dart b/test/utils/selectable_provider_test.dart new file mode 100644 index 0000000..4ef2b96 --- /dev/null +++ b/test/utils/selectable_provider_test.dart @@ -0,0 +1,76 @@ + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:lightmeter/utils/selectable_provider.dart'; + +void main() { + group('SelectableInheritedModel.updateShouldNotifyDependent', () { + final model = SelectableInheritedModel( + values: List.generate(25, (index) => index), + selected: 1, + child: const SizedBox(), + ); + + test( + '`{}`', + () { + expect( + model.updateShouldNotifyDependent( + SelectableInheritedModel( + values: List.generate(25, (index) => index), + selected: 1, + child: const SizedBox(), + ), + {}, + ), + false, + ); + }, + ); + + test( + '`{SelectableAspect.list}`', + () { + expect( + model.updateShouldNotifyDependent( + SelectableInheritedModel( + values: List.generate(25, (index) => index), + selected: 1, + child: const SizedBox(), + ), + {SelectableAspect.list}, + ), + true, + ); + }, + ); + + test( + '`{SelectableAspect.selected}`', + () { + expect( + model.updateShouldNotifyDependent( + SelectableInheritedModel( + values: List.generate(25, (index) => index), + selected: 1, + child: const SizedBox(), + ), + {SelectableAspect.selected}, + ), + false, + ); + expect( + model.updateShouldNotifyDependent( + SelectableInheritedModel( + values: List.generate(25, (index) => index), + selected: 2, + child: const SizedBox(), + ), + {SelectableAspect.selected}, + ), + true, + ); + }, + ); + }); +} diff --git a/test/utils/to_string_signed_test.dart b/test/utils/to_string_signed_test.dart new file mode 100644 index 0000000..8611204 --- /dev/null +++ b/test/utils/to_string_signed_test.dart @@ -0,0 +1,16 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:lightmeter/utils/to_string_signed.dart'; + +void main() { + test('toStringSignedAsFixed(0)', () { + expect(1.5.toStringSignedAsFixed(0), '+2'); + expect((-1.5).toStringSignedAsFixed(0), '-2'); + expect(0.0.toStringSignedAsFixed(0), '0'); + }); + + test('toStringSignedAsFixed(1)', () { + expect(1.5.toStringSignedAsFixed(1), '+1.5'); + expect((-1.5).toStringSignedAsFixed(1), '-1.5'); + expect(0.0.toStringSignedAsFixed(1), '0.0'); + }); +}