Compare commits

...

5 commits

Author SHA1 Message Date
Vadim
a5f211ad4b fixed integration tests 2025-09-10 09:33:20 +02:00
Vadim
7b46d4342c expanded logbook tests 2025-09-09 08:50:41 +02:00
Vadim
56ab8aa85a allow to hide pinhole shutter speed 2025-09-08 17:09:26 +02:00
Vadim
c40354c62b discard aperture when unselecting a pinhole profile 2025-09-08 17:05:17 +02:00
Vadim
232a9316cd set aperture when saving a pinhole photo 2025-09-08 17:00:14 +02:00
16 changed files with 295 additions and 130 deletions

View file

@ -63,23 +63,13 @@ void testLogbook(String description) {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await tester.openPickerAndSelect<ApertureValue>(S.current.apertureValue, 'f/5.6'); await tester.openPickerAndSelect<ApertureValue>(S.current.apertureValue, 'f/5.6');
await tester.openPickerAndSelect<ShutterSpeedValue>(S.current.shutterSpeedValue, '1/125'); await tester.openPickerAndSelect<ShutterSpeedValue>(S.current.shutterSpeedValue, '1/125');
expect( _expectPickerListTileValue(
find.descendant( S.current.equipmentProfile,
of: find.byWidgetPredicate( mockEquipmentProfiles.first.name,
(widget) => widget is PickerListTile && widget.title == S.current.equipmentProfile,
),
matching: find.text(mockEquipmentProfiles.first.name),
),
findsOneWidget,
); );
expect( _expectPickerListTileValue(
find.descendant( S.current.film,
of: find.byWidgetPredicate( mockFilms.first.name,
(widget) => widget is PickerListTile && widget.title == S.current.film,
),
matching: find.text(mockFilms.first.name),
),
findsOneWidget,
); );
await tester.openPickerAndSelect<Film>(S.current.film, S.current.notSet); await tester.openPickerAndSelect<Film>(S.current.film, S.current.notSet);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
@ -117,14 +107,9 @@ void testLogbook(String description) {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
/// Verify the edits were saved /// Verify the edits were saved
expect( _expectPickerListTileValue(
find.descendant( S.current.equipmentProfile,
of: find.byWidgetPredicate( S.current.notSet,
(widget) => widget is PickerListTile && widget.title == S.current.equipmentProfile,
),
matching: find.text(S.current.notSet),
),
findsOneWidget,
); );
expect(find.text('Test note'), findsOneWidget); expect(find.text('Test note'), findsOneWidget);
expect(find.text('f/5.6'), findsOneWidget); expect(find.text('f/5.6'), findsOneWidget);
@ -141,6 +126,138 @@ void testLogbook(String description) {
); );
} }
@isTest
void testLogbookEquipmentProfileChanges(String description) {
setUp(() async {
SharedPreferences.setMockInitialValues({
UserPreferencesService.evSourceTypeKey: EvSourceType.camera.index,
UserPreferencesService.seenChangelogVersionKey: await const PlatformUtils().version,
});
});
testWidgets(
description,
(tester) async {
await tester.pumpApplication(
selectedEquipmentProfileId: mockEquipmentProfiles.first.id,
selectedFilmId: mockFilms.first.id,
customFilms: {},
);
await tester.takePhoto();
await tester.openSettings();
await tester.tapDescendantTextOf<SettingsScreen>(S.current.logbook);
await tester.tap(find.byType(LogbookPhotoGridTile).first);
await tester.pumpAndSettle();
await tester.ensureVisible(find.text(mockEquipmentProfiles.first.name));
_expectPickerListTileValue(
S.current.equipmentProfile,
mockEquipmentProfiles.first.name,
);
await tester.openPickerAndSelect<IEquipmentProfile>(S.current.equipmentProfile, mockEquipmentProfiles[1].name);
await tester.pumpAndSettle();
_expectPickerListTileValue(
S.current.equipmentProfile,
mockEquipmentProfiles[1].name,
);
await tester.openPickerAndSelect<IEquipmentProfile>(
S.current.equipmentProfile,
mockPinholeEquipmentProfiles.first.name,
);
await tester.pumpAndSettle();
_expectPickerListTileValue(
S.current.equipmentProfile,
mockPinholeEquipmentProfiles.first.name,
);
_expectPickerListTileValue(
S.current.apertureValue,
ApertureValue(mockPinholeEquipmentProfiles.first.aperture, StopType.full).toString(),
reason: 'Aperture value must be automatically set when selecting a pinhole profile',
);
final aperturePickerFinder = find.descendant(
of: find.byWidgetPredicate(
(widget) => widget is PickerListTile && widget.title == S.current.apertureValue,
),
matching: find.byType(PickerListTile),
);
expect(aperturePickerFinder, findsOneWidget);
await tester.tap(aperturePickerFinder);
await tester.pumpAndSettle();
expect(
find.byType(DialogPicker<Optional<ApertureValue>>),
findsNothing,
reason: 'Aperture picker dialog must not open when pinhole profile is selected',
);
await tester.openPickerAndSelect<IEquipmentProfile>(
S.current.equipmentProfile,
mockPinholeEquipmentProfiles[1].name,
);
await tester.pumpAndSettle();
_expectPickerListTileValue(
S.current.equipmentProfile,
mockPinholeEquipmentProfiles[1].name,
);
_expectPickerListTileValue(
S.current.apertureValue,
ApertureValue(mockPinholeEquipmentProfiles[1].aperture, StopType.full).toString(),
reason: 'Aperture value must be updated when switching to a different pinhole profile',
);
await tester.tap(aperturePickerFinder);
await tester.pumpAndSettle();
expect(
find.byType(DialogPicker<Optional<ApertureValue>>),
findsNothing,
reason: 'Aperture picker dialog must not open when switching between pinhole profiles',
);
await tester.openPickerAndSelect<IEquipmentProfile>(S.current.equipmentProfile, mockEquipmentProfiles.first.name);
await tester.pumpAndSettle();
_expectPickerListTileValue(
S.current.equipmentProfile,
mockEquipmentProfiles.first.name,
);
_expectPickerListTileValue(
S.current.apertureValue,
S.current.notSet,
reason: 'Aperture value must be cleared when switching from pinhole to regular profile',
);
await tester.tap(aperturePickerFinder);
await tester.pumpAndSettle();
expect(
find.byType(DialogPicker<Optional<ApertureValue>>),
findsOneWidget,
reason: 'Aperture picker dialog must open when regular profile is selected',
);
await tester.tap(find.text('Cancel'));
await tester.pumpAndSettle();
await tester.openPickerAndSelect<IEquipmentProfile>(S.current.equipmentProfile, S.current.notSet);
await tester.pumpAndSettle();
_expectPickerListTileValue(
S.current.equipmentProfile,
S.current.notSet,
);
await tester.tap(find.byIcon(Icons.save_outlined));
await tester.pumpAndSettle();
await tester.tap(find.byType(LogbookPhotoGridTile).first);
await tester.pumpAndSettle();
_expectPickerListTileValue(
S.current.equipmentProfile,
S.current.notSet,
);
},
);
}
extension on WidgetTester { extension on WidgetTester {
Future<void> openPickerAndSelect<V>(String title, String valueToSelect) async { Future<void> openPickerAndSelect<V>(String title, String valueToSelect) async {
await tap(find.text(title)); await tap(find.text(title));
@ -156,3 +273,16 @@ extension on WidgetTester {
await tapSelectButton(); await tapSelectButton();
} }
} }
void _expectPickerListTileValue(String title, String value, {String? reason}) {
expect(
find.descendant(
of: find.byWidgetPredicate(
(widget) => widget is PickerListTile && widget.title == title,
),
matching: find.text(value),
),
findsOneWidget,
reason: reason,
);
}

View file

@ -59,7 +59,7 @@ void testToggleLayoutFeatures(String description) {
); );
expectExtremeExposurePairs( expectExtremeExposurePairs(
'f/1.0 - 1/320', 'f/1.0 - 1/320',
'f/45 - 6"', 'f/45 - 6s',
reason: 'Aperture and shutter speed ranges must be reset to default values when equipment profile is reset', reason: 'Aperture and shutter speed ranges must be reset to default values when equipment profile is reset',
); );
expectExposurePairsListItem( expectExposurePairsListItem(
@ -73,7 +73,7 @@ void testToggleLayoutFeatures(String description) {
expectExposurePairsListItem( expectExposurePairsListItem(
tester, tester,
'f/45', 'f/45',
'6"', '6s',
reason: reason:
'Aperture and shutter speed ranges must be reset to default values when equipment profile is reset.', 'Aperture and shutter speed ranges must be reset to default values when equipment profile is reset.',
); );
@ -92,10 +92,10 @@ void testToggleLayoutFeatures(String description) {
(tester) async { (tester) async {
await tester.pumpApplication(); await tester.pumpApplication();
await tester.takePhoto(); await tester.takePhoto();
expectExtremeExposurePairs('f/1.0 - 1/320', 'f/45 - 6"'); expectExtremeExposurePairs('f/1.0 - 1/320', 'f/45 - 6s');
expectExposurePairsListItem(tester, 'f/1.0', '1/320'); expectExposurePairsListItem(tester, 'f/1.0', '1/320');
await tester.scrollToTheLastExposurePair(); await tester.scrollToTheLastExposurePair();
expectExposurePairsListItem(tester, 'f/45', '6"'); expectExposurePairsListItem(tester, 'f/45', '6s');
// Disable layout feature // Disable layout feature
await tester.toggleLayoutFeature(S.current.meteringScreenFeatureExtremeExposurePairs); await tester.toggleLayoutFeature(S.current.meteringScreenFeatureExtremeExposurePairs);
@ -116,7 +116,7 @@ void testToggleLayoutFeatures(String description) {
expectExposurePairsListItem( expectExposurePairsListItem(
tester, tester,
'f/45', 'f/45',
'6"', '6s',
reason: reason:
'Exposure pairs list must not be affected by the visibility of the extreme exposure pairs container.', 'Exposure pairs list must not be affected by the visibility of the extreme exposure pairs container.',
); );
@ -125,7 +125,7 @@ void testToggleLayoutFeatures(String description) {
await tester.toggleLayoutFeature(S.current.meteringScreenFeatureExtremeExposurePairs); await tester.toggleLayoutFeature(S.current.meteringScreenFeatureExtremeExposurePairs);
expectExtremeExposurePairs( expectExtremeExposurePairs(
'f/1.0 - 1/320', 'f/1.0 - 1/320',
'f/45 - 6"', 'f/45 - 6s',
reason: reason:
'Exposure pairs list must not be affected by the visibility of the extreme exposure pairs container.', 'Exposure pairs list must not be affected by the visibility of the extreme exposure pairs container.',
); );
@ -138,10 +138,10 @@ void testToggleLayoutFeatures(String description) {
await tester.pumpApplication(selectedFilmId: mockFilms.first.id); await tester.pumpApplication(selectedFilmId: mockFilms.first.id);
await tester.takePhoto(); await tester.takePhoto();
expectPickerTitle<FilmPicker>(mockFilms.first.name); expectPickerTitle<FilmPicker>(mockFilms.first.name);
expectExtremeExposurePairs('f/1.0 - 1/320', 'f/45 - 12"'); expectExtremeExposurePairs('f/1.0 - 1/320', 'f/45 - 12s');
expectExposurePairsListItem(tester, 'f/1.0', '1/320'); expectExposurePairsListItem(tester, 'f/1.0', '1/320');
await tester.scrollToTheLastExposurePair(); await tester.scrollToTheLastExposurePair();
expectExposurePairsListItem(tester, 'f/45', '12"'); expectExposurePairsListItem(tester, 'f/45', '12s');
// Disable layout feature // Disable layout feature
await tester.toggleLayoutFeature(S.current.meteringScreenFeatureFilmPicker); await tester.toggleLayoutFeature(S.current.meteringScreenFeatureFilmPicker);
@ -153,7 +153,7 @@ void testToggleLayoutFeatures(String description) {
); );
expectExtremeExposurePairs( expectExtremeExposurePairs(
'f/1.0 - 1/320', 'f/1.0 - 1/320',
'f/45 - 6"', 'f/45 - 6s',
reason: 'Shutter speed must not be affected by reciprocity when film is discarded.', reason: 'Shutter speed must not be affected by reciprocity when film is discarded.',
); );
expectExposurePairsListItem( expectExposurePairsListItem(
@ -166,7 +166,7 @@ void testToggleLayoutFeatures(String description) {
expectExposurePairsListItem( expectExposurePairsListItem(
tester, tester,
'f/45', 'f/45',
'6"', '6s',
reason: 'Shutter speed must not be affected by reciprocity when film is discarded.', reason: 'Shutter speed must not be affected by reciprocity when film is discarded.',
); );

View file

@ -19,9 +19,10 @@ void main() {
mockCameraFocalLength(); mockCameraFocalLength();
}); });
testPurchases('Purchase & refund premium features'); testPurchases('Purchase & refund premium features test');
testGuardProTap('Guard Pro tap'); testGuardProTap('Guard Pro tap test');
testToggleLayoutFeatures('Toggle metering screen layout features'); testToggleLayoutFeatures('Toggle metering screen layout features test');
testLogbook('Logbook'); testLogbook('Logbook test');
testE2E('e2e'); testLogbookEquipmentProfileChanges('Logbook equipment profile changes test');
testE2E('e2e test');
} }

View file

@ -1,3 +1,4 @@
import 'dart:collection';
import 'dart:io'; import 'dart:io';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
@ -34,7 +35,7 @@ class LogbookPhotosProvider extends StatefulWidget {
} }
class LogbookPhotosProviderState extends State<LogbookPhotosProvider> { class LogbookPhotosProviderState extends State<LogbookPhotosProvider> {
final Map<String, LogbookPhoto> _photos = {}; final LinkedHashMap<String, LogbookPhoto> _photos = LinkedHashMap();
bool _isEnabled = true; bool _isEnabled = true;
@override @override
@ -90,6 +91,9 @@ class LogbookPhotosProviderState extends State<LogbookPhotosProvider> {
name: path, name: path,
timestamp: DateTime.timestamp(), timestamp: DateTime.timestamp(),
ev: ev100, ev: ev100,
apertureValue: equipmentProfile is PinholeEquipmentProfile
? ApertureValue(equipmentProfile.aperture, StopType.full)
: null,
iso: iso, iso: iso,
nd: nd, nd: nd,
coordinates: coordinates, coordinates: coordinates,

View file

@ -12,8 +12,15 @@ class LogbookPhotoEditBloc extends Bloc<LogbookPhotoEditEvent, LogbookPhotoEditS
LogbookPhotoEditBloc( LogbookPhotoEditBloc(
this.photosProvider, this.photosProvider,
LogbookPhoto photo, LogbookPhoto photo,
IEquipmentProfile? equipmentProfile,
) : _originalPhoto = photo, ) : _originalPhoto = photo,
_newPhoto = photo, _newPhoto = photo,
assert(
equipmentProfile == null ||
equipmentProfile is! PinholeEquipmentProfile ||
photo.apertureValue != null && equipmentProfile.aperture == photo.apertureValue!.rawValue,
"Aperture value must be the same as the equipment profile's aperture value if the equipment profile is a pinhole profile",
),
super( super(
LogbookPhotoEditState( LogbookPhotoEditState(
id: photo.id, id: photo.id,
@ -25,7 +32,7 @@ class LogbookPhotoEditBloc extends Bloc<LogbookPhotoEditEvent, LogbookPhotoEditS
coordinates: photo.coordinates, coordinates: photo.coordinates,
aperture: photo.apertureValue, aperture: photo.apertureValue,
shutterSpeed: photo.shutterSpeedValue, shutterSpeed: photo.shutterSpeedValue,
equipmentProfileId: photo.equipmentProfileId, equipmentProfile: equipmentProfile,
filmId: photo.filmId, filmId: photo.filmId,
note: photo.note, note: photo.note,
canSave: false, canSave: false,
@ -74,10 +81,22 @@ class LogbookPhotoEditBloc extends Bloc<LogbookPhotoEditEvent, LogbookPhotoEditS
} }
Future<void> _onEquipmentProfileChanged(LogbookPhotoEquipmentProfileChangedEvent event, Emitter emit) async { Future<void> _onEquipmentProfileChanged(LogbookPhotoEquipmentProfileChangedEvent event, Emitter emit) async {
_newPhoto = _newPhoto.copyWith(equipmentProfileId: Optional(event.equipmentProfileId)); final equipmentProfile = event.equipmentProfile;
Optional<ApertureValue>? apertureValue;
if (state.equipmentProfile is PinholeEquipmentProfile &&
(equipmentProfile == null || equipmentProfile is EquipmentProfile)) {
apertureValue = const Optional(null);
} else if (equipmentProfile is PinholeEquipmentProfile) {
apertureValue = Optional(ApertureValue(equipmentProfile.aperture, StopType.full));
}
_newPhoto = _newPhoto.copyWith(
apertureValue: apertureValue,
equipmentProfileId: Optional(equipmentProfile?.id),
);
emit( emit(
state.copyWith( state.copyWith(
equipmentProfileId: Optional(event.equipmentProfileId), aperture: apertureValue,
equipmentProfile: Optional(event.equipmentProfile),
canSave: _canSave(), canSave: _canSave(),
), ),
); );

View file

@ -9,7 +9,7 @@ class PickerListTile<T> extends StatelessWidget {
final T? selectedValue; final T? selectedValue;
final List<T> values; final List<T> values;
final String Function(T) titleAdapter; final String Function(T) titleAdapter;
final ValueChanged<Optional<T>> onChanged; final ValueChanged<Optional<T>>? onChanged;
const PickerListTile({ const PickerListTile({
required this.icon, required this.icon,
@ -27,7 +27,8 @@ class PickerListTile<T> extends StatelessWidget {
leading: Icon(icon), leading: Icon(icon),
title: Text(title), title: Text(title),
trailing: Text(_titleAdapter(context, selectedValue)), trailing: Text(_titleAdapter(context, selectedValue)),
onTap: () { onTap: onChanged != null
? () {
showDialog<Optional<T>>( showDialog<Optional<T>>(
context: context, context: context,
builder: (_) => DialogPicker<Optional<T>>( builder: (_) => DialogPicker<Optional<T>>(
@ -44,10 +45,11 @@ class PickerListTile<T> extends StatelessWidget {
), ),
).then((value) { ).then((value) {
if (value != null) { if (value != null) {
onChanged(value); onChanged!(value);
} }
}); });
}, }
: null,
); );
} }

View file

@ -23,9 +23,9 @@ class LogbookPhotoNoteChangedEvent extends LogbookPhotoEditEvent {
} }
class LogbookPhotoEquipmentProfileChangedEvent extends LogbookPhotoEditEvent { class LogbookPhotoEquipmentProfileChangedEvent extends LogbookPhotoEditEvent {
final String? equipmentProfileId; final IEquipmentProfile? equipmentProfile;
const LogbookPhotoEquipmentProfileChangedEvent(this.equipmentProfileId); const LogbookPhotoEquipmentProfileChangedEvent(this.equipmentProfile);
} }
class LogbookPhotoFilmChangedEvent extends LogbookPhotoEditEvent { class LogbookPhotoFilmChangedEvent extends LogbookPhotoEditEvent {

View file

@ -1,5 +1,7 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/providers/equipment_profile_provider.dart';
import 'package:lightmeter/providers/logbook_photos_provider.dart'; import 'package:lightmeter/providers/logbook_photos_provider.dart';
import 'package:lightmeter/screens/logbook_photo_edit/bloc_logbook_photo_edit.dart'; import 'package:lightmeter/screens/logbook_photo_edit/bloc_logbook_photo_edit.dart';
import 'package:lightmeter/screens/logbook_photo_edit/screen_logbook_photo_edit.dart'; import 'package:lightmeter/screens/logbook_photo_edit/screen_logbook_photo_edit.dart';
@ -25,6 +27,7 @@ class LogbookPhotoEditFlow extends StatelessWidget {
create: (_) => LogbookPhotoEditBloc( create: (_) => LogbookPhotoEditBloc(
LogbookPhotosProvider.of(context), LogbookPhotosProvider.of(context),
args.photo, args.photo,
EquipmentProfiles.of(context).firstWhereOrNull((e) => e.id == args.photo.equipmentProfileId),
), ),
child: const LogbookPhotoEditScreen(), child: const LogbookPhotoEditScreen(),
); );

View file

@ -224,13 +224,15 @@ class _AperturePickerListTile extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<LogbookPhotoEditBloc, LogbookPhotoEditState>( return BlocBuilder<LogbookPhotoEditBloc, LogbookPhotoEditState>(
buildWhen: (previous, current) => previous.aperture != current.aperture, buildWhen: (previous, current) => previous.aperture != current.aperture,
builder: (context, state) => PickerListTile( builder: (context, state) => PickerListTile<ApertureValue>(
icon: Icons.camera_outlined, icon: Icons.camera_outlined,
title: S.of(context).apertureValue, title: S.of(context).apertureValue,
values: ApertureValue.values, values: ApertureValue.values,
selectedValue: state.aperture, selectedValue: state.aperture,
titleAdapter: (value) => value.toString(), titleAdapter: (value) => value.toString(),
onChanged: (value) { onChanged: state.equipmentProfile is PinholeEquipmentProfile
? null
: (value) {
context.read<LogbookPhotoEditBloc>().add(LogbookPhotoApertureChangedEvent(value.value)); context.read<LogbookPhotoEditBloc>().add(LogbookPhotoApertureChangedEvent(value.value));
}, },
), ),
@ -265,15 +267,15 @@ class _EquipmentProfilePickerListTile extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<LogbookPhotoEditBloc, LogbookPhotoEditState>( return BlocBuilder<LogbookPhotoEditBloc, LogbookPhotoEditState>(
buildWhen: (previous, current) => previous.equipmentProfileId != current.equipmentProfileId, buildWhen: (previous, current) => previous.equipmentProfile != current.equipmentProfile,
builder: (context, state) => PickerListTile( builder: (context, state) => PickerListTile(
icon: Icons.camera_alt_outlined, icon: Icons.camera_alt_outlined,
title: S.of(context).equipmentProfile, title: S.of(context).equipmentProfile,
values: EquipmentProfiles.of(context).skip(1).toList(growable: false), values: EquipmentProfiles.of(context).skip(1).toList(growable: false),
selectedValue: EquipmentProfiles.of(context).firstWhereOrNull((e) => e.id == state.equipmentProfileId), selectedValue: state.equipmentProfile,
titleAdapter: (value) => value.name, titleAdapter: (value) => value.name,
onChanged: (value) { onChanged: (value) {
context.read<LogbookPhotoEditBloc>().add(LogbookPhotoEquipmentProfileChangedEvent(value.value?.id)); context.read<LogbookPhotoEditBloc>().add(LogbookPhotoEquipmentProfileChangedEvent(value.value));
}, },
), ),
); );

View file

@ -10,7 +10,7 @@ class LogbookPhotoEditState {
final Coordinates? coordinates; final Coordinates? coordinates;
final ApertureValue? aperture; final ApertureValue? aperture;
final ShutterSpeedValue? shutterSpeed; final ShutterSpeedValue? shutterSpeed;
final String? equipmentProfileId; final IEquipmentProfile? equipmentProfile;
final String? filmId; final String? filmId;
final String? note; final String? note;
final bool canSave; final bool canSave;
@ -26,7 +26,7 @@ class LogbookPhotoEditState {
this.coordinates, this.coordinates,
this.aperture, this.aperture,
this.shutterSpeed, this.shutterSpeed,
this.equipmentProfileId, this.equipmentProfile,
this.filmId, this.filmId,
this.note, this.note,
required this.canSave, required this.canSave,
@ -37,7 +37,7 @@ class LogbookPhotoEditState {
String? name, String? name,
Optional<ApertureValue>? aperture, Optional<ApertureValue>? aperture,
Optional<ShutterSpeedValue>? shutterSpeed, Optional<ShutterSpeedValue>? shutterSpeed,
Optional<String>? equipmentProfileId, Optional<IEquipmentProfile>? equipmentProfile,
Optional<String>? filmId, Optional<String>? filmId,
String? note, String? note,
bool? canSave, bool? canSave,
@ -52,7 +52,7 @@ class LogbookPhotoEditState {
nd: nd, nd: nd,
aperture: aperture != null ? aperture.value : this.aperture, aperture: aperture != null ? aperture.value : this.aperture,
shutterSpeed: shutterSpeed != null ? shutterSpeed.value : this.shutterSpeed, shutterSpeed: shutterSpeed != null ? shutterSpeed.value : this.shutterSpeed,
equipmentProfileId: equipmentProfileId != null ? equipmentProfileId.value : this.equipmentProfileId, equipmentProfile: equipmentProfile != null ? equipmentProfile.value : this.equipmentProfile,
filmId: filmId != null ? filmId.value : this.filmId, filmId: filmId != null ? filmId.value : this.filmId,
note: note ?? this.note, note: note ?? this.note,
canSave: canSave ?? this.canSave, canSave: canSave ?? this.canSave,

View file

@ -126,11 +126,13 @@ class CameraContainer extends StatelessWidget {
enabledFeaturesHeight += Dimens.readingContainerSingleValueHeight; enabledFeaturesHeight += Dimens.readingContainerSingleValueHeight;
enabledFeaturesHeight += Dimens.paddingS; enabledFeaturesHeight += Dimens.paddingS;
} }
if (context.meteringFeature(MeteringScreenLayoutFeature.extremeExposurePairs)) {
if (EquipmentProfiles.selectedOf(context) is PinholeEquipmentProfile) { if (EquipmentProfiles.selectedOf(context) is PinholeEquipmentProfile) {
enabledFeaturesHeight += Dimens.readingContainerSingleValueHeight; enabledFeaturesHeight += Dimens.readingContainerSingleValueHeight;
enabledFeaturesHeight += Dimens.paddingS; } else {
} else if (context.meteringFeature(MeteringScreenLayoutFeature.extremeExposurePairs)) {
enabledFeaturesHeight += Dimens.readingContainerDoubleValueHeight; enabledFeaturesHeight += Dimens.readingContainerDoubleValueHeight;
}
enabledFeaturesHeight += Dimens.paddingS; enabledFeaturesHeight += Dimens.paddingS;
} }

View file

@ -65,6 +65,7 @@ class ExposurePairsList extends StatelessWidget {
], ],
), ),
), ),
if (exposurePairs.length > 1)
Positioned( Positioned(
top: 0, top: 0,
bottom: 0, bottom: 0,

View file

@ -46,10 +46,10 @@ class ReadingsContainer extends StatelessWidget {
const EquipmentProfilePicker(), const EquipmentProfilePicker(),
const _InnerPadding(), const _InnerPadding(),
], ],
if (EquipmentProfiles.selectedOf(context) is PinholeEquipmentProfile) ...[ if (context.meteringFeature(MeteringScreenLayoutFeature.extremeExposurePairs)) ...[
ShutterSpeedContainer(shutterSpeedValue: fastest?.shutterSpeed), if (EquipmentProfiles.selectedOf(context) is PinholeEquipmentProfile)
const _InnerPadding(), ShutterSpeedContainer(shutterSpeedValue: fastest?.shutterSpeed)
] else if (context.meteringFeature(MeteringScreenLayoutFeature.extremeExposurePairs)) ...[ else
ExtremeExposurePairsContainer( ExtremeExposurePairsContainer(
fastest: fastest, fastest: fastest,
slowest: slowest, slowest: slowest,

View file

@ -24,19 +24,20 @@ class DialogPicker<T> extends StatefulWidget {
} }
class _DialogPickerState<T> extends State<DialogPicker<T>> { class _DialogPickerState<T> extends State<DialogPicker<T>> {
late T _selected = widget.selectedValue; T? _selected;
final ScrollController _scrollController = ScrollController(); final ScrollController _scrollController = ScrollController();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
SchedulerBinding.instance.addPostFrameCallback((_) { final selectedIndex = widget.values.indexOf(widget.selectedValue);
final selectedIndex = widget.values.indexOf(_selected);
if (selectedIndex >= 0) { if (selectedIndex >= 0) {
_selected = widget.selectedValue;
SchedulerBinding.instance.addPostFrameCallback((_) {
_scrollController.jumpTo((Dimens.grid56 * selectedIndex).clamp(0, _scrollController.position.maxScrollExtent)); _scrollController.jumpTo((Dimens.grid56 * selectedIndex).clamp(0, _scrollController.position.maxScrollExtent));
}
}); });
} }
}
@override @override
void dispose() { void dispose() {
@ -93,7 +94,7 @@ class _DialogPickerState<T> extends State<DialogPicker<T>> {
child: Text(S.of(context).cancel), child: Text(S.of(context).cancel),
), ),
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(_selected), onPressed: _selected != null ? () => Navigator.of(context).pop(_selected) : null,
child: Text(S.of(context).select), child: Text(S.of(context).select),
), ),
], ],

View file

@ -170,10 +170,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: built_value name: built_value
sha256: "082001b5c3dc495d4a42f1d5789990505df20d8547d42507c29050af6933ee27" sha256: a30f0a0e38671e89a492c44d005b5545b830a961575bbd8336d42869ff71066d
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.10.1" version: "8.12.0"
camera: camera:
dependency: "direct main" dependency: "direct main"
description: description:
@ -861,8 +861,8 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "."
ref: "feature/MLI-48" ref: main
resolved-ref: "4a169640bff3d3a3206a2c352a75cbcea4871b1c" resolved-ref: "3f5bae6d2500a746fb83ab345919095a815244d1"
url: "https://github.com/vodemn/m3_lightmeter_iap" url: "https://github.com/vodemn/m3_lightmeter_iap"
source: git source: git
version: "4.1.2+37" version: "4.1.2+37"
@ -870,11 +870,11 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "."
ref: "feature/MLR-18" ref: "v2.5.0"
resolved-ref: "61bb3f8a9164d19f6e47c96fbea1cbe3aaf39fc3" resolved-ref: "680affb45c5d03ed4fe61c30ee0d6e6fab0f2c12"
url: "https://github.com/vodemn/m3_lightmeter_resources" url: "https://github.com/vodemn/m3_lightmeter_resources"
source: git source: git
version: "2.4.0+13" version: "2.5.0+14"
macros: macros:
dependency: transitive dependency: transitive
description: description:
@ -967,10 +967,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: package_info_plus_platform_interface name: package_info_plus_platform_interface
sha256: "6c935fb612dff8e3cc9632c2b301720c77450a126114126ffaafe28d2e87956c" sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.2.0" version: "3.2.1"
path: path:
dependency: transitive dependency: transitive
description: description:
@ -1119,10 +1119,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: provider name: provider
sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84" sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.1.5" version: "6.1.5+1"
pub_semver: pub_semver:
dependency: transitive dependency: transitive
description: description:
@ -1524,10 +1524,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: watcher name: watcher
sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a" sha256: "5bf046f41320ac97a469d506261797f35254fa61c641741ef32dacda98b7d39c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.2" version: "1.1.3"
web: web:
dependency: transitive dependency: transitive
description: description:

View file

@ -33,11 +33,11 @@ dependencies:
m3_lightmeter_iap: m3_lightmeter_iap:
git: git:
url: "https://github.com/vodemn/m3_lightmeter_iap" url: "https://github.com/vodemn/m3_lightmeter_iap"
ref: feature/MLI-48 ref: main
m3_lightmeter_resources: m3_lightmeter_resources:
git: git:
url: "https://github.com/vodemn/m3_lightmeter_resources" url: "https://github.com/vodemn/m3_lightmeter_resources"
ref: feature/MLR-18 ref: v2.5.0
map_launcher: 3.2.0 map_launcher: 3.2.0
material_color_utilities: 0.12.0 material_color_utilities: 0.12.0
package_info_plus: 8.1.3 package_info_plus: 8.1.3