mirror of
https://github.com/vodemn/m3_lightmeter.git
synced 2024-11-22 07:20:39 +00:00
Merge branch 'main' of https://github.com/vodemn/m3_lightmeter into feature/ML-141
This commit is contained in:
commit
74360d0754
27 changed files with 193 additions and 148 deletions
4
.vscode/launch.json
vendored
4
.vscode/launch.json
vendored
|
@ -35,7 +35,7 @@
|
|||
"--flavor",
|
||||
"prod",
|
||||
],
|
||||
"program": "${workspaceFolder}/lib/main_release.dart",
|
||||
"program": "${workspaceFolder}/lib/main_prod.dart",
|
||||
},
|
||||
{
|
||||
"name": "prod-profile",
|
||||
|
@ -46,7 +46,7 @@
|
|||
"--flavor",
|
||||
"prod",
|
||||
],
|
||||
"program": "${workspaceFolder}/lib/main_release.dart",
|
||||
"program": "${workspaceFolder}/lib/main_prod.dart",
|
||||
},
|
||||
{
|
||||
"name": "dev-simulator",
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:screenOrientation="portrait"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
|
|
11
android/app/src/main/res/xml/data_extraction_rules.xml
Normal file
11
android/app/src/main/res/xml/data_extraction_rules.xml
Normal 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>
|
|
@ -26,11 +26,14 @@ class ApplicationWrapper extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final remoteConfigService = env.buildType != BuildType.dev
|
||||
? const RemoteConfigService(LightmeterAnalytics(api: LightmeterAnalyticsFirebase()))
|
||||
: const MockRemoteConfigService();
|
||||
return FutureBuilder(
|
||||
future: Future.wait<dynamic>([
|
||||
SharedPreferences.getInstance(),
|
||||
const LightSensorService(LocalPlatform()).hasSensor(),
|
||||
if (env.buildType != BuildType.dev) const RemoteConfigService().activeAndFetchFeatures(),
|
||||
remoteConfigService.activeAndFetchFeatures(),
|
||||
]),
|
||||
builder: (_, snapshot) {
|
||||
if (snapshot.data != null) {
|
||||
|
@ -47,8 +50,7 @@ class ApplicationWrapper extends StatelessWidget {
|
|||
userPreferencesService: userPreferencesService,
|
||||
volumeEventsService: const VolumeEventsService(LocalPlatform()),
|
||||
child: RemoteConfigProvider(
|
||||
remoteConfigService:
|
||||
env.buildType != BuildType.dev ? const RemoteConfigService() : const MockRemoteConfigService(),
|
||||
remoteConfigService: remoteConfigService,
|
||||
child: EquipmentProfileProvider(
|
||||
storageService: iapService,
|
||||
child: FilmsProvider(
|
||||
|
|
|
@ -3,32 +3,56 @@ import 'dart:developer';
|
|||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:lightmeter/data/analytics/api/analytics_api_interface.dart';
|
||||
import 'package:lightmeter/data/analytics/entity/analytics_event.dart';
|
||||
|
||||
class LightmeterAnalytics {
|
||||
final ILightmeterAnalyticsApi _api;
|
||||
|
||||
const LightmeterAnalytics({required ILightmeterAnalyticsApi api}) : _api = api;
|
||||
|
||||
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(
|
||||
LightmeterAnalyticsEvent event, {
|
||||
String eventName, {
|
||||
Map<String, dynamic>? parameters,
|
||||
}) async {
|
||||
if (kDebugMode) {
|
||||
log('<LightmeterAnalytics> logEvent: ${event.name} / $parameters');
|
||||
if (!kReleaseMode) {
|
||||
log('<LightmeterAnalytics> logEvent: $eventName / $parameters');
|
||||
return;
|
||||
}
|
||||
|
||||
return _api.logEvent(
|
||||
event: event,
|
||||
eventName,
|
||||
parameters: parameters,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> logUnlockProFeatures(String listTileTitle) async {
|
||||
return logEvent(
|
||||
LightmeterAnalyticsEvent.unlockProFeatures,
|
||||
parameters: {"listTileTitle": listTileTitle},
|
||||
Future<void> logCrash(
|
||||
dynamic exception,
|
||||
StackTrace? stackTrace, {
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -1,8 +1,15 @@
|
|||
import 'package:lightmeter/data/analytics/entity/analytics_event.dart';
|
||||
|
||||
abstract class ILightmeterAnalyticsApi {
|
||||
Future<void> logEvent({
|
||||
required LightmeterAnalyticsEvent event,
|
||||
Future<void> logEvent(
|
||||
String eventName, {
|
||||
Map<String, dynamic>? parameters,
|
||||
});
|
||||
|
||||
Future<void> logCrash(
|
||||
dynamic exception,
|
||||
StackTrace? stack, {
|
||||
dynamic reason,
|
||||
Iterable<Object> information = const [],
|
||||
});
|
||||
|
||||
Future<void> setCustomKey(String key, String value);
|
||||
}
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
import 'package:firebase_analytics/firebase_analytics.dart';
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:lightmeter/data/analytics/api/analytics_api_interface.dart';
|
||||
import 'package:lightmeter/data/analytics/entity/analytics_event.dart';
|
||||
|
||||
class LightmeterAnalyticsFirebase implements ILightmeterAnalyticsApi {
|
||||
const LightmeterAnalyticsFirebase();
|
||||
|
||||
@override
|
||||
Future<void> logEvent({
|
||||
required LightmeterAnalyticsEvent event,
|
||||
Future<void> logEvent(
|
||||
String eventName, {
|
||||
Map<String, dynamic>? parameters,
|
||||
}) async {
|
||||
try {
|
||||
await FirebaseAnalytics.instance.logEvent(
|
||||
name: event.name,
|
||||
name: eventName,
|
||||
parameters: parameters,
|
||||
);
|
||||
} on FirebaseException catch (e) {
|
||||
|
@ -23,4 +23,25 @@ class LightmeterAnalyticsFirebase implements ILightmeterAnalyticsApi {
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
enum LightmeterAnalyticsEvent {
|
||||
unlockProFeatures,
|
||||
}
|
|
@ -2,9 +2,9 @@ import 'dart:async';
|
|||
import 'dart:developer';
|
||||
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
||||
import 'package:firebase_remote_config/firebase_remote_config.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:lightmeter/data/analytics/analytics.dart';
|
||||
import 'package:lightmeter/data/models/feature.dart';
|
||||
|
||||
abstract class IRemoteConfigService {
|
||||
|
@ -24,7 +24,9 @@ abstract class IRemoteConfigService {
|
|||
}
|
||||
|
||||
class RemoteConfigService implements IRemoteConfigService {
|
||||
const RemoteConfigService();
|
||||
final LightmeterAnalytics analytics;
|
||||
|
||||
const RemoteConfigService(this.analytics);
|
||||
|
||||
@override
|
||||
Future<void> activeAndFetchFeatures() async {
|
||||
|
@ -73,8 +75,8 @@ class RemoteConfigService implements IRemoteConfigService {
|
|||
try {
|
||||
final feature = Feature.values.firstWhere((f) => f.name == value.key);
|
||||
result[feature] = value.value.toValue(feature);
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
} catch (e, stackTrace) {
|
||||
_logError(e, stackTrace: stackTrace);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
@ -88,8 +90,8 @@ class RemoteConfigService implements IRemoteConfigService {
|
|||
for (final key in event.updatedKeys) {
|
||||
try {
|
||||
updatedFeatures.add(Feature.values.firstWhere((element) => element.name == key));
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
} catch (e, stackTrace) {
|
||||
_logError(e, stackTrace: stackTrace);
|
||||
}
|
||||
}
|
||||
return updatedFeatures;
|
||||
|
@ -99,9 +101,7 @@ class RemoteConfigService implements IRemoteConfigService {
|
|||
@override
|
||||
bool isEnabled(Feature feature) => FirebaseRemoteConfig.instance.getBool(feature.name);
|
||||
|
||||
void _logError(dynamic throwable, {StackTrace? stackTrace}) {
|
||||
FirebaseCrashlytics.instance.recordError(throwable, stackTrace);
|
||||
}
|
||||
void _logError(dynamic throwable, {StackTrace? stackTrace}) => analytics.logCrash(throwable, stackTrace);
|
||||
}
|
||||
|
||||
class MockRemoteConfigService implements IRemoteConfigService {
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -99,7 +99,7 @@
|
|||
},
|
||||
"proFeatures": "Pro features",
|
||||
"unlockProFeatures": "Unlock Pro features",
|
||||
"unlockProFeaturesDescription": "Unlock professional features:\n \u2022 Equipment profiles containing filters for aperture, shutter speed, and more\n \u2022 List of films with compensation for what's known as reciprocity failure\n \u2022 Spot metering\n \u2022 Histogram\n\nBy unlocking Pro features you support the development and make it possible to add new features to the app.",
|
||||
"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",
|
||||
"tooltipAdd": "Add",
|
||||
"tooltipClose": "Close",
|
||||
|
|
|
@ -99,7 +99,7 @@
|
|||
},
|
||||
"proFeatures": "Fonctionnalités professionnelles",
|
||||
"unlockProFeatures": "Déverrouiller les fonctionnalités professionnelles",
|
||||
"unlockProFeaturesDescription": "Déverrouillez des fonctions professionnelles:\n \u2022 Profils d'équipement contenant des filtres pour l'ouverture, la vitesse d'obturation et plus encore, ainsi qu'une liste de films avec compensation pour ce que l'on appelle l'échec de réciprocité\n \u2022 Mesure spot\n \u2022 Histogramme\n\nEn débloquant les fonctionnalités Pro, vous soutenez le développement et permettez d'ajouter de nouvelles fonctionnalités à l'application.",
|
||||
"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",
|
||||
"tooltipAdd": "Ajouter",
|
||||
"tooltipClose": "Fermer",
|
||||
|
|
|
@ -99,7 +99,7 @@
|
|||
},
|
||||
"proFeatures": "Профессиональные настройки",
|
||||
"unlockProFeatures": "Разблокировать профессиональные настройки",
|
||||
"unlockProFeaturesDescription": "Вы можете разблокировать профессиональные настройки:\n \u2022 Профили оборудования, содержащие фильтры для диафрагмы, выдержки и других значений\n \u2022 Список пленок с компенсацией эффекта Шварцшильда\n \u2022 Точечный замер\n \u2022 Гистограмма\n\nПолучая доступ к профессиональным настройкам, вы поддерживаете разработку и делаете возможным появление новых функций в приложении.",
|
||||
"unlockProFeaturesDescription": "Вы можете разблокировать профессиональные настройки:\n \u2022 Профили оборудования, содержащие фильтры для диафрагмы, выдержки и других значений\n \u2022 Список пленок с компенсацией эффекта Шварцшильда\n \u2022 Точечный замер и гистограмма\n \u2022 И другие возможности!\n\nПолучая доступ к профессиональным настройкам, вы поддерживаете разработку и делаете возможным появление новых функций в приложении.",
|
||||
"unlock": "Разблокировать",
|
||||
"tooltipAdd": "Добавить",
|
||||
"tooltipClose": "Закрыть",
|
||||
|
|
|
@ -30,8 +30,8 @@
|
|||
"halfStops": "1/2",
|
||||
"thirdStops": "1/3",
|
||||
"calibration": "校准",
|
||||
"calibrationMessage": "此应用测量读数的准确性完全取决于设备的硬件。因此,请考虑测试此应用并手动设置 EV 校准,以获得准确的测量结果。",
|
||||
"calibrationMessageCameraOnly": "此应用程序测量读数的准确s性完全取决于设备的后置摄像头。因此,请考虑测试此应用并手动设置 EV 校准,以获得准确的测量结果。",
|
||||
"calibrationMessage": "此应用程序测量读数的准确性取决于设备的后置摄像头。如需更精确的测量结果或测量结果存在偏差,请手动校准 EV 。",
|
||||
"calibrationMessageCameraOnly": "此应用程序测量读数的准确性取决于设备的后置摄像头。如需更精确的测量结果或测量结果存在偏差,请手动校准 EV 。",
|
||||
"camera": "摄像头",
|
||||
"lightSensor": "光传感器",
|
||||
"showEv100": "显示 EV\u2081\u2080\u2080",
|
||||
|
@ -55,14 +55,14 @@
|
|||
"apertureValues": "光圈值",
|
||||
"apertureValuesFilterDescription": "选择要显示的光圈值范围。这通常由您使用的镜头决定。",
|
||||
"ndFilters": "ND 滤镜",
|
||||
"ndFiltersFilterDescription": "选择要显示的 ND 滤镜系数。这些可能是您最常用的 ND 滤镜,也可能是适合您镜头的滤光镜。",
|
||||
"ndFiltersFilterDescription": "选择要显示的 ND 滤镜系数。可能是您最常用的 ND 滤镜,也可能是适合您镜头的减光镜。",
|
||||
"shutterSpeedValues": "快门速度",
|
||||
"shutterSpeedValuesFilterDescription": "选择要显示的快门速度范围。这通常由您使用的相机机身决定。",
|
||||
"isoValues": "ISO",
|
||||
"isoValuesFilterDescription": "选择要显示的 ISO 。这些值可能是您最常用的值,也可能是相机支持的值。",
|
||||
"isoValuesFilterDescription": "选择要显示的 ISO 。这些可能是您常用的ISO值,也可以是相机支持的ISO范围。",
|
||||
"equipmentProfile": "设备配置",
|
||||
"equipmentProfiles": "设备配置",
|
||||
"tapToAdd": "點擊添加",
|
||||
"tapToAdd": "点击添加",
|
||||
"filmsInUse": "使用的胶片",
|
||||
"filmsInUseDescription": "选择你使用的胶片",
|
||||
"general": "通用",
|
||||
|
@ -99,7 +99,7 @@
|
|||
},
|
||||
"proFeatures": "专业功能",
|
||||
"unlockProFeatures": "解锁专业功能",
|
||||
"unlockProFeaturesDescription": "\n \u2022 配置文件,其中包含光圈、快门速度等参数\n \u2022 胶卷列表,对胶片倒易率失效进行曝光补偿\n \u2022 点测光\n \u2022 直方图\n\n通过解锁专业版功能,您可以支持开发工作,帮助为应用程序添加新功能。",
|
||||
"unlockProFeaturesDescription": "\n \u2022 配置文件,其中包含光圈、快门速度等参数\n \u2022 胶卷列表,对胶片倒易率失效进行曝光补偿\n \u2022 点测光和直方图\n \u2022 和更多\n\n通过解锁专业版功能,您可以支持开发工作,帮助为应用程序添加新功能。",
|
||||
"unlock": "解锁",
|
||||
"tooltipAdd": "添加",
|
||||
"tooltipClose": "关闭",
|
||||
|
|
|
@ -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:m3_lightmeter_iap/m3_lightmeter_iap.dart';
|
||||
import 'package:lightmeter/runner.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
runApp(
|
||||
IAPProducts(
|
||||
products: [IAPProduct(storeId: IAPProductType.paidFeatures.storeId)],
|
||||
child: const ApplicationWrapper(
|
||||
Environment.dev(),
|
||||
child: Application(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
Future<void> main() => runLightmeterApp(const Environment.dev());
|
||||
|
|
|
@ -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/firebase.dart';
|
||||
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
|
||||
import 'package:lightmeter/runner.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await initializeFirebase(handleErrors: true);
|
||||
runApp(
|
||||
const IAPProductsProvider(
|
||||
child: ApplicationWrapper(
|
||||
Environment.prod(),
|
||||
child: Application(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
Future<void> main() => runLightmeterApp(const Environment.prod());
|
||||
|
|
|
@ -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(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
|
@ -31,8 +31,10 @@ class ServicesProvider extends InheritedWidget {
|
|||
required super.child,
|
||||
});
|
||||
|
||||
static ServicesProvider of(BuildContext context) {
|
||||
return context.findAncestorWidgetOfExactType<ServicesProvider>()!;
|
||||
static ServicesProvider of(BuildContext context) => ServicesProvider.maybeOf(context)!;
|
||||
|
||||
static ServicesProvider? maybeOf(BuildContext context) {
|
||||
return context.findAncestorWidgetOfExactType<ServicesProvider>();
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
35
lib/runner.dart
Normal file
35
lib/runner.dart
Normal 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,
|
||||
);
|
||||
}
|
|
@ -7,6 +7,7 @@ import 'package:camera/camera.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.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/platform_config.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> {
|
||||
final MeteringInteractor _meteringInteractor;
|
||||
final LightmeterAnalytics _analytics;
|
||||
late final _WidgetsBindingObserver _observer;
|
||||
|
||||
CameraController? _cameraController;
|
||||
|
@ -42,6 +44,7 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
|
|||
CameraContainerBloc(
|
||||
this._meteringInteractor,
|
||||
MeteringCommunicationBloc communicationBloc,
|
||||
this._analytics,
|
||||
) : super(
|
||||
communicationBloc,
|
||||
const CameraInitState(),
|
||||
|
@ -223,8 +226,8 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
|
|||
Directory(file.path).deleteSync(recursive: true);
|
||||
|
||||
return await evFromImage(bytes);
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
} catch (e, stackTrace) {
|
||||
_analytics.logCrash(e, stackTrace);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ class MockCameraContainerBloc extends CameraContainerBloc {
|
|||
MockCameraContainerBloc(
|
||||
super._meteringInteractor,
|
||||
super.communicationBloc,
|
||||
super._analytics,
|
||||
);
|
||||
|
||||
@override
|
||||
|
@ -72,8 +73,8 @@ class MockCameraContainerBloc extends CameraContainerBloc {
|
|||
try {
|
||||
final bytes = (await rootBundle.load(PlatformConfig.cameraStubImage)).buffer.asUint8List();
|
||||
return await evFromImage(bytes);
|
||||
} catch (e) {
|
||||
log(e.toString());
|
||||
} catch (e, stackTrace) {
|
||||
log(e.toString(), stackTrace: stackTrace);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:lightmeter/data/models/exposure_pair.dart';
|
||||
import 'package:lightmeter/platform_config.dart';
|
||||
import 'package:lightmeter/providers/services_provider.dart';
|
||||
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
|
||||
import 'package:lightmeter/screens/metering/components/camera_container/bloc_container_camera.dart';
|
||||
import 'package:lightmeter/screens/metering/components/camera_container/event_container_camera.dart';
|
||||
|
@ -37,10 +38,12 @@ class CameraContainerProvider extends StatelessWidget {
|
|||
? MockCameraContainerBloc(
|
||||
MeteringInteractorProvider.of(context),
|
||||
context.read<MeteringCommunicationBloc>(),
|
||||
ServicesProvider.of(context).analytics,
|
||||
)
|
||||
: CameraContainerBloc(
|
||||
MeteringInteractorProvider.of(context),
|
||||
context.read<MeteringCommunicationBloc>(),
|
||||
ServicesProvider.of(context).analytics,
|
||||
))
|
||||
..add(const RequestPermissionEvent()),
|
||||
child: CameraContainer(
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/generated/l10n.dart';
|
||||
import 'package:lightmeter/providers/services_provider.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';
|
||||
|
@ -44,7 +45,12 @@ class ProFeaturesDialog extends StatelessWidget {
|
|||
),
|
||||
FilledButton(
|
||||
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),
|
||||
),
|
||||
|
|
|
@ -1,27 +1,20 @@
|
|||
import 'dart:developer';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:exif/exif.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||
|
||||
Future<double?> evFromImage(Uint8List bytes) async {
|
||||
try {
|
||||
Future<double> evFromImage(Uint8List bytes) async {
|
||||
final tags = await readExifFromBytes(bytes);
|
||||
final iso = double.tryParse("${tags["EXIF ISOSpeedRatings"]}");
|
||||
final apertureValueRatio = (tags["EXIF FNumber"]?.values as IfdRatios?)?.ratios.first;
|
||||
final speedValueRatio = (tags["EXIF ExposureTime"]?.values as IfdRatios?)?.ratios.first;
|
||||
if (iso == null || apertureValueRatio == null || speedValueRatio == null) {
|
||||
log('Error parsing EXIF: ${tags.keys}');
|
||||
return null;
|
||||
throw FlutterError('Error parsing EXIF: ${tags.keys}');
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
name: lightmeter
|
||||
description: Lightmeter app inspired by Material 3 design system.
|
||||
publish_to: "none"
|
||||
version: 0.17.0+46
|
||||
version: 0.17.2+48
|
||||
|
||||
environment:
|
||||
sdk: ">=3.0.0 <4.0.0"
|
||||
|
@ -28,7 +28,7 @@ dependencies:
|
|||
m3_lightmeter_iap:
|
||||
git:
|
||||
url: "https://github.com/vodemn/m3_lightmeter_iap"
|
||||
ref: v0.7.1
|
||||
ref: v0.7.2
|
||||
m3_lightmeter_resources:
|
||||
git:
|
||||
url: "https://github.com/vodemn/m3_lightmeter_resources"
|
||||
|
|
|
@ -2,6 +2,7 @@ import 'package:bloc_test/bloc_test.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.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/screens/metering/communication/bloc_communication_metering.dart';
|
||||
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>
|
||||
implements MeteringCommunicationBloc {}
|
||||
|
||||
class _MockLightmeterAnalytics extends Mock implements LightmeterAnalytics {}
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
late _MockMeteringInteractor meteringInteractor;
|
||||
late _MockMeteringCommunicationBloc communicationBloc;
|
||||
late _MockLightmeterAnalytics analytics;
|
||||
late CameraContainerBloc bloc;
|
||||
|
||||
const cameraMethodChannel = MethodChannel('plugins.flutter.io/camera');
|
||||
|
@ -110,16 +114,21 @@ void main() {
|
|||
|
||||
setUpAll(() {
|
||||
meteringInteractor = _MockMeteringInteractor();
|
||||
communicationBloc = _MockMeteringCommunicationBloc();
|
||||
|
||||
communicationBloc = _MockMeteringCommunicationBloc();
|
||||
when(() => meteringInteractor.cameraEvCalibration).thenReturn(0.0);
|
||||
when(meteringInteractor.quickVibration).thenAnswer((_) async {});
|
||||
|
||||
analytics = _MockLightmeterAnalytics();
|
||||
registerFallbackValue(StackTrace.empty);
|
||||
when(() => analytics.logCrash(any<dynamic>(), any<StackTrace>())).thenAnswer((_) async {});
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
bloc = CameraContainerBloc(
|
||||
meteringInteractor,
|
||||
communicationBloc,
|
||||
analytics,
|
||||
);
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(cameraMethodChannel, cameraMethodCallSuccessHandler);
|
||||
|
|
|
@ -17,7 +17,7 @@ void main() {
|
|||
'no EXIF',
|
||||
() {
|
||||
final bytes = File('assets/launcher_icon_dev_512.png').readAsBytesSync();
|
||||
expectLater(evFromImage(bytes), completion(null));
|
||||
expectLater(evFromImage(bytes), throwsFlutterError);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue