Compare commits

..

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

31 changed files with 212 additions and 327 deletions

View file

@ -1,14 +0,0 @@
content="$1"
filename="$2"
if [[ ! -n "$content" ]]; then
echo "Provide file content"
exit 1
fi
if [[ ! -n "$filename" ]]; then
echo "Provide a path to an output file"
exit 1
fi
echo -n "$content" | base64 --decode --output "$filename"

View file

@ -3,17 +3,27 @@
# separate terms of service, privacy policy, and support
# documentation.
name: Build Prod .ipa
name: Build .apk
on:
workflow_dispatch:
env:
FLAVOR: "prod"
inputs:
flavor:
description: 'Flavor'
type: choice
required: true
options:
- dev
- prod
default: 'dev'
include-iap:
type: boolean
description: Include IAP package
default: true
jobs:
build:
name: Build .ipa
name: Build .apk
runs-on: macos-11
timeout-minutes: 15
steps:
@ -23,34 +33,52 @@ jobs:
- name: Connect private iap package
uses: webfactory/ssh-agent@v0.8.0
if: ${{ inputs.include-iap }}
with:
ssh-private-key: ${{ secrets.M3_LIGHTMETER_IAP_KEY }}
- name: Override iap package with stub
if: ${{ !inputs.include-iap }}
run: bash ./.github/scripts/stub_iap.sh
- uses: actions/setup-java@v2
with:
distribution: "zulu"
java-version: "11"
- name: Install Apple Certificate
uses: apple-actions/import-codesign-certs@v1
with:
p12-file-base64: ${{ secrets.APP_STORE_P12 }}
p12-password: ${{ secrets.APP_STORE_P12_PASSWORD }}
- name: Install the provisioning profile
- name: Restore Android keystore .jsk and .properties files
env:
KEYSTORE: ${{ secrets.KEYSTORE }}
KEYSTORE_PROPERTIES: ${{ secrets.KEYSTORE_PROPERTIES }}
run: |
PROVISION_CERT_PATH=$RUNNER_TEMP/provision_prod.mobileprovision
bash .github/scripts/restore_from_base64.sh "${{ secrets.APP_STORE_PROVISION_PROD }}" $PROVISION_CERT_PATH
mkdir -p ~/Library/MobileDevice\ Profiles
cp $PROVISION_CERT_PATH ~/Library/MobileDevice\ Profiles
KEYSTORE_PATH=$RUNNER_TEMP/keystore.jks
echo -n "$KEYSTORE" | base64 --decode --output $KEYSTORE_PATH
cp $KEYSTORE_PATH ./android/app
KEYSTORE_PROPERTIES_PATH=$RUNNER_TEMP/key.properties
echo -n "$KEYSTORE_PROPERTIES" | base64 --decode --output $KEYSTORE_PROPERTIES_PATH
cp $KEYSTORE_PROPERTIES_PATH ./android
- name: Restore android/app/google-services.json
env:
GOOGLE_SERVICES_JSON_ANDROID: ${{ secrets.GOOGLE_SERVICES_JSON_ANDROID }}
run: |
GOOGLE_SERVICES_JSON_ANDROID_PATH=$RUNNER_TEMP/google-services.json
echo -n "$GOOGLE_SERVICES_JSON_ANDROID" | base64 --decode --output $GOOGLE_SERVICES_JSON_ANDROID_PATH
cp $GOOGLE_SERVICES_JSON_ANDROID_PATH ./android/app
- name: Restore firebase_options.dart
run: bash .github/scripts/restore_from_base64.sh "${{ secrets.FIREBASE_OPTIONS }}" "lib/firebase_options.dart"
env:
FIREBASE_OPTIONS: ${{ secrets.FIREBASE_OPTIONS }}
run: |
FIREBASE_OPTIONS_PATH=$RUNNER_TEMP/firebase_options.dart
echo -n "$FIREBASE_OPTIONS" | base64 --decode --output $FIREBASE_OPTIONS_PATH
cp $FIREBASE_OPTIONS_PATH ./lib
- name: Install Flutter
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: "3.10.0"
flutter-version: '3.10.0'
- name: Prepare flutter project
run: |
@ -58,11 +86,13 @@ jobs:
flutter pub get
flutter pub run intl_utils:generate
- name: Build .ipa
run: flutter build ipa --release --flavor $FLAVOR -t lib/main_$FLAVOR.dart
- name: Build .apk
env:
FLAVOR: ${{ github.event.inputs.flavor }}
run: flutter build apk --release --flavor $FLAVOR -t lib/main_$FLAVOR.dart
- name: Upload artifact
uses: actions/upload-artifact@v3
with:
name: m3_lightmeter_$FLAVOR_ipa
path: build/ios/ipa/lightmeter.ipa
name: m3_lightmeter_${{ github.event.inputs.flavor }}
path: build/app/outputs/flutter-apk/app-${{ github.event.inputs.flavor }}-release.apk

