Compare commits

..

No commits in common. "a42459de07627f0edfdda80c5bd013b50a9f1dc4" and "c7fe50a797cd802bdf1be4c2e34b3baaa338de48" have entirely different histories.

48 changed files with 362 additions and 707 deletions

15
.vscode/launch.json vendored
View file

@ -61,20 +61,5 @@
], ],
"program": "${workspaceFolder}/lib/main_dev.dart", "program": "${workspaceFolder}/lib/main_dev.dart",
}, },
{
"name": "dev-simulator",
"request": "launch",
"type": "dart",
"flutterMode": "debug",
"args": [
"--flavor",
"dev",
"--dart-define",
"cameraPreviewAspectRatio=240/320",
"--dart-define",
"cameraStubImage=assets/camera_stub_image.jpg"
],
"program": "${workspaceFolder}/lib/main_dev.dart",
},
], ],
} }

View file

@ -1,6 +1,6 @@
**Privacy Policy** **Privacy Policy**
I, Vadim Turko, built the Material Lightmeter app as a Free app. This app is provided at no cost and is intended for use as is. I, Vodemn, built the Material Lightmeter app as a Free app. This app is provided at no cost and is intended for use as is.
**Information Collection and Use** **Information Collection and Use**
@ -20,7 +20,7 @@ This app contains links to other sites. If you click on a third-party link, you
I may update our Privacy Policy from time to time. Thus, you are advised to review this page periodically for any changes. I will notify you of any changes by posting the new Privacy Policy on this page. I may update our Privacy Policy from time to time. Thus, you are advised to review this page periodically for any changes. I will notify you of any changes by posting the new Privacy Policy on this page.
This policy is effective as of 2024-01-04 This policy is effective as of 2023-02-24
**Contact Us** **Contact Us**

View file

@ -9,7 +9,7 @@
- [Backstory](#backstory) - [Backstory](#backstory)
- [Screenshots](#screenshots) - [Screenshots](#screenshots)
- [Development](#development) - [Development](#development)
- [Support](#support) - [Contribution](#contribution)
- [iOS Limitations](#ios-limitations) - [iOS Limitations](#ios-limitations)
# Backstory # Backstory
@ -36,7 +36,7 @@ Without further delay behold my new Lightmeter app inspired by Material You (a.k
To build this app you need to install Flutter 3.10.0 stable. [How to install](https://docs.flutter.dev/get-started/install). To build this app you need to install Flutter 3.10.0 stable. [How to install](https://docs.flutter.dev/get-started/install).
### 2. Project setup ### 3. Project setup
As part of the app's functionallity is in the private repo, you have to replace these lines in _pubspec.yaml_: As part of the app's functionallity is in the private repo, you have to replace these lines in _pubspec.yaml_:
@ -69,11 +69,11 @@ flutter pub get
flutter pub run intl_utils:generate flutter pub run intl_utils:generate
``` ```
### 3. (Optional) Install Firebase ### 4. (Optional) Install Firebase
Out of the box Firebase Crashlytics won't work. If you want to add Crashlytics to your local build please follow [this guide](https://firebase.google.com/docs/flutter/setup). Out of the box Firebase Crashlytics won't work. If you want to add Crashlytics to your local build please follow [this guide](https://firebase.google.com/docs/flutter/setup).
### 4. Build ### 5. Build
#### Android #### Android
@ -87,11 +87,11 @@ flutter build apk --release --flavor dev --dart-define cameraPreviewAspectRatio=
TBD TBD
# Support # Contribution
To report a bug or suggest a new feature open a new [issue](https://github.com/vodemn/m3_lightmeter/issues). To contribute to the project feel free to open a Pull Request, but you need to follow this [style guide](doc/style_guide.md). To report a bug or suggest a new feature open a new [issue](https://github.com/vodemn/m3_lightmeter/issues).
In case you have any other questions please contact me via [email](mailto:contact.vodemn@gmail.com?subject="Lightmeter"). In case you want to help develop this project feel free to open a Pull Request, but you need to follow this [style guide](doc/style_guide.md).
# iOS Limitations # iOS Limitations

View file

@ -1,5 +1,5 @@
enum Feature { showUnlockProOnMainScreen } enum Feature { unlockProFeaturesText }
const featuresDefaultValues = { const featuresDefaultValues = {
Feature.showUnlockProOnMainScreen: false, Feature.unlockProFeaturesText: true,
}; };

View file

@ -52,15 +52,9 @@ class RemoteConfigService implements IRemoteConfigService {
@override @override
Future<void> fetchConfig() async { Future<void> fetchConfig() async {
try { // https://github.com/firebase/flutterfire/issues/6196#issuecomment-927751667
// https://github.com/firebase/flutterfire/issues/6196#issuecomment-927751667 await Future.delayed(const Duration(seconds: 1));
await Future.delayed(const Duration(seconds: 1)); await FirebaseRemoteConfig.instance.fetch();
await FirebaseRemoteConfig.instance.fetch();
} on FirebaseException catch (e) {
_logError('Firebase exception during Firebase Remote Config fetch: $e');
} catch (e) {
_logError('Error during Firebase Remote Config fetch: $e');
}
} }
@override @override
@ -130,7 +124,7 @@ class MockRemoteConfigService implements IRemoteConfigService {
extension on RemoteConfigValue { extension on RemoteConfigValue {
dynamic toValue(Feature feature) { dynamic toValue(Feature feature) {
switch (feature) { switch (feature) {
case Feature.showUnlockProOnMainScreen: case Feature.unlockProFeaturesText:
return asBool(); return asBool();
} }
} }

View file

@ -16,7 +16,6 @@ class UserPreferencesService {
static const evSourceTypeKey = "evSourceType"; static const evSourceTypeKey = "evSourceType";
static const stopTypeKey = "stopType"; static const stopTypeKey = "stopType";
static const showEv100Key = "showEv100";
static const cameraEvCalibrationKey = "cameraEvCalibration"; static const cameraEvCalibrationKey = "cameraEvCalibration";
static const lightSensorEvCalibrationKey = "lightSensorEvCalibration"; static const lightSensorEvCalibrationKey = "lightSensorEvCalibration";
static const meteringScreenLayoutKey = "meteringScreenLayout"; static const meteringScreenLayoutKey = "meteringScreenLayout";
@ -85,9 +84,6 @@ class UserPreferencesService {
StopType get stopType => StopType.values[_sharedPreferences.getInt(stopTypeKey) ?? 2]; StopType get stopType => StopType.values[_sharedPreferences.getInt(stopTypeKey) ?? 2];
set stopType(StopType value) => _sharedPreferences.setInt(stopTypeKey, value.index); set stopType(StopType value) => _sharedPreferences.setInt(stopTypeKey, value.index);
bool get showEv100 => _sharedPreferences.getBool(showEv100Key) ?? false;
set showEv100(bool value) => _sharedPreferences.setBool(showEv100Key, value);
MeteringScreenLayoutConfig get meteringScreenLayout { MeteringScreenLayoutConfig get meteringScreenLayout {
final configJson = _sharedPreferences.getString(meteringScreenLayoutKey); final configJson = _sharedPreferences.getString(meteringScreenLayoutKey);
if (configJson != null) { if (configJson != null) {

View file

@ -34,7 +34,6 @@
"calibrationMessageCameraOnly": "The accuracy of the readings measured by this application depends entirely on the rear camera of the device. Therefore, consider testing this application and setting up an EV calibration value that will give you the desired measurement results.", "calibrationMessageCameraOnly": "The accuracy of the readings measured by this application depends entirely on the rear camera of the device. Therefore, consider testing this application and setting up an EV calibration value that will give you the desired measurement results.",
"camera": "Camera", "camera": "Camera",
"lightSensor": "Light sensor", "lightSensor": "Light sensor",
"showEv100": "Show EV\u2081\u2080\u2080",
"meteringScreenLayout": "Metering screen layout", "meteringScreenLayout": "Metering screen layout",
"meteringScreenLayoutHint": "Hide elements on the metering screen that you don't need so that they don't waste exposure pairs list space.", "meteringScreenLayoutHint": "Hide elements on the metering screen that you don't need so that they don't waste exposure pairs list space.",
"meteringScreenLayoutHintEquipmentProfiles": "Equipment profile picker", "meteringScreenLayoutHintEquipmentProfiles": "Equipment profile picker",
@ -48,7 +47,7 @@
"film": "Film", "film": "Film",
"filmPush": "Film (push)", "filmPush": "Film (push)",
"filmPull": "Film (pull)", "filmPull": "Film (pull)",
"filmReciprocityHint": "Applies correction for shutter speeds greater than 1 second", "filmReciprocityHint": "Applies correction for shutter speeds grater than 1 second",
"equipmentProfileName": "Equipment profile name", "equipmentProfileName": "Equipment profile name",
"equipmentProfileNameHint": "Praktica MTL5B", "equipmentProfileNameHint": "Praktica MTL5B",
"equipmentProfileAllValues": "All", "equipmentProfileAllValues": "All",
@ -97,6 +96,10 @@
} }
} }
}, },
"lightmeterPro": "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", "proFeatures": "Pro features",
"unlockProFeatures": "Unlock 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.", "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.",
@ -113,4 +116,4 @@
"tooltipUseLightSensor": "Use lightsensor", "tooltipUseLightSensor": "Use lightsensor",
"tooltipUseCamera": "Use camera", "tooltipUseCamera": "Use camera",
"tooltipOpenSettings": "Open settings" "tooltipOpenSettings": "Open settings"
} }

View file

@ -34,7 +34,6 @@
"calibrationMessageCameraOnly": "La précision des lectures mesurées par cette application dépend entièrement de la caméra arrière de l'appareil. Par conséquent, envisagez de tester cette application et de configurer une valeur d'étalonnage EV qui vous donnera les résultats de mesure souhaités.", "calibrationMessageCameraOnly": "La précision des lectures mesurées par cette application dépend entièrement de la caméra arrière de l'appareil. Par conséquent, envisagez de tester cette application et de configurer une valeur d'étalonnage EV qui vous donnera les résultats de mesure souhaités.",
"camera": "Caméra", "camera": "Caméra",
"lightSensor": "Capteur de lumière", "lightSensor": "Capteur de lumière",
"showEv100": "Montrer EV\u2081\u2080\u2080",
"meteringScreenLayout": "Disposition de l'écran de mesure", "meteringScreenLayout": "Disposition de l'écran de mesure",
"meteringScreenLayoutHint": "Masquer les éléments sur l'écran de mesure dont vous n'avez pas besoin pour qu'ils ne gaspillent pas de l'espace dans les paires d'exposition.", "meteringScreenLayoutHint": "Masquer les éléments sur l'écran de mesure dont vous n'avez pas besoin pour qu'ils ne gaspillent pas de l'espace dans les paires d'exposition.",
"meteringScreenLayoutHintEquipmentProfiles": "Sélecteur de profil de l'équipement", "meteringScreenLayoutHintEquipmentProfiles": "Sélecteur de profil de l'équipement",
@ -97,6 +96,10 @@
} }
} }
}, },
"buyLightmeterPro": "Acheter Lightmeter Pro",
"lightmeterPro": "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", "proFeatures": "Fonctionnalités professionnelles",
"unlockProFeatures": "Déverrouiller les 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.", "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.",

View file

@ -34,7 +34,6 @@
"calibrationMessageCameraOnly": "Точность измерений данного приложения полностью зависит от точности камеры вашего устройства. Поэтому рекомендуется самостоятельно подобрать калибровочное значение, которое даст желаемый результат измерений.", "calibrationMessageCameraOnly": "Точность измерений данного приложения полностью зависит от точности камеры вашего устройства. Поэтому рекомендуется самостоятельно подобрать калибровочное значение, которое даст желаемый результат измерений.",
"camera": "Камера", "camera": "Камера",
"lightSensor": "Датчик освещённости", "lightSensor": "Датчик освещённости",
"showEv100": "Показывать EV\u2081\u2080\u2080",
"meteringScreenLayout": "Элементы главного экрана", "meteringScreenLayout": "Элементы главного экрана",
"meteringScreenLayoutHint": "Здесь вы можете скрыть некоторые ненужные или неиспользуемые элементы с главного экрана.", "meteringScreenLayoutHint": "Здесь вы можете скрыть некоторые ненужные или неиспользуемые элементы с главного экрана.",
"meteringScreenLayoutHintEquipmentProfiles": "Выбор профиля оборудования", "meteringScreenLayoutHintEquipmentProfiles": "Выбор профиля оборудования",
@ -97,6 +96,10 @@
} }
} }
}, },
"buyLightmeterPro": "Купить Lightmeter Pro",
"lightmeterPro": "Lightmeter Pro",
"lightmeterProDescription": "Даёт доступ к различным функциям:\n \u2022 Профили оборудования, содержащие фильтры для диафрагмы, выдержки и других значений\n \u2022 Список пленок с компенсацией эффекта Шварцшильда\n \u2022 Точечный замер\n \u2022 Гистограмма\n\nИсходный код Lightmeter доступен на GitHub. Вы можете собрать его самостоятельно. Однако если вы хотите поддержать разработку и получать новые функции и обновления, то приобретите Lightmeter Pro.",
"buy": "Купить",
"proFeatures": "Профессиональные настройки", "proFeatures": "Профессиональные настройки",
"unlockProFeatures": "Разблокировать профессиональные настройки", "unlockProFeatures": "Разблокировать профессиональные настройки",
"unlockProFeaturesDescription": "Вы можете разблокировать профессиональные настройки:\n \u2022 Профили оборудования, содержащие фильтры для диафрагмы, выдержки и других значений\n \u2022 Список пленок с компенсацией эффекта Шварцшильда\n \u2022 Точечный замер\n \u2022 Гистограмма\n\nПолучая доступ к профессиональным настройкам, вы поддерживаете разработку и делаете возможным появление новых функций в приложении.", "unlockProFeaturesDescription": "Вы можете разблокировать профессиональные настройки:\n \u2022 Профили оборудования, содержащие фильтры для диафрагмы, выдержки и других значений\n \u2022 Список пленок с компенсацией эффекта Шварцшильда\n \u2022 Точечный замер\n \u2022 Гистограмма\n\nПолучая доступ к профессиональным настройкам, вы поддерживаете разработку и делаете возможным появление новых функций в приложении.",

