Merge branch 'main' of https://github.com/vodemn/m3_lightmeter into feature/ML-141

This commit is contained in:
Vadim 2024-01-28 21:20:54 +01:00
commit 74360d0754
27 changed files with 193 additions and 148 deletions

4
.vscode/launch.json vendored
View file

@ -35,7 +35,7 @@
"--flavor", "--flavor",
"prod", "prod",
], ],
"program": "${workspaceFolder}/lib/main_release.dart", "program": "${workspaceFolder}/lib/main_prod.dart",
}, },
{ {
"name": "prod-profile", "name": "prod-profile",
@ -46,7 +46,7 @@
"--flavor", "--flavor",
"prod", "prod",
], ],
"program": "${workspaceFolder}/lib/main_release.dart", "program": "${workspaceFolder}/lib/main_prod.dart",
}, },
{ {
"name": "dev-simulator", "name": "dev-simulator",

View file

@ -14,6 +14,7 @@
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:screenOrientation="portrait" android:screenOrientation="portrait"
android:dataExtractionRules="@xml/data_extraction_rules"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as <!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user the Android process has started. This theme is visible to the user

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<data-extraction-rules>
<cloud-backup>
<exclude domain="sharedpref" path="FlutterSecureStorage"/>
<exclude domain="sharedpref" path="FlutterSecureKeyStorage"/>
</cloud-backup>
<device-transfer>
<exclude domain="sharedpref" path="FlutterSecureStorage"/>
<exclude domain="sharedpref" path="FlutterSecureKeyStorage"/>
</device-transfer>
</data-extraction-rules>

View file

@ -26,11 +26,14 @@ class ApplicationWrapper extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final remoteConfigService = env.buildType != BuildType.dev
? const RemoteConfigService(LightmeterAnalytics(api: LightmeterAnalyticsFirebase()))
: const MockRemoteConfigService();
return FutureBuilder( return FutureBuilder(
future: Future.wait<dynamic>([ future: Future.wait<dynamic>([
SharedPreferences.getInstance(), SharedPreferences.getInstance(),
const LightSensorService(LocalPlatform()).hasSensor(), const LightSensorService(LocalPlatform()).hasSensor(),
if (env.buildType != BuildType.dev) const RemoteConfigService().activeAndFetchFeatures(), remoteConfigService.activeAndFetchFeatures(),
]), ]),
builder: (_, snapshot) { builder: (_, snapshot) {
if (snapshot.data != null) { if (snapshot.data != null) {
@ -47,8 +50,7 @@ class ApplicationWrapper extends StatelessWidget {
userPreferencesService: userPreferencesService, userPreferencesService: userPreferencesService,
volumeEventsService: const VolumeEventsService(LocalPlatform()), volumeEventsService: const VolumeEventsService(LocalPlatform()),
child: RemoteConfigProvider( child: RemoteConfigProvider(
remoteConfigService: remoteConfigService: remoteConfigService,
env.buildType != BuildType.dev ? const RemoteConfigService() : const MockRemoteConfigService(),
child: EquipmentProfileProvider( child: EquipmentProfileProvider(
storageService: iapService, storageService: iapService,
child: FilmsProvider( child: FilmsProvider(

View file

@ -3,32 +3,56 @@ import 'dart:developer';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:lightmeter/data/analytics/api/analytics_api_interface.dart'; import 'package:lightmeter/data/analytics/api/analytics_api_interface.dart';
import 'package:lightmeter/data/analytics/entity/analytics_event.dart';
class LightmeterAnalytics { class LightmeterAnalytics {
final ILightmeterAnalyticsApi _api; final ILightmeterAnalyticsApi _api;
const LightmeterAnalytics({required ILightmeterAnalyticsApi api}) : _api = api; const LightmeterAnalytics({required ILightmeterAnalyticsApi api}) : _api = api;
void init() {
FlutterError.onError = (details) {
if (details.silent) return;
logCrash(details.exception, details.stack);
};
PlatformDispatcher.instance.onError = (error, stack) {
logCrash(error, stack);
return true;
};
}
Future<void> logEvent( Future<void> logEvent(
LightmeterAnalyticsEvent event, { String eventName, {
Map<String, dynamic>? parameters, Map<String, dynamic>? parameters,
}) async { }) async {
if (kDebugMode) { if (!kReleaseMode) {
log('<LightmeterAnalytics> logEvent: ${event.name} / $parameters'); log('<LightmeterAnalytics> logEvent: $eventName / $parameters');
return; return;
} }
return _api.logEvent( return _api.logEvent(
event: event, eventName,
parameters: parameters, parameters: parameters,
); );
} }
Future<void> logUnlockProFeatures(String listTileTitle) async { Future<void> logCrash(
return logEvent( dynamic exception,
LightmeterAnalyticsEvent.unlockProFeatures, StackTrace? stackTrace, {
parameters: {"listTileTitle": listTileTitle}, dynamic reason,
Iterable<Object> information = const [],
}) async {
log(exception.toString(), stackTrace: stackTrace);
if (!kReleaseMode) {
return;
}
return _api.logCrash(
exception,
stackTrace,
reason: reason,
information: information,
); );
} }
Future<void> setCustomKey(String key, String value) async => _api.setCustomKey(key, value);
} }

View file

@ -1,8 +1,15 @@
import 'package:lightmeter/data/analytics/entity/analytics_event.dart';
abstract class ILightmeterAnalyticsApi { abstract class ILightmeterAnalyticsApi {
Future<void> logEvent({ Future<void> logEvent(
required LightmeterAnalyticsEvent event, String eventName, {
Map<String, dynamic>? parameters, Map<String, dynamic>? parameters,
}); });
Future<void> logCrash(
dynamic exception,
StackTrace? stack, {
dynamic reason,
Iterable<Object> information = const [],
});
Future<void> setCustomKey(String key, String value);
} }

View file

@ -1,20 +1,20 @@
import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:lightmeter/data/analytics/api/analytics_api_interface.dart'; import 'package:lightmeter/data/analytics/api/analytics_api_interface.dart';
import 'package:lightmeter/data/analytics/entity/analytics_event.dart';
class LightmeterAnalyticsFirebase implements ILightmeterAnalyticsApi { class LightmeterAnalyticsFirebase implements ILightmeterAnalyticsApi {
const LightmeterAnalyticsFirebase(); const LightmeterAnalyticsFirebase();
@override @override
Future<void> logEvent({ Future<void> logEvent(
required LightmeterAnalyticsEvent event, String eventName, {
Map<String, dynamic>? parameters, Map<String, dynamic>? parameters,
}) async { }) async {
try { try {
await FirebaseAnalytics.instance.logEvent( await FirebaseAnalytics.instance.logEvent(
name: event.name, name: eventName,
parameters: parameters, parameters: parameters,
); );
} on FirebaseException catch (e) { } on FirebaseException catch (e) {
@ -23,4 +23,25 @@ class LightmeterAnalyticsFirebase implements ILightmeterAnalyticsApi {
debugPrint(e.toString()); debugPrint(e.toString());
} }
} }
@override
Future<void> logCrash(
dynamic exception,
StackTrace? stackTrace, {
dynamic reason,
Iterable<Object> information = const [],
}) async {
FirebaseCrashlytics.instance.recordError(
exception,
stackTrace,
reason: reason,
information: information,
fatal: true,
);
}
@override
Future<void> setCustomKey(String key, String value) async {
await FirebaseCrashlytics.instance.setCustomKey(key, value);
}
} }

View file

@ -1,3 +0,0 @@
enum LightmeterAnalyticsEvent {
unlockProFeatures,
}

View file

@ -2,9 +2,9 @@ import 'dart:async';
import 'dart:developer'; import 'dart:developer';
import 'package:firebase_core/firebase_core.dart'; 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:firebase_remote_config/firebase_remote_config.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:lightmeter/data/analytics/analytics.dart';
import 'package:lightmeter/data/models/feature.dart'; import 'package:lightmeter/data/models/feature.dart';
abstract class IRemoteConfigService { abstract class IRemoteConfigService {
@ -24,7 +24,9 @@ abstract class IRemoteConfigService {
} }
class RemoteConfigService implements IRemoteConfigService { class RemoteConfigService implements IRemoteConfigService {
const RemoteConfigService(); final LightmeterAnalytics analytics;
const RemoteConfigService(this.analytics);
@override @override
Future<void> activeAndFetchFeatures() async { Future<void> activeAndFetchFeatures() async {
@ -73,8 +75,8 @@ class RemoteConfigService implements IRemoteConfigService {
try { try {
final feature = Feature.values.firstWhere((f) => f.name == value.key); final feature = Feature.values.firstWhere((f) => f.name == value.key);
result[feature] = value.value.toValue(feature); result[feature] = value.value.toValue(feature);
} catch (e) { } catch (e, stackTrace) {
log(e.toString()); _logError(e, stackTrace: stackTrace);
} }
} }
return result; return result;
@ -88,8 +90,8 @@ class RemoteConfigService implements IRemoteConfigService {
for (final key in event.updatedKeys) { for (final key in event.updatedKeys) {
try { try {
updatedFeatures.add(Feature.values.firstWhere((element) => element.name == key)); updatedFeatures.add(Feature.values.firstWhere((element) => element.name == key));
} catch (e) { } catch (e, stackTrace) {
log(e.toString()); _logError(e, stackTrace: stackTrace);
} }
} }
return updatedFeatures; return updatedFeatures;
@ -99,9 +101,7 @@ class RemoteConfigService implements IRemoteConfigService {
@override @override
bool isEnabled(Feature feature) => FirebaseRemoteConfig.instance.getBool(feature.name); bool isEnabled(Feature feature) => FirebaseRemoteConfig.instance.getBool(feature.name);
void _logError(dynamic throwable, {StackTrace? stackTrace}) { void _logError(dynamic throwable, {StackTrace? stackTrace}) => analytics.logCrash(throwable, stackTrace);
FirebaseCrashlytics.instance.recordError(throwable, stackTrace);
}
} }
class MockRemoteConfigService implements IRemoteConfigService { class MockRemoteConfigService implements IRemoteConfigService {

View file

@ -1,22 +0,0 @@
import 'dart:developer';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/foundation.dart';
import 'package:lightmeter/firebase_options.dart';
Future<void> initializeFirebase({required bool handleErrors}) async {
try {
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
if (handleErrors) {
FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError;
PlatformDispatcher.instance.onError = (error, stack) {
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
return true;
};
}
} catch (e) {
log(e.toString());
}
}

View file

@ -99,7 +99,7 @@
}, },
"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 & Histogram\n \u2022 And more!\n\nBy unlocking Pro features you support the development and make it possible to add new features to the app.",
"unlock": "Unlock", "unlock": "Unlock",
"tooltipAdd": "Add", "tooltipAdd": "Add",
"tooltipClose": "Close", "tooltipClose": "Close",

View file

@ -99,7 +99,7 @@
}, },
"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 & Histogramme\n \u2022 Et plus encore!\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", "unlock": "Déverrouiller",
"tooltipAdd": "Ajouter", "tooltipAdd": "Ajouter",
"tooltipClose": "Fermer", "tooltipClose": "Fermer",

View file

@ -99,7 +99,7 @@
}, },
"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Получая доступ к профессиональным настройкам, вы поддерживаете разработку и делаете возможным появление новых функций в приложении.",
"unlock": "Разблокировать", "unlock": "Разблокировать",
"tooltipAdd": "Добавить", "tooltipAdd": "Добавить",
"tooltipClose": "Закрыть", "tooltipClose": "Закрыть",

View file

@ -30,8 +30,8 @@
"halfStops": "1/2", "halfStops": "1/2",
"thirdStops": "1/3", "thirdStops": "1/3",
"calibration": "校准", "calibration": "校准",
"calibrationMessage": "此应用测量读数的准确性完全取决于设备的硬件。因此,请考虑测试此应用并手动设置 EV 校准,以获得准确的测量结果。", "calibrationMessage": "此应用程序测量读数的准确性取决于设备的后置摄像头。如需更精确的测量结果或测量结果存在偏差,请手动校准 EV 。",
"calibrationMessageCameraOnly": "此应用程序测量读数的准确s性完全取决于设备的后置摄像头。因此请考虑测试此应用并手动设置 EV 校准,以获得准确的测量结果。", "calibrationMessageCameraOnly": "此应用程序测量读数的准确性取决于设备的后置摄像头。如需更精确的测量结果或测量结果存在偏差,请手动校准 EV 。",
"camera": "摄像头", "camera": "摄像头",
"lightSensor": "光传感器", "lightSensor": "光传感器",
"showEv100": "显示 EV\u2081\u2080\u2080", "showEv100": "显示 EV\u2081\u2080\u2080",
@ -55,14 +55,14 @@
"apertureValues": "光圈值", "apertureValues": "光圈值",
"apertureValuesFilterDescription": "选择要显示的光圈值范围。这通常由您使用的镜头决定。", "apertureValuesFilterDescription": "选择要显示的光圈值范围。这通常由您使用的镜头决定。",
"ndFilters": "ND 滤镜", "ndFilters": "ND 滤镜",
"ndFiltersFilterDescription": "选择要显示的 ND 滤镜系数。这些可能是您最常用的 ND 滤镜,也可能是适合您镜头的光镜。", "ndFiltersFilterDescription": "选择要显示的 ND 滤镜系数。可能是您最常用的 ND 滤镜,也可能是适合您镜头的光镜。",
"shutterSpeedValues": "快门速度", "shutterSpeedValues": "快门速度",
"shutterSpeedValuesFilterDescription": "选择要显示的快门速度范围。这通常由您使用的相机机身决定。", "shutterSpeedValuesFilterDescription": "选择要显示的快门速度范围。这通常由您使用的相机机身决定。",
"isoValues": "ISO", "isoValues": "ISO",
"isoValuesFilterDescription": "选择要显示的 ISO 。这些值可能是您最常用的值,也可能是相机支持的值。", "isoValuesFilterDescription": "选择要显示的 ISO 。这些可能是您常用的ISO值也可以是相机支持的ISO范围。",
"equipmentProfile": "设备配置", "equipmentProfile": "设备配置",
"equipmentProfiles": "设备配置", "equipmentProfiles": "设备配置",
"tapToAdd": "點擊添加", "tapToAdd": "点击添加",
"filmsInUse": "使用的胶片", "filmsInUse": "使用的胶片",
"filmsInUseDescription": "选择你使用的胶片", "filmsInUseDescription": "选择你使用的胶片",
"general": "通用", "general": "通用",
@ -99,7 +99,7 @@
}, },
"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通过解锁专业版功能您可以支持开发工作帮助为应用程序添加新功能。",
"unlock": "解锁", "unlock": "解锁",
"tooltipAdd": "添加", "tooltipAdd": "添加",
"tooltipClose": "关闭", "tooltipClose": "关闭",

View file

@ -1,18 +1,4 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/application.dart';
import 'package:lightmeter/application_wrapper.dart';
import 'package:lightmeter/environment.dart'; import 'package:lightmeter/environment.dart';
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart'; import 'package:lightmeter/runner.dart';
Future<void> main() async { Future<void> main() => runLightmeterApp(const Environment.dev());
WidgetsFlutterBinding.ensureInitialized();
runApp(
IAPProducts(
products: [IAPProduct(storeId: IAPProductType.paidFeatures.storeId)],
child: const ApplicationWrapper(
Environment.dev(),
child: Application(),
),
),
);
}

View file

@ -1,19 +1,4 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/application.dart';
import 'package:lightmeter/application_wrapper.dart';
import 'package:lightmeter/environment.dart'; import 'package:lightmeter/environment.dart';
import 'package:lightmeter/firebase.dart'; import 'package:lightmeter/runner.dart';
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
Future<void> main() async { Future<void> main() => runLightmeterApp(const Environment.prod());
WidgetsFlutterBinding.ensureInitialized();
await initializeFirebase(handleErrors: true);
runApp(
const IAPProductsProvider(
child: ApplicationWrapper(
Environment.prod(),
child: Application(),
),
),
);
}

View file

@ -1,19 +0,0 @@
import 'package:flutter/material.dart';
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<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await initializeFirebase(handleErrors: false);
runApp(
const IAPProductsProvider(
child: ApplicationWrapper(
Environment.prod(),
child: Application(),
),
),
);
}

View file

@ -31,8 +31,10 @@ class ServicesProvider extends InheritedWidget {
required super.child, required super.child,
}); });
static ServicesProvider of(BuildContext context) { static ServicesProvider of(BuildContext context) => ServicesProvider.maybeOf(context)!;
return context.findAncestorWidgetOfExactType<ServicesProvider>()!;
static ServicesProvider? maybeOf(BuildContext context) {
return context.findAncestorWidgetOfExactType<ServicesProvider>();
} }
@override @override

35
lib/runner.dart Normal file
View file

@ -0,0 +1,35 @@
import 'dart:async';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/widgets.dart';
import 'package:lightmeter/application.dart';
import 'package:lightmeter/application_wrapper.dart';
import 'package:lightmeter/data/analytics/analytics.dart';
import 'package:lightmeter/data/analytics/api/analytics_firebase.dart';
import 'package:lightmeter/environment.dart';
import 'package:lightmeter/firebase_options.dart';
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
const _errorsLogger = LightmeterAnalytics(api: LightmeterAnalyticsFirebase());
Future<void> runLightmeterApp(Environment env) async {
runZonedGuarded(
() async {
WidgetsFlutterBinding.ensureInitialized();
if (env.buildType == BuildType.prod) {
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
}
_errorsLogger.init();
final application = ApplicationWrapper(env, child: const Application());
runApp(
env.buildType == BuildType.dev
? IAPProducts(
products: [IAPProduct(storeId: IAPProductType.paidFeatures.storeId)],
child: application,
)
: IAPProductsProvider(child: application),
);
},
_errorsLogger.logCrash,
);
}

View file

@ -7,6 +7,7 @@ import 'package:camera/camera.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/data/analytics/analytics.dart';
import 'package:lightmeter/interactors/metering_interactor.dart'; import 'package:lightmeter/interactors/metering_interactor.dart';
import 'package:lightmeter/platform_config.dart'; import 'package:lightmeter/platform_config.dart';
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart'; import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
@ -22,6 +23,7 @@ part 'mock_bloc_container_camera.dart';
class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraContainerState> { class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraContainerState> {
final MeteringInteractor _meteringInteractor; final MeteringInteractor _meteringInteractor;
final LightmeterAnalytics _analytics;
late final _WidgetsBindingObserver _observer; late final _WidgetsBindingObserver _observer;
CameraController? _cameraController; CameraController? _cameraController;
@ -42,6 +44,7 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
CameraContainerBloc( CameraContainerBloc(
this._meteringInteractor, this._meteringInteractor,
MeteringCommunicationBloc communicationBloc, MeteringCommunicationBloc communicationBloc,
this._analytics,
) : super( ) : super(
communicationBloc, communicationBloc,
const CameraInitState(), const CameraInitState(),
@ -223,8 +226,8 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
Directory(file.path).deleteSync(recursive: true); Directory(file.path).deleteSync(recursive: true);
return await evFromImage(bytes); return await evFromImage(bytes);
} catch (e) { } catch (e, stackTrace) {
log(e.toString()); _analytics.logCrash(e, stackTrace);
return null; return null;
} }
} }

View file

@ -4,6 +4,7 @@ class MockCameraContainerBloc extends CameraContainerBloc {
MockCameraContainerBloc( MockCameraContainerBloc(
super._meteringInteractor, super._meteringInteractor,
super.communicationBloc, super.communicationBloc,
super._analytics,
); );
@override @override
@ -72,8 +73,8 @@ class MockCameraContainerBloc extends CameraContainerBloc {
try { try {
final bytes = (await rootBundle.load(PlatformConfig.cameraStubImage)).buffer.asUint8List(); final bytes = (await rootBundle.load(PlatformConfig.cameraStubImage)).buffer.asUint8List();
return await evFromImage(bytes); return await evFromImage(bytes);
} catch (e) { } catch (e, stackTrace) {
log(e.toString()); log(e.toString(), stackTrace: stackTrace);
return null; return null;
} }
} }

View file

@ -2,6 +2,7 @@ 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/platform_config.dart'; import 'package:lightmeter/platform_config.dart';
import 'package:lightmeter/providers/services_provider.dart';
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart'; import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
import 'package:lightmeter/screens/metering/components/camera_container/bloc_container_camera.dart'; import 'package:lightmeter/screens/metering/components/camera_container/bloc_container_camera.dart';
import 'package:lightmeter/screens/metering/components/camera_container/event_container_camera.dart'; import 'package:lightmeter/screens/metering/components/camera_container/event_container_camera.dart';
@ -37,10 +38,12 @@ class CameraContainerProvider extends StatelessWidget {
? MockCameraContainerBloc( ? MockCameraContainerBloc(
MeteringInteractorProvider.of(context), MeteringInteractorProvider.of(context),
context.read<MeteringCommunicationBloc>(), context.read<MeteringCommunicationBloc>(),
ServicesProvider.of(context).analytics,
) )
: CameraContainerBloc( : CameraContainerBloc(
MeteringInteractorProvider.of(context), MeteringInteractorProvider.of(context),
context.read<MeteringCommunicationBloc>(), context.read<MeteringCommunicationBloc>(),
ServicesProvider.of(context).analytics,
)) ))
..add(const RequestPermissionEvent()), ..add(const RequestPermissionEvent()),
child: CameraContainer( child: CameraContainer(

View file

@ -1,5 +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/providers/services_provider.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'; 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/screens/shared/transparent_dialog/widget_dialog_transparent.dart';
@ -44,7 +45,12 @@ class ProFeaturesDialog extends StatelessWidget {
), ),
FilledButton( FilledButton(
onPressed: () { onPressed: () {
_close(context).then((_) => IAPProductsProvider.maybeOf(context)?.buy(IAPProductType.paidFeatures)); _close(context).then((_) {
ServicesProvider.maybeOf(context)
?.analytics
.setCustomKey('iap_product_type', IAPProductType.paidFeatures.storeId);
IAPProductsProvider.maybeOf(context)?.buy(IAPProductType.paidFeatures);
});
}, },
child: Text(S.of(context).unlock), child: Text(S.of(context).unlock),
), ),

View file

@ -1,27 +1,20 @@
import 'dart:developer';
import 'dart:math' as math; import 'dart:math' as math;
import 'dart:typed_data';
import 'package:exif/exif.dart'; import 'package:exif/exif.dart';
import 'package:flutter/foundation.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
Future<double?> evFromImage(Uint8List bytes) async { Future<double> evFromImage(Uint8List bytes) async {
try { final tags = await readExifFromBytes(bytes);
final tags = await readExifFromBytes(bytes); final iso = double.tryParse("${tags["EXIF ISOSpeedRatings"]}");
final iso = double.tryParse("${tags["EXIF ISOSpeedRatings"]}"); final apertureValueRatio = (tags["EXIF FNumber"]?.values as IfdRatios?)?.ratios.first;
final apertureValueRatio = (tags["EXIF FNumber"]?.values as IfdRatios?)?.ratios.first; final speedValueRatio = (tags["EXIF ExposureTime"]?.values as IfdRatios?)?.ratios.first;
final speedValueRatio = (tags["EXIF ExposureTime"]?.values as IfdRatios?)?.ratios.first; if (iso == null || apertureValueRatio == null || speedValueRatio == null) {
if (iso == null || apertureValueRatio == null || speedValueRatio == null) { throw FlutterError('Error parsing EXIF: ${tags.keys}');
log('Error parsing EXIF: ${tags.keys}');
return null;
}
final aperture = apertureValueRatio.numerator / apertureValueRatio.denominator;
final speed = speedValueRatio.numerator / speedValueRatio.denominator;
return log2(math.pow(aperture, 2)) - log2(speed) - log2(iso / 100);
} catch (e) {
log(e.toString());
return null;
} }
final aperture = apertureValueRatio.numerator / apertureValueRatio.denominator;
final speed = speedValueRatio.numerator / speedValueRatio.denominator;
return log2(math.pow(aperture, 2)) - log2(speed) - log2(iso / 100);
} }

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.17.2+48
environment: environment:
sdk: ">=3.0.0 <4.0.0" sdk: ">=3.0.0 <4.0.0"
@ -28,7 +28,7 @@ dependencies:
m3_lightmeter_iap: m3_lightmeter_iap:
git: git:
url: "https://github.com/vodemn/m3_lightmeter_iap" url: "https://github.com/vodemn/m3_lightmeter_iap"
ref: v0.7.1 ref: v0.7.2
m3_lightmeter_resources: m3_lightmeter_resources:
git: git:
url: "https://github.com/vodemn/m3_lightmeter_resources" url: "https://github.com/vodemn/m3_lightmeter_resources"

View file

@ -2,6 +2,7 @@ import 'package:bloc_test/bloc_test.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:lightmeter/data/analytics/analytics.dart';
import 'package:lightmeter/interactors/metering_interactor.dart'; import 'package:lightmeter/interactors/metering_interactor.dart';
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart'; import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart' as communication_events; import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart' as communication_events;
@ -18,11 +19,14 @@ class _MockMeteringCommunicationBloc
extends MockBloc<communication_events.MeteringCommunicationEvent, communication_states.MeteringCommunicationState> extends MockBloc<communication_events.MeteringCommunicationEvent, communication_states.MeteringCommunicationState>
implements MeteringCommunicationBloc {} implements MeteringCommunicationBloc {}
class _MockLightmeterAnalytics extends Mock implements LightmeterAnalytics {}
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
late _MockMeteringInteractor meteringInteractor; late _MockMeteringInteractor meteringInteractor;
late _MockMeteringCommunicationBloc communicationBloc; late _MockMeteringCommunicationBloc communicationBloc;
late _MockLightmeterAnalytics analytics;
late CameraContainerBloc bloc; late CameraContainerBloc bloc;
const cameraMethodChannel = MethodChannel('plugins.flutter.io/camera'); const cameraMethodChannel = MethodChannel('plugins.flutter.io/camera');
@ -110,16 +114,21 @@ void main() {
setUpAll(() { setUpAll(() {
meteringInteractor = _MockMeteringInteractor(); meteringInteractor = _MockMeteringInteractor();
communicationBloc = _MockMeteringCommunicationBloc();
communicationBloc = _MockMeteringCommunicationBloc();
when(() => meteringInteractor.cameraEvCalibration).thenReturn(0.0); when(() => meteringInteractor.cameraEvCalibration).thenReturn(0.0);
when(meteringInteractor.quickVibration).thenAnswer((_) async {}); when(meteringInteractor.quickVibration).thenAnswer((_) async {});
analytics = _MockLightmeterAnalytics();
registerFallbackValue(StackTrace.empty);
when(() => analytics.logCrash(any<dynamic>(), any<StackTrace>())).thenAnswer((_) async {});
}); });
setUp(() { setUp(() {
bloc = CameraContainerBloc( bloc = CameraContainerBloc(
meteringInteractor, meteringInteractor,
communicationBloc, communicationBloc,
analytics,
); );
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(cameraMethodChannel, cameraMethodCallSuccessHandler); .setMockMethodCallHandler(cameraMethodChannel, cameraMethodCallSuccessHandler);

View file

@ -17,7 +17,7 @@ void main() {
'no EXIF', 'no EXIF',
() { () {
final bytes = File('assets/launcher_icon_dev_512.png').readAsBytesSync(); final bytes = File('assets/launcher_icon_dev_512.png').readAsBytesSync();
expectLater(evFromImage(bytes), completion(null)); expectLater(evFromImage(bytes), throwsFlutterError);
}, },
); );
}); });