diff --git a/lib/data/models/camera_feature.dart b/lib/data/models/camera_feature.dart new file mode 100644 index 0000000..3265dd3 --- /dev/null +++ b/lib/data/models/camera_feature.dart @@ -0,0 +1,13 @@ +enum CameraFeature { + histogram, + spotMetering, +} + +typedef CameraFeaturesConfig = Map; + +extension CameraFeaturesConfigJson on CameraFeaturesConfig { + static CameraFeaturesConfig fromJson(Map data) => + {for (final f in CameraFeature.values) f: data[f.name] as bool? ?? false}; + + Map toJson() => map((key, value) => MapEntry(key.name, value)); +} diff --git a/lib/data/models/metering_screen_layout_config.dart b/lib/data/models/metering_screen_layout_config.dart index 7802195..7d550e4 100644 --- a/lib/data/models/metering_screen_layout_config.dart +++ b/lib/data/models/metering_screen_layout_config.dart @@ -1,18 +1,31 @@ enum MeteringScreenLayoutFeature { - extremeExposurePairs, - filmPicker, - histogram, - equipmentProfiles, + extremeExposurePairs, // 0 + filmPicker, // 1 + equipmentProfiles, // 3 } typedef MeteringScreenLayoutConfig = Map; extension MeteringScreenLayoutConfigJson on MeteringScreenLayoutConfig { - static MeteringScreenLayoutConfig fromJson(Map data) => - { - for (final f in MeteringScreenLayoutFeature.values) - f: data[f.index.toString()] as bool? ?? true - }; + static MeteringScreenLayoutConfig fromJson(Map 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 toJson() => map((key, value) => MapEntry(key.index.toString(), value)); + return { + for (final f in MeteringScreenLayoutFeature.values) + f: (data[migratedIndex(f).toString()] ?? data[f.name]) as bool? ?? true + }; + } + + Map toJson() => map((key, value) => MapEntry(key.name, value)); } diff --git a/lib/data/shared_prefs_service.dart b/lib/data/shared_prefs_service.dart index 96d7d9d..443f2e5 100644 --- a/lib/data/shared_prefs_service.dart +++ b/lib/data/shared_prefs_service.dart @@ -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'; @@ -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"; @@ -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]; @@ -96,7 +95,6 @@ class UserPreferencesService { MeteringScreenLayoutFeature.equipmentProfiles: true, MeteringScreenLayoutFeature.extremeExposurePairs: true, MeteringScreenLayoutFeature.filmPicker: true, - MeteringScreenLayoutFeature.histogram: true, }; } } @@ -104,6 +102,21 @@ class UserPreferencesService { 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); + } 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); @@ -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), @@ -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); diff --git a/lib/screens/metering/components/camera_container/components/camera_preview/widget_camera_preview.dart b/lib/screens/metering/components/camera_container/components/camera_preview/widget_camera_preview.dart index c38673c..a259aec 100644 --- a/lib/screens/metering/components/camera_container/components/camera_preview/widget_camera_preview.dart +++ b/lib/screens/metering/components/camera_container/components/camera_preview/widget_camera_preview.dart @@ -1,6 +1,6 @@ import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; -import 'package:lightmeter/data/models/metering_screen_layout_config.dart'; +import 'package:lightmeter/data/models/camera_feature.dart'; import 'package:lightmeter/platform_config.dart'; import 'package:lightmeter/providers/user_preferences_provider.dart'; import 'package:lightmeter/res/dimens.dart'; @@ -92,9 +92,9 @@ class _CameraPreviewBuilderState extends State<_CameraPreviewBuilder> { alignment: Alignment.bottomCenter, children: [ CameraView(controller: widget.controller), - if (UserPreferencesProvider.meteringScreenFeatureOf( + if (UserPreferencesProvider.cameraFeatureOf( context, - MeteringScreenLayoutFeature.histogram, + CameraFeature.histogram, )) Positioned( left: Dimens.grid8, @@ -102,7 +102,11 @@ class _CameraPreviewBuilderState extends State<_CameraPreviewBuilder> { bottom: Dimens.grid16, child: CameraHistogram(controller: widget.controller), ), - CameraSpotDetector(onSpotTap: widget.onSpotTap) + if (UserPreferencesProvider.cameraFeatureOf( + context, + CameraFeature.spotMetering, + )) + CameraSpotDetector(onSpotTap: widget.onSpotTap) ], ) : const SizedBox.shrink(), diff --git a/lib/screens/settings/components/metering/components/metering_screen_layout/widget_list_tile_metering_screen_layout.dart b/lib/screens/settings/components/metering/components/metering_screen_layout/widget_list_tile_metering_screen_layout.dart index 2510408..1f89b4b 100644 --- a/lib/screens/settings/components/metering/components/metering_screen_layout/widget_list_tile_metering_screen_layout.dart +++ b/lib/screens/settings/components/metering/components/metering_screen_layout/widget_list_tile_metering_screen_layout.dart @@ -47,8 +47,6 @@ class MeteringScreenLayoutListTile extends StatelessWidget { return S.of(context).meteringScreenFeatureExtremeExposurePairs; case MeteringScreenLayoutFeature.filmPicker: return S.of(context).meteringScreenFeatureFilmPicker; - case MeteringScreenLayoutFeature.histogram: - return S.of(context).meteringScreenFeatureHistogram; } } } diff --git a/screenshots/generate_screenshots.dart b/screenshots/generate_screenshots.dart index 8c8e7e9..814cfd1 100644 --- a/screenshots/generate_screenshots.dart +++ b/screenshots/generate_screenshots.dart @@ -46,7 +46,6 @@ void main() { MeteringScreenLayoutFeature.equipmentProfiles: true, MeteringScreenLayoutFeature.extremeExposurePairs: true, MeteringScreenLayoutFeature.filmPicker: true, - MeteringScreenLayoutFeature.histogram: false, }.toJson(), ), diff --git a/test/data/models/camera_features_config_test.dart b/test/data/models/camera_features_config_test.dart new file mode 100644 index 0000000..d7261a8 --- /dev/null +++ b/test/data/models/camera_features_config_test.dart @@ -0,0 +1,47 @@ +import 'package:lightmeter/data/models/camera_feature.dart'; +import 'package:test/test.dart'; + +void main() { + group( + 'fromJson()', + () { + test('All keys', () { + expect( + CameraFeaturesConfigJson.fromJson( + { + 'spotMetering': true, + 'histogram': true, + }, + ), + { + CameraFeature.spotMetering: true, + CameraFeature.histogram: true, + }, + ); + }); + + test('Legacy (no spotMetering & histogram)', () { + expect( + CameraFeaturesConfigJson.fromJson({}), + { + CameraFeature.spotMetering: false, + CameraFeature.histogram: false, + }, + ); + }); + }, + ); + + test('toJson()', () { + expect( + { + CameraFeature.spotMetering: true, + CameraFeature.histogram: true, + }.toJson(), + { + 'spotMetering': true, + 'histogram': true, + }, + ); + }); +} diff --git a/test/data/models/metering_screen_layout_config_test.dart b/test/data/models/metering_screen_layout_config_test.dart index 9e9393e..f3216ba 100644 --- a/test/data/models/metering_screen_layout_config_test.dart +++ b/test/data/models/metering_screen_layout_config_test.dart @@ -18,7 +18,6 @@ void main() { { MeteringScreenLayoutFeature.extremeExposurePairs: true, MeteringScreenLayoutFeature.filmPicker: true, - MeteringScreenLayoutFeature.histogram: true, MeteringScreenLayoutFeature.equipmentProfiles: true, }, ); @@ -35,7 +34,6 @@ void main() { { MeteringScreenLayoutFeature.extremeExposurePairs: false, MeteringScreenLayoutFeature.filmPicker: false, - MeteringScreenLayoutFeature.histogram: true, MeteringScreenLayoutFeature.equipmentProfiles: true, }, ); @@ -53,7 +51,6 @@ void main() { { MeteringScreenLayoutFeature.extremeExposurePairs: false, MeteringScreenLayoutFeature.filmPicker: false, - MeteringScreenLayoutFeature.histogram: false, MeteringScreenLayoutFeature.equipmentProfiles: true, }, ); @@ -67,13 +64,11 @@ void main() { MeteringScreenLayoutFeature.equipmentProfiles: true, MeteringScreenLayoutFeature.extremeExposurePairs: true, MeteringScreenLayoutFeature.filmPicker: true, - MeteringScreenLayoutFeature.histogram: true, }.toJson(), { - '3': true, - '0': true, - '1': true, - '2': true, + 'equipmentProfiles': true, + 'extremeExposurePairs': true, + 'filmPicker': true, }, ); }); diff --git a/test/data/shared_prefs_service_test.dart b/test/data/shared_prefs_service_test.dart index 8ec42e1..96f4e1e 100644 --- a/test/data/shared_prefs_service_test.dart +++ b/test/data/shared_prefs_service_test.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.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'; @@ -191,12 +192,11 @@ void main() { MeteringScreenLayoutFeature.extremeExposurePairs: true, MeteringScreenLayoutFeature.filmPicker: true, MeteringScreenLayoutFeature.equipmentProfiles: true, - MeteringScreenLayoutFeature.histogram: true, }, ); }); - test('get', () { + test('get (legacy)', () { when( () => sharedPreferences.getString(UserPreferencesService.meteringScreenLayoutKey), ).thenReturn("""{"0":false,"1":true}"""); @@ -206,7 +206,20 @@ void main() { MeteringScreenLayoutFeature.extremeExposurePairs: false, MeteringScreenLayoutFeature.filmPicker: true, MeteringScreenLayoutFeature.equipmentProfiles: true, - MeteringScreenLayoutFeature.histogram: true, + }, + ); + }); + + test('get', () { + when( + () => sharedPreferences.getString(UserPreferencesService.meteringScreenLayoutKey), + ).thenReturn("""{"extremeExposurePairs":false,"filmPicker":true}"""); + expect( + service.meteringScreenLayout, + { + MeteringScreenLayoutFeature.extremeExposurePairs: false, + MeteringScreenLayoutFeature.filmPicker: true, + MeteringScreenLayoutFeature.equipmentProfiles: true, }, ); }); @@ -215,19 +228,62 @@ void main() { when( () => sharedPreferences.setString( UserPreferencesService.meteringScreenLayoutKey, - """{"0":false,"1":true,"2":true,"3":true}""", + """{"extremeExposurePairs":false,"filmPicker":true,"equipmentProfiles":true}""", ), ).thenAnswer((_) => Future.value(true)); service.meteringScreenLayout = { MeteringScreenLayoutFeature.extremeExposurePairs: false, MeteringScreenLayoutFeature.filmPicker: true, - MeteringScreenLayoutFeature.histogram: true, MeteringScreenLayoutFeature.equipmentProfiles: true, }; verify( () => sharedPreferences.setString( UserPreferencesService.meteringScreenLayoutKey, - """{"0":false,"1":true,"2":true,"3":true}""", + """{"extremeExposurePairs":false,"filmPicker":true,"equipmentProfiles":true}""", + ), + ).called(1); + }); + }); + + group('cameraFeatures', () { + test('get default', () { + when(() => sharedPreferences.getString(UserPreferencesService.cameraFeaturesKey)).thenReturn(null); + expect( + service.cameraFeatures, + { + CameraFeature.spotMetering: false, + CameraFeature.histogram: false, + }, + ); + }); + + test('get', () { + when(() => sharedPreferences.getString(UserPreferencesService.cameraFeaturesKey)) + .thenReturn("""{"spotMetering":false,"histogram":true}"""); + expect( + service.cameraFeatures, + { + CameraFeature.spotMetering: false, + CameraFeature.histogram: true, + }, + ); + }); + + test('set', () { + when( + () => sharedPreferences.setString( + UserPreferencesService.cameraFeaturesKey, + """{"spotMetering":false,"histogram":true}""", + ), + ).thenAnswer((_) => Future.value(true)); + service.cameraFeatures = { + CameraFeature.spotMetering: false, + CameraFeature.histogram: true, + }; + verify( + () => sharedPreferences.setString( + UserPreferencesService.cameraFeaturesKey, + """{"spotMetering":false,"histogram":true}""", ), ).called(1); }); diff --git a/test/providers/user_preferences_provider_test.dart b/test/providers/user_preferences_provider_test.dart index ffa7993..a4d19d0 100644 --- a/test/providers/user_preferences_provider_test.dart +++ b/test/providers/user_preferences_provider_test.dart @@ -31,7 +31,6 @@ void main() { MeteringScreenLayoutFeature.extremeExposurePairs: true, MeteringScreenLayoutFeature.filmPicker: true, MeteringScreenLayoutFeature.equipmentProfiles: true, - MeteringScreenLayoutFeature.histogram: true, }); when(() => mockUserPreferencesService.locale).thenReturn(SupportedLocale.en); when(() => mockUserPreferencesService.themeType).thenReturn(ThemeType.light); @@ -184,7 +183,6 @@ void main() { MeteringScreenLayoutFeature.equipmentProfiles: true, MeteringScreenLayoutFeature.extremeExposurePairs: false, MeteringScreenLayoutFeature.filmPicker: false, - MeteringScreenLayoutFeature.histogram: true, }), child: const Text(''), ), @@ -196,20 +194,17 @@ void main() { expect(find.text("${MeteringScreenLayoutFeature.equipmentProfiles}: true"), findsNWidgets(2)); expect(find.text("${MeteringScreenLayoutFeature.extremeExposurePairs}: true"), findsNWidgets(2)); expect(find.text("${MeteringScreenLayoutFeature.filmPicker}: true"), findsNWidgets(2)); - expect(find.text("${MeteringScreenLayoutFeature.histogram}: true"), findsNWidgets(2)); await tester.tap(find.byType(ElevatedButton)); await tester.pumpAndSettle(); expect(find.text("${MeteringScreenLayoutFeature.equipmentProfiles}: true"), findsNWidgets(2)); expect(find.text("${MeteringScreenLayoutFeature.extremeExposurePairs}: false"), findsNWidgets(2)); expect(find.text("${MeteringScreenLayoutFeature.filmPicker}: false"), findsNWidgets(2)); - expect(find.text("${MeteringScreenLayoutFeature.histogram}: true"), findsNWidgets(2)); verify( () => mockUserPreferencesService.meteringScreenLayout = { MeteringScreenLayoutFeature.extremeExposurePairs: false, MeteringScreenLayoutFeature.filmPicker: false, MeteringScreenLayoutFeature.equipmentProfiles: true, - MeteringScreenLayoutFeature.histogram: true, }, ).called(1); },