View file

@ -1,68 +0,0 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
name: Build Prod .ipa
on:
workflow_dispatch:
env:
FLAVOR: "prod"
jobs:
build:
name: Build .ipa
runs-on: macos-11
timeout-minutes: 15
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Connect private iap package
uses: webfactory/ssh-agent@v0.8.0
with:
ssh-private-key: ${{ secrets.M3_LIGHTMETER_IAP_KEY }}
- uses: actions/setup-java@v2
with:
distribution: "zulu"
java-version: "11"
- name: Install Apple Certificate
uses: apple-actions/import-codesign-certs@v1
with:
p12-file-base64: ${{ secrets.APP_STORE_P12 }}
p12-password: ${{ secrets.APP_STORE_P12_PASSWORD }}
- name: Install the provisioning profile
run: |
PROVISION_CERT_PATH=$RUNNER_TEMP/provision_prod.mobileprovision
bash .github/scripts/restore_from_base64.sh "${{ secrets.APP_STORE_PROVISION_PROD }}" $PROVISION_CERT_PATH
mkdir -p ~/Library/MobileDevice\ Profiles
cp $PROVISION_CERT_PATH ~/Library/MobileDevice\ Profiles
- name: Restore firebase_options.dart
run: bash .github/scripts/restore_from_base64.sh "${{ secrets.FIREBASE_OPTIONS }}" "lib/firebase_options.dart"
- name: Install Flutter
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: "3.10.0"
- name: Prepare flutter project
run: |
flutter --version
flutter pub get
flutter pub run intl_utils:generate
- name: Build .ipa
run: flutter build ipa --release --flavor $FLAVOR -t lib/main_$FLAVOR.dart
- name: Upload artifact
uses: actions/upload-artifact@v3
with:
name: m3_lightmeter_$FLAVOR_ipa
path: build/ios/ipa/lightmeter.ipa

4
.vscode/launch.json vendored
View file

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

View file

@ -14,7 +14,6 @@
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

View file

@ -1,11 +0,0 @@
<?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

