This commit is contained in:
Vadim 2025-09-06 12:06:58 +02:00 committed by GitHub
commit c2f72bbf99
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 926 additions and 436 deletions

View file

@ -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<SettingsScreen>(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<EquipmentProfilePicker, EquipmentProfile>(mockEquipmentProfiles[0].name);
await tester.openPickerAndSelect<EquipmentProfilePicker, IEquipmentProfile>(mockEquipmentProfiles[0].name);
await tester.openPickerAndSelect<FilmPicker, Film>(mockFilms[0].name);
await tester.openPickerAndSelect<IsoValuePicker, IsoValue>('400');
expectPickerTitle<EquipmentProfilePicker>(mockEquipmentProfiles[0].name);
@ -122,7 +121,7 @@ void testE2E(String description) {
// );
/// Select another lens without ND
await tester.openPickerAndSelect<EquipmentProfilePicker, EquipmentProfile>(mockEquipmentProfiles[1].name);
await tester.openPickerAndSelect<EquipmentProfilePicker, IEquipmentProfile>(mockEquipmentProfiles[1].name);
await tester.openPickerAndSelect<NdValuePicker, NdValue>('None');
await _expectMeteringStateAndMeasure(
tester,
@ -152,8 +151,7 @@ void testE2E(String description) {
/// Delete profile
await tester.openSettings();
await tester.tapDescendantTextOf<SettingsScreen>(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);

View file

@ -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();

View file

@ -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<SettingsScreen>(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);

View file

@ -15,6 +15,7 @@ class _MockGeolocationService extends Mock implements GeolocationService {}
class MockIAPProviders extends StatefulWidget {
final TogglableMap<EquipmentProfile> equipmentProfiles;
final TogglableMap<PinholeEquipmentProfile> pinholeEquipmentProfiles;
final String selectedEquipmentProfileId;
final TogglableMap<Film> predefinedFilms;
final TogglableMap<FilmExponential> customFilms;
@ -23,6 +24,7 @@ class MockIAPProviders extends StatefulWidget {
MockIAPProviders({
TogglableMap<EquipmentProfile>? equipmentProfiles,
TogglableMap<PinholeEquipmentProfile>? pinholeEquipmentProfiles,
this.selectedEquipmentProfileId = '',
TogglableMap<Film>? predefinedFilms,
TogglableMap<FilmExponential>? 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<MockIAPProviders> {
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<EquipmentProfile>())).thenAnswer((_) async {});
when(() => mockIapStorageService.addPinholeEquipmentProfile(any<PinholeEquipmentProfile>()))
.thenAnswer((_) async {});
when(
() => mockIapStorageService.updateEquipmentProfile(
id: any<String>(named: 'id'),
@ -62,7 +70,14 @@ class _MockIAPProvidersState extends State<MockIAPProviders> {
isUsed: any<bool>(named: 'isUsed'),
),
).thenAnswer((_) async {});
when(
() => mockIapStorageService.updatePinholeEquipmentProfile(
id: any<String>(named: 'id'),
name: any<String>(named: 'name'),
),
).thenAnswer((_) async {});
when(() => mockIapStorageService.deleteEquipmentProfile(any<String>())).thenAnswer((_) async {});
when(() => mockIapStorageService.deletePinholeEquipmentProfile(any<String>())).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),

View file

@ -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<void> 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<void> editEquipmentProfile(String name) async {
await tap(find.byIcon(Icons.edit_outlined).at(mockProfiles.indexWhere((p) => p.name == name)));
await pumpAndSettle();
}
}

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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": "Выберите диапазон выдержек для вашей камеры",

View file

@ -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": "选择相机快门范围",

View file

@ -34,11 +34,10 @@ class EquipmentProfilesProvider extends StatefulWidget {
}
class EquipmentProfilesProviderState extends State<EquipmentProfilesProvider> {
final TogglableMap<EquipmentProfile> _customProfiles = {};
final TogglableMap<IEquipmentProfile> _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<EquipmentProfilesProvider> {
@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<EquipmentProfilesProvider> {
Future<void> _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<void> addProfile(EquipmentProfile profile) async {
await widget.storageService.addEquipmentProfile(profile);
_customProfiles[profile.id] = (value: profile, isUsed: true);
Future<void> 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<void> 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<void> 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<void> deleteProfile(EquipmentProfile profile) async {
await widget.storageService.deleteEquipmentProfile(profile.id);
Future<void> 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<void> toggleProfile(EquipmentProfile profile, bool enabled) async {
if (_customProfiles.containsKey(profile.id)) {
_customProfiles[profile.id] = (value: profile, isUsed: enabled);
Future<void> 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<EquipmentProfilesProvider> {
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<EquipmentProfile> profiles;
final EquipmentProfile selected;
final TogglableMap<IEquipmentProfile> 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<EquipmentProfile> of(BuildContext context) {
static List<IEquipmentProfile> of(BuildContext context) {
final model =
InheritedModel.inheritFrom<EquipmentProfiles>(context, aspect: _EquipmentProfilesModelAspect.profiles)!;
return List<EquipmentProfile>.from(
return List<IEquipmentProfile>.from(
[
EquipmentProfilesProvider.defaultProfile,
...model.profiles.values.map((p) => p.value),
@ -156,10 +192,10 @@ class EquipmentProfiles extends InheritedModel<_EquipmentProfilesModelAspect> {
);
}
static List<EquipmentProfile> inUseOf(BuildContext context) {
static List<IEquipmentProfile> inUseOf(BuildContext context) {
final model =
InheritedModel.inheritFrom<EquipmentProfiles>(context, aspect: _EquipmentProfilesModelAspect.profilesInUse)!;
return List<EquipmentProfile>.from(
return List<IEquipmentProfile>.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<EquipmentProfiles>(context, aspect: _EquipmentProfilesModelAspect.selected)!
.selected;
}

View file

@ -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<EquipmentProfileEditEvent, EquipmentProfileEditState> {
sealed class IEquipmentProfileEditBloc<T extends IEquipmentProfile>
extends Bloc<IEquipmentProfileEditEvent<T>, EquipmentProfileEditState<T>> {
@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<T>(
profile: profile,
hasChanges: false,
isValid: true,
),
) {
on<EquipmentProfileEditEvent>(
(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<IEquipmentProfileEditEvent<T>>(mapEventToState);
}
@protected
@mustCallSuper
Future<void> mapEventToState(IEquipmentProfileEditEvent<T> 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<T> createProfile(String id);
@protected
void emitProfile(T profile, Emitter emit) {
emit(
state.copyWith(
profile: profile,
hasChanges: _hasChanges(profile),
isValid: _isValid(profile),
),
);
}
Future<void> _onSave(EquipmentProfileSaveEvent<T> _, 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<void> _onCopy(EquipmentProfileCopyEvent<T> _, Emitter emit) async {
emit(state.copyWith(isLoading: true));
emit(state.copyWith(isLoading: false, profileToCopy: state.profile));
}
Future<void> _onDelete(EquipmentProfileDeleteEvent<T> _, 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<EquipmentProfile> {
EquipmentProfileEditBloc(
super.profilesProvider, {
required super.profile,
required super.isEdit,
});
@override
Future<void> mapEventToState(IEquipmentProfileEditEvent<EquipmentProfile> 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<EquipmentProfile> 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<void> _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<void> _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<void> _onShutterSpeedValuesChanged(EquipmentProfileShutterSpeedValuesChangedEvent event, Emitter emit) async {
_newEquipmentProfile = _newEquipmentProfile.copyWith(shutterSpeedValues: event.shutterSpeedValues);
emitProfile(state.profile.copyWith(shutterSpeedValues: event.shutterSpeedValues), emit);
}
Future<void> _onIsoValuesChanged(EquipmentProfileIsoValuesChangedEvent event, Emitter emit) async {
emitProfile(state.profile.copyWith(isoValues: event.isoValues), emit);
}
Future<void> _onNdValuesChanged(EquipmentProfileNdValuesChangedEvent event, Emitter emit) async {
emitProfile(state.profile.copyWith(ndValues: event.ndValues), emit);
}
Future<void> _onLensZoomChanged(EquipmentProfileLensZoomChangedEvent event, Emitter emit) async {
emitProfile(state.profile.copyWith(lensZoom: event.lensZoom), emit);
}
Future<void> _onExposureOffsetChanged(EquipmentProfileExposureOffsetChangedEvent event, Emitter emit) async {
emitProfile(state.profile.copyWith(exposureOffset: event.exposureOffset), emit);
}
}
class PinholeEquipmentProfileEditBloc extends IEquipmentProfileEditBloc<PinholeEquipmentProfile> {
PinholeEquipmentProfileEditBloc(
super.profilesProvider, {
required super.profile,
required super.isEdit,
});
@override
Future<void> mapEventToState(IEquipmentProfileEditEvent<PinholeEquipmentProfile> 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<PinholeEquipmentProfile> 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<void> _onNameChanged(EquipmentProfileNameChangedEvent event, Emitter emit) async {
emitProfile(state.profile.copyWith(name: event.name), emit);
}
Future<void> _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<void> _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<void> _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<void> _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<void> _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<void> _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<void> _onCopy(EquipmentProfileCopyEvent _, Emitter emit) async {
emit(state.copyWith(isLoading: true));
emit(state.copyWith(isLoading: false, profileToCopy: _originalEquipmentProfile));
}
Future<void> _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);
}
}

View file

@ -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<double?> onChanged;
const EquipmentProfileApertureInput({
super.key,
required this.value,
required this.onChanged,
});
@override
State<EquipmentProfileApertureInput> createState() => _EquipmentProfileApertureInputState();
}
class _EquipmentProfileApertureInputState extends State<EquipmentProfileApertureInput> {
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;
}
}

View file

@ -1,59 +1,65 @@
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
sealed class EquipmentProfileEditEvent {
const EquipmentProfileEditEvent();
sealed class IEquipmentProfileEditEvent<T extends IEquipmentProfile> {
const IEquipmentProfileEditEvent();
}
class EquipmentProfileNameChangedEvent extends EquipmentProfileEditEvent {
class EquipmentProfileNameChangedEvent<T extends IEquipmentProfile> extends IEquipmentProfileEditEvent<T> {
final String name;
const EquipmentProfileNameChangedEvent(this.name);
}
class EquipmentProfileIsoValuesChangedEvent extends EquipmentProfileEditEvent {
class EquipmentProfileIsoValuesChangedEvent<T extends IEquipmentProfile> extends IEquipmentProfileEditEvent<T> {
final List<IsoValue> isoValues;
const EquipmentProfileIsoValuesChangedEvent(this.isoValues);
}
class EquipmentProfileNdValuesChangedEvent extends EquipmentProfileEditEvent {
class EquipmentProfileNdValuesChangedEvent<T extends IEquipmentProfile> extends IEquipmentProfileEditEvent<T> {
final List<NdValue> ndValues;
const EquipmentProfileNdValuesChangedEvent(this.ndValues);
}
class EquipmentProfileApertureValuesChangedEvent extends EquipmentProfileEditEvent {
class EquipmentProfileApertureValuesChangedEvent extends IEquipmentProfileEditEvent<EquipmentProfile> {
final List<ApertureValue> apertureValues;
const EquipmentProfileApertureValuesChangedEvent(this.apertureValues);
}
class EquipmentProfileShutterSpeedValuesChangedEvent extends EquipmentProfileEditEvent {
class EquipmentProfileApertureValueChangedEvent extends IEquipmentProfileEditEvent<PinholeEquipmentProfile> {
final double? aperture;
const EquipmentProfileApertureValueChangedEvent(this.aperture);
}
class EquipmentProfileShutterSpeedValuesChangedEvent extends IEquipmentProfileEditEvent<EquipmentProfile> {
final List<ShutterSpeedValue> shutterSpeedValues;
const EquipmentProfileShutterSpeedValuesChangedEvent(this.shutterSpeedValues);
}
class EquipmentProfileLensZoomChangedEvent extends EquipmentProfileEditEvent {
class EquipmentProfileLensZoomChangedEvent<T extends IEquipmentProfile> extends IEquipmentProfileEditEvent<T> {
final double lensZoom;
const EquipmentProfileLensZoomChangedEvent(this.lensZoom);
}
class EquipmentProfileExposureOffsetChangedEvent extends EquipmentProfileEditEvent {
class EquipmentProfileExposureOffsetChangedEvent<T extends IEquipmentProfile> extends IEquipmentProfileEditEvent<T> {
final double exposureOffset;
const EquipmentProfileExposureOffsetChangedEvent(this.exposureOffset);
}
class EquipmentProfileSaveEvent extends EquipmentProfileEditEvent {
class EquipmentProfileSaveEvent<T extends IEquipmentProfile> extends IEquipmentProfileEditEvent<T> {
const EquipmentProfileSaveEvent();
}
class EquipmentProfileCopyEvent extends EquipmentProfileEditEvent {
class EquipmentProfileCopyEvent<T extends IEquipmentProfile> extends IEquipmentProfileEditEvent<T> {
const EquipmentProfileCopyEvent();
}
class EquipmentProfileDeleteEvent extends EquipmentProfileEditEvent {
class EquipmentProfileDeleteEvent<T extends IEquipmentProfile> extends IEquipmentProfileEditEvent<T> {
const EquipmentProfileDeleteEvent();
}

View file

@ -7,11 +7,14 @@ import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
enum EquipmentProfileEditType { add, edit }
class EquipmentProfileEditArgs {
class EquipmentProfileEditArgs<T extends IEquipmentProfile> {
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<EquipmentProfile, EquipmentProfileEditBloc>(
create: (_) => EquipmentProfileEditBloc(
EquipmentProfilesProvider.of(context),
profile: profile,
isEdit: _isEdit,
),
isEdit: _isEdit,
);
case final PinholeEquipmentProfile profile:
return _IEquipmentProfileBlocProvider<PinholeEquipmentProfile, PinholeEquipmentProfileEditBloc>(
create: (_) => PinholeEquipmentProfileEditBloc(
EquipmentProfilesProvider.of(context),
profile: profile,
isEdit: _isEdit,
),
isEdit: _isEdit,
);
}
}
}
class _IEquipmentProfileBlocProvider<T extends IEquipmentProfile, V extends IEquipmentProfileEditBloc<T>>
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<IEquipmentProfileEditBloc<T>>(
create: create,
child: Builder(
builder: (context) => BlocProvider<V>.value(
value: context.read<IEquipmentProfileEditBloc<T>>() as V,
child: EquipmentProfileEditScreen<T>(isEdit: isEdit),
),
),
child: EquipmentProfileEditScreen(isEdit: _isEdit),
);
}
}

View file

@ -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<T extends IEquipmentProfile> extends StatefulWidget {
final bool isEdit;
const EquipmentProfileEditScreen({
@ -26,13 +27,13 @@ class EquipmentProfileEditScreen extends StatefulWidget {
});
@override
State<EquipmentProfileEditScreen> createState() => _EquipmentProfileEditScreenState();
State<EquipmentProfileEditScreen<T>> createState() => _EquipmentProfileEditScreenState<T>();
}
class _EquipmentProfileEditScreenState extends State<EquipmentProfileEditScreen> {
class _EquipmentProfileEditScreenState<T extends IEquipmentProfile> extends State<EquipmentProfileEditScreen<T>> {
@override
Widget build(BuildContext context) {
return BlocConsumer<EquipmentProfileEditBloc, EquipmentProfileEditState>(
return BlocConsumer<IEquipmentProfileEditBloc<T>, EquipmentProfileEditState<T>>(
listenWhen: (previous, current) => previous.isLoading != current.isLoading,
listener: (context, state) {
if (state.isLoading) {
@ -41,9 +42,9 @@ class _EquipmentProfileEditScreenState extends State<EquipmentProfileEditScreen>
if (state.profileToCopy != null) {
Navigator.of(context).pushReplacementNamed(
NavigationRoutes.equipmentProfileEditScreen.name,
arguments: EquipmentProfileEditArgs(
arguments: EquipmentProfileEditArgs<T>(
editType: EquipmentProfileEditType.add,
profile: state.profileToCopy,
profile: state.profileToCopy!,
),
);
} else {
@ -57,33 +58,34 @@ class _EquipmentProfileEditScreenState extends State<EquipmentProfileEditScreen>
child: SliverScreen(
title: Text(widget.isEdit ? S.of(context).editEquipmentProfileTitle : S.of(context).addEquipmentProfileTitle),
appBarActions: [
BlocBuilder<EquipmentProfileEditBloc, EquipmentProfileEditState>(
buildWhen: (previous, current) => previous.canSave != current.canSave,
BlocBuilder<IEquipmentProfileEditBloc<T>, EquipmentProfileEditState<T>>(
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<EquipmentProfileEditBloc>().add(const EquipmentProfileSaveEvent());
context.read<IEquipmentProfileEditBloc<T>>().add(const EquipmentProfileSaveEvent());
}
: null,
icon: const Icon(Icons.save_outlined),
),
),
if (widget.isEdit)
BlocBuilder<EquipmentProfileEditBloc, EquipmentProfileEditState>(
buildWhen: (previous, current) => previous.canSave != current.canSave,
BlocBuilder<IEquipmentProfileEditBloc<T>, EquipmentProfileEditState<T>>(
buildWhen: (previous, current) => previous.isValid != current.isValid,
builder: (context, state) => IconButton(
onPressed: state.canSave
? null
: () {
context.read<EquipmentProfileEditBloc>().add(const EquipmentProfileCopyEvent());
},
onPressed: state.isValid
? () {
context.read<IEquipmentProfileEditBloc<T>>().add(const EquipmentProfileCopyEvent());
}
: null,
icon: const Icon(Icons.copy_outlined),
),
),
if (widget.isEdit)
IconButton(
onPressed: () {
context.read<EquipmentProfileEditBloc>().add(const EquipmentProfileDeleteEvent());
context.read<IEquipmentProfileEditBloc<T>>().add(const EquipmentProfileDeleteEvent());
},
icon: const Icon(Icons.delete_outlined),
),
@ -102,15 +104,19 @@ class _EquipmentProfileEditScreenState extends State<EquipmentProfileEditScreen>
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<T>(),
if (state.profile is PinholeEquipmentProfile)
const _ApertureValueListTileBuilder()
else ...[
const _ApertureValuesListTileBuilder(),
const _ShutterSpeedValuesListTileBuilder(),
],
_IsoValuesListTileBuilder<T>(),
_NdValuesListTileBuilder<T>(),
_LensZoomListTileBuilder<T>(),
_ExposureOffsetListTileBuilder<T>(),
],
),
),
@ -126,12 +132,13 @@ class _EquipmentProfileEditScreenState extends State<EquipmentProfileEditScreen>
}
}
class _NameFieldBuilder extends StatelessWidget {
class _NameFieldBuilder<T extends IEquipmentProfile> extends StatelessWidget {
const _NameFieldBuilder();
@override
Widget build(BuildContext context) {
return BlocBuilder<EquipmentProfileEditBloc, EquipmentProfileEditState>(
return BlocBuilder<IEquipmentProfileEditBloc<T>, EquipmentProfileEditState<T>>(
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<EquipmentProfileEditBloc>().add(EquipmentProfileNameChangedEvent(value));
context.read<IEquipmentProfileEditBloc<T>>().add(EquipmentProfileNameChangedEvent<T>(value));
},
),
),
@ -155,40 +162,40 @@ class _NameFieldBuilder extends StatelessWidget {
}
}
class _IsoValuesListTileBuilder extends StatelessWidget {
class _IsoValuesListTileBuilder<T extends IEquipmentProfile> extends StatelessWidget {
const _IsoValuesListTileBuilder();
@override
Widget build(BuildContext context) {
return BlocBuilder<EquipmentProfileEditBloc, EquipmentProfileEditState>(
return BlocBuilder<IEquipmentProfileEditBloc<T>, EquipmentProfileEditState<T>>(
builder: (context, state) => FilterListTile<IsoValue>(
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<EquipmentProfileEditBloc>().add(EquipmentProfileIsoValuesChangedEvent(value));
context.read<IEquipmentProfileEditBloc<T>>().add(EquipmentProfileIsoValuesChangedEvent<T>(value));
},
),
);
}
}
class _NdValuesListTileBuilder extends StatelessWidget {
class _NdValuesListTileBuilder<T extends IEquipmentProfile> extends StatelessWidget {
const _NdValuesListTileBuilder();
@override
Widget build(BuildContext context) {
return BlocBuilder<EquipmentProfileEditBloc, EquipmentProfileEditState>(
return BlocBuilder<IEquipmentProfileEditBloc<T>, EquipmentProfileEditState<T>>(
builder: (context, state) => FilterListTile<NdValue>(
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<EquipmentProfileEditBloc>().add(EquipmentProfileNdValuesChangedEvent(value));
context.read<IEquipmentProfileEditBloc<T>>().add(EquipmentProfileNdValuesChangedEvent<T>(value));
},
),
);
@ -200,13 +207,13 @@ class _ApertureValuesListTileBuilder extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<EquipmentProfileEditBloc, EquipmentProfileEditState>(
return BlocBuilder<EquipmentProfileEditBloc, EquipmentProfileEditState<EquipmentProfile>>(
builder: (context, state) => RangePickerListTile<ApertureValue>(
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<EquipmentProfileEditBloc>().add(EquipmentProfileApertureValuesChangedEvent(value));
},
@ -215,18 +222,34 @@ class _ApertureValuesListTileBuilder extends StatelessWidget {
}
}
class _ApertureValueListTileBuilder extends StatelessWidget {
const _ApertureValueListTileBuilder();
@override
Widget build(BuildContext context) {
return BlocBuilder<PinholeEquipmentProfileEditBloc, EquipmentProfileEditState<PinholeEquipmentProfile>>(
builder: (context, state) => EquipmentProfileApertureInput(
value: state.profile.aperture,
onChanged: (value) {
context.read<PinholeEquipmentProfileEditBloc>().add(EquipmentProfileApertureValueChangedEvent(value));
},
),
);
}
}
class _ShutterSpeedValuesListTileBuilder extends StatelessWidget {
const _ShutterSpeedValuesListTileBuilder();
@override
Widget build(BuildContext context) {
return BlocBuilder<EquipmentProfileEditBloc, EquipmentProfileEditState>(
return BlocBuilder<EquipmentProfileEditBloc, EquipmentProfileEditState<EquipmentProfile>>(
builder: (context, state) => RangePickerListTile<ShutterSpeedValue>(
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<T extends IEquipmentProfile> extends StatelessWidget {
const _LensZoomListTileBuilder();
@override
Widget build(BuildContext context) {
return BlocBuilder<EquipmentProfileEditBloc, EquipmentProfileEditState>(
return BlocBuilder<IEquipmentProfileEditBloc<T>, EquipmentProfileEditState<T>>(
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<EquipmentProfileEditBloc>().add(EquipmentProfileLensZoomChangedEvent(value));
context.read<IEquipmentProfileEditBloc<T>>().add(EquipmentProfileLensZoomChangedEvent<T>(value));
},
),
);
}
}
class _ExposureOffsetListTileBuilder extends StatelessWidget {
class _ExposureOffsetListTileBuilder<T extends IEquipmentProfile> extends StatelessWidget {
const _ExposureOffsetListTileBuilder();
@override
Widget build(BuildContext context) {
return BlocBuilder<EquipmentProfileEditBloc, EquipmentProfileEditState>(
return BlocBuilder<IEquipmentProfileEditBloc<T>, EquipmentProfileEditState<T>>(
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<EquipmentProfileEditBloc>().add(EquipmentProfileExposureOffsetChangedEvent(value));
context.read<IEquipmentProfileEditBloc<T>>().add(EquipmentProfileExposureOffsetChangedEvent<T>(value));
},
),
);

View file

@ -1,52 +1,32 @@
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
class EquipmentProfileEditState {
final String name;
final List<ApertureValue> apertureValues;
final List<NdValue> ndValues;
final List<ShutterSpeedValue> shutterSpeedValues;
final List<IsoValue> isoValues;
final double lensZoom;
final double exposureOffset;
final bool canSave;
class EquipmentProfileEditState<T extends IEquipmentProfile> {
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<ApertureValue>? apertureValues,
List<NdValue>? ndValues,
List<ShutterSpeedValue>? shutterSpeedValues,
List<IsoValue>? isoValues,
double? lensZoom,
double? exposureOffset,
bool? canSave,
EquipmentProfileEditState<T> 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<T>(
profile: profile ?? this.profile,
profileToCopy: profileToCopy ?? this.profileToCopy,
isValid: isValid ?? this.isValid,
hasChanges: hasChanges ?? this.hasChanges,
isLoading: isLoading ?? this.isLoading,
);
}

View file

@ -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<EquipmentProfileType?> show(BuildContext context) {
return showDialog<EquipmentProfileType>(
context: context,
builder: (_) => const EquipmentProfilesTypePicker._(),
);
}
@override
Widget build(BuildContext context) {
return DialogPicker<EquipmentProfileType>(
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,
},
);
}
}

View file

@ -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<EquipmentProfilesScreen> 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<EquipmentProfilesScreen> with
}
class _EquipmentProfilesListBuilder extends StatelessWidget {
final List<EquipmentProfile> values;
final void Function(EquipmentProfile profile) onEdit;
final void Function(EquipmentProfile profile, bool value) onCheckbox;
final List<IEquipmentProfile> 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),

View file

@ -74,10 +74,10 @@ class LogbookPhotoEditBloc extends Bloc<LogbookPhotoEditEvent, LogbookPhotoEditS
}
Future<void> _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(),
),
);

View file

@ -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 {

View file

@ -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<LogbookPhotoEditBloc>().add(LogbookPhotoEquipmentProfileChangedEvent(value.value));
context.read<LogbookPhotoEditBloc>().add(LogbookPhotoEquipmentProfileChangedEvent(value.value?.id));
},
),
);

View file

@ -19,7 +19,7 @@ class MeasureEvent extends ScreenEvent {
}
class EquipmentProfileChangedEvent extends ScreenEvent {
final EquipmentProfile profile;
final IEquipmentProfile profile;
const EquipmentProfileChangedEvent(this.profile);
}

View file

@ -21,7 +21,7 @@ class MeasureState extends SourceState {
}
class EquipmentProfileChangedState extends SourceState {
final EquipmentProfile profile;
final IEquipmentProfile profile;
const EquipmentProfileChangedState(this.profile);
}

View file

@ -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;
}

View file

@ -10,13 +10,13 @@ class EquipmentProfilePicker extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AnimatedDialogPicker<EquipmentProfile>(
return AnimatedDialogPicker<IEquipmentProfile>(
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,

View file

@ -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 '-';
}
}
}

View file

@ -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,

View file

@ -5,7 +5,7 @@ sealed class MeteringEvent {
}
class EquipmentProfileChangedEvent extends MeteringEvent {
final EquipmentProfile equipmentProfileData;
final IEquipmentProfile equipmentProfileData;
const EquipmentProfileChangedEvent(this.equipmentProfileData);
}

View file

@ -169,12 +169,22 @@ class MeteringContainerBuidler extends StatelessWidget {
static List<ExposurePair> 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;
}
}

View file

@ -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<EquipmentProfile> onDidChangeDependencies;
final ValueChanged<IEquipmentProfile> onDidChangeDependencies;
final Widget child;
const EquipmentProfileListener({

View file

@ -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());

View file

@ -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<TextInputFormatter>? inputFormatters;
final String? Function(String)? validator;
final Widget? leading;
final int? maxLength;
final int? maxLines;
@ -62,6 +64,8 @@ class _LightmeterTextFieldState extends State<LightmeterTextField> {
validator: (value) {
if (value == null || value.isEmpty) {
return '';
} else if (widget.validator != null) {
return widget.validator!(value);
} else {
return null;
}

View file

@ -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"

View file

@ -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

View file

@ -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<void> _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<void> scrollToExposurePair({
double ev = mockPhotoEv100,
EquipmentProfile equipmentProfile = defaultEquipmentProfile,
required List<ExposurePair> 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,

View file

@ -16,7 +16,7 @@ void main() {
});
setUp(() {
registerFallbackValue(_customProfiles.first);
registerFallbackValue(_mockEquipmentProfiles.first);
when(() => storageService.addEquipmentProfile(any<EquipmentProfile>())).thenAnswer((_) async {});
when(
() => storageService.updateEquipmentProfile(
@ -26,7 +26,10 @@ void main() {
),
).thenAnswer((_) async {});
when(() => storageService.deleteEquipmentProfile(any<String>())).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<EquipmentProfile>())).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<EquipmentProfile> _customProfiles = [
final List<IEquipmentProfile> _mockProfiles = [..._mockEquipmentProfiles, ..._mockPinholeEquipmentProfiles];
final List<EquipmentProfile> _mockEquipmentProfiles = [
const EquipmentProfile(
id: '1',
name: 'Test 1',
@ -291,3 +296,42 @@ final List<EquipmentProfile> _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),
],
),
];

View file

@ -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({}));

View file

@ -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<EquipmentProfilePicker>();
expect(find.byIcon(Icons.camera_alt_outlined), findsOneWidget);
expectDialogPickerText<EquipmentProfile>(S.current.equipmentProfile);
expectDialogPickerText<IEquipmentProfile>(S.current.equipmentProfile);
},
);
@ -62,7 +65,7 @@ void main() {
await pumpApplication(tester);
expectReadingValueContainerText(S.current.none);
await tester.openAnimatedPicker<EquipmentProfilePicker>();
expectRadioListTile<EquipmentProfile>(S.current.none, isSelected: true);
expectRadioListTile<IEquipmentProfile>(S.current.none, isSelected: true);
},
);
@ -73,7 +76,18 @@ void main() {
await pumpApplication(tester);
expectReadingValueContainerText(_mockEquipmentProfiles.first.name);
await tester.openAnimatedPicker<EquipmentProfilePicker>();
expectRadioListTile<EquipmentProfile>(_mockEquipmentProfiles.first.name, isSelected: true);
expectRadioListTile<IEquipmentProfile>(_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<EquipmentProfilePicker>();
expectRadioListTile<IEquipmentProfile>(_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<EquipmentProfilePicker>();
expectRadioListTile<EquipmentProfile>(S.current.none, isSelected: true);
expectRadioListTile<EquipmentProfile>(_mockEquipmentProfiles[1].name);
expectRadioListTile<IEquipmentProfile>(S.current.none, isSelected: true);
expectRadioListTile<IEquipmentProfile>(_mockEquipmentProfiles[1].name);
expectRadioListTile<IEquipmentProfile>(_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),
],
),
];

View file

@ -13,7 +13,7 @@ class _MockEquipmentProfilesStorageService extends Mock implements IapStorageSer
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final storageService = _MockEquipmentProfilesStorageService();
final onDidChangeDependencies = MockValueChanged<EquipmentProfile>();
final onDidChangeDependencies = MockValueChanged<IEquipmentProfile>();
setUp(() {
registerFallbackValue(_customProfiles.first);
@ -26,6 +26,7 @@ void main() {
).thenAnswer((_) async {});
when(() => storageService.deleteEquipmentProfile(any<String>())).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);
},