View file

@ -34,7 +34,6 @@
"calibrationMessageCameraOnly": "此应用程序测量读数的准确s性完全取决于设备的后置摄像头。因此请考虑测试此应用并手动设置 EV 校准,以获得准确的测量结果。", "calibrationMessageCameraOnly": "此应用程序测量读数的准确s性完全取决于设备的后置摄像头。因此请考虑测试此应用并手动设置 EV 校准,以获得准确的测量结果。",
"camera": "摄像头", "camera": "摄像头",
"lightSensor": "光传感器", "lightSensor": "光传感器",
"showEv100": "显示 EV\u2081\u2080\u2080",
"meteringScreenLayout": "布局", "meteringScreenLayout": "布局",
"meteringScreenLayoutHint": "隐藏不需要的元素,以免浪费曝光列表空间", "meteringScreenLayoutHint": "隐藏不需要的元素,以免浪费曝光列表空间",
"meteringScreenLayoutHintEquipmentProfiles": "设备配置选择", "meteringScreenLayoutHintEquipmentProfiles": "设备配置选择",
@ -97,6 +96,10 @@
} }
} }
}, },
"buyLightmeterPro": "购买 Lightmeter Pro",
"lightmeterPro": "Lightmeter Pro",
"lightmeterProDescription": "解锁额外功能:\n \u2022 配置文件,其中包含光圈、快门速度等参数\n \u2022 胶卷列表,对胶片倒易率失效进行曝光补偿\n \u2022 点测光\n \u2022 直方图\n\n您可以在 GitHub 上获取 Lightmeter 的源代码,欢迎自行编译。不过,如果您想支持开发并获得新功能和更新,请考虑购买 Lightmeter Pro。",
"buy": "购买",
"proFeatures": "专业功能", "proFeatures": "专业功能",
"unlockProFeatures": "解锁专业功能", "unlockProFeatures": "解锁专业功能",
"unlockProFeaturesDescription": "\n \u2022 配置文件,其中包含光圈、快门速度等参数\n \u2022 胶卷列表,对胶片倒易率失效进行曝光补偿\n \u2022 点测光\n \u2022 直方图\n\n通过解锁专业版功能您可以支持开发工作帮助为应用程序添加新功能。", "unlockProFeaturesDescription": "\n \u2022 配置文件,其中包含光圈、快门速度等参数\n \u2022 胶卷列表,对胶片倒易率失效进行曝光补偿\n \u2022 点测光\n \u2022 直方图\n\n通过解锁专业版功能您可以支持开发工作帮助为应用程序添加新功能。",

View file

@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:lightmeter/utils/context_utils.dart';
import 'package:lightmeter/utils/selectable_provider.dart'; import 'package:lightmeter/utils/selectable_provider.dart';
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
@ -53,9 +52,9 @@ class EquipmentProfileProviderState extends State<EquipmentProfileProvider> {
return EquipmentProfiles( return EquipmentProfiles(
values: [ values: [
_defaultProfile, _defaultProfile,
if (context.isPro) ..._customProfiles, if (IAPProducts.isPurchased(context, IAPProductType.paidFeatures)) ..._customProfiles,
], ],
selected: context.isPro ? _selectedProfile : _defaultProfile, selected: IAPProducts.isPurchased(context, IAPProductType.paidFeatures) ? _selectedProfile : _defaultProfile,
child: widget.child, child: widget.child,
); );
} }

View file

@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:lightmeter/utils/context_utils.dart';
import 'package:lightmeter/utils/selectable_provider.dart'; import 'package:lightmeter/utils/selectable_provider.dart';
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
@ -45,9 +44,11 @@ class FilmsProviderState extends State<FilmsProvider> {
], ],
filmsInUse: [ filmsInUse: [
const Film.other(), const Film.other(),
if (context.isPro) ..._filmsInUse, if (IAPProducts.isPurchased(context, IAPProductType.paidFeatures)) ..._filmsInUse,
], ],
selected: context.isPro ? _selected : const Film.other(), selected: IAPProducts.isPurchased(context, IAPProductType.paidFeatures)
? _selected
: const Film.other(),
child: widget.child, child: widget.child,
); );
} }

View file

@ -53,10 +53,6 @@ class UserPreferencesProvider extends StatefulWidget {
return _inheritFromEnumsModel(context, _Aspect.stopType).stopType; return _inheritFromEnumsModel(context, _Aspect.stopType).stopType;
} }
static bool showEv100Of(BuildContext context) {
return _inheritFromEnumsModel(context, _Aspect.showEv100).showEv100;
}
static CameraFeaturesConfig cameraConfigOf(BuildContext context) { static CameraFeaturesConfig cameraConfigOf(BuildContext context) {
return context.findAncestorWidgetOfExactType<_CameraFeaturesModel>()!.data; return context.findAncestorWidgetOfExactType<_CameraFeaturesModel>()!.data;
} }
@ -87,7 +83,6 @@ class UserPreferencesProvider extends StatefulWidget {
class _UserPreferencesProviderState extends State<UserPreferencesProvider> with WidgetsBindingObserver { class _UserPreferencesProviderState extends State<UserPreferencesProvider> with WidgetsBindingObserver {
late EvSourceType _evSourceType; late EvSourceType _evSourceType;
late StopType _stopType = widget.userPreferencesService.stopType; late StopType _stopType = widget.userPreferencesService.stopType;
late bool _showEv100 = widget.userPreferencesService.showEv100;
late MeteringScreenLayoutConfig _meteringScreenLayout = widget.userPreferencesService.meteringScreenLayout; late MeteringScreenLayoutConfig _meteringScreenLayout = widget.userPreferencesService.meteringScreenLayout;
late CameraFeaturesConfig _cameraFeatures = widget.userPreferencesService.cameraFeatures; late CameraFeaturesConfig _cameraFeatures = widget.userPreferencesService.cameraFeatures;
late SupportedLocale _locale = widget.userPreferencesService.locale; late SupportedLocale _locale = widget.userPreferencesService.locale;
@ -140,7 +135,6 @@ class _UserPreferencesProviderState extends State<UserPreferencesProvider> with
evSourceType: _evSourceType, evSourceType: _evSourceType,
locale: _locale, locale: _locale,
primaryColor: dynamicPrimaryColor ?? _primaryColor, primaryColor: dynamicPrimaryColor ?? _primaryColor,
showEv100: _showEv100,
stopType: _stopType, stopType: _stopType,
themeType: _themeType, themeType: _themeType,
child: _MeteringScreenLayoutModel( child: _MeteringScreenLayoutModel(
@ -207,13 +201,6 @@ class _UserPreferencesProviderState extends State<UserPreferencesProvider> with
widget.userPreferencesService.primaryColor = primaryColor; widget.userPreferencesService.primaryColor = primaryColor;
} }
void toggleShowEv100() {
setState(() {
_showEv100 = !_showEv100;
});
widget.userPreferencesService.showEv100 = _showEv100;
}
void setStopType(StopType stopType) { void setStopType(StopType stopType) {
setState(() { setState(() {
_stopType = stopType; _stopType = stopType;
@ -244,7 +231,6 @@ enum _Aspect {
dynamicColorState, dynamicColorState,
evSourceType, evSourceType,
locale, locale,
showEv100,
stopType, stopType,
theme, theme,
themeType, themeType,
@ -254,7 +240,6 @@ class _UserPreferencesModel extends InheritedModel<_Aspect> {
final DynamicColorState dynamicColorState; final DynamicColorState dynamicColorState;
final EvSourceType evSourceType; final EvSourceType evSourceType;
final SupportedLocale locale; final SupportedLocale locale;
final bool showEv100;
final StopType stopType; final StopType stopType;
final ThemeType themeType; final ThemeType themeType;
@ -267,7 +252,6 @@ class _UserPreferencesModel extends InheritedModel<_Aspect> {
required this.evSourceType, required this.evSourceType,
required this.locale, required this.locale,
required Color primaryColor, required Color primaryColor,
required this.showEv100,
required this.stopType, required this.stopType,
required this.themeType, required this.themeType,
required super.child, required super.child,
@ -283,7 +267,6 @@ class _UserPreferencesModel extends InheritedModel<_Aspect> {
evSourceType != oldWidget.evSourceType || evSourceType != oldWidget.evSourceType ||
locale != oldWidget.locale || locale != oldWidget.locale ||
_primaryColor != oldWidget._primaryColor || _primaryColor != oldWidget._primaryColor ||
showEv100 != oldWidget.showEv100 ||
stopType != oldWidget.stopType || stopType != oldWidget.stopType ||
themeType != oldWidget.themeType; themeType != oldWidget.themeType;
} }
@ -296,7 +279,6 @@ class _UserPreferencesModel extends InheritedModel<_Aspect> {
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.evSourceType) && evSourceType != oldWidget.evSourceType) ||
(dependencies.contains(_Aspect.locale) && locale != oldWidget.locale) || (dependencies.contains(_Aspect.locale) && locale != oldWidget.locale) ||
(dependencies.contains(_Aspect.showEv100) && showEv100 != oldWidget.showEv100) ||
(dependencies.contains(_Aspect.stopType) && stopType != oldWidget.stopType) || (dependencies.contains(_Aspect.stopType) && stopType != oldWidget.stopType) ||
(dependencies.contains(_Aspect.theme) && (dependencies.contains(_Aspect.theme) &&
(_brightness != oldWidget._brightness || _primaryColor != oldWidget._primaryColor)) || (_brightness != oldWidget._brightness || _primaryColor != oldWidget._primaryColor)) ||

View file

@ -1,21 +1,15 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/providers/user_preferences_provider.dart';
import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/shared/filled_circle/widget_circle_filled.dart'; import 'package:lightmeter/screens/shared/filled_circle/widget_circle_filled.dart';
import 'package:lightmeter/utils/context_utils.dart';
const String _subscript100 = '\u2081\u2080\u2080';
class MeteringMeasureButton extends StatefulWidget { class MeteringMeasureButton extends StatefulWidget {
final double? ev; final double? ev;
final double? ev100;
final bool isMetering; final bool isMetering;
final VoidCallback onTap; final VoidCallback onTap;
const MeteringMeasureButton({ const MeteringMeasureButton({
required this.ev, required this.ev,
required this.ev100,
required this.isMetering, required this.isMetering,
required this.onTap, required this.onTap,
super.key, super.key,
@ -67,7 +61,7 @@ class _MeteringMeasureButtonState extends State<MeteringMeasureButton> {
color: Theme.of(context).colorScheme.onSurface, color: Theme.of(context).colorScheme.onSurface,
size: Dimens.grid72 - Dimens.grid8, size: Dimens.grid72 - Dimens.grid8,
child: Center( child: Center(
child: widget.ev != null ? _EvValueText(ev: widget.ev!, ev100: widget.ev100!) : null, child: widget.ev != null ? _EvValueText(ev: widget.ev!) : null,
), ),
), ),
), ),
@ -89,32 +83,16 @@ class _MeteringMeasureButtonState extends State<MeteringMeasureButton> {
class _EvValueText extends StatelessWidget { class _EvValueText extends StatelessWidget {
final double ev; final double ev;
final double ev100;
const _EvValueText({ const _EvValueText({required this.ev});
required this.ev,
required this.ev100,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
return Text( return Text(
_text(context), '${ev.toStringAsFixed(1)}\n${S.of(context).ev}',
style: theme.textTheme.bodyMedium?.copyWith(color: theme.colorScheme.surface), style: theme.textTheme.bodyMedium?.copyWith(color: theme.colorScheme.surface),
textAlign: TextAlign.center, textAlign: TextAlign.center,
); );
} }
String _text(BuildContext context) {
final bool showEv100 = context.isPro && UserPreferencesProvider.showEv100Of(context);
final StringBuffer buffer = StringBuffer()
..writeAll([
(showEv100 ? ev100 : ev).toStringAsFixed(1),
'\n',
S.of(context).ev,
if (showEv100) _subscript100,
]);
return buffer.toString();
}
} }

View file

@ -5,7 +5,6 @@ import 'package:lightmeter/screens/metering/components/bottom_controls/widget_bo
class MeteringBottomControlsProvider extends StatelessWidget { class MeteringBottomControlsProvider extends StatelessWidget {
final double? ev; final double? ev;
final double? ev100;
final bool isMetering; final bool isMetering;
final VoidCallback? onSwitchEvSourceType; final VoidCallback? onSwitchEvSourceType;
final VoidCallback onMeasure; final VoidCallback onMeasure;
@ -13,7 +12,6 @@ class MeteringBottomControlsProvider extends StatelessWidget {
const MeteringBottomControlsProvider({ const MeteringBottomControlsProvider({
required this.ev, required this.ev,
required this.ev100,
required this.isMetering, required this.isMetering,
required this.onSwitchEvSourceType, required this.onSwitchEvSourceType,
required this.onMeasure, required this.onMeasure,
@ -37,7 +35,6 @@ class MeteringBottomControlsProvider extends StatelessWidget {
), ),
child: MeteringBottomControls( child: MeteringBottomControls(
ev: ev, ev: ev,
ev100: ev100,
isMetering: isMetering, isMetering: isMetering,
onSwitchEvSourceType: onSwitchEvSourceType, onSwitchEvSourceType: onSwitchEvSourceType,
onMeasure: onMeasure, onMeasure: onMeasure,

View file

@ -7,7 +7,6 @@ import 'package:lightmeter/screens/metering/components/bottom_controls/component
class MeteringBottomControls extends StatelessWidget { class MeteringBottomControls extends StatelessWidget {
final double? ev; final double? ev;
final double? ev100;
final bool isMetering; final bool isMetering;
final VoidCallback? onSwitchEvSourceType; final VoidCallback? onSwitchEvSourceType;
final VoidCallback onMeasure; final VoidCallback onMeasure;
@ -15,7 +14,6 @@ class MeteringBottomControls extends StatelessWidget {
const MeteringBottomControls({ const MeteringBottomControls({
required this.ev, required this.ev,
required this.ev100,
required this.isMetering, required this.isMetering,
required this.onSwitchEvSourceType, required this.onSwitchEvSourceType,
required this.onMeasure, required this.onMeasure,
@ -60,7 +58,6 @@ class MeteringBottomControls extends StatelessWidget {
const Spacer(), const Spacer(),
MeteringMeasureButton( MeteringMeasureButton(
ev: ev, ev: ev,
ev100: ev100,
isMetering: isMetering, isMetering: isMetering,
onTap: onMeasure, onTap: onMeasure,
), ),

View file

@ -9,7 +9,7 @@ import 'package:lightmeter/screens/metering/components/camera_container/componen
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/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/components/camera_preview/components/histogram/widget_histogram.dart';
import 'package:lightmeter/screens/metering/components/camera_container/models/camera_error_type.dart'; import 'package:lightmeter/screens/metering/components/camera_container/models/camera_error_type.dart';
import 'package:lightmeter/utils/context_utils.dart'; import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
class CameraPreview extends StatefulWidget { class CameraPreview extends StatefulWidget {
final CameraController? controller; final CameraController? controller;
@ -102,7 +102,7 @@ class _CameraPreviewBuilderState extends State<_CameraPreviewBuilder> {
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
children: [ children: [
CameraView(controller: widget.controller), CameraView(controller: widget.controller),
if (context.isPro) ...[ if (IAPProducts.isPurchased(context, IAPProductType.paidFeatures)) ...[
if (UserPreferencesProvider.cameraFeatureOf( if (UserPreferencesProvider.cameraFeatureOf(
context, context,
CameraFeature.histogram, CameraFeature.histogram,

View file

@ -3,10 +3,9 @@ import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/data/models/exposure_pair.dart'; import 'package:lightmeter/data/models/exposure_pair.dart';
import 'package:lightmeter/data/models/feature.dart';
import 'package:lightmeter/data/models/metering_screen_layout_config.dart'; import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
import 'package:lightmeter/platform_config.dart'; import 'package:lightmeter/platform_config.dart';
import 'package:lightmeter/providers/remote_config_provider.dart'; import 'package:lightmeter/providers/user_preferences_provider.dart';
import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/metering/components/camera_container/bloc_container_camera.dart'; import 'package:lightmeter/screens/metering/components/camera_container/bloc_container_camera.dart';
import 'package:lightmeter/screens/metering/components/camera_container/components/camera_controls/widget_camera_controls.dart'; import 'package:lightmeter/screens/metering/components/camera_container/components/camera_controls/widget_camera_controls.dart';
@ -18,7 +17,6 @@ import 'package:lightmeter/screens/metering/components/camera_container/state_co
import 'package:lightmeter/screens/metering/components/shared/exposure_pairs_list/widget_list_exposure_pairs.dart'; import 'package:lightmeter/screens/metering/components/shared/exposure_pairs_list/widget_list_exposure_pairs.dart';
import 'package:lightmeter/screens/metering/components/shared/metering_top_bar/widget_top_bar_metering.dart'; import 'package:lightmeter/screens/metering/components/shared/metering_top_bar/widget_top_bar_metering.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/widget_container_readings.dart'; import 'package:lightmeter/screens/metering/components/shared/readings_container/widget_container_readings.dart';
import 'package:lightmeter/utils/context_utils.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class CameraContainer extends StatelessWidget { class CameraContainer extends StatelessWidget {
@ -104,25 +102,27 @@ class CameraContainer extends StatelessWidget {
double _meteringContainerHeight(BuildContext context) { double _meteringContainerHeight(BuildContext context) {
double enabledFeaturesHeight = 0; double enabledFeaturesHeight = 0;
if (!context.isPro) { if (UserPreferencesProvider.meteringScreenFeatureOf(
if (RemoteConfig.isEnabled(context, Feature.showUnlockProOnMainScreen)) { context,
enabledFeaturesHeight += Dimens.readingContainerSingleValueHeight; MeteringScreenLayoutFeature.equipmentProfiles,
enabledFeaturesHeight += Dimens.paddingS; )) {
} enabledFeaturesHeight += Dimens.readingContainerSingleValueHeight;
} else { enabledFeaturesHeight += Dimens.paddingS;
if (context.meteringFeature(MeteringScreenLayoutFeature.equipmentProfiles)) {
enabledFeaturesHeight += Dimens.readingContainerSingleValueHeight;
enabledFeaturesHeight += Dimens.paddingS;
}
if (context.meteringFeature(MeteringScreenLayoutFeature.filmPicker)) {
enabledFeaturesHeight += Dimens.readingContainerSingleValueHeight;
enabledFeaturesHeight += Dimens.paddingS;
}
} }
if (context.meteringFeature(MeteringScreenLayoutFeature.extremeExposurePairs)) { if (UserPreferencesProvider.meteringScreenFeatureOf(
context,
MeteringScreenLayoutFeature.extremeExposurePairs,
)) {
enabledFeaturesHeight += Dimens.readingContainerDoubleValueHeight; enabledFeaturesHeight += Dimens.readingContainerDoubleValueHeight;
enabledFeaturesHeight += Dimens.paddingS; enabledFeaturesHeight += Dimens.paddingS;
} }
if (UserPreferencesProvider.meteringScreenFeatureOf(
context,
MeteringScreenLayoutFeature.filmPicker,
)) {
enabledFeaturesHeight += Dimens.readingContainerSingleValueHeight;
enabledFeaturesHeight += Dimens.paddingS;
}
return enabledFeaturesHeight + Dimens.readingContainerSingleValueHeight; // ISO & ND return enabledFeaturesHeight + Dimens.readingContainerSingleValueHeight; // ISO & ND
} }

View file

@ -1,27 +0,0 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/components/animated_dialog/widget_dialog_animated.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/reading_value_container/widget_container_reading_value.dart';
import 'package:lightmeter/screens/shared/pro_features_dialog/widget_dialog_pro_features.dart';
class LightmeterProAnimatedDialog extends StatelessWidget {
const LightmeterProAnimatedDialog({super.key});
@override
Widget build(BuildContext context) {
return AnimatedDialog(
closedChild: ReadingValueContainer(
color: Theme.of(context).colorScheme.errorContainer,
textColor: Theme.of(context).colorScheme.onErrorContainer,
values: [
ReadingValue(
label: S.of(context).proFeatures,
value: S.of(context).unlock,
),
],
),
openedChild: const ProFeaturesDialog(),
openedSize: Size.fromHeight(const ProFeaturesDialog().height(context)),
);
}
}

View file

@ -1,15 +1,9 @@
import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/res/dimens.dart';
mixin AnimatedDialogClosedChild on Widget {
Color backgroundColor(BuildContext context);
}
class AnimatedDialog extends StatefulWidget { class AnimatedDialog extends StatefulWidget {
final Size? openedSize; final Size? openedSize;
final AnimatedDialogClosedChild? closedChild; final Widget? closedChild;
final Widget? openedChild; final Widget? openedChild;
final Widget? child; final Widget? child;
@ -21,9 +15,6 @@ class AnimatedDialog extends StatefulWidget {
super.key, super.key,
}); });
static Future<void>? maybeClose(BuildContext context) =>
context.findAncestorWidgetOfExactType<_AnimatedOverlay>()?.onDismiss();
@override @override
State<AnimatedDialog> createState() => AnimatedDialogState(); State<AnimatedDialog> createState() => AnimatedDialogState();
} }
@ -104,7 +95,7 @@ class AnimatedDialogState extends State<AnimatedDialog> with SingleTickerProvide
void didChangeDependencies() { void didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();
_foregroundColorAnimation = ColorTween( _foregroundColorAnimation = ColorTween(
begin: widget.closedChild?.backgroundColor(context) ?? Theme.of(context).colorScheme.primaryContainer, begin: Theme.of(context).colorScheme.primaryContainer,
end: Theme.of(context).colorScheme.surface, end: Theme.of(context).colorScheme.surface,
).animate(_defaultCurvedAnimation); ).animate(_defaultCurvedAnimation);
@ -144,15 +135,14 @@ class AnimatedDialogState extends State<AnimatedDialog> with SingleTickerProvide
if (renderBox != null) { if (renderBox != null) {
final size = MediaQuery.sizeOf(context); final size = MediaQuery.sizeOf(context);
final padding = MediaQuery.paddingOf(context); final padding = MediaQuery.paddingOf(context);
final maxWidth = size.width - padding.horizontal - Dimens.dialogMargin.horizontal;
final maxHeight = size.height - padding.vertical - Dimens.dialogMargin.vertical;
_closedSize = _key.currentContext!.size!; _closedSize = _key.currentContext!.size!;
_sizeTween = SizeTween( _sizeTween = SizeTween(
begin: _closedSize, begin: _closedSize,
end: Size( end: widget.openedSize ??
min(widget.openedSize?.width ?? double.maxFinite, maxWidth), Size(
min(widget.openedSize?.height ?? double.maxFinite, maxHeight), size.width - padding.horizontal - Dimens.dialogMargin.horizontal,
), size.height - padding.vertical - Dimens.dialogMargin.vertical,
),
); );
_sizeAnimation = _sizeTween.animate(_defaultCurvedAnimation); _sizeAnimation = _sizeTween.animate(_defaultCurvedAnimation);
@ -191,6 +181,7 @@ class AnimatedDialogState extends State<AnimatedDialog> with SingleTickerProvide
onDismiss: close, onDismiss: close,
builder: widget.closedChild != null && widget.openedChild != null builder: widget.closedChild != null && widget.openedChild != null
? (_) => _AnimatedSwitcher( ? (_) => _AnimatedSwitcher(
sizeAnimation: _sizeAnimation,
closedOpacityAnimation: _closedOpacityAnimation, closedOpacityAnimation: _closedOpacityAnimation,
openedOpacityAnimation: _openedOpacityAnimation, openedOpacityAnimation: _openedOpacityAnimation,
closedSize: _sizeTween.begin!, closedSize: _sizeTween.begin!,
@ -232,7 +223,7 @@ class _AnimatedOverlay extends StatelessWidget {
final Animation<double> borderRadiusAnimation; final Animation<double> borderRadiusAnimation;
final Animation<Color?> foregroundColorAnimation; final Animation<Color?> foregroundColorAnimation;
final Animation<double> elevationAnimation; final Animation<double> elevationAnimation;
final Future<void> Function() onDismiss; final VoidCallback onDismiss;
final Widget? child; final Widget? child;
final Widget Function(BuildContext context)? builder; final Widget Function(BuildContext context)? builder;
@ -290,6 +281,7 @@ class _AnimatedOverlay extends StatelessWidget {
} }
class _AnimatedSwitcher extends StatelessWidget { class _AnimatedSwitcher extends StatelessWidget {
final Animation<Size?> sizeAnimation;
final Animation<double> closedOpacityAnimation; final Animation<double> closedOpacityAnimation;
final Animation<double> openedOpacityAnimation; final Animation<double> openedOpacityAnimation;
final Size closedSize; final Size closedSize;
@ -298,6 +290,7 @@ class _AnimatedSwitcher extends StatelessWidget {
final Widget openedChild; final Widget openedChild;
const _AnimatedSwitcher({ const _AnimatedSwitcher({
required this.sizeAnimation,
required this.closedOpacityAnimation, required this.closedOpacityAnimation,
required this.openedOpacityAnimation, required this.openedOpacityAnimation,
required this.closedSize, required this.closedSize,
@ -313,21 +306,17 @@ class _AnimatedSwitcher extends StatelessWidget {
children: [ children: [
Opacity( Opacity(
opacity: closedOpacityAnimation.value, opacity: closedOpacityAnimation.value,
child: FittedBox( child: Transform.scale(
child: SizedBox.fromSize( scale: sizeAnimation.value!.width / closedSize.width,
size: closedSize, child: SizedBox(
width: closedSize.width,
child: closedChild, child: closedChild,
), ),
), ),
), ),
Opacity( Opacity(
opacity: openedOpacityAnimation.value, opacity: openedOpacityAnimation.value,
child: FittedBox( child: openedChild,
child: SizedBox.fromSize(
size: openedSize,
child: openedChild,
),
),
), ),
], ],
); );

View file

@ -1,7 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/shared/transparent_dialog/widget_dialog_transparent.dart';
typedef DialogPickerItemTitleBuilder<T> = Widget Function(BuildContext context, T value); typedef DialogPickerItemTitleBuilder<T> = Widget Function(BuildContext context, T value);
typedef DialogPickerItemTrailingBuilder<T> = Widget? Function(T selected, T value); typedef DialogPickerItemTrailingBuilder<T> = Widget? Function(T selected, T value);
@ -30,14 +29,6 @@ class DialogPicker<T> extends StatefulWidget {
super.key, super.key,
}); });
double height(BuildContext context) => TransparentDialog.height(
context,
title: title,
subtitle: subtitle,
scrollableContent: true,
contextHeight: Dimens.grid56 * values.length,
);
@override @override
State<DialogPicker<T>> createState() => _DialogPickerState<T>(); State<DialogPicker<T>> createState() => _DialogPickerState<T>();
} }
@ -55,43 +46,83 @@ class _DialogPickerState<T> extends State<DialogPicker<T>> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return TransparentDialog( return Column(
icon: widget.icon, crossAxisAlignment: CrossAxisAlignment.stretch,
title: widget.title, children: [
subtitle: widget.subtitle, Column(
content: Expanded( crossAxisAlignment: CrossAxisAlignment.stretch,
child: ListView.builder( children: [
controller: _scrollController, Padding(
padding: EdgeInsets.zero, padding: Dimens.dialogTitlePadding,
itemCount: widget.values.length, child: Icon(widget.icon),
itemExtent: Dimens.grid56, ),
itemBuilder: (context, index) => RadioListTile( Padding(
value: widget.values[index], padding: Dimens.dialogIconTitlePadding,
groupValue: _selectedValue, child: Text(
title: DefaultTextStyle( widget.title,
style: Theme.of(context).textTheme.bodyLarge!, style: Theme.of(context).textTheme.headlineSmall,
child: widget.itemTitleBuilder(context, widget.values[index]), textAlign: TextAlign.center,
),
),
if (widget.subtitle != null)
Padding(
padding: const EdgeInsets.fromLTRB(
Dimens.paddingL,
0,
Dimens.paddingL,
Dimens.paddingM,
),
child: Text(
widget.subtitle!,
style: Theme.of(context).textTheme.bodyMedium,
textAlign: TextAlign.center,
),
),
],
),
const Divider(),
Expanded(
child: ListView.builder(
controller: _scrollController,
padding: EdgeInsets.zero,
itemCount: widget.values.length,
itemExtent: Dimens.grid56,
itemBuilder: (context, index) => RadioListTile(
value: widget.values[index],
groupValue: _selectedValue,
title: DefaultTextStyle(
style: Theme.of(context).textTheme.bodyLarge!,
child: widget.itemTitleBuilder(context, widget.values[index]),
),
secondary: widget.itemTrailingBuilder?.call(_selectedValue, widget.values[index]),
onChanged: (value) {
if (value != null) {
setState(() {
_selectedValue = value;
});
}
},
), ),
secondary: widget.itemTrailingBuilder?.call(_selectedValue, widget.values[index]),
onChanged: (value) {
if (value != null) {
setState(() {
_selectedValue = value;
});
}
},
), ),
), ),
), const Divider(),
scrollableContent: true, Padding(
actions: [ padding: Dimens.dialogActionsPadding,
TextButton( child: Row(
onPressed: widget.onCancel, crossAxisAlignment: CrossAxisAlignment.end,
child: Text(S.of(context).cancel), children: [
), const Spacer(),
TextButton( TextButton(
onPressed: () => widget.onSelect(_selectedValue), onPressed: widget.onCancel,
child: Text(S.of(context).select), child: Text(S.of(context).cancel),
),
const SizedBox(width: Dimens.grid16),
TextButton(
onPressed: () => widget.onSelect(_selectedValue),
child: Text(S.of(context).select),
),
],
),
), ),
], ],
); );

View file

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/components/animated_dialog/widget_dialog_animated.dart'; import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/components/animated_dialog/widget_dialog_animated.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/components/dialog_picker/widget_picker_dialog.dart'; import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/components/dialog_picker/widget_picker_dialog.dart';
// Has to be stateful, so that [GlobalKey] is not recreated. // Has to be stateful, so that [GlobalKey] is not recreated.
// Otherwise use will no be able to close the dialog after EV value has changed. // Otherwise use will no be able to close the dialog after EV value has changed.
class AnimatedDialogPicker<T> extends StatefulWidget { class AnimatedDialogPicker<T> extends StatefulWidget {
final IconData icon; final IconData icon;
@ -13,7 +13,7 @@ class AnimatedDialogPicker<T> extends StatefulWidget {
final DialogPickerItemTitleBuilder<T> itemTitleBuilder; final DialogPickerItemTitleBuilder<T> itemTitleBuilder;
final DialogPickerItemTrailingBuilder<T>? itemTrailingBuilder; final DialogPickerItemTrailingBuilder<T>? itemTrailingBuilder;
final ValueChanged<T> onChanged; final ValueChanged<T> onChanged;
final AnimatedDialogClosedChild closedChild; final Widget closedChild;
const AnimatedDialogPicker({ const AnimatedDialogPicker({
required this.icon, required this.icon,
@ -37,26 +37,24 @@ class _AnimatedDialogPickerState<T> extends State<AnimatedDialogPicker<T>> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final dialogPicker = DialogPicker<T>(
icon: widget.icon,
title: widget.title,
subtitle: widget.subtitle,
initialValue: widget.selectedValue,
values: widget.values,
itemTitleBuilder: widget.itemTitleBuilder,
itemTrailingBuilder: widget.itemTrailingBuilder,
onCancel: () {
_key.currentState?.close();
},
onSelect: (value) {
_key.currentState?.close().then((_) => widget.onChanged(value));
},
);
return AnimatedDialog( return AnimatedDialog(
key: _key, key: _key,
closedChild: widget.closedChild, closedChild: widget.closedChild,
openedChild: dialogPicker, openedChild: DialogPicker<T>(
openedSize: Size.fromHeight(dialogPicker.height(context)), icon: widget.icon,
title: widget.title,
subtitle: widget.subtitle,
initialValue: widget.selectedValue,
values: widget.values,
itemTitleBuilder: widget.itemTitleBuilder,
itemTrailingBuilder: widget.itemTrailingBuilder,
onCancel: () {
_key.currentState?.close();
},
onSelect: (value) {
_key.currentState?.close().then((_) => widget.onChanged(value));
},
),
); );
} }
} }

View file

@ -1,6 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/components/animated_dialog/widget_dialog_animated.dart';
class ReadingValue { class ReadingValue {
final String label; final String label;
@ -12,15 +11,11 @@ class ReadingValue {
}); });
} }
class ReadingValueContainer extends StatelessWidget implements AnimatedDialogClosedChild { class ReadingValueContainer extends StatelessWidget {
late final List<Widget> _items; late final List<Widget> _items;
final Color? color;
final Color? textColor;
ReadingValueContainer({ ReadingValueContainer({
required List<ReadingValue> values, required List<ReadingValue> values,
this.color,
this.textColor,
super.key, super.key,
}) { }) {
_items = []; _items = [];
@ -28,26 +23,21 @@ class ReadingValueContainer extends StatelessWidget implements AnimatedDialogClo
if (i > 0) { if (i > 0) {
_items.add(const SizedBox(height: Dimens.grid8)); _items.add(const SizedBox(height: Dimens.grid8));
} }
_items.add(_ReadingValueBuilder(values[i], textColor: textColor)); _items.add(_ReadingValueBuilder(values[i]));
} }
} }
ReadingValueContainer.singleValue({ ReadingValueContainer.singleValue({
required ReadingValue value, required ReadingValue value,
this.color,
this.textColor,
super.key, super.key,
}) : _items = [_ReadingValueBuilder(value, textColor: textColor)]; }) : _items = [_ReadingValueBuilder(value)];
@override
Color backgroundColor(BuildContext context) => color ?? Theme.of(context).colorScheme.primaryContainer;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ClipRRect( return ClipRRect(
borderRadius: BorderRadius.circular(Dimens.borderRadiusM), borderRadius: BorderRadius.circular(Dimens.borderRadiusM),
child: ColoredBox( child: ColoredBox(
color: backgroundColor(context), color: Theme.of(context).colorScheme.primaryContainer,
child: Padding( child: Padding(
padding: const EdgeInsets.all(Dimens.paddingM), padding: const EdgeInsets.all(Dimens.paddingM),
child: Column( child: Column(
@ -63,21 +53,20 @@ class ReadingValueContainer extends StatelessWidget implements AnimatedDialogClo
class _ReadingValueBuilder extends StatelessWidget { class _ReadingValueBuilder extends StatelessWidget {
final ReadingValue reading; final ReadingValue reading;
final Color? textColor;
const _ReadingValueBuilder(this.reading, {this.textColor}); const _ReadingValueBuilder(this.reading);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme; final textTheme = Theme.of(context).textTheme;
final color = textColor ?? Theme.of(context).colorScheme.onPrimaryContainer; final textColor = Theme.of(context).colorScheme.onPrimaryContainer;
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( Text(
reading.label, reading.label,
style: textTheme.labelMedium?.copyWith(color: color), style: textTheme.labelMedium?.copyWith(color: textColor),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.visible, overflow: TextOverflow.visible,
softWrap: false, softWrap: false,
@ -87,7 +76,7 @@ class _ReadingValueBuilder extends StatelessWidget {
duration: Dimens.switchDuration, duration: Dimens.switchDuration,
child: Text( child: Text(
reading.value, reading.value,
style: textTheme.titleMedium?.copyWith(color: color), style: textTheme.titleMedium?.copyWith(color: textColor),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
softWrap: false, softWrap: false,

View file

@ -1,17 +1,14 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/exposure_pair.dart'; import 'package:lightmeter/data/models/exposure_pair.dart';
import 'package:lightmeter/data/models/feature.dart';
import 'package:lightmeter/data/models/metering_screen_layout_config.dart'; import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
import 'package:lightmeter/providers/equipment_profile_provider.dart'; import 'package:lightmeter/providers/equipment_profile_provider.dart';
import 'package:lightmeter/providers/remote_config_provider.dart'; import 'package:lightmeter/providers/user_preferences_provider.dart';
import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/equipment_profile_picker/widget_picker_equipment_profiles.dart'; import 'package:lightmeter/screens/metering/components/shared/readings_container/components/equipment_profile_picker/widget_picker_equipment_profiles.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/extreme_exposure_pairs_container/widget_container_extreme_exposure_pairs.dart'; import 'package:lightmeter/screens/metering/components/shared/readings_container/components/extreme_exposure_pairs_container/widget_container_extreme_exposure_pairs.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/film_picker/widget_picker_film.dart'; import 'package:lightmeter/screens/metering/components/shared/readings_container/components/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/iso_picker/widget_picker_iso.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/lightmeter_pro/widget_lightmeter_pro.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/nd_picker/widget_picker_nd.dart'; import 'package:lightmeter/screens/metering/components/shared/readings_container/components/nd_picker/widget_picker_nd.dart';
import 'package:lightmeter/utils/context_utils.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class ReadingsContainer extends StatelessWidget { class ReadingsContainer extends StatelessWidget {
@ -37,22 +34,27 @@ class ReadingsContainer extends StatelessWidget {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
if (!context.isPro && RemoteConfig.isEnabled(context, Feature.showUnlockProOnMainScreen)) ...[ if (UserPreferencesProvider.meteringScreenFeatureOf(
const LightmeterProAnimatedDialog(), context,
const _InnerPadding(), MeteringScreenLayoutFeature.equipmentProfiles,
], )) ...[
if (context.isPro && context.meteringFeature(MeteringScreenLayoutFeature.equipmentProfiles)) ...[
const EquipmentProfilePicker(), const EquipmentProfilePicker(),
const _InnerPadding(), const _InnerPadding(),
], ],
if (context.meteringFeature(MeteringScreenLayoutFeature.extremeExposurePairs)) ...[ if (UserPreferencesProvider.meteringScreenFeatureOf(
context,
MeteringScreenLayoutFeature.extremeExposurePairs,
)) ...[
ExtremeExposurePairsContainer( ExtremeExposurePairsContainer(
fastest: fastest, fastest: fastest,
slowest: slowest, slowest: slowest,
), ),
const _InnerPadding(), const _InnerPadding(),
], ],
if (context.isPro && context.meteringFeature(MeteringScreenLayoutFeature.filmPicker)) ...[ if (UserPreferencesProvider.meteringScreenFeatureOf(
context,
MeteringScreenLayoutFeature.filmPicker,
)) ...[
FilmPicker(selectedIso: iso), FilmPicker(selectedIso: iso),
const _InnerPadding(), const _InnerPadding(),
], ],

View file

@ -40,7 +40,6 @@ class MeteringScreen extends StatelessWidget {
BlocBuilder<MeteringBloc, MeteringState>( BlocBuilder<MeteringBloc, MeteringState>(
builder: (context, state) => MeteringBottomControlsProvider( builder: (context, state) => MeteringBottomControlsProvider(
ev: state is MeteringDataState ? state.ev : null, ev: state is MeteringDataState ? state.ev : null,
ev100: state is MeteringDataState ? state.ev100 : null,
isMetering: state.isMetering, isMetering: state.isMetering,
onSwitchEvSourceType: ServicesProvider.of(context).environment.hasLightSensor onSwitchEvSourceType: ServicesProvider.of(context).environment.hasLightSensor
? UserPreferencesProvider.of(context).toggleEvSourceType ? UserPreferencesProvider.of(context).toggleEvSourceType

View file

@ -1,7 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/feature.dart';
import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/providers/remote_config_provider.dart';
import 'package:lightmeter/providers/services_provider.dart';
import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/shared/pro_features_dialog/widget_dialog_pro_features.dart'; import 'package:lightmeter/screens/settings/utils/show_buy_pro_dialog.dart';
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
class BuyProListTile extends StatelessWidget { class BuyProListTile extends StatelessWidget {
@ -9,17 +12,18 @@ class BuyProListTile extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final unlockFeaturesEnabled = RemoteConfig.isEnabled(context, Feature.unlockProFeaturesText);
final status = IAPProducts.productOf(context, IAPProductType.paidFeatures)?.status; final status = IAPProducts.productOf(context, IAPProductType.paidFeatures)?.status;
final isPending = status == IAPProductStatus.purchased || status == null; final isPending = status == IAPProductStatus.purchased || status == null;
return ListTile( return ListTile(
leading: const Icon(Icons.star), leading: const Icon(Icons.star),
title: Text(S.of(context).unlockProFeatures), title: Text(unlockFeaturesEnabled ? S.of(context).unlockProFeatures : S.of(context).buyLightmeterPro),
onTap: !isPending onTap: !isPending
? () { ? () {
showDialog( showBuyProDialog(context);
context: context, ServicesProvider.of(context)
builder: (_) => const Dialog(child: ProFeaturesDialog()), .analytics
); .logUnlockProFeatures(unlockFeaturesEnabled ? 'Unlock Pro features' : 'Buy Lightmeter Pro');
} }
: null, : null,
trailing: isPending trailing: isPending

View file

@ -1,5 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/feature.dart';
import 'package:lightmeter/generated/l10n.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/lightmeter_pro/components/buy_pro/widget_list_tile_buy_pro.dart';
import 'package:lightmeter/screens/settings/components/shared/settings_section/widget_settings_section.dart'; import 'package:lightmeter/screens/settings/components/shared/settings_section/widget_settings_section.dart';
@ -9,7 +11,9 @@ class LightmeterProSettingsSection extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SettingsSection( return SettingsSection(
title: S.of(context).proFeatures, title: RemoteConfig.isEnabled(context, Feature.unlockProFeaturesText)
? S.of(context).proFeatures
: S.of(context).lightmeterPro,
children: const [BuyProListTile()], children: const [BuyProListTile()],
); );
} }

View file

@ -5,7 +5,6 @@ import 'package:lightmeter/providers/equipment_profile_provider.dart';
import 'package:lightmeter/providers/films_provider.dart'; import 'package:lightmeter/providers/films_provider.dart';
import 'package:lightmeter/providers/user_preferences_provider.dart'; import 'package:lightmeter/providers/user_preferences_provider.dart';
import 'package:lightmeter/screens/settings/components/shared/dialog_switch/widget_dialog_switch.dart'; import 'package:lightmeter/screens/settings/components/shared/dialog_switch/widget_dialog_switch.dart';
import 'package:lightmeter/utils/context_utils.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class MeteringScreenLayoutListTile extends StatelessWidget { class MeteringScreenLayoutListTile extends StatelessWidget {
@ -25,15 +24,6 @@ class MeteringScreenLayoutListTile extends StatelessWidget {
description: S.of(context).meteringScreenLayoutHint, description: S.of(context).meteringScreenLayoutHint,
values: UserPreferencesProvider.meteringScreenConfigOf(context), values: UserPreferencesProvider.meteringScreenConfigOf(context),
titleAdapter: _toStringLocalized, titleAdapter: _toStringLocalized,
enabledAdapter: (value) {
switch (value) {
case MeteringScreenLayoutFeature.equipmentProfiles:
case MeteringScreenLayoutFeature.filmPicker:
return context.isPro;
default:
return true;
}
},
onSave: (value) { onSave: (value) {
if (!value[MeteringScreenLayoutFeature.equipmentProfiles]!) { if (!value[MeteringScreenLayoutFeature.equipmentProfiles]!) {
EquipmentProfileProvider.of(context).setProfile(EquipmentProfiles.of(context).first); EquipmentProfileProvider.of(context).setProfile(EquipmentProfiles.of(context).first);

View file

@ -1,24 +0,0 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/providers/user_preferences_provider.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/settings/components/shared/disable/widget_disable.dart';
import 'package:lightmeter/utils/context_utils.dart';
class ShowEv100ListTile extends StatelessWidget {
const ShowEv100ListTile({super.key});
@override
Widget build(BuildContext context) {
return Disable(
disable: !context.isPro,
child: SwitchListTile(
secondary: const Icon(Icons.adjust),
title: Text(S.of(context).showEv100),
value: context.isPro && UserPreferencesProvider.showEv100Of(context),
onChanged: (_) => UserPreferencesProvider.of(context).toggleShowEv100(),
contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM),
),
);
}
}

View file

@ -6,7 +6,6 @@ import 'package:lightmeter/screens/settings/components/metering/components/equip
import 'package:lightmeter/screens/settings/components/metering/components/films/widget_list_tile_films.dart'; import 'package:lightmeter/screens/settings/components/metering/components/films/widget_list_tile_films.dart';
import 'package:lightmeter/screens/settings/components/metering/components/fractional_stops/widget_list_tile_fractional_stops.dart'; import 'package:lightmeter/screens/settings/components/metering/components/fractional_stops/widget_list_tile_fractional_stops.dart';
import 'package:lightmeter/screens/settings/components/metering/components/metering_screen_layout/widget_list_tile_metering_screen_layout.dart'; import 'package:lightmeter/screens/settings/components/metering/components/metering_screen_layout/widget_list_tile_metering_screen_layout.dart';
import 'package:lightmeter/screens/settings/components/metering/components/show_ev_100/widget_list_tile_show_ev_100.dart';
import 'package:lightmeter/screens/settings/components/shared/settings_section/widget_settings_section.dart'; import 'package:lightmeter/screens/settings/components/shared/settings_section/widget_settings_section.dart';
class MeteringSettingsSection extends StatelessWidget { class MeteringSettingsSection extends StatelessWidget {
@ -19,7 +18,6 @@ class MeteringSettingsSection extends StatelessWidget {
children: const [ children: const [
StopTypeListTile(), StopTypeListTile(),
CalibrationListTile(), CalibrationListTile(),
ShowEv100ListTile(),
MeteringScreenLayoutListTile(), MeteringScreenLayoutListTile(),
EquipmentProfilesListTile(), EquipmentProfilesListTile(),
FilmsListTile(), FilmsListTile(),

View file

@ -1,7 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/settings/components/shared/disable/widget_disable.dart';
typedef StringAdapter<T> = String Function(BuildContext context, T value); typedef StringAdapter<T> = String Function(BuildContext context, T value);
@ -12,7 +11,6 @@ class DialogSwitch<T> extends StatefulWidget {
final Map<T, bool> values; final Map<T, bool> values;
final StringAdapter<T> titleAdapter; final StringAdapter<T> titleAdapter;
final StringAdapter<T>? subtitleAdapter; final StringAdapter<T>? subtitleAdapter;
final bool Function(T value)? enabledAdapter;
final ValueChanged<Map<T, bool>> onSave; final ValueChanged<Map<T, bool>> onSave;
const DialogSwitch({ const DialogSwitch({
@ -22,7 +20,6 @@ class DialogSwitch<T> extends StatefulWidget {
required this.values, required this.values,
required this.titleAdapter, required this.titleAdapter,
this.subtitleAdapter, this.subtitleAdapter,
this.enabledAdapter,
required this.onSave, required this.onSave,
super.key, super.key,
}); });
@ -55,12 +52,9 @@ class _DialogSwitchState<T> extends State<DialogSwitch<T>> {
], ],
ListView( ListView(
shrinkWrap: true, shrinkWrap: true,
children: _features.entries.map( children: _features.entries
(entry) { .map(
final isEnabled = widget.enabledAdapter?.call(entry.key) ?? true; (entry) => SwitchListTile(
return Disable(
disable: !isEnabled,
child: SwitchListTile(
contentPadding: EdgeInsets.symmetric(horizontal: Dimens.dialogTitlePadding.left), contentPadding: EdgeInsets.symmetric(horizontal: Dimens.dialogTitlePadding.left),
title: Text(widget.titleAdapter(context, entry.key)), title: Text(widget.titleAdapter(context, entry.key)),
subtitle: widget.subtitleAdapter != null subtitle: widget.subtitleAdapter != null
@ -69,16 +63,15 @@ class _DialogSwitchState<T> extends State<DialogSwitch<T>> {
style: Theme.of(context).listTileTheme.subtitleTextStyle, style: Theme.of(context).listTileTheme.subtitleTextStyle,
) )
: null, : null,
value: isEnabled && _features[entry.key]!, value: _features[entry.key]!,
onChanged: (value) { onChanged: (value) {
setState(() { setState(() {
_features.update(entry.key, (_) => value); _features.update(entry.key, (_) => value);
}); });
}, },
), ),
); )
}, .toList(),
).toList(),
), ),
], ],
), ),

View file

@ -1,24 +0,0 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/res/dimens.dart';
class Disable extends StatelessWidget {
final bool disable;
final Widget? child;
const Disable({
this.disable = true,
this.child,
super.key,
});
@override
Widget build(BuildContext context) {
return Opacity(
opacity: disable ? Dimens.disabledOpacity : Dimens.enabledOpacity,
child: IgnorePointer(
ignoring: disable,
child: child,
),
);
}
}

View file

@ -1,6 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:lightmeter/screens/settings/components/shared/disable/widget_disable.dart'; import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/utils/context_utils.dart';
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
/// Depends on the product status and replaces [onTap] with purchase callback /// Depends on the product status and replaces [onTap] with purchase callback
@ -23,12 +22,13 @@ class IAPListTile extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Disable( final isPurchased = IAPProducts.isPurchased(context, IAPProductType.paidFeatures);
disable: !context.isPro, return Opacity(
opacity: isPurchased ? Dimens.enabledOpacity : Dimens.disabledOpacity,
child: ListTile( child: ListTile(
leading: leading, leading: leading,
title: title, title: title,
onTap: onTap, onTap: isPurchased ? onTap : null,
), ),
); );
} }

View file

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/dynamic_colors_state.dart'; import 'package:lightmeter/data/models/dynamic_colors_state.dart';
import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/providers/user_preferences_provider.dart'; import 'package:lightmeter/providers/user_preferences_provider.dart';
import 'package:lightmeter/screens/settings/components/shared/disable/widget_disable.dart'; import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/settings/components/theme/components/primary_color/components/primary_color_picker_dialog/widget_dialog_picker_primary_color.dart'; import 'package:lightmeter/screens/settings/components/theme/components/primary_color/components/primary_color_picker_dialog/widget_dialog_picker_primary_color.dart';
class PrimaryColorListTile extends StatelessWidget { class PrimaryColorListTile extends StatelessWidget {
@ -11,10 +11,13 @@ class PrimaryColorListTile extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (UserPreferencesProvider.dynamicColorStateOf(context) == DynamicColorState.enabled) { if (UserPreferencesProvider.dynamicColorStateOf(context) == DynamicColorState.enabled) {
return Disable( return Opacity(
child: ListTile( opacity: Dimens.disabledOpacity,
leading: const Icon(Icons.palette), child: IgnorePointer(
title: Text(S.of(context).primaryColor), child: ListTile(
leading: const Icon(Icons.palette),
title: Text(S.of(context).primaryColor),
),
), ),
); );
} }

View file

@ -7,7 +7,7 @@ import 'package:lightmeter/screens/settings/components/metering/widget_settings_
import 'package:lightmeter/screens/settings/components/theme/widget_settings_section_theme.dart'; import 'package:lightmeter/screens/settings/components/theme/widget_settings_section_theme.dart';
import 'package:lightmeter/screens/settings/flow_settings.dart'; import 'package:lightmeter/screens/settings/flow_settings.dart';
import 'package:lightmeter/screens/shared/sliver_screen/screen_sliver.dart'; import 'package:lightmeter/screens/shared/sliver_screen/screen_sliver.dart';
import 'package:lightmeter/utils/context_utils.dart'; import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
class SettingsScreen extends StatefulWidget { class SettingsScreen extends StatefulWidget {
const SettingsScreen({super.key}); const SettingsScreen({super.key});
@ -38,7 +38,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
SliverList( SliverList(
delegate: SliverChildListDelegate( delegate: SliverChildListDelegate(
<Widget>[ <Widget>[
if (!context.isPro) const LightmeterProSettingsSection(), if (!IAPProducts.isPurchased(context, IAPProductType.paidFeatures))
const LightmeterProSettingsSection(),
const MeteringSettingsSection(), const MeteringSettingsSection(),
const GeneralSettingsSection(), const GeneralSettingsSection(),
const ThemeSettingsSection(), const ThemeSettingsSection(),

View file

@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/feature.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/providers/remote_config_provider.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
Future<void> showBuyProDialog(BuildContext context) {
final unlockFeaturesEnabled = RemoteConfig.isEnabled(context, Feature.unlockProFeaturesText);
Widget splitDescription() {
final description =
unlockFeaturesEnabled ? S.of(context).unlockProFeaturesDescription : S.of(context).lightmeterProDescription;
final paragraphs = description.split('\n\n');
final features = paragraphs.first.split('\n \u2022 ').sublist(1);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(paragraphs.first.split('\n \u2022 ').first),
...features.map(
(f) => Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('\u2022 '),
Flexible(child: Text(f)),
],
),
),
Text('\n${paragraphs.last}'),
],
);
}
return showDialog(
context: context,
builder: (_) => AlertDialog(
icon: const Icon(Icons.star),
titlePadding: Dimens.dialogIconTitlePadding,
title: Text(unlockFeaturesEnabled ? S.of(context).proFeatures : S.of(context).lightmeterPro),
contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingL),
content: SingleChildScrollView(child: splitDescription()),
actionsPadding: Dimens.dialogActionsPadding,
actions: [
TextButton(
onPressed: Navigator.of(context).pop,
child: Text(S.of(context).cancel),
),
FilledButton(
onPressed: () {
Navigator.of(context).pop();
IAPProductsProvider.maybeOf(context)?.buy(IAPProductType.paidFeatures);
},
child: Text(unlockFeaturesEnabled ? S.of(context).unlock : S.of(context).buy),
),
],
),
);
}

View file

@ -1,56 +0,0 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/animated_dialog_picker/components/animated_dialog/widget_dialog_animated.dart';
import 'package:lightmeter/screens/shared/transparent_dialog/widget_dialog_transparent.dart';
import 'package:lightmeter/utils/text_height.dart';
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
class ProFeaturesDialog extends StatelessWidget {
const ProFeaturesDialog({super.key});
double height(BuildContext context) => TransparentDialog.height(
context,
title: S.of(context).proFeatures,
contextHeight: dialogTextHeight(
context,
S.of(context).unlockProFeaturesDescription,
Theme.of(context).textTheme.bodyMedium,
Dimens.paddingL * 2,
),
);
@override
Widget build(BuildContext context) {
return TransparentDialog(
icon: Icons.star,
title: S.of(context).proFeatures,
scrollableContent: false,
content: Flexible(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: Dimens.paddingL),
child: Text(
S.of(context).unlockProFeaturesDescription,
style: Theme.of(context).textTheme.bodyMedium,
),
),
),
),
actions: [
TextButton(
onPressed: () => _close(context),
child: Text(S.of(context).cancel),
),
FilledButton(
onPressed: () {
_close(context).then((_) => IAPProductsProvider.maybeOf(context)?.buy(IAPProductType.paidFeatures));
},
child: Text(S.of(context).unlock),
),
],
);
}
Future<void> _close(BuildContext context) async => AnimatedDialog.maybeClose(context) ?? Navigator.of(context).pop();
}

View file

@ -1,104 +0,0 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/utils/text_height.dart';
class TransparentDialog extends StatelessWidget {
final IconData icon;
final String title;
final String? subtitle;
final Widget content;
final bool scrollableContent;
final List<Widget> actions;
const TransparentDialog({
required this.icon,
required this.title,
this.subtitle,
required this.content,
required this.scrollableContent,
this.actions = const [],
super.key,
});
static double height(
BuildContext context, {
required String title,
String? subtitle,
required double contextHeight,
bool scrollableContent = false,
}) {
double height = IconTheme.of(context).size! + Dimens.dialogTitlePadding.vertical;
height += dialogTextHeight(
context,
title,
Theme.of(context).textTheme.headlineSmall,
Dimens.dialogIconTitlePadding.horizontal,
) +
Dimens.dialogIconTitlePadding.vertical;
if (subtitle != null) {
height += dialogTextHeight(
context,
subtitle,
Theme.of(context).textTheme.bodyMedium,
Dimens.dialogIconTitlePadding.horizontal,
) +
Dimens.dialogIconTitlePadding.vertical;
}
height += contextHeight;
if (scrollableContent) height += 1;
return height += 48 + Dimens.dialogActionsPadding.vertical;
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: Dimens.dialogTitlePadding,
child: Icon(icon),
),
Padding(
padding: Dimens.dialogIconTitlePadding,
child: Text(
title,
style: Theme.of(context).textTheme.headlineSmall,
textAlign: TextAlign.center,
),
),
if (subtitle != null)
Padding(
padding: Dimens.dialogIconTitlePadding,
child: Text(
subtitle!,
style: Theme.of(context).textTheme.bodyMedium,
textAlign: TextAlign.center,
),
),
],
),
if (scrollableContent) const Divider(),
content,
if (scrollableContent) const Divider(),
Padding(
padding: Dimens.dialogActionsPadding,
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: _actions().toList(),
),
),
],
);
}
Iterable<Widget> _actions() sync* {
for (int i = 0; i < actions.length; i++) {
yield i == 0 ? const Spacer() : const SizedBox(width: Dimens.grid16);
yield actions[i];
}
}
}

View file

@ -1,12 +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';
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
extension BuildContextUtils on BuildContext {
bool meteringFeature(MeteringScreenLayoutFeature feature) {
return UserPreferencesProvider.meteringScreenFeatureOf(this, feature);
}
bool get isPro => IAPProducts.isPurchased(this, IAPProductType.paidFeatures);
}

View file

@ -1,29 +0,0 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/res/dimens.dart';
double dialogTextHeight(
BuildContext context,
String text,
TextStyle? style,
double textPadding,
) =>
textHeight(
text,
style,
MediaQuery.sizeOf(context).width - Dimens.dialogMargin.horizontal - textPadding,
);
double textHeight(
String text,
TextStyle? style,
double maxWidth,
) {
final TextPainter titlePainter = TextPainter(
text: TextSpan(
text: text,
style: style,
),
textDirection: TextDirection.ltr,
)..layout(maxWidth: maxWidth);
return titlePainter.height;
}

View file

@ -1,7 +1,7 @@
name: lightmeter name: lightmeter
description: Lightmeter app inspired by Material 3 design system. description: Lightmeter app inspired by Material 3 design system.
publish_to: "none" publish_to: "none"
version: 0.17.0+46 version: 0.16.0+45
environment: environment:
sdk: ">=3.0.0 <4.0.0" sdk: ">=3.0.0 <4.0.0"

View file

@ -2,18 +2,12 @@ import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/res/theme.dart'; import 'package:lightmeter/res/theme.dart';
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
/// Provides [MaterialApp] with default theme and "en" localization /// Provides [MaterialApp] with default theme and "en" localization
class WidgetTestApplicationMock extends StatelessWidget { class WidgetTestApplicationMock extends StatelessWidget {
final IAPProductStatus productStatus;
final Widget child; final Widget child;
const WidgetTestApplicationMock({ const WidgetTestApplicationMock({required this.child, super.key});
this.productStatus = IAPProductStatus.purchased,
required this.child,
super.key,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View file

@ -181,25 +181,6 @@ void main() {
}); });
}); });
group('showEv100', () {
test('get default', () {
when(() => sharedPreferences.getBool(UserPreferencesService.showEv100Key)).thenReturn(null);
expect(service.showEv100, false);
});
test('get', () {
when(() => sharedPreferences.getBool(UserPreferencesService.showEv100Key)).thenReturn(true);
expect(service.showEv100, true);
});
test('set', () {
when(() => sharedPreferences.setBool(UserPreferencesService.showEv100Key, false))
.thenAnswer((_) => Future.value(true));
service.showEv100 = false;
verify(() => sharedPreferences.setBool(UserPreferencesService.showEv100Key, false)).called(1);
});
});
group('meteringScreenLayout', () { group('meteringScreenLayout', () {
test('get default', () { test('get default', () {
when( when(

View file

@ -19,8 +19,8 @@ void main() {
setUp(() { setUp(() {
when(() => mockRemoteConfigService.fetchConfig()).thenAnswer((_) async {}); when(() => mockRemoteConfigService.fetchConfig()).thenAnswer((_) async {});
when(() => mockRemoteConfigService.getValue(Feature.showUnlockProOnMainScreen)).thenReturn(false); when(() => mockRemoteConfigService.getValue(Feature.unlockProFeaturesText)).thenReturn(false);
when(() => mockRemoteConfigService.getAll()).thenReturn({Feature.showUnlockProOnMainScreen: false}); when(() => mockRemoteConfigService.getAll()).thenReturn({Feature.unlockProFeaturesText: false});
}); });
tearDown(() { tearDown(() {
@ -42,7 +42,7 @@ void main() {
when(() => mockRemoteConfigService.onConfigUpdated()).thenAnswer((_) => const Stream.empty()); when(() => mockRemoteConfigService.onConfigUpdated()).thenAnswer((_) => const Stream.empty());
await pumpTestWidget(tester); await pumpTestWidget(tester);
expect(find.text('showUnlockProOnMainScreen: false'), findsOneWidget); expect(find.text('unlockProFeaturesText: false'), findsOneWidget);
}, },
); );
@ -53,34 +53,34 @@ void main() {
when(() => mockRemoteConfigService.onConfigUpdated()).thenAnswer((_) => remoteConfigUpdateController.stream); when(() => mockRemoteConfigService.onConfigUpdated()).thenAnswer((_) => remoteConfigUpdateController.stream);
await pumpTestWidget(tester); await pumpTestWidget(tester);
expect(find.text('showUnlockProOnMainScreen: false'), findsOneWidget); expect(find.text('unlockProFeaturesText: false'), findsOneWidget);
when(() => mockRemoteConfigService.getValue(Feature.showUnlockProOnMainScreen)).thenReturn(true); when(() => mockRemoteConfigService.getValue(Feature.unlockProFeaturesText)).thenReturn(true);
remoteConfigUpdateController.add({Feature.showUnlockProOnMainScreen}); remoteConfigUpdateController.add({Feature.unlockProFeaturesText});
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text('showUnlockProOnMainScreen: true'), findsOneWidget); expect(find.text('unlockProFeaturesText: true'), findsOneWidget);
await remoteConfigUpdateController.close(); await remoteConfigUpdateController.close();
}, },
); );
test('RemoteConfig.updateShouldNotifyDependent', () { test('RemoteConfig.updateShouldNotifyDependent', () {
const config = RemoteConfig(config: {Feature.showUnlockProOnMainScreen: false}, child: SizedBox()); const config = RemoteConfig(config: {Feature.unlockProFeaturesText: false}, child: SizedBox());
expect( expect(
config.updateShouldNotifyDependent(config, {}), config.updateShouldNotifyDependent(config, {}),
false, false,
); );
expect( expect(
config.updateShouldNotifyDependent( config.updateShouldNotifyDependent(
const RemoteConfig(config: {Feature.showUnlockProOnMainScreen: false}, child: SizedBox()), const RemoteConfig(config: {Feature.unlockProFeaturesText: false}, child: SizedBox()),
{Feature.showUnlockProOnMainScreen}, {Feature.unlockProFeaturesText},
), ),
false, false,
); );
expect( expect(
config.updateShouldNotifyDependent( config.updateShouldNotifyDependent(
const RemoteConfig(config: {Feature.showUnlockProOnMainScreen: true}, child: SizedBox()), const RemoteConfig(config: {Feature.unlockProFeaturesText: true}, child: SizedBox()),
{Feature.showUnlockProOnMainScreen}, {Feature.unlockProFeaturesText},
), ),
true, true,
); );
@ -96,7 +96,7 @@ class _Application extends StatelessWidget {
home: Scaffold( home: Scaffold(
body: Center( body: Center(
child: Text( child: Text(
"${Feature.showUnlockProOnMainScreen.name}: ${RemoteConfig.isEnabled(context, Feature.showUnlockProOnMainScreen)}", "${Feature.unlockProFeaturesText.name}: ${RemoteConfig.isEnabled(context, Feature.unlockProFeaturesText)}",
), ),
), ),
), ),

View file

@ -27,7 +27,6 @@ void main() {
setUp(() { setUp(() {
when(() => mockUserPreferencesService.evSourceType).thenReturn(EvSourceType.camera); when(() => mockUserPreferencesService.evSourceType).thenReturn(EvSourceType.camera);
when(() => mockUserPreferencesService.showEv100).thenReturn(false);
when(() => mockUserPreferencesService.stopType).thenReturn(StopType.third); when(() => mockUserPreferencesService.stopType).thenReturn(StopType.third);
when(() => mockUserPreferencesService.meteringScreenLayout).thenReturn({ when(() => mockUserPreferencesService.meteringScreenLayout).thenReturn({
MeteringScreenLayoutFeature.extremeExposurePairs: true, MeteringScreenLayoutFeature.extremeExposurePairs: true,
@ -165,27 +164,6 @@ void main() {
}, },
); );
testWidgets(
'Toggle Ev100',
(tester) async {
when(() => mockUserPreferencesService.showEv100).thenReturn(false);
await pumpTestWidget(
tester,
builder: (context) => ElevatedButton(
onPressed: () => UserPreferencesProvider.of(context).toggleShowEv100(),
child: Text('${UserPreferencesProvider.showEv100Of(context)}'),
),
);
await tester.pumpAndSettle();
expect(find.text("${false}"), findsOneWidget);
await tester.tap(find.text("${false}"));
await tester.pumpAndSettle();
expect(find.text("${true}"), findsOneWidget);
verify(() => mockUserPreferencesService.showEv100 = true).called(1);
},
);
testWidgets( testWidgets(
'Set metering screen layout config', 'Set metering screen layout config',
(tester) async { (tester) async {

View file

@ -1,60 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/screens/settings/components/lightmeter_pro/components/buy_pro/widget_list_tile_buy_pro.dart';
import 'package:lightmeter/screens/settings/components/lightmeter_pro/widget_settings_section_lightmeter_pro.dart';
import 'package:lightmeter/screens/shared/transparent_dialog/widget_dialog_transparent.dart';
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
import '../../../application_mock.dart';
void main() {
Future<void> pumpApplication(WidgetTester tester) async {
await tester.pumpWidget(
IAPProducts(
products: [
IAPProduct(
storeId: IAPProductType.paidFeatures.storeId,
),
],
child: const WidgetTestApplicationMock(
child: LightmeterProSettingsSection(),
),
),
);
await tester.pumpAndSettle();
}
testWidgets(
'`showBuyProDialog` and buy',
(tester) async {
await pumpApplication(tester);
await tester.tap(find.byType(BuyProListTile));
await tester.pumpAndSettle();
expect(find.byType(TransparentDialog), findsOneWidget);
expect(find.text(S.current.proFeatures), findsNWidgets(2));
expect(find.text(S.current.cancel), findsOneWidget);
expect(find.text(S.current.unlock), findsOneWidget);
await tester.tap(find.text(S.current.unlock));
await tester.pumpAndSettle();
expect(find.byType(TransparentDialog), findsNothing);
},
);
testWidgets(
'`showBuyProDialog` and cancel',
(tester) async {
await pumpApplication(tester);
await tester.tap(find.byType(BuyProListTile));
await tester.pumpAndSettle();
expect(find.byType(TransparentDialog), findsOneWidget);
expect(find.text(S.current.proFeatures), findsNWidgets(2));
expect(find.text(S.current.cancel), findsOneWidget);
expect(find.text(S.current.unlock), findsOneWidget);
await tester.tap(find.text(S.current.cancel));
await tester.pumpAndSettle();
expect(find.byType(TransparentDialog), findsNothing);
},
);
}

View file

@ -0,0 +1,61 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.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/utils/show_buy_pro_dialog.dart';
import '../../../application_mock.dart';
void main() {
Future<void> pumpApplication(WidgetTester tester) async {
await tester.pumpWidget(
RemoteConfig(
config: const {Feature.unlockProFeaturesText: false},
child: WidgetTestApplicationMock(
child: Builder(
builder: (context) => ElevatedButton(
onPressed: () => showBuyProDialog(context),
child: const SizedBox(),
),
),
),
),
);
await tester.pumpAndSettle();
}
testWidgets(
'`showBuyProDialog` and buy',
(tester) async {
await pumpApplication(tester);
await tester.tap(find.byType(ElevatedButton));
await tester.pumpAndSettle();
expect(find.byType(AlertDialog), findsOneWidget);
expect(find.text(S.current.lightmeterPro), findsOneWidget);
expect(find.text(S.current.cancel), findsOneWidget);
expect(find.text(S.current.buy), findsOneWidget);
await tester.tap(find.text(S.current.buy));
await tester.pumpAndSettle();
expect(find.byType(AlertDialog), findsNothing);
},
);
testWidgets(
'`showBuyProDialog` and cancel',
(tester) async {
await pumpApplication(tester);
await tester.tap(find.byType(ElevatedButton));
await tester.pumpAndSettle();
expect(find.byType(AlertDialog), findsOneWidget);
expect(find.text(S.current.lightmeterPro), findsOneWidget);
expect(find.text(S.current.cancel), findsOneWidget);
expect(find.text(S.current.buy), findsOneWidget);
await tester.tap(find.text(S.current.cancel));
await tester.pumpAndSettle();
expect(find.byType(AlertDialog), findsNothing);
},
);
}

View file

@ -1,24 +0,0 @@
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:lightmeter/utils/ev_from_bytes.dart';
void main() {
group('evFromImage', () {
test(
'camera_stub_image.jpg',
() {
final bytes = File('assets/camera_stub_image.jpg').readAsBytesSync();
expectLater(evFromImage(bytes), completion(8.25230310752341));
},
);
test(
'no EXIF',
() {
final bytes = File('assets/launcher_icon_dev_512.png').readAsBytesSync();
expectLater(evFromImage(bytes), completion(null));
},
);
});
}