@ -399,11 +399,9 @@
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 489Z6UQMGN;
DEVELOPMENT_TEAM = 489Z6UQMGN;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Lightmeter;
@ -414,7 +412,6 @@
PRODUCT_BUNDLE_IDENTIFIER = com.vodemn.lightmeter;
PRODUCT_NAME = Lightmeter;
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = m3_lightmeter_provision;
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
@ -536,11 +533,9 @@
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 489Z6UQMGN;
DEVELOPMENT_TEAM = 489Z6UQMGN;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Lightmeter;
@ -551,7 +546,6 @@
PRODUCT_BUNDLE_IDENTIFIER = com.vodemn.lightmeter;
PRODUCT_NAME = Lightmeter;
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = m3_lightmeter_provision;
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
@ -567,11 +561,9 @@
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 489Z6UQMGN;
DEVELOPMENT_TEAM = 489Z6UQMGN;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Lightmeter;
@ -582,7 +574,6 @@
PRODUCT_BUNDLE_IDENTIFIER = com.vodemn.lightmeter;
PRODUCT_NAME = Lightmeter;
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = m3_lightmeter_provision;
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
@ -652,11 +643,9 @@
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 489Z6UQMGN;
DEVELOPMENT_TEAM = 489Z6UQMGN;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Lightmeter (DEV)";
@ -667,7 +656,6 @@
PRODUCT_BUNDLE_IDENTIFIER = com.vodemn.lightmeter.dev;
PRODUCT_NAME = "Lightmeter (DEV)";
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = m3_lightmeter_provision;
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
@ -735,11 +723,9 @@
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 489Z6UQMGN;
DEVELOPMENT_TEAM = 489Z6UQMGN;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Lightmeter (DEV)";
@ -750,7 +736,6 @@
PRODUCT_BUNDLE_IDENTIFIER = com.vodemn.lightmeter.dev;
PRODUCT_NAME = "Lightmeter (DEV)";
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = m3_lightmeter_provision;
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
@ -815,11 +800,9 @@
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Manual;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = "";
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 489Z6UQMGN;
DEVELOPMENT_TEAM = 489Z6UQMGN;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Lightmeter (DEV)";
@ -830,7 +813,6 @@
PRODUCT_BUNDLE_IDENTIFIER = com.vodemn.lightmeter.dev;
PRODUCT_NAME = "Lightmeter (DEV)";
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = m3_lightmeter_provision;
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";

View file

