Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ML-129 Spot metering #136

Merged
merged 15 commits into from
Nov 11, 2023
5 changes: 3 additions & 2 deletions lib/application_wrapper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class ApplicationWrapper extends StatelessWidget {
future: Future.wait<dynamic>([
SharedPreferences.getInstance(),
const LightSensorService(LocalPlatform()).hasSensor(),
const RemoteConfigService().activeAndFetchFeatures(),
if (env.buildType != BuildType.dev) const RemoteConfigService().activeAndFetchFeatures(),
]),
builder: (_, snapshot) {
if (snapshot.data != null) {
Expand All @@ -47,7 +47,8 @@ class ApplicationWrapper extends StatelessWidget {
userPreferencesService: userPreferencesService,
volumeEventsService: const VolumeEventsService(LocalPlatform()),
child: RemoteConfigProvider(
remoteConfigService: const RemoteConfigService(),
remoteConfigService:
env.buildType != BuildType.dev ? const RemoteConfigService() : const MockRemoteConfigService(),
child: EquipmentProfileProvider(
storageService: iapService,
child: FilmsProvider(
Expand Down
13 changes: 13 additions & 0 deletions lib/data/models/camera_feature.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
enum CameraFeature {
spotMetering,
histogram,
}

typedef CameraFeaturesConfig = Map<CameraFeature, bool>;

extension CameraFeaturesConfigJson on CameraFeaturesConfig {
static CameraFeaturesConfig fromJson(Map<String, dynamic> data) =>
<CameraFeature, bool>{for (final f in CameraFeature.values) f: data[f.name] as bool? ?? false};

Map<String, dynamic> toJson() => map((key, value) => MapEntry(key.name, value));
}
2 changes: 1 addition & 1 deletion lib/data/models/feature.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
enum Feature { unlockProFeaturesText }

const featuresDefaultValues = {
Feature.unlockProFeaturesText: false,
Feature.unlockProFeaturesText: true,
};
33 changes: 23 additions & 10 deletions lib/data/models/metering_screen_layout_config.dart
Original file line number Diff line number Diff line change
@@ -1,18 +1,31 @@
enum MeteringScreenLayoutFeature {
extremeExposurePairs,
filmPicker,
histogram,
equipmentProfiles,
extremeExposurePairs, // 0
filmPicker, // 1
equipmentProfiles, // 3
}

typedef MeteringScreenLayoutConfig = Map<MeteringScreenLayoutFeature, bool>;

extension MeteringScreenLayoutConfigJson on MeteringScreenLayoutConfig {
static MeteringScreenLayoutConfig fromJson(Map<String, dynamic> data) =>
<MeteringScreenLayoutFeature, bool>{
for (final f in MeteringScreenLayoutFeature.values)
f: data[f.index.toString()] as bool? ?? true
};
static MeteringScreenLayoutConfig fromJson(Map<String, dynamic> data) {
int? migratedIndex(MeteringScreenLayoutFeature feature) {
switch (feature) {
case MeteringScreenLayoutFeature.extremeExposurePairs:
return 0;
case MeteringScreenLayoutFeature.filmPicker:
return 1;
case MeteringScreenLayoutFeature.equipmentProfiles:
return 3;
default:
return null;
}
}

Map<String, dynamic> toJson() => map((key, value) => MapEntry(key.index.toString(), value));
return <MeteringScreenLayoutFeature, bool>{
for (final f in MeteringScreenLayoutFeature.values)
f: (data[migratedIndex(f).toString()] ?? data[f.name]) as bool? ?? true
};
}

Map<String, dynamic> toJson() => map((key, value) => MapEntry(key.name, value));
}
49 changes: 47 additions & 2 deletions lib/data/remote_config_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,26 @@ import 'package:firebase_remote_config/firebase_remote_config.dart';
import 'package:flutter/foundation.dart';
import 'package:lightmeter/data/models/feature.dart';

class RemoteConfigService {
abstract class IRemoteConfigService {
const IRemoteConfigService();

Future<void> activeAndFetchFeatures();

Future<void> fetchConfig();

dynamic getValue(Feature feature);

Map<Feature, dynamic> getAll();

Stream<Set<Feature>> onConfigUpdated();

bool isEnabled(Feature feature);
}

class RemoteConfigService implements IRemoteConfigService {
const RemoteConfigService();

@override
Future<void> activeAndFetchFeatures() async {
final FirebaseRemoteConfig remoteConfig = FirebaseRemoteConfig.instance;
const cacheStaleDuration = kDebugMode ? Duration(minutes: 1) : Duration(hours: 12);
Expand All @@ -28,19 +45,22 @@ class RemoteConfigService {
log('Firebase remote config initialized successfully');
} on FirebaseException catch (e) {
_logError('Firebase exception during Firebase Remote Config initialization: $e');
} on Exception catch (e) {
} catch (e) {
_logError('Error during Firebase Remote Config initialization: $e');
}
}

@override
Future<void> fetchConfig() async {
// https://github.com/firebase/flutterfire/issues/6196#issuecomment-927751667
await Future.delayed(const Duration(seconds: 1));
await FirebaseRemoteConfig.instance.fetch();
}

@override
dynamic getValue(Feature feature) => FirebaseRemoteConfig.instance.getValue(feature.name).toValue(feature);

@override
Map<Feature, dynamic> getAll() {
final Map<Feature, dynamic> result = {};
for (final value in FirebaseRemoteConfig.instance.getAll().entries) {
Expand All @@ -54,6 +74,7 @@ class RemoteConfigService {
return result;
}

@override
Stream<Set<Feature>> onConfigUpdated() => FirebaseRemoteConfig.instance.onConfigUpdated.asyncMap(
(event) async {
await FirebaseRemoteConfig.instance.activate();
Expand All @@ -69,13 +90,37 @@ class RemoteConfigService {
},
);

@override
bool isEnabled(Feature feature) => FirebaseRemoteConfig.instance.getBool(feature.name);

void _logError(dynamic throwable, {StackTrace? stackTrace}) {
FirebaseCrashlytics.instance.recordError(throwable, stackTrace);
}
}

class MockRemoteConfigService implements IRemoteConfigService {
const MockRemoteConfigService();

@override
Future<void> activeAndFetchFeatures() async {}

@override
Future<void> fetchConfig() async {}

@override
Map<Feature, dynamic> getAll() => featuresDefaultValues;

@override
dynamic getValue(Feature feature) => featuresDefaultValues[feature];

@override
// ignore: cast_nullable_to_non_nullable
bool isEnabled(Feature feature) => featuresDefaultValues[feature] as bool;

@override
Stream<Set<Feature>> onConfigUpdated() => const Stream.empty();
}

extension on RemoteConfigValue {
dynamic toValue(Feature feature) {
switch (feature) {
Expand Down
39 changes: 24 additions & 15 deletions lib/data/shared_prefs_service.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/camera_feature.dart';
import 'package:lightmeter/data/models/ev_source_type.dart';
import 'package:lightmeter/data/models/metering_screen_layout_config.dart';
import 'package:lightmeter/data/models/supported_locale.dart';
Expand All @@ -18,6 +19,7 @@ class UserPreferencesService {
static const cameraEvCalibrationKey = "cameraEvCalibration";
static const lightSensorEvCalibrationKey = "lightSensorEvCalibration";
static const meteringScreenLayoutKey = "meteringScreenLayout";
static const cameraFeaturesKey = "cameraFeatures";

static const caffeineKey = "caffeine";
static const hapticsKey = "haptics";
Expand Down Expand Up @@ -70,16 +72,13 @@ class UserPreferencesService {
}
}

IsoValue get iso =>
IsoValue.values.firstWhere((v) => v.value == (_sharedPreferences.getInt(isoKey) ?? 100));
IsoValue get iso => IsoValue.values.firstWhere((v) => v.value == (_sharedPreferences.getInt(isoKey) ?? 100));
set iso(IsoValue value) => _sharedPreferences.setInt(isoKey, value.value);

NdValue get ndFilter =>
NdValue.values.firstWhere((v) => v.value == (_sharedPreferences.getInt(ndFilterKey) ?? 0));
NdValue get ndFilter => NdValue.values.firstWhere((v) => v.value == (_sharedPreferences.getInt(ndFilterKey) ?? 0));
set ndFilter(NdValue value) => _sharedPreferences.setInt(ndFilterKey, value.value);

EvSourceType get evSourceType =>
EvSourceType.values[_sharedPreferences.getInt(evSourceTypeKey) ?? 0];
EvSourceType get evSourceType => EvSourceType.values[_sharedPreferences.getInt(evSourceTypeKey) ?? 0];
set evSourceType(EvSourceType value) => _sharedPreferences.setInt(evSourceTypeKey, value.index);

StopType get stopType => StopType.values[_sharedPreferences.getInt(stopTypeKey) ?? 2];
Expand All @@ -96,14 +95,28 @@ class UserPreferencesService {
MeteringScreenLayoutFeature.equipmentProfiles: true,
MeteringScreenLayoutFeature.extremeExposurePairs: true,
MeteringScreenLayoutFeature.filmPicker: true,
MeteringScreenLayoutFeature.histogram: true,
};
}
}

set meteringScreenLayout(MeteringScreenLayoutConfig value) =>
_sharedPreferences.setString(meteringScreenLayoutKey, json.encode(value.toJson()));

CameraFeaturesConfig get cameraFeatures {
final configJson = _sharedPreferences.getString(cameraFeaturesKey);
if (configJson != null) {
return CameraFeaturesConfigJson.fromJson(json.decode(configJson) as Map<String, dynamic>);
} else {
return {
CameraFeature.spotMetering: false,
CameraFeature.histogram: false,
};
}
}

set cameraFeatures(CameraFeaturesConfig value) =>
_sharedPreferences.setString(cameraFeaturesKey, json.encode(value.toJson()));

bool get caffeine => _sharedPreferences.getBool(caffeineKey) ?? false;
set caffeine(bool value) => _sharedPreferences.setBool(caffeineKey, value);

Expand All @@ -114,8 +127,7 @@ class UserPreferencesService {
(e) => e.toString() == _sharedPreferences.getString(volumeActionKey),
orElse: () => VolumeAction.shutter,
);
set volumeAction(VolumeAction value) =>
_sharedPreferences.setString(volumeActionKey, value.toString());
set volumeAction(VolumeAction value) => _sharedPreferences.setString(volumeActionKey, value.toString());

SupportedLocale get locale => SupportedLocale.values.firstWhere(
(e) => e.toString() == _sharedPreferences.getString(localeKey),
Expand All @@ -124,13 +136,10 @@ class UserPreferencesService {
set locale(SupportedLocale value) => _sharedPreferences.setString(localeKey, value.toString());

double get cameraEvCalibration => _sharedPreferences.getDouble(cameraEvCalibrationKey) ?? 0.0;
set cameraEvCalibration(double value) =>
_sharedPreferences.setDouble(cameraEvCalibrationKey, value);
set cameraEvCalibration(double value) => _sharedPreferences.setDouble(cameraEvCalibrationKey, value);

double get lightSensorEvCalibration =>
_sharedPreferences.getDouble(lightSensorEvCalibrationKey) ?? 0.0;
set lightSensorEvCalibration(double value) =>
_sharedPreferences.setDouble(lightSensorEvCalibrationKey, value);
double get lightSensorEvCalibration => _sharedPreferences.getDouble(lightSensorEvCalibrationKey) ?? 0.0;
set lightSensorEvCalibration(double value) => _sharedPreferences.setDouble(lightSensorEvCalibrationKey, value);

ThemeType get themeType => ThemeType.values[_sharedPreferences.getInt(themeTypeKey) ?? 0];
set themeType(ThemeType value) => _sharedPreferences.setInt(themeTypeKey, value.index);
Expand Down
10 changes: 7 additions & 3 deletions lib/l10n/intl_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,11 @@
"meteringScreenLayoutHintEquipmentProfiles": "Equipment profile picker",
"meteringScreenFeatureExtremeExposurePairs": "Fastest & shortest exposure pairs",
"meteringScreenFeatureFilmPicker": "Film picker",
"meteringScreenFeatureHistogram": "Histogram",
"cameraFeatures": "Camera features",
"cameraFeatureSpotMetering": "Spot metering",
"cameraFeatureSpotMeteringHint": "Long press the camera view to remove metering spot",
"cameraFeatureHistogram": "Histogram",
"cameraFeatureHistogramHint": "Enabling histogram can encrease battery drain",
"film": "Film",
"filmPush": "Film (push)",
"filmPull": "Film (pull)",
Expand Down Expand Up @@ -94,11 +98,11 @@
},
"lightmeterPro": "Lightmeter Pro",
"buyLightmeterPro": "Buy Lightmeter Pro",
"lightmeterProDescription": "Unlocks extra features, such as equipment profiles containing filters for aperture, shutter speed, and more; and a list of films with compensation for what's known as reciprocity failure.\n\nThe source code of Lightmeter is available on GitHub. You are welcome to compile it yourself. However, if you want to support the development and receive new features and updates, consider purchasing Lightmeter Pro.",
"lightmeterProDescription": "Unlocks extra features:\n \u2022 Equipment profiles containing filters for aperture, shutter speed, and more\n \u2022 List of films with compensation for what's known as reciprocity failure\n \u2022 Spot metering\n \u2022 Histogram\n\nThe source code of Lightmeter is available on GitHub. You are welcome to compile it yourself. However, if you want to support the development and receive new features and updates, consider purchasing Lightmeter Pro.",
"buy": "Buy",
"proFeatures": "Pro features",
"unlockProFeatures": "Unlock Pro features",
"unlockProFeaturesDescription": "Unlock professional features, such as equipment profiles containing filters for aperture, shutter speed, and more; and a list of films with compensation for what's known as reciprocity failure.\n\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",
Expand Down
10 changes: 7 additions & 3 deletions lib/l10n/intl_fr.arb
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,11 @@
"meteringScreenLayoutHintEquipmentProfiles": "Sélecteur de profil de l'équipement",
"meteringScreenFeatureExtremeExposurePairs": "Paires d'exposition les plus rapides et les plus courtes",
"meteringScreenFeatureFilmPicker": "Sélecteur de film",
"meteringScreenFeatureHistogram": "Histogramme",
"cameraFeatures": "Fonctionnalités de la caméra",
"cameraFeatureSpotMetering": "Mesure spot",
"cameraFeatureSpotMeteringHint": "Appuyez longuement sur la vue de l'appareil photo pour supprimer le spot de mesure",
"cameraFeatureHistogram": "Histogramme",
"cameraFeatureHistogramHint": "L'activation de l'histogramme peut augmenter la consommation de la batterie",
"film": "Pellicule",
"filmPush": "Pellicule (push)",
"filmPull": "Pellicule (pull)",
Expand Down Expand Up @@ -94,11 +98,11 @@
},
"buyLightmeterPro": "Acheter Lightmeter Pro",
"lightmeterPro": "Lightmeter Pro",
"lightmeterProDescription": "Déverrouille des fonctionnalités supplémentaires, telles que des profils d'équipement contenant des filtres pour l'ouverture, la vitesse d'obturation et plus encore, ainsi qu'une liste de films avec une compensation pour ce que l'on appelle l'échec de réciprocité.\n\nLe code source du Lightmeter est disponible sur GitHub. Vous pouvez le compiler vous-même. Cependant, si vous souhaitez soutenir le développement et recevoir de nouvelles fonctionnalités et mises à jour, envisagez d'acheter Lightmeter Pro.",
"lightmeterProDescription": "Débloque des fonctionnalités supplémentaires:\n \u2022 Profils d'équipement contenant des filtres pour l'ouverture, la vitesse d'obturation et plus encore\n \u2022 Liste de films avec une compensation pour ce que l'on appelle l'échec de réciprocité\n \u2022 Mesure spot\n \u2022 Histogramme\n\nLe code source du Lightmeter est disponible sur GitHub. Vous pouvez le compiler vous-même. Cependant, si vous souhaitez soutenir le développement et recevoir de nouvelles fonctionnalités et mises à jour, envisagez d'acheter Lightmeter Pro.",
"buy": "Acheter",
"proFeatures": "Fonctionnalités professionnelles",
"unlockProFeatures": "Déverrouiller les fonctionnalités professionnelles",
"unlockProFeaturesDescription": "Déverrouillez des fonctions professionnelles, telles que des profils d'équipement contenant des filtres pour l'ouverture, la vitesse d'obturation et plus encore, ainsi qu'une liste de films avec compensation pour ce que l'on appelle l'échec de réciprocité.\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",
Expand Down
Loading
Loading