diff --git a/.github/workflows/pr_check.yml b/.github/workflows/pr_check.yml
index caf7a97..113bba0 100644
--- a/.github/workflows/pr_check.yml
+++ b/.github/workflows/pr_check.yml
@@ -15,7 +15,7 @@ jobs:
analyze_and_test:
name: Analyze & test
runs-on: macos-11
- timeout-minutes: 5
+ timeout-minutes: 10
steps:
- uses: 8BitJonny/gh-get-current-pr@2.2.0
id: PR
diff --git a/.gitignore b/.gitignore
index 703a8b6..4904e66 100644
--- a/.gitignore
+++ b/.gitignore
@@ -60,4 +60,5 @@ ios/Runner/GoogleService-Info.plist
/lib/firebase_options.dart
coverage/
-screenshots/
\ No newline at end of file
+test/coverage_helper_test.dart
+screenshots/*.png
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 97cf9fc..0ddf5c0 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -7,10 +7,9 @@
"files.watcherExclude": {
"**/.fvm": true
},
- "dart.lineLength": 100,
+ "dart.lineLength": 120,
"[dart]": {
"editor.rulers": [
- 100,
120,
],
"editor.selectionHighlight": true,
diff --git a/README.md b/README.md
index dae7291..ef38b93 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,8 @@
+![](https://github.com/vodemn/m3_lightmeter/actions/workflows/pr_check.yml/badge.svg)
+![](https://github.com/vodemn/m3_lightmeter/actions/workflows/create_release.yml/badge.svg)
+
# Table of contents
- [Table of contents](#table-of-contents)
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/m3_lightmeter_iap.dart b/iap/lib/m3_lightmeter_iap.dart
index 171fe47..43e69aa 100644
--- a/iap/lib/m3_lightmeter_iap.dart
+++ b/iap/lib/m3_lightmeter_iap.dart
@@ -1,34 +1,9 @@
library m3_lightmeter_iap;
-import 'package:flutter/material.dart';
-import 'package:m3_lightmeter_iap/src/providers/equipment_profile_provider.dart';
-import 'package:m3_lightmeter_iap/src/providers/films_provider.dart';
-import 'package:m3_lightmeter_iap/src/providers/iap_products_provider.dart';
+import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
export 'src/data/models/iap_product.dart';
-
-export 'src/providers/equipment_profile_provider.dart';
-export 'src/providers/films_provider.dart';
export 'src/providers/iap_products_provider.dart';
+export 'src/data/iap_storage_service.dart';
-class IAPProviders extends StatelessWidget {
- final Object sharedPreferences;
- final Widget child;
-
- const IAPProviders({
- required this.sharedPreferences,
- required this.child,
- super.key,
- });
-
- @override
- Widget build(BuildContext context) {
- return IAPProductsProvider(
- child: FilmsProvider(
- child: EquipmentProfileProvider(
- child: child,
- ),
- ),
- );
- }
-}
+const List films = [];
diff --git a/iap/lib/src/data/iap_storage_service.dart b/iap/lib/src/data/iap_storage_service.dart
new file mode 100644
index 0000000..f62f622
--- /dev/null
+++ b/iap/lib/src/data/iap_storage_service.dart
@@ -0,0 +1,17 @@
+import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
+
+class IAPStorageService {
+ const IAPStorageService(Object _);
+
+ String get selectedEquipmentProfileId => '';
+ set selectedEquipmentProfileId(String id) {}
+
+ List get equipmentProfiles => [];
+ set equipmentProfiles(List profiles) {}
+
+ Film get selectedFilm => const Film.other();
+ set selectedFilm(Film value) {}
+
+ List get filmsInUse => [];
+ set filmsInUse(List profiles) {}
+}
diff --git a/iap/lib/src/providers/equipment_profile_provider.dart b/iap/lib/src/providers/equipment_profile_provider.dart
deleted file mode 100644
index 0a037a9..0000000
--- a/iap/lib/src/providers/equipment_profile_provider.dart
+++ /dev/null
@@ -1,61 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:m3_lightmeter_iap/src/providers/selectable_provider.dart';
-import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
-
-class EquipmentProfileProvider extends StatefulWidget {
- final Widget child;
-
- const EquipmentProfileProvider({required this.child, super.key});
-
- static EquipmentProfileProviderState of(BuildContext context) {
- return context.findAncestorStateOfType()!;
- }
-
- @override
- State createState() => EquipmentProfileProviderState();
-}
-
-class EquipmentProfileProviderState extends State {
- static const EquipmentProfile _defaultProfile = EquipmentProfile(
- id: '',
- name: '',
- apertureValues: ApertureValue.values,
- ndValues: NdValue.values,
- shutterSpeedValues: ShutterSpeedValue.values,
- isoValues: IsoValue.values,
- );
-
- @override
- Widget build(BuildContext context) {
- return EquipmentProfiles(
- values: const [_defaultProfile],
- selected: _defaultProfile,
- child: widget.child,
- );
- }
-
- void setProfile(EquipmentProfile data) {}
-
- void addProfile(String name, [EquipmentProfile? copyFrom]) {}
-
- void updateProdile(EquipmentProfile data) {}
-
- void deleteProfile(EquipmentProfile data) {}
-}
-
-class EquipmentProfiles extends SelectableInheritedModel {
- const EquipmentProfiles({
- super.key,
- required super.values,
- required super.selected,
- required super.child,
- });
-
- static List of(BuildContext context) {
- return InheritedModel.inheritFrom(context, aspect: SelectableAspect.list)!.values;
- }
-
- static EquipmentProfile selectedOf(BuildContext context) {
- return InheritedModel.inheritFrom(context, aspect: SelectableAspect.selected)!.selected;
- }
-}
diff --git a/iap/lib/src/providers/films_provider.dart b/iap/lib/src/providers/films_provider.dart
deleted file mode 100644
index e75ccd3..0000000
--- a/iap/lib/src/providers/films_provider.dart
+++ /dev/null
@@ -1,65 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:m3_lightmeter_iap/src/providers/selectable_provider.dart';
-import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
-
-class FilmsProvider extends StatefulWidget {
- final Widget child;
-
- const FilmsProvider({
- required this.child,
- super.key,
- });
-
- static FilmsProviderState of(BuildContext context) {
- return context.findAncestorStateOfType()!;
- }
-
- @override
- State createState() => FilmsProviderState();
-}
-
-class FilmsProviderState extends State {
- @override
- Widget build(BuildContext context) {
- return Films(
- values: const [Film.other()],
- filmsInUse: const [Film.other()],
- selected: const Film.other(),
- child: widget.child,
- );
- }
-
- void setFilm(Film film) {}
-
- void saveFilms(List films) {}
-}
-
-class Films extends SelectableInheritedModel {
- final List filmsInUse;
-
- const Films({
- super.key,
- required super.values,
- required this.filmsInUse,
- required super.selected,
- required super.child,
- });
-
- /// [Film.other()] + all the custom fields with actual reciprocity formulas
- static List of(BuildContext context) {
- return InheritedModel.inheritFrom(context)!.values;
- }
-
- /// [Film.other()] + films in use selected by user
- static List inUseOf(BuildContext context) {
- return InheritedModel.inheritFrom(
- context,
- aspect: SelectableAspect.list,
- )!
- .filmsInUse;
- }
-
- static Film selectedOf(BuildContext context) {
- return InheritedModel.inheritFrom(context, aspect: SelectableAspect.selected)!.selected;
- }
-}
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/integration_test/generate_screenshots.dart b/integration_test/generate_screenshots.dart
deleted file mode 100644
index 7838f8d..0000000
--- a/integration_test/generate_screenshots.dart
+++ /dev/null
@@ -1,248 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:integration_test/integration_test.dart';
-import 'package:lightmeter/application.dart';
-import 'package:lightmeter/data/caffeine_service.dart';
-import 'package:lightmeter/data/haptics_service.dart';
-import 'package:lightmeter/data/light_sensor_service.dart';
-import 'package:lightmeter/data/models/ev_source_type.dart';
-import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
-import 'package:lightmeter/data/models/supported_locale.dart';
-import 'package:lightmeter/data/models/theme_type.dart';
-import 'package:lightmeter/data/models/volume_action.dart';
-import 'package:lightmeter/data/permissions_service.dart';
-import 'package:lightmeter/data/shared_prefs_service.dart';
-import 'package:lightmeter/data/volume_events_service.dart';
-import 'package:lightmeter/environment.dart';
-import 'package:lightmeter/generated/l10n.dart';
-import 'package:lightmeter/providers/services_provider.dart';
-import 'package:lightmeter/providers/user_preferences_provider.dart';
-import 'package:lightmeter/res/dimens.dart';
-import 'package:lightmeter/res/theme.dart';
-import 'package:lightmeter/screens/metering/components/bottom_controls/components/measure_button/widget_button_measure.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/shared/animated_dialog_picker/components/dialog_picker/widget_picker_dialog.dart';
-import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart';
-import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.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 'package:permission_handler/permission_handler.dart';
-import 'package:shared_preferences/shared_preferences.dart';
-
-import 'utils/widget_tester_extension.dart';
-
-class _MockSharedPreferences extends Mock implements SharedPreferences {}
-
-class _MockUserPreferencesService extends Mock implements UserPreferencesService {}
-
-class _MockCaffeineService extends Mock implements CaffeineService {}
-
-class _MockHapticsService extends Mock implements HapticsService {}
-
-class _MockPermissionsService extends Mock implements PermissionsService {}
-
-class _MockLightSensorService extends Mock implements LightSensorService {}
-
-class _MockVolumeEventsService extends Mock implements VolumeEventsService {}
-
-//https://stackoverflow.com/a/67186625/13167574
-void main() {
- late _MockUserPreferencesService mockUserPreferencesService;
- late _MockCaffeineService mockCaffeineService;
- late _MockHapticsService mockHapticsService;
- late _MockPermissionsService mockPermissionsService;
- late _MockLightSensorService mockLightSensorService;
- late _MockVolumeEventsService mockVolumeEventsService;
-
- final binding = IntegrationTestWidgetsFlutterBinding();
- IntegrationTestWidgetsFlutterBinding.ensureInitialized();
-
- setUpAll(() {
- mockUserPreferencesService = _MockUserPreferencesService();
- when(() => mockUserPreferencesService.evSourceType).thenReturn(EvSourceType.camera);
- when(() => mockUserPreferencesService.stopType).thenReturn(StopType.third);
- when(() => mockUserPreferencesService.locale).thenReturn(SupportedLocale.en);
- when(() => mockUserPreferencesService.caffeine).thenReturn(true);
- when(() => mockUserPreferencesService.volumeAction).thenReturn(VolumeAction.shutter);
- when(() => mockUserPreferencesService.cameraEvCalibration).thenReturn(0.0);
- when(() => mockUserPreferencesService.lightSensorEvCalibration).thenReturn(0.0);
- when(() => mockUserPreferencesService.iso).thenReturn(const IsoValue(400, StopType.full));
- when(() => mockUserPreferencesService.ndFilter).thenReturn(NdValue.values.first);
- when(() => mockUserPreferencesService.haptics).thenReturn(true);
- when(() => mockUserPreferencesService.meteringScreenLayout).thenReturn({
- MeteringScreenLayoutFeature.equipmentProfiles: true,
- MeteringScreenLayoutFeature.extremeExposurePairs: true,
- MeteringScreenLayoutFeature.filmPicker: true,
- MeteringScreenLayoutFeature.histogram: false,
- });
- when(() => mockUserPreferencesService.themeType).thenReturn(ThemeType.light);
- when(() => mockUserPreferencesService.dynamicColor).thenReturn(false);
-
- mockCaffeineService = _MockCaffeineService();
- when(() => mockCaffeineService.isKeepScreenOn()).thenAnswer((_) async => false);
- when(() => mockCaffeineService.keepScreenOn(true)).thenAnswer((_) async => true);
- when(() => mockCaffeineService.keepScreenOn(false)).thenAnswer((_) async => false);
-
- mockHapticsService = _MockHapticsService();
- when(() => mockHapticsService.quickVibration()).thenAnswer((_) async {});
- when(() => mockHapticsService.responseVibration()).thenAnswer((_) async {});
- when(() => mockHapticsService.errorVibration()).thenAnswer((_) async {});
-
- mockPermissionsService = _MockPermissionsService();
- when(() => mockPermissionsService.requestCameraPermission())
- .thenAnswer((_) async => PermissionStatus.granted);
- when(() => mockPermissionsService.checkCameraPermission())
- .thenAnswer((_) async => PermissionStatus.granted);
-
- mockLightSensorService = _MockLightSensorService();
- when(() => mockLightSensorService.hasSensor()).thenAnswer((_) async => true);
- when(() => mockLightSensorService.luxStream()).thenAnswer((_) => Stream.fromIterable([100]));
-
- mockVolumeEventsService = _MockVolumeEventsService();
- when(() => mockVolumeEventsService.setVolumeHandling(true)).thenAnswer((_) async => true);
- when(() => mockVolumeEventsService.setVolumeHandling(false)).thenAnswer((_) async => false);
- when(() => mockVolumeEventsService.volumeButtonsEventStream())
- .thenAnswer((_) => const Stream.empty());
-
- when(() => mockHapticsService.quickVibration()).thenAnswer((_) async {});
- when(() => mockHapticsService.responseVibration()).thenAnswer((_) async {});
- });
-
- Future pumpApplication(WidgetTester tester) async {
- await tester.pumpWidget(
- IAPProviders(
- sharedPreferences: _MockSharedPreferences(),
- child: EquipmentProfiles(
- selected: _mockEquipmentProfiles[0],
- values: _mockEquipmentProfiles,
- child: Films(
- selected: const Film('Ilford HP5+', 400),
- values: const [Film.other(), Film('Ilford HP5+', 400)],
- filmsInUse: const [Film.other(), Film('Ilford HP5+', 400)],
- child: ServicesProvider(
- environment: const Environment.prod().copyWith(hasLightSensor: true),
- userPreferencesService: mockUserPreferencesService,
- caffeineService: mockCaffeineService,
- hapticsService: mockHapticsService,
- permissionsService: mockPermissionsService,
- lightSensorService: mockLightSensorService,
- volumeEventsService: mockVolumeEventsService,
- child: const UserPreferencesProvider(child: Application()),
- ),
- ),
- ),
- ),
- );
- await tester.pumpAndSettle();
- }
-
- /// Generates several screenshots with the light theme
- /// and the additionally the first one with the dark theme
- void generateScreenshots(Color color) {
- testWidgets('${color.value}_light', (tester) async {
- when(() => mockUserPreferencesService.themeType).thenReturn(ThemeType.light);
- when(() => mockUserPreferencesService.primaryColor).thenReturn(color);
- await pumpApplication(tester);
-
- await tester.takePhoto();
- await tester.takeScreenshot(binding, '${color.value}_metering_reflected');
-
- await tester.tap(find.byTooltip(S.current.tooltipUseLightSensor));
- await tester.pumpAndSettle();
- await tester.tap(find.byType(MeteringMeasureButton));
- await tester.tap(find.byType(MeteringMeasureButton));
- await tester.takeScreenshot(binding, '${color.value}_metering_incident');
-
- expect(find.byType(IsoValuePicker), findsOneWidget);
- await tester.tap(find.byType(IsoValuePicker));
- await tester.pumpAndSettle(Dimens.durationL);
- expect(find.byType(DialogPicker), findsOneWidget);
- await tester.takeScreenshot(binding, '${color.value}_metering_iso_picker');
-
- await tester.tapCancelButton();
- expect(find.byType(DialogPicker), findsNothing);
- await tester.openSettings();
- await tester.takeScreenshot(binding, '${color.value}_settings');
-
- await tester.tapListTile(S.current.meteringScreenLayout);
- await tester.takeScreenshot(binding, '${color.value}_settings_metering_screen_layout');
-
- await tester.tapCancelButton();
- await tester.tapListTile(S.current.equipmentProfiles);
- expect(find.byType(EquipmentProfilesScreen), findsOneWidget);
- await tester.tap(find.byType(EquipmentProfileContainer).first);
- await tester.pumpAndSettle();
- await tester.takeScreenshot(binding, '${color.value}-equipment_profiles');
-
- await tester.tap(find.byIcon(Icons.iso).first);
- await tester.pumpAndSettle();
- await tester.takeScreenshot(binding, '${color.value}_equipment_profiles_iso_picker');
- });
-
- testWidgets(
- '${color.value}_dark',
- (tester) async {
- when(() => mockUserPreferencesService.themeType).thenReturn(ThemeType.dark);
- when(() => mockUserPreferencesService.primaryColor).thenReturn(color);
- await pumpApplication(tester);
-
- await tester.takePhoto();
- await tester.takeScreenshot(binding, '${color.value}_metering_reflected_dark');
-
- await tester.tap(find.byTooltip(S.current.tooltipUseLightSensor));
- await tester.pumpAndSettle();
- await tester.tap(find.byType(MeteringMeasureButton));
- await tester.tap(find.byType(MeteringMeasureButton));
- await tester.takeScreenshot(binding, '${color.value}_metering_incident_dark');
- },
- );
- }
-
- generateScreenshots(primaryColorsList[5]);
- generateScreenshots(primaryColorsList[3]);
- generateScreenshots(primaryColorsList[9]);
-}
-
-final _mockEquipmentProfiles = [
- const EquipmentProfile(
- id: '',
- name: '',
- apertureValues: ApertureValue.values,
- ndValues: NdValue.values,
- shutterSpeedValues: ShutterSpeedValue.values,
- isoValues: IsoValue.values,
- ),
- EquipmentProfile(
- id: '1',
- name: 'Praktica + Zenitar',
- apertureValues: ApertureValue.values.sublist(
- ApertureValue.values.indexOf(const ApertureValue(1.7, StopType.half)),
- ApertureValue.values.indexOf(const ApertureValue(16, StopType.full)) + 1,
- ),
- ndValues: NdValue.values.sublist(0, 3),
- shutterSpeedValues: ShutterSpeedValue.values.sublist(
- ShutterSpeedValue.values.indexOf(const ShutterSpeedValue(1000, true, StopType.full)),
- ShutterSpeedValue.values.indexOf(const ShutterSpeedValue(16, false, StopType.full)) + 1,
- ),
- isoValues: const [
- IsoValue(50, StopType.full),
- IsoValue(100, StopType.full),
- IsoValue(200, StopType.full),
- IsoValue(250, StopType.third),
- IsoValue(400, StopType.full),
- IsoValue(500, StopType.third),
- IsoValue(800, StopType.full),
- IsoValue(1600, StopType.full),
- IsoValue(3200, StopType.full),
- ],
- ),
- const EquipmentProfile(
- id: '2',
- name: 'Praktica + Jupiter',
- apertureValues: ApertureValue.values,
- ndValues: NdValue.values,
- shutterSpeedValues: ShutterSpeedValue.values,
- isoValues: IsoValue.values,
- ),
-];
diff --git a/integration_test/mocks/paid_features_mock.dart b/integration_test/mocks/paid_features_mock.dart
new file mode 100644
index 0000000..5d38c52
--- /dev/null
+++ b/integration_test/mocks/paid_features_mock.dart
@@ -0,0 +1,105 @@
+import 'package:flutter/material.dart';
+import 'package:lightmeter/providers/equipment_profile_provider.dart';
+import 'package:lightmeter/providers/films_provider.dart';
+import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
+import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
+import 'package:mocktail/mocktail.dart';
+
+class _MockIAPStorageService extends Mock implements IAPStorageService {}
+
+class MockIAPProviders extends StatefulWidget {
+ final String selectedEquipmentProfileId;
+ final Film selectedFilm;
+ final Widget child;
+
+ const MockIAPProviders({
+ this.selectedEquipmentProfileId = '',
+ this.selectedFilm = const Film.other(),
+ required this.child,
+ super.key,
+ });
+
+ @override
+ State createState() => _MockIAPProvidersState();
+}
+
+class _MockIAPProvidersState extends State {
+ late final _MockIAPStorageService mockIAPStorageService;
+
+ @override
+ void initState() {
+ super.initState();
+ mockIAPStorageService = _MockIAPStorageService();
+ when(() => mockIAPStorageService.equipmentProfiles).thenReturn(mockEquipmentProfiles);
+ when(() => mockIAPStorageService.selectedEquipmentProfileId).thenReturn(widget.selectedEquipmentProfileId);
+ when(() => mockIAPStorageService.filmsInUse).thenReturn(mockFilms);
+ when(() => mockIAPStorageService.selectedFilm).thenReturn(widget.selectedFilm);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return EquipmentProfileProvider(
+ storageService: mockIAPStorageService,
+ child: FilmsProvider(
+ storageService: mockIAPStorageService,
+ availableFilms: mockFilms,
+ child: widget.child,
+ ),
+ );
+ }
+}
+
+const defaultEquipmentProfile = EquipmentProfile(
+ id: '',
+ name: '',
+ apertureValues: ApertureValue.values,
+ ndValues: NdValue.values,
+ shutterSpeedValues: ShutterSpeedValue.values,
+ isoValues: IsoValue.values,
+);
+
+final mockEquipmentProfiles = [
+ EquipmentProfile(
+ id: '1',
+ name: 'Praktica + Zenitar',
+ apertureValues: ApertureValue.values.sublist(
+ ApertureValue.values.indexOf(const ApertureValue(1.7, StopType.half)),
+ ApertureValue.values.indexOf(const ApertureValue(16, StopType.full)) + 1,
+ ),
+ ndValues: NdValue.values.sublist(0, 3),
+ shutterSpeedValues: ShutterSpeedValue.values.sublist(
+ ShutterSpeedValue.values.indexOf(const ShutterSpeedValue(1000, true, StopType.full)),
+ ShutterSpeedValue.values.indexOf(const ShutterSpeedValue(16, false, StopType.full)) + 1,
+ ),
+ isoValues: const [
+ IsoValue(50, StopType.full),
+ IsoValue(100, StopType.full),
+ IsoValue(200, StopType.full),
+ IsoValue(250, StopType.third),
+ IsoValue(400, StopType.full),
+ IsoValue(500, StopType.third),
+ IsoValue(800, StopType.full),
+ IsoValue(1600, StopType.full),
+ IsoValue(3200, StopType.full),
+ ],
+ ),
+ const EquipmentProfile(
+ id: '2',
+ name: 'Praktica + Jupiter',
+ apertureValues: ApertureValue.values,
+ ndValues: NdValue.values,
+ shutterSpeedValues: ShutterSpeedValue.values,
+ isoValues: IsoValue.values,
+ ),
+];
+
+const mockFilms = [_MockFilm(100, 2), _MockFilm(400, 2), _MockFilm(3, 800), _MockFilm(400, 1.5)];
+
+class _MockFilm extends Film {
+ final double reciprocityMultiplier;
+
+ const _MockFilm(int iso, this.reciprocityMultiplier) : super('Mock film $iso x$reciprocityMultiplier', iso);
+
+ @override
+ double reciprocityFormula(double t) => t * reciprocityMultiplier;
+}
diff --git a/integration_test/utils/platform_channel_mock.dart b/integration_test/utils/platform_channel_mock.dart
new file mode 100644
index 0000000..383f243
--- /dev/null
+++ b/integration_test/utils/platform_channel_mock.dart
@@ -0,0 +1,59 @@
+import 'dart:math';
+
+import 'package:flutter/services.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:light_sensor/light_sensor.dart';
+
+void setLightSensorAvilability({required bool hasSensor}) {
+ TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
+ LightSensor.methodChannel,
+ (methodCall) async {
+ switch (methodCall.method) {
+ case "sensor":
+ return hasSensor;
+ default:
+ return null;
+ }
+ },
+ );
+}
+
+void resetLightSensorAvilability() {
+ TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
+ LightSensor.methodChannel,
+ null,
+ );
+}
+
+Future sendMockIncidentEv(double ev) => sendMockLux((2.5 * pow(2, ev)).toInt());
+
+Future sendMockLux([int lux = 100]) async {
+ await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
+ LightSensor.eventChannel.name,
+ const StandardMethodCodec().encodeSuccessEnvelope(lux),
+ (ByteData? data) {},
+ );
+}
+
+void setupLightSensorStreamHandler() {
+ TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
+ MethodChannel(LightSensor.eventChannel.name),
+ (methodCall) async {
+ switch (methodCall.method) {
+ case "listen":
+ return;
+ case "cancel":
+ return;
+ default:
+ return null;
+ }
+ },
+ );
+}
+
+void resetLightSensorStreamHandler() {
+ TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
+ MethodChannel(LightSensor.eventChannel.name),
+ null,
+ );
+}
diff --git a/integration_test/utils/widget_tester_actions.dart b/integration_test/utils/widget_tester_actions.dart
new file mode 100644
index 0000000..8411634
--- /dev/null
+++ b/integration_test/utils/widget_tester_actions.dart
@@ -0,0 +1,84 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:lightmeter/application.dart';
+import 'package:lightmeter/application_wrapper.dart';
+import 'package:lightmeter/environment.dart';
+import 'package:lightmeter/generated/l10n.dart';
+import 'package:lightmeter/res/dimens.dart';
+import 'package:lightmeter/screens/metering/components/bottom_controls/components/measure_button/widget_button_measure.dart';
+import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
+import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
+
+import '../mocks/paid_features_mock.dart';
+import 'platform_channel_mock.dart';
+
+extension WidgetTesterCommonActions on WidgetTester {
+ Future pumpApplication({
+ IAPProductStatus productStatus = IAPProductStatus.purchased,
+ String selectedEquipmentProfileId = '',
+ Film selectedFilm = const Film.other(),
+ }) async {
+ await pumpWidget(
+ IAPProducts(
+ products: [
+ IAPProduct(
+ storeId: IAPProductType.paidFeatures.storeId,
+ status: productStatus,
+ ),
+ ],
+ child: ApplicationWrapper(
+ const Environment.dev(),
+ child: MockIAPProviders(
+ selectedEquipmentProfileId: selectedEquipmentProfileId,
+ selectedFilm: selectedFilm,
+ child: const Application(),
+ ),
+ ),
+ ),
+ );
+ await pumpAndSettle();
+ }
+
+ Future takePhoto() async {
+ await tap(find.byType(MeteringMeasureButton));
+ await pump(const Duration(seconds: 2)); // wait for circular progress indicator
+ await pump(const Duration(seconds: 1)); // wait for circular progress indicator
+ await pumpAndSettle();
+ }
+
+ Future toggleIncidentMetering(double ev) async {
+ await tap(find.byType(MeteringMeasureButton));
+ await sendMockIncidentEv(ev);
+ await tap(find.byType(MeteringMeasureButton));
+ await pumpAndSettle();
+ }
+
+ Future openAnimatedPicker() async {
+ await tap(find.byType(T));
+ await pumpAndSettle(Dimens.durationL);
+ }
+}
+
+extension WidgetTesterListTileActions on WidgetTester {
+ /// Useful for tapping a specific [ListTile] inside a specific screen or dialog
+ Future tapDescendantTextOf(String text) async {
+ await tap(find.descendant(of: find.byType(T), matching: find.text(text)));
+ }
+}
+
+extension WidgetTesterTextButtonActions on WidgetTester {
+ Future tapSelectButton() => _tapTextButton(S.current.select);
+
+ Future tapCancelButton() => _tapTextButton(S.current.cancel);
+
+ Future tapSaveButton() => _tapTextButton(S.current.save);
+
+ Future _tapTextButton(String text) async {
+ final button = find.byWidgetPredicate(
+ (widget) => widget is TextButton && widget.child is Text && (widget.child as Text?)?.data == text,
+ );
+ expect(button, findsOneWidget);
+ await tap(button);
+ await pumpAndSettle();
+ }
+}
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_wrapper.dart b/lib/application_wrapper.dart
index f8627f9..ae7d1ae 100644
--- a/lib/application_wrapper.dart
+++ b/lib/application_wrapper.dart
@@ -1,11 +1,17 @@
import 'package:flutter/material.dart';
+import 'package:lightmeter/data/analytics/analytics.dart';
+import 'package:lightmeter/data/analytics/api/analytics_firebase.dart';
import 'package:lightmeter/data/caffeine_service.dart';
import 'package:lightmeter/data/haptics_service.dart';
import 'package:lightmeter/data/light_sensor_service.dart';
import 'package:lightmeter/data/permissions_service.dart';
+import 'package:lightmeter/data/remote_config_service.dart';
import 'package:lightmeter/data/shared_prefs_service.dart';
import 'package:lightmeter/data/volume_events_service.dart';
import 'package:lightmeter/environment.dart';
+import 'package:lightmeter/providers/equipment_profile_provider.dart';
+import 'package:lightmeter/providers/films_provider.dart';
+import 'package:lightmeter/providers/remote_config_provider.dart';
import 'package:lightmeter/providers/services_provider.dart';
import 'package:lightmeter/providers/user_preferences_provider.dart';
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
@@ -21,25 +27,38 @@ class ApplicationWrapper extends StatelessWidget {
@override
Widget build(BuildContext context) {
return FutureBuilder(
- future: Future.wait([
+ future: Future.wait([
SharedPreferences.getInstance(),
const LightSensorService(LocalPlatform()).hasSensor(),
+ if (env.buildType != BuildType.dev) const RemoteConfigService().activeAndFetchFeatures(),
]),
builder: (_, snapshot) {
if (snapshot.data != null) {
- return IAPProviders(
- sharedPreferences: snapshot.data![0] as SharedPreferences,
- child: ServicesProvider(
- caffeineService: const CaffeineService(),
- environment: env.copyWith(hasLightSensor: snapshot.data![1] as bool),
- hapticsService: const HapticsService(),
- lightSensorService: const LightSensorService(LocalPlatform()),
- permissionsService: const PermissionsService(),
- userPreferencesService:
- UserPreferencesService(snapshot.data![0] as SharedPreferences),
- volumeEventsService: const VolumeEventsService(LocalPlatform()),
- child: UserPreferencesProvider(
- child: child,
+ final iapService = IAPStorageService(snapshot.data![0] as SharedPreferences);
+ final userPreferencesService = UserPreferencesService(snapshot.data![0] as SharedPreferences);
+ final hasLightSensor = snapshot.data![1] as bool;
+ return ServicesProvider(
+ analytics: const LightmeterAnalytics(api: LightmeterAnalyticsFirebase()),
+ caffeineService: const CaffeineService(),
+ environment: env.copyWith(hasLightSensor: hasLightSensor),
+ hapticsService: const HapticsService(),
+ lightSensorService: const LightSensorService(LocalPlatform()),
+ permissionsService: const PermissionsService(),
+ userPreferencesService: userPreferencesService,
+ volumeEventsService: const VolumeEventsService(LocalPlatform()),
+ child: RemoteConfigProvider(
+ remoteConfigService:
+ env.buildType != BuildType.dev ? const RemoteConfigService() : const MockRemoteConfigService(),
+ child: EquipmentProfileProvider(
+ storageService: iapService,
+ child: FilmsProvider(
+ storageService: iapService,
+ child: UserPreferencesProvider(
+ hasLightSensor: hasLightSensor,
+ userPreferencesService: userPreferencesService,
+ child: child,
+ ),
+ ),
),
),
);
diff --git a/lib/data/analytics/analytics.dart b/lib/data/analytics/analytics.dart
new file mode 100644
index 0000000..1bd6496
--- /dev/null
+++ b/lib/data/analytics/analytics.dart
@@ -0,0 +1,34 @@
+import 'dart:async';
+import 'dart:developer';
+
+import 'package:flutter/foundation.dart';
+import 'package:lightmeter/data/analytics/api/analytics_api_interface.dart';
+import 'package:lightmeter/data/analytics/entity/analytics_event.dart';
+
+class LightmeterAnalytics {
+ final ILightmeterAnalyticsApi _api;
+
+ const LightmeterAnalytics({required ILightmeterAnalyticsApi api}) : _api = api;
+
+ Future logEvent(
+ LightmeterAnalyticsEvent event, {
+ Map? parameters,
+ }) async {
+ if (kDebugMode) {
+ log(' logEvent: ${event.name} / $parameters');
+ return;
+ }
+
+ return _api.logEvent(
+ event: event,
+ parameters: parameters,
+ );
+ }
+
+ Future logUnlockProFeatures(String listTileTitle) async {
+ return logEvent(
+ LightmeterAnalyticsEvent.unlockProFeatures,
+ parameters: {"listTileTitle": listTileTitle},
+ );
+ }
+}
diff --git a/lib/data/analytics/api/analytics_api_interface.dart b/lib/data/analytics/api/analytics_api_interface.dart
new file mode 100644
index 0000000..1aa007f
--- /dev/null
+++ b/lib/data/analytics/api/analytics_api_interface.dart
@@ -0,0 +1,8 @@
+import 'package:lightmeter/data/analytics/entity/analytics_event.dart';
+
+abstract class ILightmeterAnalyticsApi {
+ Future logEvent({
+ required LightmeterAnalyticsEvent event,
+ Map? parameters,
+ });
+}
diff --git a/lib/data/analytics/api/analytics_firebase.dart b/lib/data/analytics/api/analytics_firebase.dart
new file mode 100644
index 0000000..fb11d02
--- /dev/null
+++ b/lib/data/analytics/api/analytics_firebase.dart
@@ -0,0 +1,26 @@
+import 'package:firebase_analytics/firebase_analytics.dart';
+import 'package:firebase_core/firebase_core.dart';
+import 'package:flutter/foundation.dart';
+import 'package:lightmeter/data/analytics/api/analytics_api_interface.dart';
+import 'package:lightmeter/data/analytics/entity/analytics_event.dart';
+
+class LightmeterAnalyticsFirebase implements ILightmeterAnalyticsApi {
+ const LightmeterAnalyticsFirebase();
+
+ @override
+ Future logEvent({
+ required LightmeterAnalyticsEvent event,
+ Map? parameters,
+ }) async {
+ try {
+ await FirebaseAnalytics.instance.logEvent(
+ name: event.name,
+ parameters: parameters,
+ );
+ } on FirebaseException catch (e) {
+ debugPrint('Firebase Analytics Exception: $e');
+ } catch (e) {
+ debugPrint(e.toString());
+ }
+ }
+}
diff --git a/lib/data/analytics/entity/analytics_event.dart b/lib/data/analytics/entity/analytics_event.dart
new file mode 100644
index 0000000..8275869
--- /dev/null
+++ b/lib/data/analytics/entity/analytics_event.dart
@@ -0,0 +1,3 @@
+enum LightmeterAnalyticsEvent {
+ unlockProFeatures,
+}
diff --git a/lib/data/light_sensor_service.dart b/lib/data/light_sensor_service.dart
index c837dde..a4b95d7 100644
--- a/lib/data/light_sensor_service.dart
+++ b/lib/data/light_sensor_service.dart
@@ -11,7 +11,7 @@ class LightSensorService {
return false;
}
try {
- return await LightSensor.hasSensor ?? false;
+ return await LightSensor.hasSensor();
} catch (_) {
return false;
}
@@ -21,6 +21,6 @@ class LightSensorService {
if (!localPlatform.isAndroid) {
return const Stream.empty();
}
- return LightSensor.lightSensorStream;
+ return LightSensor.luxStream();
}
}
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
new file mode 100644
index 0000000..b022db7
--- /dev/null
+++ b/lib/data/models/feature.dart
@@ -0,0 +1,5 @@
+enum Feature { unlockProFeaturesText }
+
+const featuresDefaultValues = {
+ 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
new file mode 100644
index 0000000..f8c123b
--- /dev/null
+++ b/lib/data/remote_config_service.dart
@@ -0,0 +1,131 @@
+import 'dart:async';
+import 'dart:developer';
+
+import 'package:firebase_core/firebase_core.dart';
+import 'package:firebase_crashlytics/firebase_crashlytics.dart';
+import 'package:firebase_remote_config/firebase_remote_config.dart';
+import 'package:flutter/foundation.dart';
+import 'package:lightmeter/data/models/feature.dart';
+
+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);
+
+ try {
+ await remoteConfig.setConfigSettings(
+ RemoteConfigSettings(
+ fetchTimeout: const Duration(seconds: 15),
+ minimumFetchInterval: cacheStaleDuration,
+ ),
+ );
+ await remoteConfig.setDefaults(featuresDefaultValues.map((key, value) => MapEntry(key.name, value)));
+ await remoteConfig.activate();
+ await remoteConfig.ensureInitialized();
+
+ log('Firebase remote config initialized successfully');
+ } on FirebaseException catch (e) {
+ _logError('Firebase exception during Firebase Remote Config initialization: $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) {
+ try {
+ final feature = Feature.values.firstWhere((f) => f.name == value.key);
+ result[feature] = value.value.toValue(feature);
+ } catch (e) {
+ log(e.toString());
+ }
+ }
+ return result;
+ }
+
+ @override
+ Stream> onConfigUpdated() => FirebaseRemoteConfig.instance.onConfigUpdated.asyncMap(
+ (event) async {
+ await FirebaseRemoteConfig.instance.activate();
+ final Set updatedFeatures = {};
+ for (final key in event.updatedKeys) {
+ try {
+ updatedFeatures.add(Feature.values.firstWhere((element) => element.name == key));
+ } catch (e) {
+ log(e.toString());
+ }
+ }
+ return updatedFeatures;
+ },
+ );
+
+ @override
+ bool isEnabled(Feature feature) => FirebaseRemoteConfig.instance.getBool(feature.name);
+
+ void _logError(dynamic throwable, {StackTrace? stackTrace}) {
+ FirebaseCrashlytics.instance.recordError(throwable, stackTrace);
+ }
+}
+
+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) {
+ case Feature.unlockProFeaturesText:
+ return asBool();
+ }
+ }
+}
diff --git a/lib/data/shared_prefs_service.dart b/lib/data/shared_prefs_service.dart
index 96d7d9d..443f2e5 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';
@@ -18,6 +19,7 @@ class UserPreferencesService {
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,16 +72,13 @@ 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];
@@ -96,7 +95,6 @@ class UserPreferencesService {
MeteringScreenLayoutFeature.equipmentProfiles: true,
MeteringScreenLayoutFeature.extremeExposurePairs: true,
MeteringScreenLayoutFeature.filmPicker: true,
- MeteringScreenLayoutFeature.histogram: true,
};
}
}
@@ -104,6 +102,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 +127,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 +136,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 2f2fa27..02bb95a 100644
--- a/lib/l10n/intl_en.arb
+++ b/lib/l10n/intl_en.arb
@@ -39,7 +39,11 @@
"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)",
@@ -92,10 +96,14 @@
}
}
},
- "buyLightmeterPro": "Buy Lightmeter Pro",
"lightmeterPro": "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.",
+ "buyLightmeterPro": "Buy Lightmeter Pro",
+ "lightmeterProDescription": "Unlocks extra 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\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:\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",
"tooltipExpand": "Expand",
diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb
index 895b8e2..f8b52c3 100644
--- a/lib/l10n/intl_fr.arb
+++ b/lib/l10n/intl_fr.arb
@@ -39,7 +39,11 @@
"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)",
@@ -94,8 +98,12 @@
},
"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.",
+ "lightmeterProDescription": "Débloque des fonctionnalités supplémentaires:\n \u2022 Profils d'équipement contenant des filtres pour l'ouverture, la vitesse d'obturation et plus encore\n \u2022 Liste de films avec une compensation pour ce que l'on appelle l'échec de réciprocité\n \u2022 Mesure spot\n \u2022 Histogramme\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:\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",
"tooltipExpand": "Élargir",
diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb
index f4c7b0b..f25dc8e 100644
--- a/lib/l10n/intl_ru.arb
+++ b/lib/l10n/intl_ru.arb
@@ -39,7 +39,11 @@
"meteringScreenLayoutHintEquipmentProfiles": "Выбор профиля оборудования",
"meteringScreenFeatureExtremeExposurePairs": "Длинная и короткая выдержки",
"meteringScreenFeatureFilmPicker": "Выбор пленки",
- "meteringScreenFeatureHistogram": "Гистограмма",
+ "cameraFeatures": "Возможности камеры",
+ "cameraFeatureSpotMetering": "Точечный замер",
+ "cameraFeatureSpotMeteringHint": "Используйте долгое нажатие, чтобы удалить точку замера",
+ "cameraFeatureHistogram": "Гистограмма",
+ "cameraFeatureHistogramHint": "Использование гистограммы может увеличить расход аккумулятора",
"film": "Пленка",
"filmPush": "Пленка (push)",
"filmPull": "Пленка (pull)",
@@ -94,8 +98,12 @@
},
"buyLightmeterPro": "Купить Lightmeter Pro",
"lightmeterPro": "Lightmeter Pro",
- "lightmeterProDescription": "Даёт доступ к таким функциям как профили оборудования, содержащие фильтры для диафрагмы, выдержки и других значений, а также набору пленок с компенсацией эффекта Шварцшильда.\n\nИсходный код Lightmeter доступен на GitHub. Вы можете собрать его самостоятельно. Однако если вы хотите поддержать разработку и получать новые функции и обновления, то приобретите Lightmeter Pro.",
+ "lightmeterProDescription": "Даёт доступ к различным функциям:\n \u2022 Профили оборудования, содержащие фильтры для диафрагмы, выдержки и других значений\n \u2022 Список пленок с компенсацией эффекта Шварцшильда\n \u2022 Точечный замер\n \u2022 Гистограмма\n\nИсходный код Lightmeter доступен на GitHub. Вы можете собрать его самостоятельно. Однако если вы хотите поддержать разработку и получать новые функции и обновления, то приобретите Lightmeter Pro.",
"buy": "Купить",
+ "proFeatures": "Профессиональные настройки",
+ "unlockProFeatures": "Разблокировать профессиональные настройки",
+ "unlockProFeaturesDescription": "Вы можете разблокировать профессиональные настройки:\n \u2022 Профили оборудования, содержащие фильтры для диафрагмы, выдержки и других значений\n \u2022 Список пленок с компенсацией эффекта Шварцшильда\n \u2022 Точечный замер\n \u2022 Гистограмма\n\nПолучая доступ к профессиональным настройкам, вы поддерживаете разработку и делаете возможным появление новых функций в приложении.",
+ "unlock": "Разблокировать",
"tooltipAdd": "Добавить",
"tooltipClose": "Закрыть",
"tooltipExpand": "Развернуть",
diff --git a/lib/l10n/intl_zh.arb b/lib/l10n/intl_zh.arb
index 0701781..3844a76 100644
--- a/lib/l10n/intl_zh.arb
+++ b/lib/l10n/intl_zh.arb
@@ -39,7 +39,11 @@
"meteringScreenLayoutHintEquipmentProfiles": "设备配置选择",
"meteringScreenFeatureExtremeExposurePairs": "最快 & 最慢曝光组合",
"meteringScreenFeatureFilmPicker": "胶片选择",
- "meteringScreenFeatureHistogram": "直方图",
+ "cameraFeatures": "相机功能",
+ "cameraFeatureSpotMetering": "点测光",
+ "cameraFeatureSpotMeteringHint": "长按相机视图可移除测光点",
+ "cameraFeatureHistogram": "直方图",
+ "cameraFeatureHistogramHint": "启用直方图会增加电池消耗",
"film": "胶片",
"filmPush": "胶片 (push)",
"filmPull": "胶片 (pull)",
@@ -94,8 +98,12 @@
},
"buyLightmeterPro": "购买 Lightmeter Pro",
"lightmeterPro": "Lightmeter Pro",
- "lightmeterProDescription": "购买以解锁额外功能。例如包含光圈、快门速度等参数的配置文件;以及一个胶卷预设列表来提供倒易率失效时的曝光补偿。\n\n您可以在 GitHub 上获取 Lightmeter 的源代码,欢迎自行编译。不过,如果您想支持开发并获得新功能和更新,请考虑购买 Lightmeter Pro。",
+ "lightmeterProDescription": "解锁额外功能:\n \u2022 配置文件,其中包含光圈、快门速度等参数\n \u2022 胶卷列表,对胶片倒易率失效进行曝光补偿\n \u2022 点测光\n \u2022 直方图\n\n您可以在 GitHub 上获取 Lightmeter 的源代码,欢迎自行编译。不过,如果您想支持开发并获得新功能和更新,请考虑购买 Lightmeter Pro。",
"buy": "购买",
+ "proFeatures": "专业功能",
+ "unlockProFeatures": "解锁专业功能",
+ "unlockProFeaturesDescription": "\n \u2022 配置文件,其中包含光圈、快门速度等参数\n \u2022 胶卷列表,对胶片倒易率失效进行曝光补偿\n \u2022 点测光\n \u2022 直方图\n\n通过解锁专业版功能,您可以支持开发工作,帮助为应用程序添加新功能。",
+ "unlock": "解锁",
"tooltipAdd": "添加",
"tooltipClose": "关闭",
"tooltipExpand": "展开",
diff --git a/lib/main_dev.dart b/lib/main_dev.dart
index 9ef3faf..b43352f 100644
--- a/lib/main_dev.dart
+++ b/lib/main_dev.dart
@@ -2,8 +2,17 @@ import 'package:flutter/material.dart';
import 'package:lightmeter/application.dart';
import 'package:lightmeter/application_wrapper.dart';
import 'package:lightmeter/environment.dart';
+import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
Future main() async {
WidgetsFlutterBinding.ensureInitialized();
- runApp(const ApplicationWrapper(Environment.dev(), child: Application()));
+ runApp(
+ IAPProducts(
+ products: [IAPProduct(storeId: IAPProductType.paidFeatures.storeId)],
+ child: const ApplicationWrapper(
+ Environment.dev(),
+ child: Application(),
+ ),
+ ),
+ );
}
diff --git a/lib/main_prod.dart b/lib/main_prod.dart
index b75513e..3460f32 100644
--- a/lib/main_prod.dart
+++ b/lib/main_prod.dart
@@ -3,9 +3,17 @@ import 'package:lightmeter/application.dart';
import 'package:lightmeter/application_wrapper.dart';
import 'package:lightmeter/environment.dart';
import 'package:lightmeter/firebase.dart';
+import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
Future main() async {
WidgetsFlutterBinding.ensureInitialized();
await initializeFirebase(handleErrors: true);
- runApp(const ApplicationWrapper(Environment.prod(), child: Application()));
+ runApp(
+ const IAPProductsProvider(
+ child: ApplicationWrapper(
+ Environment.prod(),
+ child: Application(),
+ ),
+ ),
+ );
}
diff --git a/lib/main_release.dart b/lib/main_release.dart
index bb6384a..eea83e2 100644
--- a/lib/main_release.dart
+++ b/lib/main_release.dart
@@ -3,9 +3,17 @@ import 'package:lightmeter/application.dart';
import 'package:lightmeter/application_wrapper.dart';
import 'package:lightmeter/environment.dart';
import 'package:lightmeter/firebase.dart';
+import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
Future main() async {
WidgetsFlutterBinding.ensureInitialized();
await initializeFirebase(handleErrors: false);
- runApp(const ApplicationWrapper(Environment.prod(), child: Application()));
+ runApp(
+ const IAPProductsProvider(
+ child: ApplicationWrapper(
+ Environment.prod(),
+ child: Application(),
+ ),
+ ),
+ );
}
diff --git a/lib/providers/equipment_profile_provider.dart b/lib/providers/equipment_profile_provider.dart
new file mode 100644
index 0000000..564c5ef
--- /dev/null
+++ b/lib/providers/equipment_profile_provider.dart
@@ -0,0 +1,129 @@
+import 'package:flutter/material.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';
+import 'package:uuid/uuid.dart';
+
+class EquipmentProfileProvider extends StatefulWidget {
+ final IAPStorageService storageService;
+ final Widget child;
+
+ const EquipmentProfileProvider({
+ required this.storageService,
+ required this.child,
+ super.key,
+ });
+
+ static EquipmentProfileProviderState of(BuildContext context) {
+ return context.findAncestorStateOfType()!;
+ }
+
+ @override
+ State createState() => EquipmentProfileProviderState();
+}
+
+class EquipmentProfileProviderState extends State {
+ static const EquipmentProfile _defaultProfile = EquipmentProfile(
+ id: '',
+ name: '',
+ apertureValues: ApertureValue.values,
+ ndValues: NdValue.values,
+ shutterSpeedValues: ShutterSpeedValue.values,
+ isoValues: IsoValue.values,
+ );
+
+ List _customProfiles = [];
+ String _selectedId = '';
+
+ EquipmentProfile get _selectedProfile => _customProfiles.firstWhere(
+ (e) => e.id == _selectedId,
+ orElse: () => _defaultProfile,
+ );
+
+ @override
+ void initState() {
+ super.initState();
+ _selectedId = widget.storageService.selectedEquipmentProfileId;
+ _customProfiles = widget.storageService.equipmentProfiles;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return EquipmentProfiles(
+ values: [
+ _defaultProfile,
+ if (IAPProducts.isPurchased(context, IAPProductType.paidFeatures)) ..._customProfiles,
+ ],
+ selected: IAPProducts.isPurchased(context, IAPProductType.paidFeatures) ? _selectedProfile : _defaultProfile,
+ child: widget.child,
+ );
+ }
+
+ void setProfile(EquipmentProfile data) {
+ if (_selectedId != data.id) {
+ setState(() {
+ _selectedId = data.id;
+ });
+ widget.storageService.selectedEquipmentProfileId = _selectedProfile.id;
+ }
+ }
+
+ /// Creates a default equipment profile
+ void addProfile(String name, [EquipmentProfile? copyFrom]) {
+ _customProfiles.add(
+ EquipmentProfile(
+ id: const Uuid().v1(),
+ name: name,
+ apertureValues: copyFrom?.apertureValues ?? ApertureValue.values,
+ ndValues: copyFrom?.ndValues ?? NdValue.values,
+ shutterSpeedValues: copyFrom?.shutterSpeedValues ?? ShutterSpeedValue.values,
+ isoValues: copyFrom?.isoValues ?? IsoValue.values,
+ ),
+ );
+ _refreshSavedProfiles();
+ }
+
+ void updateProfile(EquipmentProfile data) {
+ final indexToUpdate = _customProfiles.indexWhere((element) => element.id == data.id);
+ if (indexToUpdate >= 0) {
+ _customProfiles[indexToUpdate] = data;
+ _refreshSavedProfiles();
+ }
+ }
+
+ void deleteProfile(EquipmentProfile data) {
+ if (data.id == _selectedId) {
+ _selectedId = _defaultProfile.id;
+ widget.storageService.selectedEquipmentProfileId = _defaultProfile.id;
+ }
+ _customProfiles.remove(data);
+ _refreshSavedProfiles();
+ }
+
+ void _refreshSavedProfiles() {
+ widget.storageService.equipmentProfiles = _customProfiles;
+ setState(() {});
+ }
+}
+
+class EquipmentProfiles extends SelectableInheritedModel {
+ const EquipmentProfiles({
+ super.key,
+ required super.values,
+ required super.selected,
+ required super.child,
+ });
+
+ /// [_defaultProfile] + profiles created by the user
+ static List of(BuildContext context) {
+ return InheritedModel.inheritFrom(context, aspect: SelectableAspect.list)!.values;
+ }
+
+ static EquipmentProfile selectedOf(BuildContext context) {
+ return InheritedModel.inheritFrom(
+ context,
+ aspect: SelectableAspect.selected,
+ )!
+ .selected;
+ }
+}
diff --git a/lib/providers/films_provider.dart b/lib/providers/films_provider.dart
new file mode 100644
index 0000000..aff6d01
--- /dev/null
+++ b/lib/providers/films_provider.dart
@@ -0,0 +1,107 @@
+import 'package:flutter/material.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';
+
+class FilmsProvider extends StatefulWidget {
+ final IAPStorageService storageService;
+ final List? availableFilms;
+ final Widget child;
+
+ const FilmsProvider({
+ required this.storageService,
+ this.availableFilms,
+ required this.child,
+ super.key,
+ });
+
+ static FilmsProviderState of(BuildContext context) {
+ return context.findAncestorStateOfType()!;
+ }
+
+ @override
+ State createState() => FilmsProviderState();
+}
+
+class FilmsProviderState extends State {
+ late List _filmsInUse;
+ late Film _selected;
+
+ @override
+ void initState() {
+ super.initState();
+ _filmsInUse = widget.storageService.filmsInUse;
+ _selected = widget.storageService.selectedFilm;
+ _discardSelectedIfNotIncluded();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Films(
+ values: [
+ const Film.other(),
+ ...widget.availableFilms ?? films,
+ ],
+ filmsInUse: [
+ const Film.other(),
+ if (IAPProducts.isPurchased(context, IAPProductType.paidFeatures)) ..._filmsInUse,
+ ],
+ selected: IAPProducts.isPurchased(context, IAPProductType.paidFeatures)
+ ? _selected
+ : const Film.other(),
+ child: widget.child,
+ );
+ }
+
+ void setFilm(Film film) {
+ if (_selected != film) {
+ _selected = film;
+ widget.storageService.selectedFilm = film;
+ setState(() {});
+ }
+ }
+
+ void saveFilms(List films) {
+ _filmsInUse = films;
+ widget.storageService.filmsInUse = films;
+ _discardSelectedIfNotIncluded();
+ setState(() {});
+ }
+
+ void _discardSelectedIfNotIncluded() {
+ if (_selected != const Film.other() && !_filmsInUse.contains(_selected)) {
+ _selected = const Film.other();
+ widget.storageService.selectedFilm = const Film.other();
+ }
+ }
+}
+
+class Films extends SelectableInheritedModel {
+ final List filmsInUse;
+
+ const Films({
+ super.key,
+ required super.values,
+ required this.filmsInUse,
+ required super.selected,
+ required super.child,
+ });
+
+ /// [Film.other()] + all the custom fields with actual reciprocity formulas
+ static List of(BuildContext context) {
+ return InheritedModel.inheritFrom(context)!.values;
+ }
+
+ /// [Film.other()] + films in use selected by user
+ static List inUseOf(BuildContext context) {
+ return InheritedModel.inheritFrom(
+ context,
+ aspect: SelectableAspect.list,
+ )!
+ .filmsInUse;
+ }
+
+ static Film selectedOf(BuildContext context) {
+ return InheritedModel.inheritFrom(context, aspect: SelectableAspect.selected)!.selected;
+ }
+}
diff --git a/lib/providers/remote_config_provider.dart b/lib/providers/remote_config_provider.dart
new file mode 100644
index 0000000..4557ed6
--- /dev/null
+++ b/lib/providers/remote_config_provider.dart
@@ -0,0 +1,83 @@
+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 IRemoteConfigService remoteConfigService;
+ final Widget child;
+
+ const RemoteConfigProvider({
+ required this.remoteConfigService,
+ required this.child,
+ super.key,
+ });
+
+ @override
+ State createState() => RemoteConfigProviderState();
+}
+
+class RemoteConfigProviderState extends State {
+ late final Map _config = widget.remoteConfigService.getAll();
+ late final StreamSubscription> _updatesSubscription;
+
+ @override
+ void initState() {
+ super.initState();
+ widget.remoteConfigService.fetchConfig();
+ _updatesSubscription = widget.remoteConfigService.onConfigUpdated().listen(
+ _updateFeatures,
+ onError: (e) => log(e.toString()),
+ );
+ }
+
+ @override
+ void dispose() {
+ _updatesSubscription.cancel();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return RemoteConfig(
+ config: _config,
+ child: widget.child,
+ );
+ }
+
+ void _updateFeatures(Set updatedFeatures) {
+ for (final feature in updatedFeatures) {
+ _config[feature] = widget.remoteConfigService.getValue(feature);
+ }
+ setState(() {});
+ }
+}
+
+class RemoteConfig extends InheritedModel {
+ final Map _config;
+
+ const RemoteConfig({
+ super.key,
+ required Map config,
+ required super.child,
+ }) : _config = config;
+
+ static bool isEnabled(BuildContext context, Feature feature) {
+ return InheritedModel.inheritFrom(context)!._config[feature] as bool;
+ }
+
+ @override
+ bool updateShouldNotify(RemoteConfig oldWidget) => true;
+
+ @override
+ bool updateShouldNotifyDependent(RemoteConfig oldWidget, Set features) {
+ for (final feature in features) {
+ if (oldWidget._config[feature] != _config[feature]) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/lib/providers/services_provider.dart b/lib/providers/services_provider.dart
index c2c548f..36f5674 100644
--- a/lib/providers/services_provider.dart
+++ b/lib/providers/services_provider.dart
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
+import 'package:lightmeter/data/analytics/analytics.dart';
import 'package:lightmeter/data/caffeine_service.dart';
import 'package:lightmeter/data/haptics_service.dart';
import 'package:lightmeter/data/light_sensor_service.dart';
@@ -7,7 +8,9 @@ import 'package:lightmeter/data/shared_prefs_service.dart';
import 'package:lightmeter/data/volume_events_service.dart';
import 'package:lightmeter/environment.dart';
+// coverage:ignore-start
class ServicesProvider extends InheritedWidget {
+ final LightmeterAnalytics analytics;
final CaffeineService caffeineService;
final Environment environment;
final HapticsService hapticsService;
@@ -17,6 +20,7 @@ class ServicesProvider extends InheritedWidget {
final VolumeEventsService volumeEventsService;
const ServicesProvider({
+ required this.analytics,
required this.caffeineService,
required this.environment,
required this.hapticsService,
@@ -34,3 +38,4 @@ class ServicesProvider extends InheritedWidget {
@override
bool updateShouldNotify(ServicesProvider oldWidget) => false;
}
+// coverage:ignore-end
diff --git a/lib/providers/user_preferences_provider.dart b/lib/providers/user_preferences_provider.dart
index af644d1..764f282 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';
@@ -8,14 +9,21 @@ import 'package:lightmeter/data/models/supported_locale.dart';
import 'package:lightmeter/data/models/theme_type.dart';
import 'package:lightmeter/data/shared_prefs_service.dart';
import 'package:lightmeter/generated/l10n.dart';
-import 'package:lightmeter/providers/services_provider.dart';
import 'package:lightmeter/res/theme.dart';
+import 'package:lightmeter/utils/map_model.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class UserPreferencesProvider extends StatefulWidget {
+ final bool hasLightSensor;
+ final UserPreferencesService userPreferencesService;
final Widget child;
- const UserPreferencesProvider({required this.child, super.key});
+ const UserPreferencesProvider({
+ required this.hasLightSensor,
+ required this.userPreferencesService,
+ required this.child,
+ super.key,
+ });
static _UserPreferencesProviderState of(BuildContext context) {
return context.findAncestorStateOfType<_UserPreferencesProviderState>()!;
@@ -38,14 +46,21 @@ class UserPreferencesProvider extends StatefulWidget {
}
static bool meteringScreenFeatureOf(BuildContext context, MeteringScreenLayoutFeature feature) {
- return InheritedModel.inheritFrom<_MeteringScreenLayoutModel>(context, aspect: feature)!
- .data[feature]!;
+ return InheritedModel.inheritFrom<_MeteringScreenLayoutModel>(context, aspect: feature)!.data[feature]!;
}
static StopType stopTypeOf(BuildContext context) {
return _inheritFromEnumsModel(context, _Aspect.stopType).stopType;
}
+ 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;
}
@@ -65,28 +80,22 @@ class UserPreferencesProvider extends StatefulWidget {
State createState() => _UserPreferencesProviderState();
}
-class _UserPreferencesProviderState extends State
- with WidgetsBindingObserver {
- UserPreferencesService get userPreferencesService =>
- ServicesProvider.of(context).userPreferencesService;
-
- late bool dynamicColor = userPreferencesService.dynamicColor;
- late EvSourceType evSourceType;
- late MeteringScreenLayoutConfig meteringScreenLayout =
- userPreferencesService.meteringScreenLayout;
- late Color primaryColor = userPreferencesService.primaryColor;
- late StopType stopType = userPreferencesService.stopType;
- late SupportedLocale locale = userPreferencesService.locale;
- late ThemeType themeType = userPreferencesService.themeType;
+class _UserPreferencesProviderState extends State with WidgetsBindingObserver {
+ late EvSourceType _evSourceType;
+ late StopType _stopType = widget.userPreferencesService.stopType;
+ 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;
+ late bool _dynamicColor = widget.userPreferencesService.dynamicColor;
@override
void initState() {
super.initState();
- evSourceType = userPreferencesService.evSourceType;
- evSourceType = evSourceType == EvSourceType.sensor &&
- !ServicesProvider.of(context).environment.hasLightSensor
- ? EvSourceType.camera
- : evSourceType;
+ _evSourceType = widget.userPreferencesService.evSourceType;
+ _evSourceType =
+ _evSourceType == EvSourceType.sensor && !widget.hasLightSensor ? EvSourceType.camera : _evSourceType;
WidgetsBinding.instance.addObserver(this);
}
@@ -109,9 +118,8 @@ class _UserPreferencesProviderState extends State
late final DynamicColorState state;
late final Color? dynamicPrimaryColor;
if (lightDynamic != null && darkDynamic != null) {
- if (dynamicColor) {
- dynamicPrimaryColor =
- (_themeBrightness == Brightness.light ? lightDynamic : darkDynamic).primary;
+ if (_dynamicColor) {
+ dynamicPrimaryColor = (_themeBrightness == Brightness.light ? lightDynamic : darkDynamic).primary;
state = DynamicColorState.enabled;
} else {
dynamicPrimaryColor = null;
@@ -124,14 +132,17 @@ class _UserPreferencesProviderState extends State
return _UserPreferencesModel(
brightness: _themeBrightness,
dynamicColorState: state,
- evSourceType: evSourceType,
- locale: locale,
- primaryColor: dynamicPrimaryColor ?? primaryColor,
- stopType: stopType,
- themeType: themeType,
+ evSourceType: _evSourceType,
+ locale: _locale,
+ primaryColor: dynamicPrimaryColor ?? _primaryColor,
+ stopType: _stopType,
+ themeType: _themeType,
child: _MeteringScreenLayoutModel(
- data: meteringScreenLayout,
- child: widget.child,
+ data: _meteringScreenLayout,
+ child: _CameraFeaturesModel(
+ data: _cameraFeatures,
+ child: widget.child,
+ ),
),
);
},
@@ -140,65 +151,72 @@ class _UserPreferencesProviderState extends State
void enableDynamicColor(bool enable) {
setState(() {
- dynamicColor = enable;
+ _dynamicColor = enable;
});
- userPreferencesService.dynamicColor = enable;
+ widget.userPreferencesService.dynamicColor = enable;
}
void toggleEvSourceType() {
- if (!ServicesProvider.of(context).environment.hasLightSensor) {
+ if (!widget.hasLightSensor) {
return;
}
setState(() {
- switch (evSourceType) {
+ switch (_evSourceType) {
case EvSourceType.camera:
- evSourceType = EvSourceType.sensor;
+ _evSourceType = EvSourceType.sensor;
case EvSourceType.sensor:
- evSourceType = EvSourceType.camera;
+ _evSourceType = EvSourceType.camera;
}
});
- userPreferencesService.evSourceType = evSourceType;
+ widget.userPreferencesService.evSourceType = _evSourceType;
}
void setLocale(SupportedLocale locale) {
S.load(Locale(locale.intlName)).then((value) {
setState(() {
- this.locale = locale;
+ _locale = locale;
});
- userPreferencesService.locale = locale;
+ widget.userPreferencesService.locale = locale;
});
}
void setMeteringScreenLayout(MeteringScreenLayoutConfig config) {
setState(() {
- meteringScreenLayout = config;
+ _meteringScreenLayout = config;
});
- userPreferencesService.meteringScreenLayout = meteringScreenLayout;
+ widget.userPreferencesService.meteringScreenLayout = _meteringScreenLayout;
+ }
+
+ void setCameraFeature(CameraFeaturesConfig config) {
+ setState(() {
+ _cameraFeatures = config;
+ });
+ widget.userPreferencesService.cameraFeatures = _cameraFeatures;
}
void setPrimaryColor(Color primaryColor) {
setState(() {
- this.primaryColor = primaryColor;
+ _primaryColor = primaryColor;
});
- userPreferencesService.primaryColor = primaryColor;
+ widget.userPreferencesService.primaryColor = primaryColor;
}
void setStopType(StopType stopType) {
setState(() {
- this.stopType = stopType;
+ _stopType = stopType;
});
- userPreferencesService.stopType = stopType;
+ widget.userPreferencesService.stopType = stopType;
}
void setThemeType(ThemeType themeType) {
setState(() {
- this.themeType = themeType;
+ _themeType = themeType;
});
- userPreferencesService.themeType = themeType;
+ widget.userPreferencesService.themeType = themeType;
}
Brightness get _themeBrightness {
- switch (themeType) {
+ switch (_themeType) {
case ThemeType.light:
return Brightness.light;
case ThemeType.dark:
@@ -258,8 +276,7 @@ class _UserPreferencesModel extends InheritedModel<_Aspect> {
_UserPreferencesModel oldWidget,
Set<_Aspect> dependencies,
) {
- return (dependencies.contains(_Aspect.dynamicColorState) &&
- dynamicColorState != oldWidget.dynamicColorState) ||
+ return (dependencies.contains(_Aspect.dynamicColorState) && dynamicColorState != oldWidget.dynamicColorState) ||
(dependencies.contains(_Aspect.evSourceType) && evSourceType != oldWidget.evSourceType) ||
(dependencies.contains(_Aspect.locale) && locale != oldWidget.locale) ||
(dependencies.contains(_Aspect.stopType) && stopType != oldWidget.stopType) ||
@@ -269,27 +286,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/camera_container/bloc_container_camera.dart b/lib/screens/metering/components/camera_container/bloc_container_camera.dart
index 1d7d4b5..2e6b051 100644
--- a/lib/screens/metering/components/camera_container/bloc_container_camera.dart
+++ b/lib/screens/metering/components/camera_container/bloc_container_camera.dart
@@ -11,10 +11,8 @@ 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';
@@ -57,6 +55,7 @@ class CameraContainerBloc extends EvSourceBlocBase(_onZoomChanged);
on(_onExposureOffsetChanged);
on(_onExposureOffsetResetEvent);
+ on(_onExposureSpotChangedEvent);
}
@override
@@ -166,9 +165,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 +185,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(
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..e6f1699 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:m3_lightmeter_iap/m3_lightmeter_iap.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 (IAPProducts.isPurchased(context, IAPProductType.paidFeatures)) ...[
+ 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/widget_container_camera.dart b/lib/screens/metering/components/camera_container/widget_container_camera.dart
index 944aa34..a23545e 100644
--- a/lib/screens/metering/components/camera_container/widget_container_camera.dart
+++ b/lib/screens/metering/components/camera_container/widget_container_camera.dart
@@ -143,6 +143,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/exposure_pairs_list/widget_list_exposure_pairs.dart b/lib/screens/metering/components/shared/exposure_pairs_list/widget_list_exposure_pairs.dart
index 4051c5e..cb78275 100644
--- a/lib/screens/metering/components/shared/exposure_pairs_list/widget_list_exposure_pairs.dart
+++ b/lib/screens/metering/components/shared/exposure_pairs_list/widget_list_exposure_pairs.dart
@@ -1,11 +1,10 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/exposure_pair.dart';
import 'package:lightmeter/generated/l10n.dart';
+import 'package:lightmeter/providers/films_provider.dart';
import 'package:lightmeter/res/dimens.dart';
-
import 'package:lightmeter/screens/metering/components/shared/exposure_pairs_list/components/exposure_pairs_list_item/widget_item_list_exposure_pairs.dart';
import 'package:lightmeter/screens/shared/icon_placeholder/widget_icon_placeholder.dart';
-import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
class ExposurePairsList extends StatelessWidget {
final List exposurePairs;
@@ -33,6 +32,7 @@ class ExposurePairsList extends StatelessWidget {
alignment: Alignment.center,
children: [
Row(
+ key: ValueKey(index),
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
diff --git a/lib/screens/metering/components/shared/readings_container/components/equipment_profile_picker/widget_picker_equipment_profiles.dart b/lib/screens/metering/components/shared/readings_container/components/equipment_profile_picker/widget_picker_equipment_profiles.dart
index f47cd53..adbe1d2 100644
--- a/lib/screens/metering/components/shared/readings_container/components/equipment_profile_picker/widget_picker_equipment_profiles.dart
+++ b/lib/screens/metering/components/shared/readings_container/components/equipment_profile_picker/widget_picker_equipment_profiles.dart
@@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
+import 'package:lightmeter/providers/equipment_profile_provider.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/widget_picker_dialog_animated.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/reading_value_container/widget_container_reading_value.dart';
-import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class EquipmentProfilePicker extends StatelessWidget {
diff --git a/lib/screens/metering/components/shared/readings_container/components/extreme_exposure_pairs_container/widget_container_extreme_exposure_pairs.dart b/lib/screens/metering/components/shared/readings_container/components/extreme_exposure_pairs_container/widget_container_extreme_exposure_pairs.dart
index 54c786c..5c701e2 100644
--- a/lib/screens/metering/components/shared/readings_container/components/extreme_exposure_pairs_container/widget_container_extreme_exposure_pairs.dart
+++ b/lib/screens/metering/components/shared/readings_container/components/extreme_exposure_pairs_container/widget_container_extreme_exposure_pairs.dart
@@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/exposure_pair.dart';
import 'package:lightmeter/generated/l10n.dart';
+import 'package:lightmeter/providers/films_provider.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/reading_value_container/widget_container_reading_value.dart';
-import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
class ExtremeExposurePairsContainer extends StatelessWidget {
final ExposurePair? fastest;
diff --git a/lib/screens/metering/components/shared/readings_container/components/film_picker/widget_picker_film.dart b/lib/screens/metering/components/shared/readings_container/components/film_picker/widget_picker_film.dart
index ae1e6fe..13a9366 100644
--- a/lib/screens/metering/components/shared/readings_container/components/film_picker/widget_picker_film.dart
+++ b/lib/screens/metering/components/shared/readings_container/components/film_picker/widget_picker_film.dart
@@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
+import 'package:lightmeter/providers/films_provider.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/widget_picker_dialog_animated.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/reading_value_container/widget_container_reading_value.dart';
-import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class FilmPicker extends StatelessWidget {
diff --git a/lib/screens/metering/components/shared/readings_container/components/nd_picker/widget_picker_nd.dart b/lib/screens/metering/components/shared/readings_container/components/nd_picker/widget_picker_nd.dart
index eda016c..fdd8f0a 100644
--- a/lib/screens/metering/components/shared/readings_container/components/nd_picker/widget_picker_nd.dart
+++ b/lib/screens/metering/components/shared/readings_container/components/nd_picker/widget_picker_nd.dart
@@ -27,14 +27,13 @@ class NdValuePicker extends StatelessWidget {
value.value == 0 ? S.of(context).none : value.value.toString(),
),
// using descending order, because ND filter darkens image & lowers EV
- itemTrailingBuilder: (selected, value) => value.value != selected.value
- ? Text(S.of(context).evValue(value.toStringDifference(selected)))
- : null,
+ itemTrailingBuilder: (selected, value) =>
+ value.value != selected.value ? Text(S.of(context).evValue(value.toStringDifference(selected))) : null,
onChanged: onChanged,
closedChild: ReadingValueContainer.singleValue(
value: ReadingValue(
label: S.of(context).nd,
- value: selectedValue.value.toString(),
+ value: selectedValue.value == 0 ? S.of(context).none : selectedValue.value.toString(),
),
),
);
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 f10546d..cb8af05 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
@@ -1,6 +1,7 @@
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';
@@ -8,7 +9,6 @@ import 'package:lightmeter/screens/metering/components/shared/readings_container
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/nd_picker/widget_picker_nd.dart';
-import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class ReadingsContainer extends StatelessWidget {
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 f1d11fb..380411f 100644
--- a/lib/screens/metering/screen_metering.dart
+++ b/lib/screens/metering/screen_metering.dart
@@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/data/models/ev_source_type.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/services_provider.dart';
import 'package:lightmeter/providers/user_preferences_provider.dart';
import 'package:lightmeter/screens/metering/bloc_metering.dart';
@@ -13,9 +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/listener_metering_layout_feature.dart';
-import 'package:lightmeter/screens/metering/utils/listsner_equipment_profiles.dart';
-import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
+import 'package:lightmeter/screens/metering/utils/listener_equipment_profiles.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class MeteringScreen extends StatelessWidget {
@@ -73,15 +71,7 @@ class _InheritedListeners extends StatelessWidget {
onDidChangeDependencies: (value) {
context.read().add(EquipmentProfileChangedEvent(value));
},
- child: MeteringScreenLayoutFeatureListener(
- feature: MeteringScreenLayoutFeature.filmPicker,
- onDidChangeDependencies: (value) {
- if (!value) {
- FilmsProvider.of(context).setFilm(const Film.other());
- }
- },
- child: child,
- ),
+ child: child,
);
}
}
diff --git a/lib/screens/metering/utils/listsner_equipment_profiles.dart b/lib/screens/metering/utils/listener_equipment_profiles.dart
similarity index 91%
rename from lib/screens/metering/utils/listsner_equipment_profiles.dart
rename to lib/screens/metering/utils/listener_equipment_profiles.dart
index 68d03dc..ec604ce 100644
--- a/lib/screens/metering/utils/listsner_equipment_profiles.dart
+++ b/lib/screens/metering/utils/listener_equipment_profiles.dart
@@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
-import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
+import 'package:lightmeter/providers/equipment_profile_provider.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class EquipmentProfileListener extends StatefulWidget {
diff --git a/lib/screens/metering/utils/listener_metering_layout_feature.dart b/lib/screens/metering/utils/listener_metering_layout_feature.dart
deleted file mode 100644
index c245ec3..0000000
--- a/lib/screens/metering/utils/listener_metering_layout_feature.dart
+++ /dev/null
@@ -1,52 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
-import 'package:lightmeter/providers/user_preferences_provider.dart';
-
-/// Listening to multiple dependencies at the same time causes firing an event for all dependencies
-/// even though some of them didn't change:
-/// ```dart
-/// @override
-/// void didChangeDependencies() {
-/// super.didChangeDependencies();
-/// _bloc.add(EquipmentProfileChangedEvent(EquipmentProfile.of(context)));
-/// if (!MeteringScreenLayout.featureStatusOf(context, MeteringScreenLayoutFeature.filmPicker)) {
-/// _bloc.add(const FilmChangedEvent(Film.other()));
-/// }
-/// }
-/// ```
-/// To overcome this issue I've decided to create a generic listener,
-/// that will listen to each dependency separately.
-class MeteringScreenLayoutFeatureListener extends StatefulWidget {
- final MeteringScreenLayoutFeature feature;
- final ValueChanged onDidChangeDependencies;
- final Widget child;
-
- const MeteringScreenLayoutFeatureListener({
- required this.feature,
- required this.onDidChangeDependencies,
- required this.child,
- super.key,
- });
-
- @override
- State createState() =>
- _MeteringScreenLayoutFeatureListenerState();
-}
-
-class _MeteringScreenLayoutFeatureListenerState extends State {
- @override
- void didChangeDependencies() {
- super.didChangeDependencies();
- widget.onDidChangeDependencies(
- UserPreferencesProvider.meteringScreenFeatureOf(
- context,
- widget.feature,
- ),
- );
- }
-
- @override
- Widget build(BuildContext context) {
- return widget.child;
- }
-}
diff --git a/lib/screens/metering/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 11e8a4f..9854a14 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,20 +1,38 @@
import 'package:flutter/material.dart';
+import 'package:lightmeter/data/models/feature.dart';
import 'package:lightmeter/generated/l10n.dart';
-import 'package:lightmeter/screens/settings/components/shared/iap_list_tile/widget_list_tile_iap.dart';
-import 'package:lightmeter/screens/settings/components/utils/show_buy_pro_dialog.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/utils/show_buy_pro_dialog.dart';
+import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
class BuyProListTile extends StatelessWidget {
const BuyProListTile({super.key});
@override
Widget build(BuildContext context) {
- return IAPListTile(
+ 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(S.of(context).buyLightmeterPro),
- onTap: () {
- showBuyProDialog(context);
- },
- showPendingTrailing: true,
+ title: Text(unlockFeaturesEnabled ? S.of(context).unlockProFeatures : S.of(context).buyLightmeterPro),
+ onTap: !isPending
+ ? () {
+ showBuyProDialog(context);
+ ServicesProvider.of(context)
+ .analytics
+ .logUnlockProFeatures(unlockFeaturesEnabled ? 'Unlock Pro features' : 'Buy Lightmeter Pro');
+ }
+ : null,
+ trailing: isPending
+ ? const SizedBox(
+ height: Dimens.grid24,
+ width: Dimens.grid24,
+ child: CircularProgressIndicator(),
+ )
+ : null,
);
}
}
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 c060dc1..7050ae2 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,5 +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/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';
@@ -9,7 +11,9 @@ class LightmeterProSettingsSection extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SettingsSection(
- title: S.of(context).lightmeterPro,
+ title: RemoteConfig.isEnabled(context, Feature.unlockProFeaturesText)
+ ? S.of(context).proFeatures
+ : S.of(context).lightmeterPro,
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 3c72918..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
@@ -1,12 +1,11 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
-
+import 'package:lightmeter/providers/equipment_profile_provider.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart';
import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_name_dialog/widget_dialog_equipment_profile_name.dart';
import 'package:lightmeter/screens/shared/icon_placeholder/widget_icon_placeholder.dart';
import 'package:lightmeter/screens/shared/sliver_screen/screen_sliver.dart';
-import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class EquipmentProfilesScreen extends StatefulWidget {
@@ -91,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/films/widget_list_tile_films.dart b/lib/screens/settings/components/metering/components/films/widget_list_tile_films.dart
index c343e2b..72ff433 100644
--- a/lib/screens/settings/components/metering/components/films/widget_list_tile_films.dart
+++ b/lib/screens/settings/components/metering/components/films/widget_list_tile_films.dart
@@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
+import 'package:lightmeter/providers/films_provider.dart';
import 'package:lightmeter/screens/settings/components/shared/dialog_filter/widget_dialog_filter.dart';
import 'package:lightmeter/screens/settings/components/shared/iap_list_tile/widget_list_tile_iap.dart';
-import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class FilmsListTile extends StatelessWidget {
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 57aaf24..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,94 +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/user_preferences_provider.dart';
-import 'package:lightmeter/res/dimens.dart';
-import 'package:m3_lightmeter_iap/m3_lightmeter_iap.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);
- }
- 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..1f89b4b 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,11 @@
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:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class MeteringScreenLayoutListTile extends StatelessWidget {
const MeteringScreenLayoutListTile({super.key});
@@ -14,9 +18,35 @@ 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,
+ 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/widget_settings_section_metering.dart b/lib/screens/settings/components/metering/widget_settings_section_metering.dart
index 90de68d..0c6c07d 100644
--- a/lib/screens/settings/components/metering/widget_settings_section_metering.dart
+++ b/lib/screens/settings/components/metering/widget_settings_section_metering.dart
@@ -1,6 +1,7 @@
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';
@@ -20,6 +21,7 @@ class MeteringSettingsSection extends StatelessWidget {
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 ValueChanged