@ -26,14 +26,11 @@ 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(),
remoteConfigService.activeAndFetchFeatures(),
if (env.buildType != BuildType.dev) const RemoteConfigService().activeAndFetchFeatures(),
]),
builder: (_, snapshot) {
if (snapshot.data != null) {
@ -50,7 +47,8 @@ class ApplicationWrapper extends StatelessWidget {
userPreferencesService: userPreferencesService,
volumeEventsService: const VolumeEventsService(LocalPlatform()),
child: RemoteConfigProvider(
remoteConfigService: remoteConfigService,
remoteConfigService:
env.buildType != BuildType.dev ? const RemoteConfigService() : const MockRemoteConfigService(),
child: EquipmentProfileProvider(
storageService: iapService,
child: FilmsProvider(

View file

@ -3,56 +3,32 @@ 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(
String eventName, {
LightmeterAnalyticsEvent event, {
Map<String, dynamic>? parameters,
}) async {
if (!kReleaseMode) {
log('<LightmeterAnalytics> logEvent: $eventName / $parameters');
if (kDebugMode) {
log('<LightmeterAnalytics> logEvent: ${event.name} / $parameters');
return;
}
return _api.logEvent(
eventName,
event: event,
parameters: parameters,
);
}
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> logUnlockProFeatures(String listTileTitle) async {
return logEvent(
LightmeterAnalyticsEvent.unlockProFeatures,
parameters: {"listTileTitle": listTileTitle},
);
}
Future<void> setCustomKey(String key, String value) async => _api.setCustomKey(key, value);
}

View file

@ -1,15 +1,8 @@
import 'package:lightmeter/data/analytics/entity/analytics_event.dart';
abstract class ILightmeterAnalyticsApi {
Future<void> logEvent(
String eventName, {
Future<void> logEvent({
required LightmeterAnalyticsEvent event,
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_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(
String eventName, {
Future<void> logEvent({
required LightmeterAnalyticsEvent event,
Map<String, dynamic>? parameters,
}) async {
try {
await FirebaseAnalytics.instance.logEvent(
name: eventName,
name: event.name,
parameters: parameters,
);
} on FirebaseException catch (e) {
@ -23,25 +23,4 @@ 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);
}
}

View file

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

View file

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

22
lib/firebase.dart Normal file
View file

@ -0,0 +1,22 @@
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",
"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 & 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.",
"unlockProFeaturesDescription": "Unlock professional features:\n \u2022 Equipment profiles containing filters for aperture, shutter speed, and more\n \u2022 List of films with compensation for what's known as reciprocity failure\n \u2022 Spot metering\n \u2022 Histogram\n\nBy unlocking Pro features you support the development and make it possible to add new features to the app.",
"unlock": "Unlock",
"tooltipAdd": "Add",
"tooltipClose": "Close",

View file

@ -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 & 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.",
"unlockProFeaturesDescription": "Déverrouillez des fonctions professionnelles:\n \u2022 Profils d'équipement contenant des filtres pour l'ouverture, la vitesse d'obturation et plus encore, ainsi qu'une liste de films avec compensation pour ce que l'on appelle l'échec de réciprocité\n \u2022 Mesure spot\n \u2022 Histogramme\n\nEn débloquant les fonctionnalités Pro, vous soutenez le développement et permettez d'ajouter de nouvelles fonctionnalités à l'application.",
"unlock": "Déverrouiller",
"tooltipAdd": "Ajouter",
"tooltipClose": "Fermer",

View file

@ -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": "Закрыть",

View file

@ -30,8 +30,8 @@
"halfStops": "1/2",
"thirdStops": "1/3",
"calibration": "校准",
"calibrationMessage": "此应用程序测量读数的准确性取决于设备的后置摄像头。如需更精确的测量结果或测量结果存在偏差,请手动校准 EV 。",
"calibrationMessageCameraOnly": "此应用程序测量读数的准确性取决于设备的后置摄像头。如需更精确的测量结果或测量结果存在偏差,请手动校准 EV 。",
"calibrationMessage": "此应用测量读数的准确性完全取决于设备的硬件。因此,请考虑测试此应用并手动设置 EV 校准,以获得准确的测量结果。",
"calibrationMessageCameraOnly": "此应用程序测量读数的准确s性完全取决于设备的后置摄像头。因此请考虑测试此应用并手动设置 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 。这些可能是您常用的ISO值也可以是相机支持的ISO范围。",
"isoValuesFilterDescription": "选择要显示的 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": "关闭",

View file

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

View file

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

19
lib/main_release.dart Normal file
View file

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

View file

@ -1,35 +0,0 @@
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,7 +7,6 @@ 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';
@ -23,7 +22,6 @@ part 'mock_bloc_container_camera.dart';
class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraContainerState> {
final MeteringInteractor _meteringInteractor;
final LightmeterAnalytics _analytics;
late final _WidgetsBindingObserver _observer;
CameraController? _cameraController;
@ -44,7 +42,6 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
CameraContainerBloc(
this._meteringInteractor,
MeteringCommunicationBloc communicationBloc,
this._analytics,
) : super(
communicationBloc,
const CameraInitState(),
@ -226,8 +223,8 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
Directory(file.path).deleteSync(recursive: true);
return await evFromImage(bytes);
} catch (e, stackTrace) {
_analytics.logCrash(e, stackTrace);
} catch (e) {
log(e.toString());
return null;
}
}

View file

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

View file

@ -2,7 +2,6 @@ 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';
@ -38,12 +37,10 @@ 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(

View file

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

View file

@ -1,20 +1,27 @@
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 {
Future<double?> evFromImage(Uint8List bytes) async {
try {
final tags = await readExifFromBytes(bytes);
final iso = double.tryParse("${tags["EXIF ISOSpeedRatings"]}");
final apertureValueRatio = (tags["EXIF FNumber"]?.values as IfdRatios?)?.ratios.first;
final speedValueRatio = (tags["EXIF ExposureTime"]?.values as IfdRatios?)?.ratios.first;
if (iso == null || apertureValueRatio == null || speedValueRatio == null) {
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;
}
}

View file

@ -1,7 +1,7 @@
name: lightmeter
description: Lightmeter app inspired by Material 3 design system.
publish_to: "none"
version: 0.17.2+48
version: 0.17.0+46
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.2
ref: v0.7.1
m3_lightmeter_resources:
git:
url: "https://github.com/vodemn/m3_lightmeter_resources"

View file

@ -2,7 +2,6 @@ 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;
@ -19,14 +18,11 @@ 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');
@ -114,21 +110,16 @@ void main() {
setUpAll(() {
meteringInteractor = _MockMeteringInteractor();
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);

View file

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