diff --git a/integration_test/e2e_test.dart b/integration_test/e2e_test.dart index 97022f1..6491b63 100644 --- a/integration_test/e2e_test.dart +++ b/integration_test/e2e_test.dart @@ -7,6 +7,7 @@ import 'package:lightmeter/data/models/metering_screen_layout_config.dart'; import 'package:lightmeter/data/shared_prefs_service.dart'; import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/res/dimens.dart'; +import 'package:lightmeter/screens/equipment_profiles/components/equipment_profile_type_picker/widget_picker_equipment_profile_type.dart'; import 'package:lightmeter/screens/metering/components/bottom_controls/widget_bottom_controls.dart'; import 'package:lightmeter/screens/metering/components/shared/readings_container/components/equipment_profile_picker/widget_picker_equipment_profiles.dart'; import 'package:lightmeter/screens/metering/components/shared/readings_container/components/film_picker/widget_picker_film.dart'; @@ -56,8 +57,7 @@ void testE2E(String description) { /// Create Praktica + Zenitar profile from scratch await tester.openSettings(); await tester.tapDescendantTextOf(S.current.equipmentProfiles); - await tester.tap(find.byIcon(Icons.add_outlined).first); - await tester.pumpAndSettle(); + await tester.addEquipmentProfile(EquipmentProfileType.regular); await tester.enterProfileName(mockEquipmentProfiles[0].name); await tester.setIsoValues(mockEquipmentProfiles[0].isoValues); await tester.setNdValues(mockEquipmentProfiles[0].ndValues); @@ -70,8 +70,7 @@ void testE2E(String description) { await tester.saveEdits(); /// Create Praktica + Jupiter profile from Zenitar profile - await tester.tap(find.byIcon(Icons.edit_outlined)); - await tester.pumpAndSettle(); + await tester.editEquipmentProfile(mockEquipmentProfiles[1].name); await tester.tap(find.byIcon(Icons.copy_outlined).first); await tester.pumpAndSettle(); await tester.enterProfileName(mockEquipmentProfiles[1].name); @@ -90,7 +89,7 @@ void testE2E(String description) { /// Select some initial settings according to the selected gear and film /// Then take a photo and verify, that exposure pairs range and EV matches the selected settings - await tester.openPickerAndSelect(mockEquipmentProfiles[0].name); + await tester.openPickerAndSelect(mockEquipmentProfiles[0].name); await tester.openPickerAndSelect(mockFilms[0].name); await tester.openPickerAndSelect('400'); expectPickerTitle(mockEquipmentProfiles[0].name); @@ -122,7 +121,7 @@ void testE2E(String description) { // ); /// Select another lens without ND - await tester.openPickerAndSelect(mockEquipmentProfiles[1].name); + await tester.openPickerAndSelect(mockEquipmentProfiles[1].name); await tester.openPickerAndSelect('None'); await _expectMeteringStateAndMeasure( tester, @@ -152,8 +151,7 @@ void testE2E(String description) { /// Delete profile await tester.openSettings(); await tester.tapDescendantTextOf(S.current.equipmentProfiles); - await tester.tap(find.byIcon(Icons.edit_outlined).first); - await tester.pumpAndSettle(); + await tester.editEquipmentProfile(mockEquipmentProfiles[0].name); await tester.deleteEdits(); expect(find.text(mockEquipmentProfiles[0].name), findsNothing); expect(find.text(mockEquipmentProfiles[1].name), findsOneWidget); diff --git a/integration_test/guard_pro_tap_test.dart b/integration_test/guard_pro_tap_test.dart index cb2a292..e180a8a 100644 --- a/integration_test/guard_pro_tap_test.dart +++ b/integration_test/guard_pro_tap_test.dart @@ -6,7 +6,7 @@ import 'package:lightmeter/data/models/ev_source_type.dart'; import 'package:lightmeter/data/models/metering_screen_layout_config.dart'; import 'package:lightmeter/data/shared_prefs_service.dart'; import 'package:lightmeter/generated/l10n.dart'; -import 'package:lightmeter/screens/equipment_profile_edit/screen_equipment_profile_edit.dart'; +import 'package:lightmeter/screens/equipment_profiles/components/equipment_profile_type_picker/widget_picker_equipment_profile_type.dart'; import 'package:lightmeter/screens/lightmeter_pro/screen_lightmeter_pro.dart'; import 'package:lightmeter/screens/logbook_photos/screen_logbook_photos.dart'; import 'package:lightmeter/screens/settings/screen_settings.dart'; @@ -49,7 +49,7 @@ void testGuardProTap(String description) { await tester.navigatorPop(true); await tester.pumpAndSettle(); expect(find.byType(LightmeterProScreen), findsNothing); - expect(find.byType(EquipmentProfileEditScreen), findsOneWidget); + expect(find.byType(EquipmentProfilesTypePicker), findsOneWidget); await tester.navigatorPop(); await tester.navigatorPop(); diff --git a/integration_test/logbook_test.dart b/integration_test/logbook_test.dart index f51ef11..546d2c7 100644 --- a/integration_test/logbook_test.dart +++ b/integration_test/logbook_test.dart @@ -104,8 +104,7 @@ void testLogbook(String description) { /// Got back and delete the equipment profile used to take the first picture await tester.navigatorPop(); await tester.tapDescendantTextOf(S.current.equipmentProfiles); - await tester.tap(find.byIcon(Icons.edit_outlined).first); - await tester.pumpAndSettle(); + await tester.editEquipmentProfile(mockEquipmentProfiles.first.name); await tester.tap(find.byIcon(Icons.delete_outlined)); await tester.pumpAndSettle(Dimens.durationML); expect(find.text(mockEquipmentProfiles[0].name), findsNothing); diff --git a/integration_test/mocks/paid_features_mock.dart b/integration_test/mocks/paid_features_mock.dart index f7a4bdb..d7fbd83 100644 --- a/integration_test/mocks/paid_features_mock.dart +++ b/integration_test/mocks/paid_features_mock.dart @@ -15,6 +15,7 @@ class _MockGeolocationService extends Mock implements GeolocationService {} class MockIAPProviders extends StatefulWidget { final TogglableMap equipmentProfiles; + final TogglableMap pinholeEquipmentProfiles; final String selectedEquipmentProfileId; final TogglableMap predefinedFilms; final TogglableMap customFilms; @@ -23,6 +24,7 @@ class MockIAPProviders extends StatefulWidget { MockIAPProviders({ TogglableMap? equipmentProfiles, + TogglableMap? pinholeEquipmentProfiles, this.selectedEquipmentProfileId = '', TogglableMap? predefinedFilms, TogglableMap? customFilms, @@ -30,6 +32,7 @@ class MockIAPProviders extends StatefulWidget { required this.child, super.key, }) : equipmentProfiles = equipmentProfiles ?? mockEquipmentProfiles.toTogglableMap(), + pinholeEquipmentProfiles = pinholeEquipmentProfiles ?? mockPinholeEquipmentProfiles.toTogglableMap(), predefinedFilms = predefinedFilms ?? mockFilms.toTogglableMap(), customFilms = customFilms ?? mockFilms.toTogglableMap(), selectedFilmId = selectedFilmId ?? const FilmStub().id; @@ -46,15 +49,20 @@ class _MockIAPProvidersState extends State { void initState() { super.initState(); registerFallbackValue(defaultEquipmentProfile); + registerFallbackValue(mockPinholeEquipmentProfiles.first); registerFallbackValue(defaultCustomPhotos.first); registerFallbackValue(ApertureValue.values.first); registerFallbackValue(ShutterSpeedValue.values.first); mockIapStorageService = _MockIapStorageService(); when(() => mockIapStorageService.init()).thenAnswer((_) async {}); - when(() => mockIapStorageService.getEquipmentProfiles()).thenAnswer((_) => Future.value(widget.equipmentProfiles)); when(() => mockIapStorageService.selectedEquipmentProfileId).thenReturn(widget.selectedEquipmentProfileId); + when(() => mockIapStorageService.getEquipmentProfiles()).thenAnswer((_) => Future.value(widget.equipmentProfiles)); + when(() => mockIapStorageService.getPinholeEquipmentProfiles()) + .thenAnswer((_) => Future.value(widget.pinholeEquipmentProfiles)); when(() => mockIapStorageService.addEquipmentProfile(any())).thenAnswer((_) async {}); + when(() => mockIapStorageService.addPinholeEquipmentProfile(any())) + .thenAnswer((_) async {}); when( () => mockIapStorageService.updateEquipmentProfile( id: any(named: 'id'), @@ -62,7 +70,14 @@ class _MockIAPProvidersState extends State { isUsed: any(named: 'isUsed'), ), ).thenAnswer((_) async {}); + when( + () => mockIapStorageService.updatePinholeEquipmentProfile( + id: any(named: 'id'), + name: any(named: 'name'), + ), + ).thenAnswer((_) async {}); when(() => mockIapStorageService.deleteEquipmentProfile(any())).thenAnswer((_) async {}); + when(() => mockIapStorageService.deletePinholeEquipmentProfile(any())).thenAnswer((_) async {}); when(() => mockIapStorageService.getPredefinedFilms()).thenAnswer((_) => Future.value(widget.predefinedFilms)); when(() => mockIapStorageService.getCustomFilms()).thenAnswer((_) => Future.value(widget.customFilms)); @@ -108,6 +123,9 @@ const defaultEquipmentProfile = EquipmentProfile( isoValues: IsoValue.values, ); +final mockProfiles = [...mockEquipmentProfiles, ...mockPinholeEquipmentProfiles] + ..sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase())); + final mockEquipmentProfiles = [ EquipmentProfile( id: '1', @@ -175,6 +193,31 @@ final mockEquipmentProfiles = [ ), ]; +final mockPinholeEquipmentProfiles = [ + PinholeEquipmentProfile( + id: '3', + name: 'Pinhole Camera f/64', + aperture: 64.0, + isoValues: [ + IsoValue.values[1], + IsoValue.values[2], + IsoValue.values[3], + ], + ndValues: const [NdValue(0)], + ), + PinholeEquipmentProfile( + id: '4', + name: 'Pinhole Camera f/128', + aperture: 128.0, + isoValues: [ + IsoValue.values[1], + IsoValue.values[2], + IsoValue.values[3], + ], + ndValues: const [NdValue(0)], + ), +]; + const mockFilms = [ _FilmMultiplying(id: '1', name: 'Mock film 1', iso: 100, reciprocityMultiplier: 2), _FilmMultiplying(id: '2', name: 'Mock film 2', iso: 400, reciprocityMultiplier: 2), diff --git a/integration_test/utils/widget_tester_actions.dart b/integration_test/utils/widget_tester_actions.dart index 477f209..dab3835 100644 --- a/integration_test/utils/widget_tester_actions.dart +++ b/integration_test/utils/widget_tester_actions.dart @@ -5,6 +5,7 @@ import 'package:lightmeter/application_wrapper.dart'; import 'package:lightmeter/environment.dart'; import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/res/dimens.dart'; +import 'package:lightmeter/screens/equipment_profiles/components/equipment_profile_type_picker/widget_picker_equipment_profile_type.dart'; import 'package:lightmeter/screens/metering/components/shared/exposure_pairs_list/widget_list_exposure_pairs.dart'; import 'package:lightmeter/screens/metering/screen_metering.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; @@ -117,3 +118,24 @@ extension WidgetTesterExposurePairsListActions on WidgetTester { ); } } + +extension WidgetTesterEquipmentProfilesActions on WidgetTester { + Future addEquipmentProfile(EquipmentProfileType type) async { + await tap(find.byIcon(Icons.add_outlined).first); + await pumpAndSettle(); + await tap( + find.text( + switch (type) { + EquipmentProfileType.regular => S.current.camera, + EquipmentProfileType.pinhole => S.current.pinholeCamera, + }, + ), + ); + await tapSelectButton(); + } + + Future editEquipmentProfile(String name) async { + await tap(find.byIcon(Icons.edit_outlined).at(mockProfiles.indexWhere((p) => p.name == name))); + await pumpAndSettle(); + } +} diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index 35464c7..dbe733d 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -34,6 +34,8 @@ "calibrationMessage": "Die Messgenauigkeit hängt von der Gerätehardware ab. Testen Sie die App und kalibrieren Sie EV-Werte für beste Ergebnisse", "calibrationMessageCameraOnly": "Die Messgenauigkeit hängt von der Kamera ab. Testen Sie die App und kalibrieren Sie EV-Werte für beste Ergebnisse", "camera": "Kamera", + "pinholeCamera": "Lochkamera", + "equipmentProfileType": "Ausrüstungsprofiltyp", "lightSensor": "Lichtsensor", "showEv100": "EV\u2081\u2080\u2080 anzeigen", "meteringScreenLayout": "Messansicht Layout", @@ -60,6 +62,7 @@ "apertureValuesFilterDescription": "Wählen Sie den Blendenbereich für Ihr Objektiv", "ndFilters": "ND Filter", "ndFiltersFilterDescription": "Wählen Sie verfügbare ND-Filter", + "shutterSpeed": "Belichtungszeit", "shutterSpeedValues": "Belichtungszeiten", "shutterSpeedValue": "Belichtungszeit", "shutterSpeedValuesFilterDescription": "Wählen Sie den Verschlusszeitenbereich für Ihre Kamera", diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 9d2773e..6641b68 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -34,6 +34,8 @@ "calibrationMessage": "Measurement accuracy depends on your device's hardware. Test the app and calibrate EV values for optimal results", "calibrationMessageCameraOnly": "Measurement accuracy depends on your device's camera. Test the app and calibrate EV values for optimal results", "camera": "Camera", + "pinholeCamera": "Pinhole Camera", + "equipmentProfileType": "Equipment profile type", "lightSensor": "Light sensor", "showEv100": "Show EV\u2081\u2080\u2080", "meteringScreenLayout": "Metering screen layout", @@ -60,6 +62,7 @@ "apertureValuesFilterDescription": "Select aperture range for your lens", "ndFilters": "ND filters", "ndFiltersFilterDescription": "Select available ND filters", + "shutterSpeed": "Shutter speed", "shutterSpeedValues": "Shutter speed values", "shutterSpeedValue": "Shutter speed value", "shutterSpeedValuesFilterDescription": "Select shutter speed range for your camera", diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index 913d0ed..2a8f1b0 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -34,6 +34,8 @@ "calibrationMessage": "La précision dépend du matériel de l'appareil. Testez l'app et calibrez les valeurs EV pour de meilleurs résultats", "calibrationMessageCameraOnly": "La précision dépend de la caméra de l'appareil. Testez l'app et calibrez les valeurs EV pour de meilleurs résultats", "camera": "Caméra", + "pinholeCamera": "Sténopé", + "equipmentProfileType": "Type de profil d'équipement", "lightSensor": "Capteur de lumière", "showEv100": "Montrer EV\u2081\u2080\u2080", "meteringScreenLayout": "Disposition de l'écran de mesure", @@ -60,6 +62,7 @@ "apertureValuesFilterDescription": "Sélectionnez la plage d'ouverture pour votre objectif", "ndFilters": "Filtres ND", "ndFiltersFilterDescription": "Sélectionnez les filtres ND disponibles", + "shutterSpeed": "Vitesse d'obturation", "shutterSpeedValues": "Valeurs de la vitesse d'obturation", "shutterSpeedValue": "Valeur de la vitesse d'obturation", "shutterSpeedValuesFilterDescription": "Sélectionnez la plage de vitesses pour votre appareil", diff --git a/lib/l10n/intl_pl.arb b/lib/l10n/intl_pl.arb index 5aba72f..a153890 100644 --- a/lib/l10n/intl_pl.arb +++ b/lib/l10n/intl_pl.arb @@ -34,6 +34,8 @@ "calibrationMessage": "Dokładność pomiaru zależy od sprzętu urządzenia. Przetestuj aplikację i skalibruj wartości EV dla optymalnych wyników", "calibrationMessageCameraOnly": "Dokładność pomiaru zależy od kamery urządzenia. Przetestuj aplikację i skalibruj wartości EV dla optymalnych wyników", "camera": "Kamera", + "pinholeCamera": "Kamera otworkowa", + "equipmentProfileType": "Typ profilu sprzętu", "lightSensor": "Czujnik światła", "showEv100": "Pokaż EV\u2081\u2080\u2080", "meteringScreenLayout": "Układ ekranu pomiaru", @@ -60,6 +62,7 @@ "apertureValuesFilterDescription": "Wybierz zakres przysłony dla swojego obiektywu", "ndFilters": "Filtry ND", "ndFiltersFilterDescription": "Wybierz dostępne filtry ND", + "shutterSpeed": "Czas naświetlania", "shutterSpeedValues": "Czasy naświetlania", "shutterSpeedValue": "Czas naświetlania", "shutterSpeedValuesFilterDescription": "Wybierz zakres czasów naświetlania dla swojej kamery", diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index 1597201..53e9045 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -34,6 +34,8 @@ "calibrationMessage": "Точность измерений данного приложения полностью зависит от точности камеры и датчика освещенности вашего устройства. Поэтому рекомендуется самостоятельно подобрать калибровочные значения, которые дадут желаемый результат измерений.", "calibrationMessageCameraOnly": "Точность измерений данного приложения полностью зависит от точности камеры вашего устройства. Поэтому рекомендуется самостоятельно подобрать калибровочное значение, которое даст желаемый результат измерений.", "camera": "Камера", + "pinholeCamera": "Пинхол-камера", + "equipmentProfileType": "Тип профиля оборудования", "lightSensor": "Датчик освещённости", "showEv100": "Показывать EV\u2081\u2080\u2080", "meteringScreenLayout": "Элементы главного экрана", @@ -60,6 +62,7 @@ "apertureValuesFilterDescription": "Выберите диапазон диафрагмы для вашего объектива", "ndFilters": "ND фильтры", "ndFiltersFilterDescription": "Выберите доступные ND фильтры", + "shutterSpeed": "Выдержка", "shutterSpeedValues": "Значения выдержки", "shutterSpeedValue": "Значение выдержки", "shutterSpeedValuesFilterDescription": "Выберите диапазон выдержек для вашей камеры", diff --git a/lib/l10n/intl_zh.arb b/lib/l10n/intl_zh.arb index 9c069ee..668b364 100644 --- a/lib/l10n/intl_zh.arb +++ b/lib/l10n/intl_zh.arb @@ -34,6 +34,8 @@ "calibrationMessage": "测量精度取决于设备硬件。请测试并校准 EV 值以获得最佳结果。", "calibrationMessageCameraOnly": "测量精度取决于设备摄像头。请测试并校准 EV 值以获得最佳结果。", "camera": "相机", + "pinholeCamera": "针孔相机", + "equipmentProfileType": "设备配置类型", "lightSensor": "光线传感器", "showEv100": "显示 EV\u2081\u2080\u2080", "meteringScreenLayout": "布局", @@ -60,6 +62,7 @@ "apertureValuesFilterDescription": "选择镜头光圈范围", "ndFilters": "ND 滤镜", "ndFiltersFilterDescription": "选择可用的 ND 滤镜", + "shutterSpeed": "快门速度", "shutterSpeedValues": "快门速度", "shutterSpeedValue": "快门速度", "shutterSpeedValuesFilterDescription": "选择相机快门范围", diff --git a/lib/providers/equipment_profile_provider.dart b/lib/providers/equipment_profile_provider.dart index f0b51e4..d1c4a2b 100644 --- a/lib/providers/equipment_profile_provider.dart +++ b/lib/providers/equipment_profile_provider.dart @@ -34,11 +34,10 @@ class EquipmentProfilesProvider extends StatefulWidget { } class EquipmentProfilesProviderState extends State { - final TogglableMap _customProfiles = {}; + final TogglableMap _profiles = {}; String _selectedId = ''; - EquipmentProfile get _selectedProfile => - _customProfiles[_selectedId]?.value ?? EquipmentProfilesProvider.defaultProfile; + IEquipmentProfile get _selectedProfile => _profiles[_selectedId]?.value ?? EquipmentProfilesProvider.defaultProfile; @override void initState() { @@ -49,7 +48,7 @@ class EquipmentProfilesProviderState extends State { @override Widget build(BuildContext context) { return EquipmentProfiles( - profiles: context.isPro ? _customProfiles : {}, + profiles: context.isPro ? _profiles : {}, selected: context.isPro ? _selectedProfile : EquipmentProfilesProvider.defaultProfile, child: widget.child, ); @@ -57,61 +56,91 @@ class EquipmentProfilesProviderState extends State { Future _init() async { _selectedId = widget.storageService.selectedEquipmentProfileId; - _customProfiles.addAll(await widget.storageService.getEquipmentProfiles()); + _profiles + ..addAll(await widget.storageService.getEquipmentProfiles()) + ..addAll(await widget.storageService.getPinholeEquipmentProfiles()); + _sortProfiles(); _discardSelectedIfNotIncluded(); if (mounted) setState(() {}); widget.onInitialized?.call(); } - Future addProfile(EquipmentProfile profile) async { - await widget.storageService.addEquipmentProfile(profile); - _customProfiles[profile.id] = (value: profile, isUsed: true); + Future addProfile(IEquipmentProfile profile) async { + switch (profile) { + case final PinholeEquipmentProfile profile: + await widget.storageService.addPinholeEquipmentProfile(profile); + case final EquipmentProfile profile: + await widget.storageService.addEquipmentProfile(profile); + } + _profiles[profile.id] = (value: profile, isUsed: true); + _sortProfiles(); setState(() {}); } - Future updateProfile(EquipmentProfile profile) async { - final oldProfile = _customProfiles[profile.id]!.value; - await widget.storageService.updateEquipmentProfile( - id: profile.id, - name: oldProfile.name.changedOrNull(profile.name), - apertureValues: oldProfile.apertureValues.changedOrNull(profile.apertureValues), - shutterSpeedValues: oldProfile.shutterSpeedValues.changedOrNull(profile.shutterSpeedValues), - isoValues: oldProfile.isoValues.changedOrNull(profile.isoValues), - ndValues: oldProfile.ndValues.changedOrNull(profile.ndValues), - lensZoom: oldProfile.lensZoom.changedOrNull(profile.lensZoom), - exposureOffset: oldProfile.exposureOffset.changedOrNull(profile.exposureOffset), - ); - _customProfiles[profile.id] = (value: profile, isUsed: _customProfiles[profile.id]!.isUsed); + Future updateProfile(IEquipmentProfile profile) async { + switch (profile) { + case final PinholeEquipmentProfile profile: + final oldProfile = _profiles[profile.id]!.value as PinholeEquipmentProfile; + await widget.storageService.updatePinholeEquipmentProfile( + id: profile.id, + name: profile.name, + aperture: oldProfile.aperture.changedOrNull(profile.aperture), + isoValues: oldProfile.isoValues.changedOrNull(profile.isoValues), + ndValues: oldProfile.ndValues.changedOrNull(profile.ndValues), + lensZoom: oldProfile.lensZoom.changedOrNull(profile.lensZoom), + exposureOffset: oldProfile.exposureOffset.changedOrNull(profile.exposureOffset), + ); + case final EquipmentProfile profile: + final oldProfile = _profiles[profile.id]!.value as EquipmentProfile; + await widget.storageService.updateEquipmentProfile( + id: profile.id, + name: oldProfile.name.changedOrNull(profile.name), + apertureValues: oldProfile.apertureValues.changedOrNull(profile.apertureValues), + shutterSpeedValues: oldProfile.shutterSpeedValues.changedOrNull(profile.shutterSpeedValues), + isoValues: oldProfile.isoValues.changedOrNull(profile.isoValues), + ndValues: oldProfile.ndValues.changedOrNull(profile.ndValues), + lensZoom: oldProfile.lensZoom.changedOrNull(profile.lensZoom), + exposureOffset: oldProfile.exposureOffset.changedOrNull(profile.exposureOffset), + ); + } + final bool shouldSort = _profiles[profile.id]!.value.name != profile.name; + _profiles[profile.id] = (value: profile, isUsed: _profiles[profile.id]!.isUsed); + if (shouldSort) _sortProfiles(); setState(() {}); } - Future deleteProfile(EquipmentProfile profile) async { - await widget.storageService.deleteEquipmentProfile(profile.id); + Future deleteProfile(IEquipmentProfile profile) async { if (profile.id == _selectedId) { _selectedId = EquipmentProfilesProvider.defaultProfile.id; widget.storageService.selectedEquipmentProfileId = EquipmentProfilesProvider.defaultProfile.id; } - _customProfiles.remove(profile.id); + switch (profile) { + case final PinholeEquipmentProfile profile: + await widget.storageService.deletePinholeEquipmentProfile(profile.id); + case final EquipmentProfile profile: + await widget.storageService.deleteEquipmentProfile(profile.id); + } + _profiles.remove(profile.id); _discardSelectedIfNotIncluded(); setState(() {}); } - void selectProfile(EquipmentProfile data) { - if (_selectedId != data.id) { + void selectProfile(String id) { + if (_selectedId != id) { setState(() { - _selectedId = data.id; + _selectedId = id; }); widget.storageService.selectedEquipmentProfileId = _selectedProfile.id; } } - Future toggleProfile(EquipmentProfile profile, bool enabled) async { - if (_customProfiles.containsKey(profile.id)) { - _customProfiles[profile.id] = (value: profile, isUsed: enabled); + Future toggleProfile(String id, bool enabled) async { + if (_profiles.containsKey(id)) { + _profiles[id] = (value: _profiles[id]!.value, isUsed: enabled); + await widget.storageService.updateEquipmentProfile(id: id, isUsed: enabled); } else { return; } - await widget.storageService.updateEquipmentProfile(id: profile.id, isUsed: enabled); _discardSelectedIfNotIncluded(); setState(() {}); } @@ -120,12 +149,19 @@ class EquipmentProfilesProviderState extends State { if (_selectedId == EquipmentProfilesProvider.defaultProfile.id) { return; } - final isSelectedUsed = _customProfiles[_selectedId]?.isUsed ?? false; + final isSelectedUsed = _profiles[_selectedId]?.isUsed ?? false; if (!isSelectedUsed) { _selectedId = EquipmentProfilesProvider.defaultProfile.id; widget.storageService.selectedEquipmentProfileId = _selectedId; } } + + void _sortProfiles() { + final sortedByName = _profiles.values.toList(growable: false) + ..sort((a, b) => a.value.name.toLowerCase().compareTo(b.value.name.toLowerCase())); + _profiles.clear(); + _profiles.addEntries(sortedByName.map((e) => MapEntry(e.value.id, e))); + } } enum _EquipmentProfilesModelAspect { @@ -135,8 +171,8 @@ enum _EquipmentProfilesModelAspect { } class EquipmentProfiles extends InheritedModel<_EquipmentProfilesModelAspect> { - final TogglableMap profiles; - final EquipmentProfile selected; + final TogglableMap profiles; + final IEquipmentProfile selected; const EquipmentProfiles({ required this.profiles, @@ -145,10 +181,10 @@ class EquipmentProfiles extends InheritedModel<_EquipmentProfilesModelAspect> { }); /// _default + profiles create by the user - static List of(BuildContext context) { + static List of(BuildContext context) { final model = InheritedModel.inheritFrom(context, aspect: _EquipmentProfilesModelAspect.profiles)!; - return List.from( + return List.from( [ EquipmentProfilesProvider.defaultProfile, ...model.profiles.values.map((p) => p.value), @@ -156,10 +192,10 @@ class EquipmentProfiles extends InheritedModel<_EquipmentProfilesModelAspect> { ); } - static List inUseOf(BuildContext context) { + static List inUseOf(BuildContext context) { final model = InheritedModel.inheritFrom(context, aspect: _EquipmentProfilesModelAspect.profilesInUse)!; - return List.from( + return List.from( [ EquipmentProfilesProvider.defaultProfile, ...model.profiles.values.where((p) => p.isUsed).map((p) => p.value), @@ -167,7 +203,7 @@ class EquipmentProfiles extends InheritedModel<_EquipmentProfilesModelAspect> { ); } - static EquipmentProfile selectedOf(BuildContext context) { + static IEquipmentProfile selectedOf(BuildContext context) { return InheritedModel.inheritFrom(context, aspect: _EquipmentProfilesModelAspect.selected)! .selected; } diff --git a/lib/screens/equipment_profile_edit/bloc_equipment_profile_edit.dart b/lib/screens/equipment_profile_edit/bloc_equipment_profile_edit.dart index 6f60f81..6c1aebb 100644 --- a/lib/screens/equipment_profile_edit/bloc_equipment_profile_edit.dart +++ b/lib/screens/equipment_profile_edit/bloc_equipment_profile_edit.dart @@ -1,3 +1,4 @@ +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:lightmeter/providers/equipment_profile_provider.dart'; import 'package:lightmeter/screens/equipment_profile_edit/event_equipment_profile_edit.dart'; @@ -5,189 +6,237 @@ import 'package:lightmeter/screens/equipment_profile_edit/state_equipment_profil import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; import 'package:uuid/uuid.dart'; -class EquipmentProfileEditBloc extends Bloc { +sealed class IEquipmentProfileEditBloc + extends Bloc, EquipmentProfileEditState> { + @protected final EquipmentProfilesProviderState profilesProvider; - final EquipmentProfile _originalEquipmentProfile; - EquipmentProfile _newEquipmentProfile; - final bool _isEdit; + @protected + final T originalEquipmentProfile; + @protected + final bool isEdit; - factory EquipmentProfileEditBloc( - EquipmentProfilesProviderState profilesProvider, { - required EquipmentProfile? profile, - required bool isEdit, - }) => - profile != null - ? EquipmentProfileEditBloc._( - profilesProvider, - profile, - isEdit, - ) - : EquipmentProfileEditBloc._( - profilesProvider, - EquipmentProfilesProvider.defaultProfile, - isEdit, - ); - - EquipmentProfileEditBloc._( - this.profilesProvider, - EquipmentProfile profile, - this._isEdit, - ) : _originalEquipmentProfile = profile, - _newEquipmentProfile = profile, + IEquipmentProfileEditBloc( + this.profilesProvider, { + required T profile, + required this.isEdit, + }) : originalEquipmentProfile = profile, super( - EquipmentProfileEditState( - name: profile.name, - apertureValues: profile.apertureValues, - shutterSpeedValues: profile.shutterSpeedValues, - isoValues: profile.isoValues, - ndValues: profile.ndValues, - lensZoom: profile.lensZoom, - exposureOffset: profile.exposureOffset, - canSave: false, + EquipmentProfileEditState( + profile: profile, + hasChanges: false, + isValid: true, ), ) { - on( - (event, emit) async { - switch (event) { - case final EquipmentProfileNameChangedEvent e: - await _onNameChanged(e, emit); - case final EquipmentProfileApertureValuesChangedEvent e: - await _onApertureValuesChanged(e, emit); - case final EquipmentProfileShutterSpeedValuesChangedEvent e: - await _onShutterSpeedValuesChanged(e, emit); - case final EquipmentProfileIsoValuesChangedEvent e: - await _onIsoValuesChanged(e, emit); - case final EquipmentProfileNdValuesChangedEvent e: - await _onNdValuesChanged(e, emit); - case final EquipmentProfileLensZoomChangedEvent e: - await _onLensZoomChanged(e, emit); - case final EquipmentProfileExposureOffsetChangedEvent e: - await _onExposureOffsetChanged(e, emit); - case EquipmentProfileSaveEvent(): - await _onSave(event, emit); - case EquipmentProfileCopyEvent(): - await _onCopy(event, emit); - case EquipmentProfileDeleteEvent(): - await _onDelete(event, emit); - } - }, + on>(mapEventToState); + } + + @protected + @mustCallSuper + Future mapEventToState(IEquipmentProfileEditEvent event, Emitter emit) async { + switch (event) { + case EquipmentProfileSaveEvent(): + await _onSave(event, emit); + case EquipmentProfileCopyEvent(): + await _onCopy(event, emit); + case EquipmentProfileDeleteEvent(): + await _onDelete(event, emit); + default: + } + } + + @protected + Future createProfile(String id); + + @protected + void emitProfile(T profile, Emitter emit) { + emit( + state.copyWith( + profile: profile, + hasChanges: _hasChanges(profile), + isValid: _isValid(profile), + ), + ); + } + + Future _onSave(EquipmentProfileSaveEvent _, Emitter emit) async { + emit(state.copyWith(isLoading: true)); + if (isEdit) { + final newProfile = await createProfile(originalEquipmentProfile.id); + assert( + newProfile.id == originalEquipmentProfile.id, + 'The edited profile id must be the same as the original profile id', + ); + await profilesProvider.updateProfile(newProfile); + } else { + final newId = const Uuid().v1(); + final newProfile = await createProfile(newId); + assert( + newProfile.id == newId, + 'A profile to be added must have a unique id.', + ); + await profilesProvider.addProfile(newProfile); + } + emit(state.copyWith(isLoading: false)); + } + + Future _onCopy(EquipmentProfileCopyEvent _, Emitter emit) async { + emit(state.copyWith(isLoading: true)); + emit(state.copyWith(isLoading: false, profileToCopy: state.profile)); + } + + Future _onDelete(EquipmentProfileDeleteEvent _, Emitter emit) async { + emit(state.copyWith(isLoading: true)); + await profilesProvider.deleteProfile(originalEquipmentProfile); + emit(state.copyWith(isLoading: false)); + } + + bool _hasChanges(T profile) => profile != originalEquipmentProfile; + + bool _isValid(T profile) => profile.name.isNotEmpty; +} + +class EquipmentProfileEditBloc extends IEquipmentProfileEditBloc { + EquipmentProfileEditBloc( + super.profilesProvider, { + required super.profile, + required super.isEdit, + }); + + @override + Future mapEventToState(IEquipmentProfileEditEvent event, Emitter emit) async { + switch (event) { + case final EquipmentProfileNameChangedEvent e: + await _onNameChanged(e, emit); + case final EquipmentProfileApertureValuesChangedEvent e: + await _onApertureValuesChanged(e, emit); + case final EquipmentProfileShutterSpeedValuesChangedEvent e: + await _onShutterSpeedValuesChanged(e, emit); + case final EquipmentProfileIsoValuesChangedEvent e: + await _onIsoValuesChanged(e, emit); + case final EquipmentProfileNdValuesChangedEvent e: + await _onNdValuesChanged(e, emit); + case final EquipmentProfileLensZoomChangedEvent e: + await _onLensZoomChanged(e, emit); + case final EquipmentProfileExposureOffsetChangedEvent e: + await _onExposureOffsetChanged(e, emit); + default: + return super.mapEventToState(event, emit); + } + } + + @override + Future createProfile(String id) async { + return EquipmentProfile( + id: id, + name: state.profile.name, + apertureValues: state.profile.apertureValues, + shutterSpeedValues: state.profile.shutterSpeedValues, + isoValues: state.profile.isoValues, + ndValues: state.profile.ndValues, + lensZoom: state.profile.lensZoom, + exposureOffset: state.profile.exposureOffset, ); } Future _onNameChanged(EquipmentProfileNameChangedEvent event, Emitter emit) async { - _newEquipmentProfile = _newEquipmentProfile.copyWith(name: event.name); - emit( - state.copyWith( - name: event.name, - canSave: _canSave(event.name, state.lensZoom), - ), - ); + emitProfile(state.profile.copyWith(name: event.name), emit); } Future _onApertureValuesChanged(EquipmentProfileApertureValuesChangedEvent event, Emitter emit) async { - _newEquipmentProfile = _newEquipmentProfile.copyWith(apertureValues: event.apertureValues); - emit( - state.copyWith( - apertureValues: event.apertureValues, - canSave: _canSave(state.name, state.lensZoom), - ), - ); + emitProfile(state.profile.copyWith(apertureValues: event.apertureValues), emit); } Future _onShutterSpeedValuesChanged(EquipmentProfileShutterSpeedValuesChangedEvent event, Emitter emit) async { - _newEquipmentProfile = _newEquipmentProfile.copyWith(shutterSpeedValues: event.shutterSpeedValues); + emitProfile(state.profile.copyWith(shutterSpeedValues: event.shutterSpeedValues), emit); + } + + Future _onIsoValuesChanged(EquipmentProfileIsoValuesChangedEvent event, Emitter emit) async { + emitProfile(state.profile.copyWith(isoValues: event.isoValues), emit); + } + + Future _onNdValuesChanged(EquipmentProfileNdValuesChangedEvent event, Emitter emit) async { + emitProfile(state.profile.copyWith(ndValues: event.ndValues), emit); + } + + Future _onLensZoomChanged(EquipmentProfileLensZoomChangedEvent event, Emitter emit) async { + emitProfile(state.profile.copyWith(lensZoom: event.lensZoom), emit); + } + + Future _onExposureOffsetChanged(EquipmentProfileExposureOffsetChangedEvent event, Emitter emit) async { + emitProfile(state.profile.copyWith(exposureOffset: event.exposureOffset), emit); + } +} + +class PinholeEquipmentProfileEditBloc extends IEquipmentProfileEditBloc { + PinholeEquipmentProfileEditBloc( + super.profilesProvider, { + required super.profile, + required super.isEdit, + }); + + @override + Future mapEventToState(IEquipmentProfileEditEvent event, Emitter emit) async { + switch (event) { + case final EquipmentProfileNameChangedEvent e: + await _onNameChanged(e, emit); + case final EquipmentProfileApertureValueChangedEvent e: + await _onApertureValuesChanged(e, emit); + case final EquipmentProfileIsoValuesChangedEvent e: + await _onIsoValuesChanged(e, emit); + case final EquipmentProfileNdValuesChangedEvent e: + await _onNdValuesChanged(e, emit); + case final EquipmentProfileLensZoomChangedEvent e: + await _onLensZoomChanged(e, emit); + case final EquipmentProfileExposureOffsetChangedEvent e: + await _onExposureOffsetChanged(e, emit); + default: + return super.mapEventToState(event, emit); + } + } + + @override + Future createProfile(String id) async { + return PinholeEquipmentProfile( + id: id, + name: state.profile.name, + aperture: state.profile.aperture, + isoValues: state.profile.isoValues, + ndValues: state.profile.ndValues, + lensZoom: state.profile.lensZoom, + exposureOffset: state.profile.exposureOffset, + ); + } + + Future _onNameChanged(EquipmentProfileNameChangedEvent event, Emitter emit) async { + emitProfile(state.profile.copyWith(name: event.name), emit); + } + + Future _onApertureValuesChanged(EquipmentProfileApertureValueChangedEvent event, Emitter emit) async { + emitProfile(state.profile.copyWith(aperture: event.aperture ?? 0), emit); + + final profile = state.profile.copyWith(aperture: event.aperture); emit( state.copyWith( - shutterSpeedValues: event.shutterSpeedValues, - canSave: _canSave(state.name, state.lensZoom), + profile: profile, + hasChanges: _hasChanges(profile), + isValid: _isValid(profile) && event.aperture != null, ), ); } Future _onIsoValuesChanged(EquipmentProfileIsoValuesChangedEvent event, Emitter emit) async { - _newEquipmentProfile = _newEquipmentProfile.copyWith(isoValues: event.isoValues); - emit( - state.copyWith( - isoValues: event.isoValues, - canSave: _canSave(state.name, state.lensZoom), - ), - ); + emitProfile(state.profile.copyWith(isoValues: event.isoValues), emit); } Future _onNdValuesChanged(EquipmentProfileNdValuesChangedEvent event, Emitter emit) async { - _newEquipmentProfile = _newEquipmentProfile.copyWith(ndValues: event.ndValues); - emit( - state.copyWith( - ndValues: event.ndValues, - canSave: _canSave(state.name, state.lensZoom), - ), - ); + emitProfile(state.profile.copyWith(ndValues: event.ndValues), emit); } Future _onLensZoomChanged(EquipmentProfileLensZoomChangedEvent event, Emitter emit) async { - _newEquipmentProfile = _newEquipmentProfile.copyWith(lensZoom: event.lensZoom); - emit( - state.copyWith( - lensZoom: event.lensZoom, - canSave: _canSave(state.name, event.lensZoom), - ), - ); + emitProfile(state.profile.copyWith(lensZoom: event.lensZoom), emit); } Future _onExposureOffsetChanged(EquipmentProfileExposureOffsetChangedEvent event, Emitter emit) async { - _newEquipmentProfile = _newEquipmentProfile.copyWith(exposureOffset: event.exposureOffset); - emit( - state.copyWith( - exposureOffset: event.exposureOffset, - canSave: _canSave(state.name, event.exposureOffset), - ), - ); - } - - Future _onSave(EquipmentProfileSaveEvent _, Emitter emit) async { - emit(state.copyWith(isLoading: true)); - if (_isEdit) { - await profilesProvider.updateProfile( - EquipmentProfile( - id: _originalEquipmentProfile.id, - name: state.name, - apertureValues: state.apertureValues, - ndValues: state.ndValues, - shutterSpeedValues: state.shutterSpeedValues, - isoValues: state.isoValues, - lensZoom: state.lensZoom, - exposureOffset: state.exposureOffset, - ), - ); - } else { - await profilesProvider.addProfile( - EquipmentProfile( - id: const Uuid().v1(), - name: state.name, - apertureValues: state.apertureValues, - ndValues: state.ndValues, - shutterSpeedValues: state.shutterSpeedValues, - isoValues: state.isoValues, - lensZoom: state.lensZoom, - exposureOffset: state.exposureOffset, - ), - ); - } - emit(state.copyWith(isLoading: false)); - } - - Future _onCopy(EquipmentProfileCopyEvent _, Emitter emit) async { - emit(state.copyWith(isLoading: true)); - emit(state.copyWith(isLoading: false, profileToCopy: _originalEquipmentProfile)); - } - - Future _onDelete(EquipmentProfileDeleteEvent _, Emitter emit) async { - emit(state.copyWith(isLoading: true)); - await profilesProvider.deleteProfile(_newEquipmentProfile); - emit(state.copyWith(isLoading: false)); - } - - bool _canSave(String name, double? lensZoom) { - return name.isNotEmpty && _newEquipmentProfile != _originalEquipmentProfile; + emitProfile(state.profile.copyWith(exposureOffset: event.exposureOffset), emit); } } diff --git a/lib/screens/equipment_profile_edit/components/aperture_input/widget_input_aperture_equipment_profile.dart b/lib/screens/equipment_profile_edit/components/aperture_input/widget_input_aperture_equipment_profile.dart new file mode 100644 index 0000000..17234c4 --- /dev/null +++ b/lib/screens/equipment_profile_edit/components/aperture_input/widget_input_aperture_equipment_profile.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:lightmeter/generated/l10n.dart'; +import 'package:lightmeter/res/dimens.dart'; +import 'package:lightmeter/screens/shared/text_field/widget_text_field.dart'; + +class EquipmentProfileApertureInput extends StatefulWidget { + final double? value; + final ValueChanged onChanged; + + const EquipmentProfileApertureInput({ + super.key, + required this.value, + required this.onChanged, + }); + + @override + State createState() => _EquipmentProfileApertureInputState(); +} + +class _EquipmentProfileApertureInputState extends State { + TextStyle get style => + Theme.of(context).textTheme.bodyMedium!.copyWith(color: Theme.of(context).listTileTheme.textColor); + + @override + Widget build(BuildContext context) { + return ListTile( + leading: const Icon(Icons.camera), + title: Text( + S.of(context).apertureValue, + style: Theme.of(context).listTileTheme.titleTextStyle, + ), + trailing: SizedBox( + width: _textInputWidth(context), + child: LightmeterTextField( + initialValue: widget.value?.toString() ?? '', + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp("[0-9.]")), + LengthLimitingTextInputFormatter(6), + ], + onChanged: (value) { + final parsed = double.tryParse(value); + widget.onChanged(parsed != null && parsed > 0 ? parsed : null); + }, + validator: (value) { + final parsed = double.tryParse(value); + if (parsed != null && parsed > 0) { + return null; + } else { + return ''; + } + }, + textAlign: TextAlign.end, + style: style, + ), + ), + ); + } + + double _textInputWidth(BuildContext context) { + final textPainter = TextPainter( + text: TextSpan(text: widget.value.toString(), style: style), + maxLines: 1, + textDirection: TextDirection.ltr, + )..layout(); + return textPainter.maxIntrinsicWidth + Dimens.grid4; + } +} diff --git a/lib/screens/equipment_profile_edit/event_equipment_profile_edit.dart b/lib/screens/equipment_profile_edit/event_equipment_profile_edit.dart index 8ad8309..56fa719 100644 --- a/lib/screens/equipment_profile_edit/event_equipment_profile_edit.dart +++ b/lib/screens/equipment_profile_edit/event_equipment_profile_edit.dart @@ -1,59 +1,65 @@ import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; -sealed class EquipmentProfileEditEvent { - const EquipmentProfileEditEvent(); +sealed class IEquipmentProfileEditEvent { + const IEquipmentProfileEditEvent(); } -class EquipmentProfileNameChangedEvent extends EquipmentProfileEditEvent { +class EquipmentProfileNameChangedEvent extends IEquipmentProfileEditEvent { final String name; const EquipmentProfileNameChangedEvent(this.name); } -class EquipmentProfileIsoValuesChangedEvent extends EquipmentProfileEditEvent { +class EquipmentProfileIsoValuesChangedEvent extends IEquipmentProfileEditEvent { final List isoValues; const EquipmentProfileIsoValuesChangedEvent(this.isoValues); } -class EquipmentProfileNdValuesChangedEvent extends EquipmentProfileEditEvent { +class EquipmentProfileNdValuesChangedEvent extends IEquipmentProfileEditEvent { final List ndValues; const EquipmentProfileNdValuesChangedEvent(this.ndValues); } -class EquipmentProfileApertureValuesChangedEvent extends EquipmentProfileEditEvent { +class EquipmentProfileApertureValuesChangedEvent extends IEquipmentProfileEditEvent { final List apertureValues; const EquipmentProfileApertureValuesChangedEvent(this.apertureValues); } -class EquipmentProfileShutterSpeedValuesChangedEvent extends EquipmentProfileEditEvent { +class EquipmentProfileApertureValueChangedEvent extends IEquipmentProfileEditEvent { + final double? aperture; + + const EquipmentProfileApertureValueChangedEvent(this.aperture); +} + +class EquipmentProfileShutterSpeedValuesChangedEvent extends IEquipmentProfileEditEvent { final List shutterSpeedValues; const EquipmentProfileShutterSpeedValuesChangedEvent(this.shutterSpeedValues); } -class EquipmentProfileLensZoomChangedEvent extends EquipmentProfileEditEvent { +class EquipmentProfileLensZoomChangedEvent extends IEquipmentProfileEditEvent { final double lensZoom; const EquipmentProfileLensZoomChangedEvent(this.lensZoom); } -class EquipmentProfileExposureOffsetChangedEvent extends EquipmentProfileEditEvent { +class EquipmentProfileExposureOffsetChangedEvent extends IEquipmentProfileEditEvent { final double exposureOffset; const EquipmentProfileExposureOffsetChangedEvent(this.exposureOffset); } -class EquipmentProfileSaveEvent extends EquipmentProfileEditEvent { +class EquipmentProfileSaveEvent extends IEquipmentProfileEditEvent { const EquipmentProfileSaveEvent(); } -class EquipmentProfileCopyEvent extends EquipmentProfileEditEvent { +class EquipmentProfileCopyEvent extends IEquipmentProfileEditEvent { const EquipmentProfileCopyEvent(); } -class EquipmentProfileDeleteEvent extends EquipmentProfileEditEvent { +class EquipmentProfileDeleteEvent extends IEquipmentProfileEditEvent { const EquipmentProfileDeleteEvent(); } diff --git a/lib/screens/equipment_profile_edit/flow_equipment_profile_edit.dart b/lib/screens/equipment_profile_edit/flow_equipment_profile_edit.dart index 94db81a..be01402 100644 --- a/lib/screens/equipment_profile_edit/flow_equipment_profile_edit.dart +++ b/lib/screens/equipment_profile_edit/flow_equipment_profile_edit.dart @@ -7,11 +7,14 @@ import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; enum EquipmentProfileEditType { add, edit } -class EquipmentProfileEditArgs { +class EquipmentProfileEditArgs { final EquipmentProfileEditType editType; - final EquipmentProfile? profile; + final T profile; - const EquipmentProfileEditArgs({required this.editType, this.profile}); + const EquipmentProfileEditArgs({ + required this.editType, + required this.profile, + }); } class EquipmentProfileEditFlow extends StatelessWidget { @@ -25,13 +28,50 @@ class EquipmentProfileEditFlow extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocProvider( - create: (_) => EquipmentProfileEditBloc( - EquipmentProfilesProvider.of(context), - profile: args.profile, - isEdit: _isEdit, + switch (args.profile) { + case final EquipmentProfile profile: + return _IEquipmentProfileBlocProvider( + create: (_) => EquipmentProfileEditBloc( + EquipmentProfilesProvider.of(context), + profile: profile, + isEdit: _isEdit, + ), + isEdit: _isEdit, + ); + case final PinholeEquipmentProfile profile: + return _IEquipmentProfileBlocProvider( + create: (_) => PinholeEquipmentProfileEditBloc( + EquipmentProfilesProvider.of(context), + profile: profile, + isEdit: _isEdit, + ), + isEdit: _isEdit, + ); + } + } +} + +class _IEquipmentProfileBlocProvider> + extends StatelessWidget { + final V Function(BuildContext context) create; + final bool isEdit; + + const _IEquipmentProfileBlocProvider({ + required this.create, + required this.isEdit, + super.key, + }); + + @override + Widget build(BuildContext context) { + return BlocProvider>( + create: create, + child: Builder( + builder: (context) => BlocProvider.value( + value: context.read>() as V, + child: EquipmentProfileEditScreen(isEdit: isEdit), + ), ), - child: EquipmentProfileEditScreen(isEdit: _isEdit), ); } } diff --git a/lib/screens/equipment_profile_edit/screen_equipment_profile_edit.dart b/lib/screens/equipment_profile_edit/screen_equipment_profile_edit.dart index f64a7e4..40fd380 100644 --- a/lib/screens/equipment_profile_edit/screen_equipment_profile_edit.dart +++ b/lib/screens/equipment_profile_edit/screen_equipment_profile_edit.dart @@ -4,6 +4,7 @@ import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/navigation/routes.dart'; import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/screens/equipment_profile_edit/bloc_equipment_profile_edit.dart'; +import 'package:lightmeter/screens/equipment_profile_edit/components/aperture_input/widget_input_aperture_equipment_profile.dart'; import 'package:lightmeter/screens/equipment_profile_edit/components/filter_list_tile/widget_list_tile_filter.dart'; import 'package:lightmeter/screens/equipment_profile_edit/components/range_picker_list_tile/widget_list_tile_range_picker.dart'; import 'package:lightmeter/screens/equipment_profile_edit/components/slider_picker_list_tile/widget_list_tile_slider_picker.dart'; @@ -17,7 +18,7 @@ import 'package:lightmeter/utils/double_to_zoom.dart'; import 'package:lightmeter/utils/to_string_signed.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; -class EquipmentProfileEditScreen extends StatefulWidget { +class EquipmentProfileEditScreen extends StatefulWidget { final bool isEdit; const EquipmentProfileEditScreen({ @@ -26,13 +27,13 @@ class EquipmentProfileEditScreen extends StatefulWidget { }); @override - State createState() => _EquipmentProfileEditScreenState(); + State> createState() => _EquipmentProfileEditScreenState(); } -class _EquipmentProfileEditScreenState extends State { +class _EquipmentProfileEditScreenState extends State> { @override Widget build(BuildContext context) { - return BlocConsumer( + return BlocConsumer, EquipmentProfileEditState>( listenWhen: (previous, current) => previous.isLoading != current.isLoading, listener: (context, state) { if (state.isLoading) { @@ -41,9 +42,9 @@ class _EquipmentProfileEditScreenState extends State if (state.profileToCopy != null) { Navigator.of(context).pushReplacementNamed( NavigationRoutes.equipmentProfileEditScreen.name, - arguments: EquipmentProfileEditArgs( + arguments: EquipmentProfileEditArgs( editType: EquipmentProfileEditType.add, - profile: state.profileToCopy, + profile: state.profileToCopy!, ), ); } else { @@ -57,33 +58,34 @@ class _EquipmentProfileEditScreenState extends State child: SliverScreen( title: Text(widget.isEdit ? S.of(context).editEquipmentProfileTitle : S.of(context).addEquipmentProfileTitle), appBarActions: [ - BlocBuilder( - buildWhen: (previous, current) => previous.canSave != current.canSave, + BlocBuilder, EquipmentProfileEditState>( + buildWhen: (previous, current) => + previous.hasChanges != current.hasChanges || previous.isValid != current.isValid, builder: (context, state) => IconButton( - onPressed: state.canSave + onPressed: state.hasChanges && state.isValid ? () { - context.read().add(const EquipmentProfileSaveEvent()); + context.read>().add(const EquipmentProfileSaveEvent()); } : null, icon: const Icon(Icons.save_outlined), ), ), if (widget.isEdit) - BlocBuilder( - buildWhen: (previous, current) => previous.canSave != current.canSave, + BlocBuilder, EquipmentProfileEditState>( + buildWhen: (previous, current) => previous.isValid != current.isValid, builder: (context, state) => IconButton( - onPressed: state.canSave - ? null - : () { - context.read().add(const EquipmentProfileCopyEvent()); - }, + onPressed: state.isValid + ? () { + context.read>().add(const EquipmentProfileCopyEvent()); + } + : null, icon: const Icon(Icons.copy_outlined), ), ), if (widget.isEdit) IconButton( onPressed: () { - context.read().add(const EquipmentProfileDeleteEvent()); + context.read>().add(const EquipmentProfileDeleteEvent()); }, icon: const Icon(Icons.delete_outlined), ), @@ -102,15 +104,19 @@ class _EquipmentProfileEditScreenState extends State padding: const EdgeInsets.symmetric(vertical: Dimens.paddingM), child: Opacity( opacity: state.isLoading ? Dimens.disabledOpacity : Dimens.enabledOpacity, - child: const Column( + child: Column( children: [ - _NameFieldBuilder(), - _IsoValuesListTileBuilder(), - _NdValuesListTileBuilder(), - _ApertureValuesListTileBuilder(), - _ShutterSpeedValuesListTileBuilder(), - _LensZoomListTileBuilder(), - _ExposureOffsetListTileBuilder(), + _NameFieldBuilder(), + if (state.profile is PinholeEquipmentProfile) + const _ApertureValueListTileBuilder() + else ...[ + const _ApertureValuesListTileBuilder(), + const _ShutterSpeedValuesListTileBuilder(), + ], + _IsoValuesListTileBuilder(), + _NdValuesListTileBuilder(), + _LensZoomListTileBuilder(), + _ExposureOffsetListTileBuilder(), ], ), ), @@ -126,12 +132,13 @@ class _EquipmentProfileEditScreenState extends State } } -class _NameFieldBuilder extends StatelessWidget { +class _NameFieldBuilder extends StatelessWidget { const _NameFieldBuilder(); @override Widget build(BuildContext context) { - return BlocBuilder( + return BlocBuilder, EquipmentProfileEditState>( + buildWhen: (previous, current) => previous.profile.name != current.profile.name, builder: (context, state) => Padding( padding: const EdgeInsets.only( left: Dimens.paddingM, @@ -141,13 +148,13 @@ class _NameFieldBuilder extends StatelessWidget { ), child: LightmeterTextField( autofocus: true, - initialValue: state.name, + initialValue: state.profile.name, maxLength: 48, hintText: S.of(context).name, style: Theme.of(context).listTileTheme.titleTextStyle, leading: const Icon(Icons.edit_outlined), onChanged: (value) { - context.read().add(EquipmentProfileNameChangedEvent(value)); + context.read>().add(EquipmentProfileNameChangedEvent(value)); }, ), ), @@ -155,40 +162,40 @@ class _NameFieldBuilder extends StatelessWidget { } } -class _IsoValuesListTileBuilder extends StatelessWidget { +class _IsoValuesListTileBuilder extends StatelessWidget { const _IsoValuesListTileBuilder(); @override Widget build(BuildContext context) { - return BlocBuilder( + return BlocBuilder, EquipmentProfileEditState>( builder: (context, state) => FilterListTile( icon: Icons.iso_outlined, title: S.of(context).isoValues, description: S.of(context).isoValuesFilterDescription, values: IsoValue.values, - selectedValues: state.isoValues, + selectedValues: state.profile.isoValues, onChanged: (value) { - context.read().add(EquipmentProfileIsoValuesChangedEvent(value)); + context.read>().add(EquipmentProfileIsoValuesChangedEvent(value)); }, ), ); } } -class _NdValuesListTileBuilder extends StatelessWidget { +class _NdValuesListTileBuilder extends StatelessWidget { const _NdValuesListTileBuilder(); @override Widget build(BuildContext context) { - return BlocBuilder( + return BlocBuilder, EquipmentProfileEditState>( builder: (context, state) => FilterListTile( icon: Icons.filter_b_and_w_outlined, title: S.of(context).ndFilters, description: S.of(context).ndFiltersFilterDescription, values: NdValue.values, - selectedValues: state.ndValues, + selectedValues: state.profile.ndValues, onChanged: (value) { - context.read().add(EquipmentProfileNdValuesChangedEvent(value)); + context.read>().add(EquipmentProfileNdValuesChangedEvent(value)); }, ), ); @@ -200,13 +207,13 @@ class _ApertureValuesListTileBuilder extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocBuilder( + return BlocBuilder>( builder: (context, state) => RangePickerListTile( icon: Icons.camera_outlined, title: S.of(context).apertureValues, description: S.of(context).apertureValuesFilterDescription, values: ApertureValue.values, - selectedValues: state.apertureValues, + selectedValues: state.profile.apertureValues, onChanged: (value) { context.read().add(EquipmentProfileApertureValuesChangedEvent(value)); }, @@ -215,18 +222,34 @@ class _ApertureValuesListTileBuilder extends StatelessWidget { } } +class _ApertureValueListTileBuilder extends StatelessWidget { + const _ApertureValueListTileBuilder(); + + @override + Widget build(BuildContext context) { + return BlocBuilder>( + builder: (context, state) => EquipmentProfileApertureInput( + value: state.profile.aperture, + onChanged: (value) { + context.read().add(EquipmentProfileApertureValueChangedEvent(value)); + }, + ), + ); + } +} + class _ShutterSpeedValuesListTileBuilder extends StatelessWidget { const _ShutterSpeedValuesListTileBuilder(); @override Widget build(BuildContext context) { - return BlocBuilder( + return BlocBuilder>( builder: (context, state) => RangePickerListTile( icon: Icons.shutter_speed_outlined, title: S.of(context).shutterSpeedValues, description: S.of(context).shutterSpeedValuesFilterDescription, values: ShutterSpeedValue.values, - selectedValues: state.shutterSpeedValues, + selectedValues: state.profile.shutterSpeedValues, trailingAdapter: (context, value) => value.value == 1 ? S.of(context).shutterSpeedManualShort : value.toString(), dialogValueAdapter: (context, value) => value.value == 1 ? S.of(context).shutterSpeedManual : value.toString(), @@ -238,42 +261,44 @@ class _ShutterSpeedValuesListTileBuilder extends StatelessWidget { } } -class _LensZoomListTileBuilder extends StatelessWidget { +class _LensZoomListTileBuilder extends StatelessWidget { const _LensZoomListTileBuilder(); @override Widget build(BuildContext context) { - return BlocBuilder( + return BlocBuilder, EquipmentProfileEditState>( + buildWhen: (previous, current) => previous.profile.lensZoom != current.profile.lensZoom, builder: (context, state) => SliderPickerListTile( icon: Icons.zoom_in_outlined, title: S.of(context).lensZoom, description: S.of(context).lensZoomDescription, - value: state.lensZoom, + value: state.profile.lensZoom, range: CameraContainerBloc.zoomMaxRange, valueAdapter: (context, value) => value.toZoom(context), onChanged: (value) { - context.read().add(EquipmentProfileLensZoomChangedEvent(value)); + context.read>().add(EquipmentProfileLensZoomChangedEvent(value)); }, ), ); } } -class _ExposureOffsetListTileBuilder extends StatelessWidget { +class _ExposureOffsetListTileBuilder extends StatelessWidget { const _ExposureOffsetListTileBuilder(); @override Widget build(BuildContext context) { - return BlocBuilder( + return BlocBuilder, EquipmentProfileEditState>( + buildWhen: (previous, current) => previous.profile.exposureOffset != current.profile.exposureOffset, builder: (context, state) => SliderPickerListTile( icon: Icons.light_mode_outlined, title: S.of(context).exposureOffset, description: S.of(context).exposureOffsetDescription, - value: state.exposureOffset, + value: state.profile.exposureOffset, range: CameraContainerBloc.exposureMaxRange, valueAdapter: (context, value) => S.of(context).evValue(value.toStringSignedAsFixed(1)), onChanged: (value) { - context.read().add(EquipmentProfileExposureOffsetChangedEvent(value)); + context.read>().add(EquipmentProfileExposureOffsetChangedEvent(value)); }, ), ); diff --git a/lib/screens/equipment_profile_edit/state_equipment_profile_edit.dart b/lib/screens/equipment_profile_edit/state_equipment_profile_edit.dart index 4727fbd..e095ee3 100644 --- a/lib/screens/equipment_profile_edit/state_equipment_profile_edit.dart +++ b/lib/screens/equipment_profile_edit/state_equipment_profile_edit.dart @@ -1,52 +1,32 @@ import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; -class EquipmentProfileEditState { - final String name; - final List apertureValues; - final List ndValues; - final List shutterSpeedValues; - final List isoValues; - final double lensZoom; - final double exposureOffset; - final bool canSave; +class EquipmentProfileEditState { + final T profile; + final T? profileToCopy; + final bool hasChanges; + final bool isValid; final bool isLoading; - final EquipmentProfile? profileToCopy; const EquipmentProfileEditState({ - required this.name, - required this.apertureValues, - required this.ndValues, - required this.shutterSpeedValues, - required this.isoValues, - required this.lensZoom, - required this.exposureOffset, - required this.canSave, + required this.profile, + required this.hasChanges, + required this.isValid, this.isLoading = false, this.profileToCopy, }); - EquipmentProfileEditState copyWith({ - String? name, - List? apertureValues, - List? ndValues, - List? shutterSpeedValues, - List? isoValues, - double? lensZoom, - double? exposureOffset, - bool? canSave, + EquipmentProfileEditState copyWith({ + T? profile, + T? profileToCopy, + bool? isValid, + bool? hasChanges, bool? isLoading, - EquipmentProfile? profileToCopy, }) => - EquipmentProfileEditState( - name: name ?? this.name, - apertureValues: apertureValues ?? this.apertureValues, - ndValues: ndValues ?? this.ndValues, - shutterSpeedValues: shutterSpeedValues ?? this.shutterSpeedValues, - isoValues: isoValues ?? this.isoValues, - lensZoom: lensZoom ?? this.lensZoom, - exposureOffset: exposureOffset ?? this.exposureOffset, - canSave: canSave ?? this.canSave, - isLoading: isLoading ?? this.isLoading, + EquipmentProfileEditState( + profile: profile ?? this.profile, profileToCopy: profileToCopy ?? this.profileToCopy, + isValid: isValid ?? this.isValid, + hasChanges: hasChanges ?? this.hasChanges, + isLoading: isLoading ?? this.isLoading, ); } diff --git a/lib/screens/equipment_profiles/components/equipment_profile_type_picker/widget_picker_equipment_profile_type.dart b/lib/screens/equipment_profiles/components/equipment_profile_type_picker/widget_picker_equipment_profile_type.dart new file mode 100644 index 0000000..8857bab --- /dev/null +++ b/lib/screens/equipment_profiles/components/equipment_profile_type_picker/widget_picker_equipment_profile_type.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'package:lightmeter/generated/l10n.dart'; +import 'package:lightmeter/screens/settings/components/shared/dialog_picker/widget_dialog_picker.dart'; + +enum EquipmentProfileType { regular, pinhole } + +class EquipmentProfilesTypePicker extends StatelessWidget { + const EquipmentProfilesTypePicker._(); + + static Future show(BuildContext context) { + return showDialog( + context: context, + builder: (_) => const EquipmentProfilesTypePicker._(), + ); + } + + @override + Widget build(BuildContext context) { + return DialogPicker( + icon: Icons.camera_alt_outlined, + title: S.of(context).equipmentProfileType, + selectedValue: EquipmentProfileType.regular, + values: EquipmentProfileType.values, + titleAdapter: (context, value) => switch (value) { + EquipmentProfileType.regular => S.of(context).camera, + EquipmentProfileType.pinhole => S.of(context).pinholeCamera, + }, + ); + } +} diff --git a/lib/screens/equipment_profiles/screen_equipment_profiles.dart b/lib/screens/equipment_profiles/screen_equipment_profiles.dart index 4fb1afa..4494541 100644 --- a/lib/screens/equipment_profiles/screen_equipment_profiles.dart +++ b/lib/screens/equipment_profiles/screen_equipment_profiles.dart @@ -4,6 +4,7 @@ import 'package:lightmeter/navigation/routes.dart'; import 'package:lightmeter/providers/equipment_profile_provider.dart'; import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/screens/equipment_profile_edit/flow_equipment_profile_edit.dart'; +import 'package:lightmeter/screens/equipment_profiles/components/equipment_profile_type_picker/widget_picker_equipment_profile_type.dart'; import 'package:lightmeter/screens/shared/sliver_placeholder/widget_sliver_placeholder.dart'; import 'package:lightmeter/screens/shared/sliver_screen/screen_sliver.dart'; import 'package:lightmeter/utils/guard_pro_tap.dart'; @@ -45,15 +46,34 @@ class _EquipmentProfilesScreenState extends State with guardProTap( context, () { - Navigator.of(context).pushNamed( - NavigationRoutes.equipmentProfileEditScreen.name, - arguments: const EquipmentProfileEditArgs(editType: EquipmentProfileEditType.add), - ); + EquipmentProfilesTypePicker.show(context).then((value) { + if (value != null && mounted) { + Navigator.of(context).pushNamed( + NavigationRoutes.equipmentProfileEditScreen.name, + arguments: switch (value) { + EquipmentProfileType.regular => const EquipmentProfileEditArgs( + editType: EquipmentProfileEditType.add, + profile: EquipmentProfilesProvider.defaultProfile, + ), + EquipmentProfileType.pinhole => EquipmentProfileEditArgs( + editType: EquipmentProfileEditType.add, + profile: PinholeEquipmentProfile( + id: EquipmentProfilesProvider.defaultProfile.id, + name: EquipmentProfilesProvider.defaultProfile.name, + aperture: 22, + isoValues: EquipmentProfilesProvider.defaultProfile.isoValues, + ndValues: EquipmentProfilesProvider.defaultProfile.ndValues, + ), + ), + }, + ); + } + }); }, ); } - void _editProfile(EquipmentProfile profile) { + void _editProfile(IEquipmentProfile profile) { Navigator.of(context).pushNamed( NavigationRoutes.equipmentProfileEditScreen.name, arguments: EquipmentProfileEditArgs( @@ -65,9 +85,9 @@ class _EquipmentProfilesScreenState extends State with } class _EquipmentProfilesListBuilder extends StatelessWidget { - final List values; - final void Function(EquipmentProfile profile) onEdit; - final void Function(EquipmentProfile profile, bool value) onCheckbox; + final List values; + final void Function(IEquipmentProfile profile) onEdit; + final void Function(String id, bool value) onCheckbox; const _EquipmentProfilesListBuilder({ required this.values, @@ -102,7 +122,7 @@ class _EquipmentProfilesListBuilder extends StatelessWidget { title: Text(values[index].name), controlAffinity: ListTileControlAffinity.leading, value: EquipmentProfiles.inUseOf(context).contains(values[index]), - onChanged: (value) => onCheckbox(values[index], value ?? false), + onChanged: (value) => onCheckbox(values[index].id, value ?? false), secondary: IconButton( onPressed: () => onEdit(values[index]), icon: const Icon(Icons.edit_outlined), diff --git a/lib/screens/logbook_photo_edit/bloc_logbook_photo_edit.dart b/lib/screens/logbook_photo_edit/bloc_logbook_photo_edit.dart index 1f1a1f8..903e02a 100644 --- a/lib/screens/logbook_photo_edit/bloc_logbook_photo_edit.dart +++ b/lib/screens/logbook_photo_edit/bloc_logbook_photo_edit.dart @@ -74,10 +74,10 @@ class LogbookPhotoEditBloc extends Bloc _onEquipmentProfileChanged(LogbookPhotoEquipmentProfileChangedEvent event, Emitter emit) async { - _newPhoto = _newPhoto.copyWith(equipmentProfileId: Optional(event.equipmentProfile?.id)); + _newPhoto = _newPhoto.copyWith(equipmentProfileId: Optional(event.equipmentProfileId)); emit( state.copyWith( - equipmentProfileId: Optional(event.equipmentProfile?.id), + equipmentProfileId: Optional(event.equipmentProfileId), canSave: _canSave(), ), ); diff --git a/lib/screens/logbook_photo_edit/event_logbook_photo_edit.dart b/lib/screens/logbook_photo_edit/event_logbook_photo_edit.dart index e5c5ff8..918bba6 100644 --- a/lib/screens/logbook_photo_edit/event_logbook_photo_edit.dart +++ b/lib/screens/logbook_photo_edit/event_logbook_photo_edit.dart @@ -23,9 +23,9 @@ class LogbookPhotoNoteChangedEvent extends LogbookPhotoEditEvent { } class LogbookPhotoEquipmentProfileChangedEvent extends LogbookPhotoEditEvent { - final EquipmentProfile? equipmentProfile; + final String? equipmentProfileId; - const LogbookPhotoEquipmentProfileChangedEvent(this.equipmentProfile); + const LogbookPhotoEquipmentProfileChangedEvent(this.equipmentProfileId); } class LogbookPhotoFilmChangedEvent extends LogbookPhotoEditEvent { diff --git a/lib/screens/logbook_photo_edit/screen_logbook_photo_edit.dart b/lib/screens/logbook_photo_edit/screen_logbook_photo_edit.dart index 13c4e56..4ac775d 100644 --- a/lib/screens/logbook_photo_edit/screen_logbook_photo_edit.dart +++ b/lib/screens/logbook_photo_edit/screen_logbook_photo_edit.dart @@ -273,7 +273,7 @@ class _EquipmentProfilePickerListTile extends StatelessWidget { selectedValue: EquipmentProfiles.of(context).firstWhereOrNull((e) => e.id == state.equipmentProfileId), titleAdapter: (value) => value.name, onChanged: (value) { - context.read().add(LogbookPhotoEquipmentProfileChangedEvent(value.value)); + context.read().add(LogbookPhotoEquipmentProfileChangedEvent(value.value?.id)); }, ), ); diff --git a/lib/screens/metering/communication/event_communication_metering.dart b/lib/screens/metering/communication/event_communication_metering.dart index 439bb13..50242ea 100644 --- a/lib/screens/metering/communication/event_communication_metering.dart +++ b/lib/screens/metering/communication/event_communication_metering.dart @@ -19,7 +19,7 @@ class MeasureEvent extends ScreenEvent { } class EquipmentProfileChangedEvent extends ScreenEvent { - final EquipmentProfile profile; + final IEquipmentProfile profile; const EquipmentProfileChangedEvent(this.profile); } diff --git a/lib/screens/metering/communication/state_communication_metering.dart b/lib/screens/metering/communication/state_communication_metering.dart index f208a2c..b688407 100644 --- a/lib/screens/metering/communication/state_communication_metering.dart +++ b/lib/screens/metering/communication/state_communication_metering.dart @@ -21,7 +21,7 @@ class MeasureState extends SourceState { } class EquipmentProfileChangedState extends SourceState { - final EquipmentProfile profile; + final IEquipmentProfile profile; const EquipmentProfileChangedState(this.profile); } diff --git a/lib/screens/metering/components/camera_container/widget_container_camera.dart b/lib/screens/metering/components/camera_container/widget_container_camera.dart index 8dd55ef..871a41c 100644 --- a/lib/screens/metering/components/camera_container/widget_container_camera.dart +++ b/lib/screens/metering/components/camera_container/widget_container_camera.dart @@ -6,6 +6,7 @@ import 'package:lightmeter/data/models/exposure_pair.dart'; import 'package:lightmeter/data/models/feature.dart'; import 'package:lightmeter/data/models/metering_screen_layout_config.dart'; import 'package:lightmeter/platform_config.dart'; +import 'package:lightmeter/providers/equipment_profile_provider.dart'; import 'package:lightmeter/providers/remote_config_provider.dart'; import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/screens/metering/components/camera_container/bloc_container_camera.dart'; @@ -125,7 +126,10 @@ class CameraContainer extends StatelessWidget { enabledFeaturesHeight += Dimens.readingContainerSingleValueHeight; enabledFeaturesHeight += Dimens.paddingS; } - if (context.meteringFeature(MeteringScreenLayoutFeature.extremeExposurePairs)) { + if (EquipmentProfiles.selectedOf(context) is PinholeEquipmentProfile) { + enabledFeaturesHeight += Dimens.readingContainerSingleValueHeight; + enabledFeaturesHeight += Dimens.paddingS; + } else if (context.meteringFeature(MeteringScreenLayoutFeature.extremeExposurePairs)) { enabledFeaturesHeight += Dimens.readingContainerDoubleValueHeight; enabledFeaturesHeight += Dimens.paddingS; } diff --git a/lib/screens/metering/components/shared/readings_container/components/equipment_profile_picker/widget_picker_equipment_profiles.dart b/lib/screens/metering/components/shared/readings_container/components/equipment_profile_picker/widget_picker_equipment_profiles.dart index 65a73b5..6709614 100644 --- a/lib/screens/metering/components/shared/readings_container/components/equipment_profile_picker/widget_picker_equipment_profiles.dart +++ b/lib/screens/metering/components/shared/readings_container/components/equipment_profile_picker/widget_picker_equipment_profiles.dart @@ -10,13 +10,13 @@ class EquipmentProfilePicker extends StatelessWidget { @override Widget build(BuildContext context) { - return AnimatedDialogPicker( + return AnimatedDialogPicker( icon: Icons.camera_alt_outlined, title: S.of(context).equipmentProfile, selectedValue: EquipmentProfiles.selectedOf(context), values: EquipmentProfiles.inUseOf(context), itemTitleBuilder: (_, value) => Text(value.id.isEmpty ? S.of(context).none : value.name), - onChanged: EquipmentProfilesProvider.of(context).selectProfile, + onChanged: (profile) => EquipmentProfilesProvider.of(context).selectProfile(profile.id), closedChild: ReadingValueContainer.singleValue( value: ReadingValue( label: S.of(context).equipmentProfile, diff --git a/lib/screens/metering/components/shared/readings_container/components/shutter_speed_container/widget_container_shutter_speed.dart b/lib/screens/metering/components/shared/readings_container/components/shutter_speed_container/widget_container_shutter_speed.dart new file mode 100644 index 0000000..d458ad4 --- /dev/null +++ b/lib/screens/metering/components/shared/readings_container/components/shutter_speed_container/widget_container_shutter_speed.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:lightmeter/generated/l10n.dart'; +import 'package:lightmeter/providers/films_provider.dart'; +import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shared/reading_value_container/widget_container_reading_value.dart'; +import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; + +class ShutterSpeedContainer extends StatelessWidget { + final ShutterSpeedValue? shutterSpeedValue; + + const ShutterSpeedContainer({ + required this.shutterSpeedValue, + super.key, + }); + + @override + Widget build(BuildContext context) { + return ReadingValueContainer.singleValue( + value: ReadingValue( + label: S.of(context).shutterSpeed, + value: _shutterSpeed(context), + ), + ); + } + + String _shutterSpeed(BuildContext context) { + if (shutterSpeedValue case final shutterSpeedValue?) { + return Films.selectedOf(context).reciprocityFailure(shutterSpeedValue).toString(); + } else { + return '-'; + } + } +} diff --git a/lib/screens/metering/components/shared/readings_container/widget_container_readings.dart b/lib/screens/metering/components/shared/readings_container/widget_container_readings.dart index fe8c8b0..034ece1 100644 --- a/lib/screens/metering/components/shared/readings_container/widget_container_readings.dart +++ b/lib/screens/metering/components/shared/readings_container/widget_container_readings.dart @@ -11,6 +11,7 @@ import 'package:lightmeter/screens/metering/components/shared/readings_container import 'package:lightmeter/screens/metering/components/shared/readings_container/components/iso_picker/widget_picker_iso.dart'; import 'package:lightmeter/screens/metering/components/shared/readings_container/components/lightmeter_pro_badge/widget_badge_lightmeter_pro.dart'; import 'package:lightmeter/screens/metering/components/shared/readings_container/components/nd_picker/widget_picker_nd.dart'; +import 'package:lightmeter/screens/metering/components/shared/readings_container/components/shutter_speed_container/widget_container_shutter_speed.dart'; import 'package:lightmeter/utils/context_utils.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; @@ -45,7 +46,10 @@ class ReadingsContainer extends StatelessWidget { const EquipmentProfilePicker(), const _InnerPadding(), ], - if (context.meteringFeature(MeteringScreenLayoutFeature.extremeExposurePairs)) ...[ + if (EquipmentProfiles.selectedOf(context) is PinholeEquipmentProfile) ...[ + ShutterSpeedContainer(shutterSpeedValue: fastest?.shutterSpeed), + const _InnerPadding(), + ] else if (context.meteringFeature(MeteringScreenLayoutFeature.extremeExposurePairs)) ...[ ExtremeExposurePairsContainer( fastest: fastest, slowest: slowest, diff --git a/lib/screens/metering/event_metering.dart b/lib/screens/metering/event_metering.dart index f6f8120..de4c023 100644 --- a/lib/screens/metering/event_metering.dart +++ b/lib/screens/metering/event_metering.dart @@ -5,7 +5,7 @@ sealed class MeteringEvent { } class EquipmentProfileChangedEvent extends MeteringEvent { - final EquipmentProfile equipmentProfileData; + final IEquipmentProfile equipmentProfileData; const EquipmentProfileChangedEvent(this.equipmentProfileData); } diff --git a/lib/screens/metering/screen_metering.dart b/lib/screens/metering/screen_metering.dart index 4f81e4a..7d504e3 100644 --- a/lib/screens/metering/screen_metering.dart +++ b/lib/screens/metering/screen_metering.dart @@ -169,12 +169,22 @@ class MeteringContainerBuidler extends StatelessWidget { static List buildExposureValues( double ev, StopType stopType, - EquipmentProfile equipmentProfile, + IEquipmentProfile equipmentProfile, ) { if (ev.isNaN || ev.isInfinite) { return List.empty(); } + if (equipmentProfile.id != "" && equipmentProfile is PinholeEquipmentProfile) { + final t = pow(equipmentProfile.aperture, 2) / pow(2, ev); + return [ + ExposurePair( + ApertureValue(equipmentProfile.aperture, StopType.full), + ShutterSpeedValue(t, false, StopType.full), + ), + ]; + } + /// Depending on the `stopType` the exposure pairs list length is multiplied by 1,2 or 3 final int evSteps = (ev * (stopType.index + 1)).round(); @@ -236,31 +246,31 @@ class MeteringContainerBuidler extends StatelessWidget { ); /// Full equipment profile, nothing to cut - if (equipmentProfile.id == "") { - return exposurePairs; + if (equipmentProfile.id != "" && equipmentProfile is EquipmentProfile) { + final equipmentApertureValues = equipmentProfile.apertureValues.whereStopType(stopType); + final equipmentShutterSpeedValues = equipmentProfile.shutterSpeedValues.whereStopType(stopType); + + final startCutEV = max( + exposurePairs.first.aperture.difference(equipmentApertureValues.first), + exposurePairs.first.shutterSpeed.difference(equipmentShutterSpeedValues.first), + ); + final endCutEV = max( + equipmentApertureValues.last.difference(exposurePairs.last.aperture), + equipmentShutterSpeedValues.last != ShutterSpeedValue.values.last + ? equipmentShutterSpeedValues.last.difference(exposurePairs.last.shutterSpeed) + : double.negativeInfinity, + ); + + final startCut = (startCutEV * (stopType.index + 1)).round().clamp(0, itemsCount); + final endCut = (endCutEV * (stopType.index + 1)).round().clamp(0, itemsCount); + + if (startCut > itemsCount - endCut) { + return const []; + } + return exposurePairs.sublist(startCut, itemsCount - endCut); } - final equipmentApertureValues = equipmentProfile.apertureValues.whereStopType(stopType); - final equipmentShutterSpeedValues = equipmentProfile.shutterSpeedValues.whereStopType(stopType); - - final startCutEV = max( - exposurePairs.first.aperture.difference(equipmentApertureValues.first), - exposurePairs.first.shutterSpeed.difference(equipmentShutterSpeedValues.first), - ); - final endCutEV = max( - equipmentApertureValues.last.difference(exposurePairs.last.aperture), - equipmentShutterSpeedValues.last != ShutterSpeedValue.values.last - ? equipmentShutterSpeedValues.last.difference(exposurePairs.last.shutterSpeed) - : double.negativeInfinity, - ); - - final startCut = (startCutEV * (stopType.index + 1)).round().clamp(0, itemsCount); - final endCut = (endCutEV * (stopType.index + 1)).round().clamp(0, itemsCount); - - if (startCut > itemsCount - endCut) { - return const []; - } - return exposurePairs.sublist(startCut, itemsCount - endCut); + return exposurePairs; } } diff --git a/lib/screens/metering/utils/listener_equipment_profiles.dart b/lib/screens/metering/utils/listener_equipment_profiles.dart index ec604ce..6e4cc29 100644 --- a/lib/screens/metering/utils/listener_equipment_profiles.dart +++ b/lib/screens/metering/utils/listener_equipment_profiles.dart @@ -3,7 +3,7 @@ import 'package:lightmeter/providers/equipment_profile_provider.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; class EquipmentProfileListener extends StatefulWidget { - final ValueChanged onDidChangeDependencies; + final ValueChanged onDidChangeDependencies; final Widget child; const EquipmentProfileListener({ 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 0898541..05f37a2 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 @@ -34,7 +34,7 @@ class MeteringScreenLayoutListTile extends StatelessWidget { .toList(growable: false), onSave: (value) { if (!value[MeteringScreenLayoutFeature.equipmentProfiles]!) { - EquipmentProfilesProvider.of(context).selectProfile(EquipmentProfiles.of(context).first); + EquipmentProfilesProvider.of(context).selectProfile(EquipmentProfiles.of(context).first.id); } if (!value[MeteringScreenLayoutFeature.filmPicker]!) { FilmsProvider.of(context).selectFilm(const FilmStub()); diff --git a/lib/screens/shared/text_field/widget_text_field.dart b/lib/screens/shared/text_field/widget_text_field.dart index f801845..dd9bc8c 100644 --- a/lib/screens/shared/text_field/widget_text_field.dart +++ b/lib/screens/shared/text_field/widget_text_field.dart @@ -8,6 +8,7 @@ class LightmeterTextField extends StatefulWidget { this.hintText, this.initialValue, this.inputFormatters, + this.validator, this.leading, this.maxLength, this.maxLines = 1, @@ -21,6 +22,7 @@ class LightmeterTextField extends StatefulWidget { final String? hintText; final String? initialValue; final List? inputFormatters; + final String? Function(String)? validator; final Widget? leading; final int? maxLength; final int? maxLines; @@ -62,6 +64,8 @@ class _LightmeterTextFieldState extends State { validator: (value) { if (value == null || value.isEmpty) { return ''; + } else if (widget.validator != null) { + return widget.validator!(value); } else { return null; } diff --git a/pubspec.lock b/pubspec.lock index 48ef682..a91fc43 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -861,8 +861,8 @@ packages: dependency: "direct main" description: path: "." - ref: "v4.1.2" - resolved-ref: "6bb16eb49232649eca02704b3a40890b0184d8b5" + ref: "feature/MLI-48" + resolved-ref: "4a169640bff3d3a3206a2c352a75cbcea4871b1c" url: "https://github.com/vodemn/m3_lightmeter_iap" source: git version: "4.1.2+37" @@ -870,8 +870,8 @@ packages: dependency: "direct main" description: path: "." - ref: "v2.4.0" - resolved-ref: cc9ae43a7859398a6ab2ecf7f8713153dbfd99cd + ref: "feature/MLR-18" + resolved-ref: "61bb3f8a9164d19f6e47c96fbea1cbe3aaf39fc3" url: "https://github.com/vodemn/m3_lightmeter_resources" source: git version: "2.4.0+13" diff --git a/pubspec.yaml b/pubspec.yaml index dbc38d2..0db7015 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -33,11 +33,11 @@ dependencies: m3_lightmeter_iap: git: url: "https://github.com/vodemn/m3_lightmeter_iap" - ref: v4.1.2 + ref: feature/MLI-48 m3_lightmeter_resources: git: url: "https://github.com/vodemn/m3_lightmeter_resources" - ref: v2.4.0 + ref: feature/MLR-18 map_launcher: 3.2.0 material_color_utilities: 0.12.0 package_info_plus: 8.1.3 diff --git a/screenshots/generate_screenshots.dart b/screenshots/generate_screenshots.dart index c80575f..4c5cfce 100644 --- a/screenshots/generate_screenshots.dart +++ b/screenshots/generate_screenshots.dart @@ -218,14 +218,9 @@ void main() { testWidgets( 'Generate timer screenshot', (tester) async { - const timerExposurePair = ExposurePair( - ApertureValue(16, StopType.full), - ShutterSpeedValue(8, false, StopType.full), - ); await mockSharedPrefs( - iso: 100, nd: 8, - calibration: -0.3, + calibration: -2.3, theme: ThemeType.light, color: _lightThemeColor, ); @@ -236,13 +231,22 @@ void main() { ); await tester.takePhoto(); - await tester.scrollToExposurePair( - ev: 5, - exposurePair: timerExposurePair, + + final exposurePairs = MeteringContainerBuidler.buildExposureValues( + 5, + StopType.third, + defaultEquipmentProfile, ); - await tester.tap(find.text(_mockFilm.reciprocityFailure(timerExposurePair.shutterSpeed).toString())); + final timerExposurePair = exposurePairs.firstWhere((e) => e.aperture == const ApertureValue(16, StopType.full)); + await tester.scrollToExposurePair( + exposurePairs: exposurePairs, + exposurePair: exposurePairs.firstWhere((e) => e.aperture == const ApertureValue(16, StopType.full)), + ); + + final correctedShutterSpeed = _mockFilm.reciprocityFailure(timerExposurePair.shutterSpeed); + await tester.tap(find.text(correctedShutterSpeed.toString())); await tester.pumpAndSettle(); - await tester.mockTimerResumedState(timerExposurePair.shutterSpeed); + await tester.mockTimerResumedState(correctedShutterSpeed); await tester.takeScreenshotLight(binding, 'timer'); }, ); @@ -257,32 +261,28 @@ extension on WidgetTester { _takeScreenshot(binding, name, _themeDark); Future _takeScreenshot(IntegrationTestWidgetsFlutterBinding binding, String name, ThemeData theme) async { - final Color backgroundColor = theme.colorScheme.surface; - await binding.takeScreenshot( - ScreenshotArgs( - name: name, - deviceName: const String.fromEnvironment('deviceName'), - platformFolder: _platformFolder, - backgroundColor: backgroundColor.toInt().toRadixString(16), - isDark: theme.brightness == Brightness.dark, - ).toString(), - ); - await pumpAndSettle(); + const deviceName = String.fromEnvironment('deviceName'); + if (deviceName.isNotEmpty) { + final Color backgroundColor = theme.colorScheme.surface; + await binding.takeScreenshot( + ScreenshotArgs( + name: name, + deviceName: deviceName, + platformFolder: _platformFolder, + backgroundColor: backgroundColor.toInt().toRadixString(16), + isDark: theme.brightness == Brightness.dark, + ).toString(), + ); + await pumpAndSettle(); + } } } extension on WidgetTester { Future scrollToExposurePair({ - double ev = mockPhotoEv100, - EquipmentProfile equipmentProfile = defaultEquipmentProfile, + required List exposurePairs, required ExposurePair exposurePair, }) async { - final exposurePairs = MeteringContainerBuidler.buildExposureValues( - ev, - StopType.third, - equipmentProfile, - ); - await scrollUntilVisible( find.byWidgetPredicate((widget) => widget is Row && widget.key == ValueKey(exposurePairs.indexOf(exposurePair))), 56, diff --git a/test/providers/equipment_profile_provider_test.dart b/test/providers/equipment_profile_provider_test.dart index 8aacb8b..b243bee 100644 --- a/test/providers/equipment_profile_provider_test.dart +++ b/test/providers/equipment_profile_provider_test.dart @@ -16,7 +16,7 @@ void main() { }); setUp(() { - registerFallbackValue(_customProfiles.first); + registerFallbackValue(_mockEquipmentProfiles.first); when(() => storageService.addEquipmentProfile(any())).thenAnswer((_) async {}); when( () => storageService.updateEquipmentProfile( @@ -26,7 +26,10 @@ void main() { ), ).thenAnswer((_) async {}); when(() => storageService.deleteEquipmentProfile(any())).thenAnswer((_) async {}); - when(() => storageService.getEquipmentProfiles()).thenAnswer((_) => Future.value(_customProfiles.toTogglableMap())); + when(() => storageService.getEquipmentProfiles()) + .thenAnswer((_) => Future.value(_mockEquipmentProfiles.toTogglableMap())); + when(() => storageService.getPinholeEquipmentProfiles()) + .thenAnswer((_) => Future.value(_mockPinholeEquipmentProfiles.toTogglableMap())); }); tearDown(() { @@ -62,17 +65,15 @@ void main() { 'EquipmentProfilesProvider dependency on IAPProductStatus', () { setUp(() { - when(() => storageService.selectedEquipmentProfileId).thenReturn(_customProfiles.first.id); - when(() => storageService.getEquipmentProfiles()) - .thenAnswer((_) => Future.value(_customProfiles.toTogglableMap())); + when(() => storageService.selectedEquipmentProfileId).thenReturn(_mockProfiles.first.id); }); testWidgets( 'Pro - show all saved profiles', (tester) async { await pumpTestWidget(tester, true); - expectEquipmentProfilesCount(_customProfiles.length + 1); - expectSelectedEquipmentProfileName(_customProfiles.first.name); + expectEquipmentProfilesCount(_mockProfiles.length + 1); + expectSelectedEquipmentProfileName(_mockProfiles.first.name); }, ); @@ -90,19 +91,19 @@ void main() { testWidgets( 'toggleProfile', (tester) async { - when(() => storageService.selectedEquipmentProfileId).thenReturn(_customProfiles.first.id); + when(() => storageService.selectedEquipmentProfileId).thenReturn(_mockEquipmentProfiles.first.id); await pumpTestWidget(tester, true); - expectEquipmentProfilesCount(_customProfiles.length + 1); - expectEquipmentProfilesInUseCount(_customProfiles.length + 1); - expectSelectedEquipmentProfileName(_customProfiles.first.name); + expectEquipmentProfilesCount(_mockProfiles.length + 1); + expectEquipmentProfilesInUseCount(_mockProfiles.length + 1); + expectSelectedEquipmentProfileName(_mockEquipmentProfiles.first.name); - await tester.equipmentProfilesProvider.toggleProfile(_customProfiles.first, false); + await tester.equipmentProfilesProvider.toggleProfile(_mockEquipmentProfiles.first.id, false); await tester.pump(); - expectEquipmentProfilesCount(_customProfiles.length + 1); - expectEquipmentProfilesInUseCount(_customProfiles.length + 1 - 1); + expectEquipmentProfilesCount(_mockProfiles.length + 1); + expectEquipmentProfilesInUseCount(_mockProfiles.length + 1 - 1); expectSelectedEquipmentProfileName(''); - verify(() => storageService.updateEquipmentProfile(id: _customProfiles.first.id, isUsed: false)).called(1); + verify(() => storageService.updateEquipmentProfile(id: _mockEquipmentProfiles.first.id, isUsed: false)).called(1); verify(() => storageService.selectedEquipmentProfileId = '').called(1); }, ); @@ -111,6 +112,7 @@ void main() { 'EquipmentProfilesProvider CRUD', (tester) async { when(() => storageService.getEquipmentProfiles()).thenAnswer((_) async => {}); + when(() => storageService.getPinholeEquipmentProfiles()).thenAnswer((_) async => {}); when(() => storageService.selectedEquipmentProfileId).thenReturn(''); await pumpTestWidget(tester, true); @@ -118,44 +120,45 @@ void main() { expectSelectedEquipmentProfileName(''); /// Add first profile and verify - await tester.equipmentProfilesProvider.addProfile(_customProfiles.first); + await tester.equipmentProfilesProvider.addProfile(_mockEquipmentProfiles.first); await tester.pump(); expectEquipmentProfilesCount(2); expectSelectedEquipmentProfileName(''); verify(() => storageService.addEquipmentProfile(any())).called(1); /// Add the other profiles and select the 1st one - for (final profile in _customProfiles.skip(1)) { + for (final profile in _mockEquipmentProfiles.skip(1)) { await tester.equipmentProfilesProvider.addProfile(profile); } - tester.equipmentProfilesProvider.selectProfile(_customProfiles.first); + tester.equipmentProfilesProvider.selectProfile(_mockEquipmentProfiles.first.id); await tester.pump(); - expectEquipmentProfilesCount(1 + _customProfiles.length); - expectSelectedEquipmentProfileName(_customProfiles.first.name); + expectEquipmentProfilesCount(1 + _mockEquipmentProfiles.length); + expectSelectedEquipmentProfileName(_mockEquipmentProfiles.first.name); /// Edit the selected profile - final updatedName = "${_customProfiles.first} updated"; - await tester.equipmentProfilesProvider.updateProfile(_customProfiles.first.copyWith(name: updatedName)); + final updatedName = "${_mockEquipmentProfiles.first} updated"; + await tester.equipmentProfilesProvider.updateProfile(_mockEquipmentProfiles.first.copyWith(name: updatedName)); await tester.pump(); - expectEquipmentProfilesCount(1 + _customProfiles.length); + expectEquipmentProfilesCount(1 + _mockEquipmentProfiles.length); expectSelectedEquipmentProfileName(updatedName); - verify(() => storageService.updateEquipmentProfile(id: _customProfiles.first.id, name: updatedName)).called(1); + verify(() => storageService.updateEquipmentProfile(id: _mockEquipmentProfiles.first.id, name: updatedName)) + .called(1); /// Delete a non-selected profile - await tester.equipmentProfilesProvider.deleteProfile(_customProfiles.last); + await tester.equipmentProfilesProvider.deleteProfile(_mockEquipmentProfiles.last); await tester.pump(); - expectEquipmentProfilesCount(1 + _customProfiles.length - 1); + expectEquipmentProfilesCount(1 + _mockEquipmentProfiles.length - 1); expectSelectedEquipmentProfileName(updatedName); verifyNever(() => storageService.selectedEquipmentProfileId = ''); - verify(() => storageService.deleteEquipmentProfile(_customProfiles.last.id)).called(1); + verify(() => storageService.deleteEquipmentProfile(_mockEquipmentProfiles.last.id)).called(1); /// Delete the selected profile - await tester.equipmentProfilesProvider.deleteProfile(_customProfiles.first); + await tester.equipmentProfilesProvider.deleteProfile(_mockEquipmentProfiles.first); await tester.pump(); - expectEquipmentProfilesCount(1 + _customProfiles.length - 2); + expectEquipmentProfilesCount(1 + _mockEquipmentProfiles.length - 2); expectSelectedEquipmentProfileName(''); verify(() => storageService.selectedEquipmentProfileId = '').called(1); - verify(() => storageService.deleteEquipmentProfile(_customProfiles.first.id)).called(1); + verify(() => storageService.deleteEquipmentProfile(_mockEquipmentProfiles.first.id)).called(1); }, ); } @@ -221,7 +224,9 @@ class _SelectedEquipmentProfile extends StatelessWidget { } } -final List _customProfiles = [ +final List _mockProfiles = [..._mockEquipmentProfiles, ..._mockPinholeEquipmentProfiles]; + +final List _mockEquipmentProfiles = [ const EquipmentProfile( id: '1', name: 'Test 1', @@ -291,3 +296,42 @@ final List _customProfiles = [ ], ), ]; + +final _mockPinholeEquipmentProfiles = [ + const PinholeEquipmentProfile( + id: '3', + name: 'Pinhole Camera f/64', + aperture: 64.0, + isoValues: [ + IsoValue(100, StopType.full), + IsoValue(200, StopType.full), + IsoValue(400, StopType.full), + IsoValue(800, StopType.full), + ], + ndValues: [ + NdValue(0), + NdValue(2), + NdValue(4), + ], + ), + const PinholeEquipmentProfile( + id: '4', + name: 'Pinhole Camera f/128', + aperture: 128.0, + isoValues: [ + IsoValue(50, StopType.full), + IsoValue(100, StopType.full), + IsoValue(200, StopType.full), + IsoValue(400, StopType.full), + IsoValue(800, StopType.full), + IsoValue(1600, StopType.full), + ], + ndValues: [ + NdValue(0), + NdValue(1), + NdValue(2), + NdValue(4), + NdValue(8), + ], + ), +]; diff --git a/test/providers/logbook_photos_provider_test.dart b/test/providers/logbook_photos_provider_test.dart index e8ad03f..269e972 100644 --- a/test/providers/logbook_photos_provider_test.dart +++ b/test/providers/logbook_photos_provider_test.dart @@ -40,6 +40,7 @@ void main() { when(() => storageService.selectedEquipmentProfileId).thenReturn(''); when(() => storageService.getEquipmentProfiles()).thenAnswer((_) => Future.value({})); + when(() => storageService.getPinholeEquipmentProfiles()).thenAnswer((_) => Future.value({})); when(() => storageService.selectedFilmId).thenReturn(const FilmStub().id); when(() => storageService.getPredefinedFilms()).thenAnswer((_) => Future.value({})); diff --git a/test/screens/metering/components/shared/readings_container/equipment_profile_picker_test.dart b/test/screens/metering/components/shared/readings_container/equipment_profile_picker_test.dart index 6b44fd6..ac21632 100644 --- a/test/screens/metering/components/shared/readings_container/equipment_profile_picker_test.dart +++ b/test/screens/metering/components/shared/readings_container/equipment_profile_picker_test.dart @@ -19,7 +19,10 @@ void main() { setUpAll(() { storageService = _MockEquipmentProfilesStorageService(); - when(() => storageService.getEquipmentProfiles()).thenAnswer((_) async => _mockEquipmentProfiles.toTogglableMap()); + when(() => storageService.getEquipmentProfiles()) + .thenAnswer((_) async => _mockEquipmentProfiles.toList().toTogglableMap()); + when(() => storageService.getPinholeEquipmentProfiles()) + .thenAnswer((_) async => _mockPinholeEquipmentProfiles.toList().toTogglableMap()); when(() => storageService.selectedEquipmentProfileId).thenReturn(''); }); @@ -48,7 +51,7 @@ void main() { expectReadingValueContainerText(S.current.equipmentProfile); await tester.openAnimatedPicker(); expect(find.byIcon(Icons.camera_alt_outlined), findsOneWidget); - expectDialogPickerText(S.current.equipmentProfile); + expectDialogPickerText(S.current.equipmentProfile); }, ); @@ -62,7 +65,7 @@ void main() { await pumpApplication(tester); expectReadingValueContainerText(S.current.none); await tester.openAnimatedPicker(); - expectRadioListTile(S.current.none, isSelected: true); + expectRadioListTile(S.current.none, isSelected: true); }, ); @@ -73,7 +76,18 @@ void main() { await pumpApplication(tester); expectReadingValueContainerText(_mockEquipmentProfiles.first.name); await tester.openAnimatedPicker(); - expectRadioListTile(_mockEquipmentProfiles.first.name, isSelected: true); + expectRadioListTile(_mockEquipmentProfiles.first.name, isSelected: true); + }, + ); + + testWidgets( + 'Pinhole Camera f/64', + (tester) async { + when(() => storageService.selectedEquipmentProfileId).thenReturn(_mockPinholeEquipmentProfiles.first.id); + await pumpApplication(tester); + expectReadingValueContainerText(_mockPinholeEquipmentProfiles.first.name); + await tester.openAnimatedPicker(); + expectRadioListTile(_mockPinholeEquipmentProfiles.first.name, isSelected: true); }, ); }, @@ -84,10 +98,13 @@ void main() { (tester) async { when(() => storageService.getEquipmentProfiles()) .thenAnswer((_) async => _mockEquipmentProfiles.skip(1).toList().toTogglableMap()); + when(() => storageService.getPinholeEquipmentProfiles()) + .thenAnswer((_) async => _mockPinholeEquipmentProfiles.skip(1).toList().toTogglableMap()); await pumpApplication(tester); await tester.openAnimatedPicker(); - expectRadioListTile(S.current.none, isSelected: true); - expectRadioListTile(_mockEquipmentProfiles[1].name); + expectRadioListTile(S.current.none, isSelected: true); + expectRadioListTile(_mockEquipmentProfiles[1].name); + expectRadioListTile(_mockPinholeEquipmentProfiles[1].name); }, ); } @@ -126,3 +143,42 @@ final _mockEquipmentProfiles = [ isoValues: IsoValue.values, ), ]; + +final _mockPinholeEquipmentProfiles = [ + const PinholeEquipmentProfile( + id: '3', + name: 'Pinhole Camera f/64', + aperture: 64.0, + isoValues: [ + IsoValue(100, StopType.full), + IsoValue(200, StopType.full), + IsoValue(400, StopType.full), + IsoValue(800, StopType.full), + ], + ndValues: [ + NdValue(0), + NdValue(2), + NdValue(4), + ], + ), + const PinholeEquipmentProfile( + id: '4', + name: 'Pinhole Camera f/128', + aperture: 128.0, + isoValues: [ + IsoValue(50, StopType.full), + IsoValue(100, StopType.full), + IsoValue(200, StopType.full), + IsoValue(400, StopType.full), + IsoValue(800, StopType.full), + IsoValue(1600, StopType.full), + ], + ndValues: [ + NdValue(0), + NdValue(1), + NdValue(2), + NdValue(4), + NdValue(8), + ], + ), +]; diff --git a/test/screens/metering/utils/listener_equipment_profiles_test.dart b/test/screens/metering/utils/listener_equipment_profiles_test.dart index cf04ef4..11da028 100644 --- a/test/screens/metering/utils/listener_equipment_profiles_test.dart +++ b/test/screens/metering/utils/listener_equipment_profiles_test.dart @@ -13,7 +13,7 @@ class _MockEquipmentProfilesStorageService extends Mock implements IapStorageSer void main() { TestWidgetsFlutterBinding.ensureInitialized(); final storageService = _MockEquipmentProfilesStorageService(); - final onDidChangeDependencies = MockValueChanged(); + final onDidChangeDependencies = MockValueChanged(); setUp(() { registerFallbackValue(_customProfiles.first); @@ -26,6 +26,7 @@ void main() { ).thenAnswer((_) async {}); when(() => storageService.deleteEquipmentProfile(any())).thenAnswer((_) async {}); when(() => storageService.getEquipmentProfiles()).thenAnswer((_) => Future.value(_customProfiles.toTogglableMap())); + when(() => storageService.getPinholeEquipmentProfiles()).thenAnswer((_) => Future.value({})); }); tearDown(() { @@ -56,7 +57,7 @@ void main() { when(() => storageService.selectedEquipmentProfileId).thenReturn(''); await pumpTestWidget(tester); - tester.equipmentProfilesProvider.selectProfile(_customProfiles[0]); + tester.equipmentProfilesProvider.selectProfile(_customProfiles[0].id); await tester.pump(); verify(() => onDidChangeDependencies.onChanged(_customProfiles[0])).called(1); },