ML-258 Allow to set equipment profile and film for logbook photo (#259)
* added film and equipment profile picker to logbook edit * unified camera icon usages * Update settings_screen.png * save equipment profile and film when taking a photo * extended logbook integration test * cover cascade deletion * updated dependencies * Update logbook_photos_provider_test.dart * regenerated screenshots * refined integration tests * update stub pubspec * fixed lens zoom on screenshots
|
@ -12,7 +12,7 @@ dependencies:
|
||||||
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: v2.3.0
|
ref: v2.4.0
|
||||||
shared_preferences:
|
shared_preferences:
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
|
|
@ -3,6 +3,8 @@ import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:lightmeter/data/models/ev_source_type.dart';
|
import 'package:lightmeter/data/models/ev_source_type.dart';
|
||||||
import 'package:lightmeter/data/shared_prefs_service.dart';
|
import 'package:lightmeter/data/shared_prefs_service.dart';
|
||||||
import 'package:lightmeter/generated/l10n.dart';
|
import 'package:lightmeter/generated/l10n.dart';
|
||||||
|
import 'package:lightmeter/res/dimens.dart';
|
||||||
|
import 'package:lightmeter/screens/logbook_photo_edit/components/picker_list_tile/widget_list_tile_picker.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';
|
||||||
import 'package:lightmeter/screens/logbook_photos/components/grid_tile/widget_grid_tile_logbook_photo.dart';
|
import 'package:lightmeter/screens/logbook_photos/components/grid_tile/widget_grid_tile_logbook_photo.dart';
|
||||||
import 'package:lightmeter/screens/settings/components/shared/dialog_picker/widget_dialog_picker.dart';
|
import 'package:lightmeter/screens/settings/components/shared/dialog_picker/widget_dialog_picker.dart';
|
||||||
|
@ -28,8 +30,8 @@ void testLogbook(String description) {
|
||||||
description,
|
description,
|
||||||
(tester) async {
|
(tester) async {
|
||||||
await tester.pumpApplication(
|
await tester.pumpApplication(
|
||||||
equipmentProfiles: {},
|
selectedEquipmentProfileId: mockEquipmentProfiles.first.id,
|
||||||
predefinedFilms: mockFilms.toTogglableMap(),
|
selectedFilmId: mockFilms.first.id,
|
||||||
customFilms: {},
|
customFilms: {},
|
||||||
);
|
);
|
||||||
await tester.takePhoto();
|
await tester.takePhoto();
|
||||||
|
@ -45,6 +47,9 @@ void testLogbook(String description) {
|
||||||
await tester.tap(find.byType(LogbookPhotoGridTile).first);
|
await tester.tap(find.byType(LogbookPhotoGridTile).first);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.ensureVisible(find.text(mockEquipmentProfiles.first.name));
|
||||||
|
await tester.ensureVisible(find.text(mockFilms.first.name));
|
||||||
|
|
||||||
/// Add a note, select aperture value and shutter speed value
|
/// Add a note, select aperture value and shutter speed value
|
||||||
await tester.enterText(
|
await tester.enterText(
|
||||||
find.descendant(
|
find.descendant(
|
||||||
|
@ -58,6 +63,26 @@ 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(
|
||||||
|
find.descendant(
|
||||||
|
of: find.byWidgetPredicate(
|
||||||
|
(widget) => widget is PickerListTile && widget.title == S.current.equipmentProfile,
|
||||||
|
),
|
||||||
|
matching: find.text(mockEquipmentProfiles.first.name),
|
||||||
|
),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
find.descendant(
|
||||||
|
of: find.byWidgetPredicate(
|
||||||
|
(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.pumpAndSettle();
|
||||||
|
|
||||||
/// Save the edits
|
/// Save the edits
|
||||||
await tester.tap(find.byIcon(Icons.save_outlined));
|
await tester.tap(find.byIcon(Icons.save_outlined));
|
||||||
|
@ -76,11 +101,32 @@ void testLogbook(String description) {
|
||||||
/// Verify that only one photo is present
|
/// Verify that only one photo is present
|
||||||
expect(find.byType(LogbookPhotoGridTile), findsOneWidget);
|
expect(find.byType(LogbookPhotoGridTile), findsOneWidget);
|
||||||
|
|
||||||
|
/// 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.tap(find.byIcon(Icons.delete_outlined));
|
||||||
|
await tester.pumpAndSettle(Dimens.durationML);
|
||||||
|
expect(find.text(mockEquipmentProfiles[0].name), findsNothing);
|
||||||
|
expect(find.text(mockEquipmentProfiles[1].name), findsOneWidget);
|
||||||
|
await tester.navigatorPop();
|
||||||
|
|
||||||
/// Open photo again
|
/// Open photo again
|
||||||
|
await tester.tapDescendantTextOf<SettingsScreen>(S.current.logbook);
|
||||||
await tester.tap(find.byType(LogbookPhotoGridTile).first);
|
await tester.tap(find.byType(LogbookPhotoGridTile).first);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
/// Verify the edits were saved
|
/// Verify the edits were saved
|
||||||
|
expect(
|
||||||
|
find.descendant(
|
||||||
|
of: find.byWidgetPredicate(
|
||||||
|
(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);
|
||||||
expect(find.text('1/125'), findsOneWidget);
|
expect(find.text('1/125'), findsOneWidget);
|
||||||
|
@ -101,7 +147,7 @@ extension on WidgetTester {
|
||||||
await tap(find.text(title));
|
await tap(find.text(title));
|
||||||
await pumpAndSettle();
|
await pumpAndSettle();
|
||||||
final dialogFinder = find.byType(DialogPicker<Optional<V>>);
|
final dialogFinder = find.byType(DialogPicker<Optional<V>>);
|
||||||
final listTileFinder = find.text(valueToSelect);
|
final listTileFinder = find.descendant(of: dialogFinder, matching: find.text(valueToSelect));
|
||||||
await scrollUntilVisible(
|
await scrollUntilVisible(
|
||||||
listTileFinder,
|
listTileFinder,
|
||||||
56,
|
56,
|
||||||
|
|
|
@ -114,6 +114,7 @@ class FilmsProviderState extends State<FilmsProvider> {
|
||||||
}
|
}
|
||||||
|
|
||||||
enum _FilmsModelAspect {
|
enum _FilmsModelAspect {
|
||||||
|
all,
|
||||||
customFilms,
|
customFilms,
|
||||||
predefinedFilms,
|
predefinedFilms,
|
||||||
filmsInUse,
|
filmsInUse,
|
||||||
|
@ -134,6 +135,14 @@ class Films extends InheritedModel<_FilmsModelAspect> {
|
||||||
required super.child,
|
required super.child,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
static List<Film> of<T>(BuildContext context) {
|
||||||
|
final model = InheritedModel.inheritFrom<Films>(context, aspect: _FilmsModelAspect.all)!;
|
||||||
|
return [
|
||||||
|
...model.customFilms.values.map((e) => e.value),
|
||||||
|
...model.predefinedFilms.values.map((e) => e.value),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
static List<Film> predefinedFilmsOf<T>(BuildContext context) {
|
static List<Film> predefinedFilmsOf<T>(BuildContext context) {
|
||||||
return InheritedModel.inheritFrom<Films>(context, aspect: _FilmsModelAspect.predefinedFilms)!
|
return InheritedModel.inheritFrom<Films>(context, aspect: _FilmsModelAspect.predefinedFilms)!
|
||||||
.predefinedFilms
|
.predefinedFilms
|
||||||
|
@ -171,10 +180,12 @@ class Films extends InheritedModel<_FilmsModelAspect> {
|
||||||
bool updateShouldNotifyDependent(Films oldWidget, Set<_FilmsModelAspect> dependencies) {
|
bool updateShouldNotifyDependent(Films oldWidget, Set<_FilmsModelAspect> dependencies) {
|
||||||
return (dependencies.contains(_FilmsModelAspect.selected) && oldWidget.selected != selected) ||
|
return (dependencies.contains(_FilmsModelAspect.selected) && oldWidget.selected != selected) ||
|
||||||
((dependencies.contains(_FilmsModelAspect.predefinedFilms) ||
|
((dependencies.contains(_FilmsModelAspect.predefinedFilms) ||
|
||||||
dependencies.contains(_FilmsModelAspect.filmsInUse)) &&
|
dependencies.contains(_FilmsModelAspect.filmsInUse) ||
|
||||||
|
dependencies.contains(_FilmsModelAspect.all)) &&
|
||||||
const DeepCollectionEquality().equals(oldWidget.predefinedFilms, predefinedFilms)) ||
|
const DeepCollectionEquality().equals(oldWidget.predefinedFilms, predefinedFilms)) ||
|
||||||
((dependencies.contains(_FilmsModelAspect.customFilms) ||
|
((dependencies.contains(_FilmsModelAspect.customFilms) ||
|
||||||
dependencies.contains(_FilmsModelAspect.filmsInUse)) &&
|
dependencies.contains(_FilmsModelAspect.filmsInUse) ||
|
||||||
|
dependencies.contains(_FilmsModelAspect.all)) &&
|
||||||
const DeepCollectionEquality().equals(oldWidget.customFilms, customFilms));
|
const DeepCollectionEquality().equals(oldWidget.customFilms, customFilms));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,8 @@ import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:lightmeter/data/geolocation_service.dart';
|
import 'package:lightmeter/data/geolocation_service.dart';
|
||||||
import 'package:lightmeter/platform_config.dart';
|
import 'package:lightmeter/platform_config.dart';
|
||||||
|
import 'package:lightmeter/providers/equipment_profile_provider.dart';
|
||||||
|
import 'package:lightmeter/providers/films_provider.dart';
|
||||||
import 'package:lightmeter/utils/context_utils.dart';
|
import 'package:lightmeter/utils/context_utils.dart';
|
||||||
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
|
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
|
||||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||||
|
@ -74,6 +76,13 @@ class LogbookPhotosProviderState extends State<LogbookPhotosProvider> {
|
||||||
required int nd,
|
required int nd,
|
||||||
}) async {
|
}) async {
|
||||||
if (context.isPro && _isEnabled) {
|
if (context.isPro && _isEnabled) {
|
||||||
|
final equipmentProfile = EquipmentProfiles.selectedOf(context);
|
||||||
|
final equipmentProfileId =
|
||||||
|
equipmentProfile == EquipmentProfilesProvider.defaultProfile ? null : equipmentProfile.id;
|
||||||
|
|
||||||
|
final selectedFilm = Films.selectedOf(context);
|
||||||
|
final filmId = selectedFilm == const FilmStub() ? null : selectedFilm.id;
|
||||||
|
|
||||||
final coordinates = await widget.geolocationService.getCurrentPosition();
|
final coordinates = await widget.geolocationService.getCurrentPosition();
|
||||||
|
|
||||||
final photo = LogbookPhoto(
|
final photo = LogbookPhoto(
|
||||||
|
@ -84,6 +93,8 @@ class LogbookPhotosProviderState extends State<LogbookPhotosProvider> {
|
||||||
iso: iso,
|
iso: iso,
|
||||||
nd: nd,
|
nd: nd,
|
||||||
coordinates: coordinates,
|
coordinates: coordinates,
|
||||||
|
equipmentProfileId: equipmentProfileId,
|
||||||
|
filmId: filmId,
|
||||||
);
|
);
|
||||||
await widget.storageService.addPhoto(photo);
|
await widget.storageService.addPhoto(photo);
|
||||||
_photos[photo.id] = photo;
|
_photos[photo.id] = photo;
|
||||||
|
|
|
@ -25,6 +25,8 @@ 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,
|
||||||
|
filmId: photo.filmId,
|
||||||
note: photo.note,
|
note: photo.note,
|
||||||
canSave: false,
|
canSave: false,
|
||||||
),
|
),
|
||||||
|
@ -36,6 +38,10 @@ class LogbookPhotoEditBloc extends Bloc<LogbookPhotoEditEvent, LogbookPhotoEditS
|
||||||
await _onApertureChanged(e, emit);
|
await _onApertureChanged(e, emit);
|
||||||
case final LogbookPhotoShutterSpeedChangedEvent e:
|
case final LogbookPhotoShutterSpeedChangedEvent e:
|
||||||
await _onShutterSpeedChanged(e, emit);
|
await _onShutterSpeedChanged(e, emit);
|
||||||
|
case final LogbookPhotoEquipmentProfileChangedEvent e:
|
||||||
|
await _onEquipmentProfileChanged(e, emit);
|
||||||
|
case final LogbookPhotoFilmChangedEvent e:
|
||||||
|
await _onFilmChanged(e, emit);
|
||||||
case final LogbookPhotoNoteChangedEvent e:
|
case final LogbookPhotoNoteChangedEvent e:
|
||||||
await _onNoteChanged(e, emit);
|
await _onNoteChanged(e, emit);
|
||||||
case LogbookPhotoSaveEvent():
|
case LogbookPhotoSaveEvent():
|
||||||
|
@ -67,6 +73,26 @@ class LogbookPhotoEditBloc extends Bloc<LogbookPhotoEditEvent, LogbookPhotoEditS
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _onEquipmentProfileChanged(LogbookPhotoEquipmentProfileChangedEvent event, Emitter emit) async {
|
||||||
|
_newPhoto = _newPhoto.copyWith(equipmentProfileId: Optional(event.equipmentProfile?.id));
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
equipmentProfileId: Optional(event.equipmentProfile?.id),
|
||||||
|
canSave: _canSave(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onFilmChanged(LogbookPhotoFilmChangedEvent event, Emitter emit) async {
|
||||||
|
_newPhoto = _newPhoto.copyWith(filmId: Optional(event.film?.id));
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
filmId: Optional(event.film?.id),
|
||||||
|
canSave: _canSave(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _onNoteChanged(LogbookPhotoNoteChangedEvent event, Emitter emit) async {
|
Future<void> _onNoteChanged(LogbookPhotoNoteChangedEvent event, Emitter emit) async {
|
||||||
_newPhoto = _newPhoto.copyWith(note: event.note);
|
_newPhoto = _newPhoto.copyWith(note: event.note);
|
||||||
emit(
|
emit(
|
||||||
|
|
|
@ -3,11 +3,12 @@ import 'package:lightmeter/generated/l10n.dart';
|
||||||
import 'package:lightmeter/screens/settings/components/shared/dialog_picker/widget_dialog_picker.dart';
|
import 'package:lightmeter/screens/settings/components/shared/dialog_picker/widget_dialog_picker.dart';
|
||||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||||
|
|
||||||
class PickerListTile<T extends PhotographyValue> extends StatelessWidget {
|
class PickerListTile<T> extends StatelessWidget {
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
final String title;
|
final String title;
|
||||||
final T? selectedValue;
|
final T? selectedValue;
|
||||||
final List<T> values;
|
final List<T> values;
|
||||||
|
final String Function(T) titleAdapter;
|
||||||
final ValueChanged<Optional<T>> onChanged;
|
final ValueChanged<Optional<T>> onChanged;
|
||||||
|
|
||||||
const PickerListTile({
|
const PickerListTile({
|
||||||
|
@ -15,6 +16,7 @@ class PickerListTile<T extends PhotographyValue> extends StatelessWidget {
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.selectedValue,
|
required this.selectedValue,
|
||||||
required this.values,
|
required this.values,
|
||||||
|
required this.titleAdapter,
|
||||||
required this.onChanged,
|
required this.onChanged,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
@ -24,7 +26,7 @@ class PickerListTile<T extends PhotographyValue> extends StatelessWidget {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: Icon(icon),
|
leading: Icon(icon),
|
||||||
title: Text(title),
|
title: Text(title),
|
||||||
trailing: Text(selectedValue?.toString() ?? S.of(context).notSet),
|
trailing: Text(_titleAdapter(context, selectedValue)),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
showDialog<Optional<T>>(
|
showDialog<Optional<T>>(
|
||||||
context: context,
|
context: context,
|
||||||
|
@ -36,7 +38,7 @@ class PickerListTile<T extends PhotographyValue> extends StatelessWidget {
|
||||||
const Optional(null),
|
const Optional(null),
|
||||||
...values.toSet().map((e) => Optional(e)),
|
...values.toSet().map((e) => Optional(e)),
|
||||||
],
|
],
|
||||||
titleAdapter: (context, value) => value.value?.toString() ?? S.of(context).notSet,
|
titleAdapter: (context, value) => _titleAdapter(context, value.value),
|
||||||
),
|
),
|
||||||
).then((value) {
|
).then((value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
|
@ -46,4 +48,8 @@ class PickerListTile<T extends PhotographyValue> extends StatelessWidget {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _titleAdapter(BuildContext context, T? value) {
|
||||||
|
return value != null ? titleAdapter(value) : S.of(context).notSet;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,18 @@ class LogbookPhotoNoteChangedEvent extends LogbookPhotoEditEvent {
|
||||||
const LogbookPhotoNoteChangedEvent(this.note);
|
const LogbookPhotoNoteChangedEvent(this.note);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class LogbookPhotoEquipmentProfileChangedEvent extends LogbookPhotoEditEvent {
|
||||||
|
final EquipmentProfile? equipmentProfile;
|
||||||
|
|
||||||
|
const LogbookPhotoEquipmentProfileChangedEvent(this.equipmentProfile);
|
||||||
|
}
|
||||||
|
|
||||||
|
class LogbookPhotoFilmChangedEvent extends LogbookPhotoEditEvent {
|
||||||
|
final Film? film;
|
||||||
|
|
||||||
|
const LogbookPhotoFilmChangedEvent(this.film);
|
||||||
|
}
|
||||||
|
|
||||||
class LogbookPhotoSaveEvent extends LogbookPhotoEditEvent {
|
class LogbookPhotoSaveEvent extends LogbookPhotoEditEvent {
|
||||||
const LogbookPhotoSaveEvent();
|
const LogbookPhotoSaveEvent();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
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/generated/l10n.dart';
|
import 'package:lightmeter/generated/l10n.dart';
|
||||||
import 'package:lightmeter/platform_config.dart';
|
import 'package:lightmeter/platform_config.dart';
|
||||||
|
import 'package:lightmeter/providers/equipment_profile_provider.dart';
|
||||||
|
import 'package:lightmeter/providers/films_provider.dart';
|
||||||
import 'package:lightmeter/res/dimens.dart';
|
import 'package:lightmeter/res/dimens.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/components/coordinates_list_tile/widget_list_tile_coordinates_logbook_photo.dart';
|
import 'package:lightmeter/screens/logbook_photo_edit/components/coordinates_list_tile/widget_list_tile_coordinates_logbook_photo.dart';
|
||||||
|
@ -73,6 +76,8 @@ class _LogbookPhotoEditScreenState extends State<LogbookPhotoEditScreen> {
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
LogbookPhotoCoordinatesListTile(),
|
LogbookPhotoCoordinatesListTile(),
|
||||||
|
_EquipmentProfilePickerListTile(),
|
||||||
|
_FilmPickerListTile(),
|
||||||
_NoteListTile(),
|
_NoteListTile(),
|
||||||
_EvListTile(),
|
_EvListTile(),
|
||||||
_IsoListTile(),
|
_IsoListTile(),
|
||||||
|
@ -224,6 +229,7 @@ class _AperturePickerListTile extends StatelessWidget {
|
||||||
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(),
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
context.read<LogbookPhotoEditBloc>().add(LogbookPhotoApertureChangedEvent(value.value));
|
context.read<LogbookPhotoEditBloc>().add(LogbookPhotoApertureChangedEvent(value.value));
|
||||||
},
|
},
|
||||||
|
@ -244,6 +250,7 @@ class _ShutterSpeedPickerListTile extends StatelessWidget {
|
||||||
title: S.of(context).shutterSpeedValue,
|
title: S.of(context).shutterSpeedValue,
|
||||||
values: ShutterSpeedValue.values,
|
values: ShutterSpeedValue.values,
|
||||||
selectedValue: state.shutterSpeed,
|
selectedValue: state.shutterSpeed,
|
||||||
|
titleAdapter: (value) => value.toString(),
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
context.read<LogbookPhotoEditBloc>().add(LogbookPhotoShutterSpeedChangedEvent(value.value));
|
context.read<LogbookPhotoEditBloc>().add(LogbookPhotoShutterSpeedChangedEvent(value.value));
|
||||||
},
|
},
|
||||||
|
@ -251,3 +258,45 @@ class _ShutterSpeedPickerListTile extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _EquipmentProfilePickerListTile extends StatelessWidget {
|
||||||
|
const _EquipmentProfilePickerListTile();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocBuilder<LogbookPhotoEditBloc, LogbookPhotoEditState>(
|
||||||
|
buildWhen: (previous, current) => previous.equipmentProfileId != current.equipmentProfileId,
|
||||||
|
builder: (context, state) => PickerListTile(
|
||||||
|
icon: Icons.camera_alt_outlined,
|
||||||
|
title: S.of(context).equipmentProfile,
|
||||||
|
values: EquipmentProfiles.of(context).skip(1).toList(growable: false),
|
||||||
|
selectedValue: EquipmentProfiles.of(context).firstWhereOrNull((e) => e.id == state.equipmentProfileId),
|
||||||
|
titleAdapter: (value) => value.name,
|
||||||
|
onChanged: (value) {
|
||||||
|
context.read<LogbookPhotoEditBloc>().add(LogbookPhotoEquipmentProfileChangedEvent(value.value));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FilmPickerListTile extends StatelessWidget {
|
||||||
|
const _FilmPickerListTile();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocBuilder<LogbookPhotoEditBloc, LogbookPhotoEditState>(
|
||||||
|
buildWhen: (previous, current) => previous.filmId != current.filmId,
|
||||||
|
builder: (context, state) => PickerListTile(
|
||||||
|
icon: Icons.camera_roll_outlined,
|
||||||
|
title: S.of(context).film,
|
||||||
|
values: Films.of(context),
|
||||||
|
selectedValue: Films.of(context).firstWhereOrNull((e) => e.id == state.filmId),
|
||||||
|
titleAdapter: (value) => value.name,
|
||||||
|
onChanged: (value) {
|
||||||
|
context.read<LogbookPhotoEditBloc>().add(LogbookPhotoFilmChangedEvent(value.value));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -10,6 +10,8 @@ class LogbookPhotoEditState {
|
||||||
final Coordinates? coordinates;
|
final Coordinates? coordinates;
|
||||||
final ApertureValue? aperture;
|
final ApertureValue? aperture;
|
||||||
final ShutterSpeedValue? shutterSpeed;
|
final ShutterSpeedValue? shutterSpeed;
|
||||||
|
final String? equipmentProfileId;
|
||||||
|
final String? filmId;
|
||||||
final String? note;
|
final String? note;
|
||||||
final bool canSave;
|
final bool canSave;
|
||||||
final bool isLoading;
|
final bool isLoading;
|
||||||
|
@ -24,6 +26,8 @@ class LogbookPhotoEditState {
|
||||||
this.coordinates,
|
this.coordinates,
|
||||||
this.aperture,
|
this.aperture,
|
||||||
this.shutterSpeed,
|
this.shutterSpeed,
|
||||||
|
this.equipmentProfileId,
|
||||||
|
this.filmId,
|
||||||
this.note,
|
this.note,
|
||||||
required this.canSave,
|
required this.canSave,
|
||||||
this.isLoading = false,
|
this.isLoading = false,
|
||||||
|
@ -33,6 +37,8 @@ class LogbookPhotoEditState {
|
||||||
String? name,
|
String? name,
|
||||||
Optional<ApertureValue>? aperture,
|
Optional<ApertureValue>? aperture,
|
||||||
Optional<ShutterSpeedValue>? shutterSpeed,
|
Optional<ShutterSpeedValue>? shutterSpeed,
|
||||||
|
Optional<String>? equipmentProfileId,
|
||||||
|
Optional<String>? filmId,
|
||||||
String? note,
|
String? note,
|
||||||
bool? canSave,
|
bool? canSave,
|
||||||
bool? isLoading,
|
bool? isLoading,
|
||||||
|
@ -46,6 +52,8 @@ 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,
|
||||||
|
filmId: filmId != null ? filmId.value : this.filmId,
|
||||||
note: note ?? this.note,
|
note: note ?? this.note,
|
||||||
canSave: canSave ?? this.canSave,
|
canSave: canSave ?? this.canSave,
|
||||||
isLoading: isLoading ?? this.isLoading,
|
isLoading: isLoading ?? this.isLoading,
|
||||||
|
|
|
@ -26,7 +26,7 @@ class _EquipmentProfilePickerState extends State<EquipmentProfilePicker> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return AnimatedDialogPicker<EquipmentProfile>(
|
return AnimatedDialogPicker<EquipmentProfile>(
|
||||||
icon: Icons.camera_outlined,
|
icon: Icons.camera_alt_outlined,
|
||||||
title: S.of(context).equipmentProfile,
|
title: S.of(context).equipmentProfile,
|
||||||
selectedValue: EquipmentProfiles.selectedOf(context),
|
selectedValue: EquipmentProfiles.selectedOf(context),
|
||||||
values: EquipmentProfiles.inUseOf(context),
|
values: EquipmentProfiles.inUseOf(context),
|
||||||
|
|
|
@ -11,7 +11,7 @@ class CameraFeaturesListTile extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: const Icon(Icons.camera_alt_outlined),
|
leading: const Icon(Icons.camera_enhance_outlined),
|
||||||
title: Text(S.of(context).cameraFeatures),
|
title: Text(S.of(context).cameraFeatures),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
UserPreferencesProvider.cameraConfigOf(context).entries.map(
|
UserPreferencesProvider.cameraConfigOf(context).entries.map(
|
||||||
|
@ -26,7 +26,7 @@ class CameraFeaturesListTile extends StatelessWidget {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (_) => DialogSwitch<CameraFeature>(
|
builder: (_) => DialogSwitch<CameraFeature>(
|
||||||
icon: Icons.camera_alt_outlined,
|
icon: Icons.camera_enhance_outlined,
|
||||||
title: S.of(context).cameraFeatures,
|
title: S.of(context).cameraFeatures,
|
||||||
items: [
|
items: [
|
||||||
DialogSwitchListItem(
|
DialogSwitchListItem(
|
||||||
|
|
|
@ -8,7 +8,7 @@ class EquipmentProfilesListTile extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: const Icon(Icons.camera_outlined),
|
leading: const Icon(Icons.camera_alt_outlined),
|
||||||
title: Text(S.of(context).equipmentProfiles),
|
title: Text(S.of(context).equipmentProfiles),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.of(context).pushNamed(NavigationRoutes.equipmentProfilesListScreen.name);
|
Navigator.of(context).pushNamed(NavigationRoutes.equipmentProfilesListScreen.name);
|
||||||
|
|
12
pubspec.lock
|
@ -861,20 +861,20 @@ packages:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: "v4.0.1"
|
ref: "v4.1.0"
|
||||||
resolved-ref: ce431af9fc86694b3bd8d1e2a2a988d9b6184b17
|
resolved-ref: "15adfa4f6fea06a6c62c58f5171a41813058040c"
|
||||||
url: "https://github.com/vodemn/m3_lightmeter_iap"
|
url: "https://github.com/vodemn/m3_lightmeter_iap"
|
||||||
source: git
|
source: git
|
||||||
version: "4.0.1+34"
|
version: "4.1.0+35"
|
||||||
m3_lightmeter_resources:
|
m3_lightmeter_resources:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: "v2.3.1"
|
ref: "v2.4.0"
|
||||||
resolved-ref: "8b39ac1927b791652618509abe0391f844229b93"
|
resolved-ref: cc9ae43a7859398a6ab2ecf7f8713153dbfd99cd
|
||||||
url: "https://github.com/vodemn/m3_lightmeter_resources"
|
url: "https://github.com/vodemn/m3_lightmeter_resources"
|
||||||
source: git
|
source: git
|
||||||
version: "2.3.1+12"
|
version: "2.4.0+13"
|
||||||
macros:
|
macros:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -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: v4.0.1
|
ref: v4.1.0
|
||||||
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: v2.3.1
|
ref: v2.4.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
|
||||||
|
@ -68,10 +68,6 @@ dev_dependencies:
|
||||||
|
|
||||||
dependency_overrides:
|
dependency_overrides:
|
||||||
geolocator_android: 4.6.1
|
geolocator_android: 4.6.1
|
||||||
m3_lightmeter_resources:
|
|
||||||
git:
|
|
||||||
url: "https://github.com/vodemn/m3_lightmeter_resources"
|
|
||||||
ref: v2.3.1
|
|
||||||
material_color_utilities: 0.11.1
|
material_color_utilities: 0.11.1
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
|
|
|
@ -36,6 +36,31 @@ import 'models/screenshot_args.dart';
|
||||||
|
|
||||||
//https://stackoverflow.com/a/67186625/13167574
|
//https://stackoverflow.com/a/67186625/13167574
|
||||||
|
|
||||||
|
final _mockEquipmentProfile = EquipmentProfile(
|
||||||
|
id: '1',
|
||||||
|
name: 'Praktica + Zenitar',
|
||||||
|
apertureValues: ApertureValue.values.sublist(
|
||||||
|
ApertureValue.values.indexOf(const ApertureValue(1.7, StopType.half)),
|
||||||
|
ApertureValue.values.indexOf(const ApertureValue(16, StopType.full)) + 1,
|
||||||
|
),
|
||||||
|
ndValues: NdValue.values.sublist(0, 3),
|
||||||
|
shutterSpeedValues: ShutterSpeedValue.values.sublist(
|
||||||
|
ShutterSpeedValue.values.indexOf(const ShutterSpeedValue(1000, true, StopType.full)),
|
||||||
|
ShutterSpeedValue.values.indexOf(const ShutterSpeedValue(1, false, StopType.full)) + 1,
|
||||||
|
),
|
||||||
|
isoValues: const [
|
||||||
|
IsoValue(50, StopType.full),
|
||||||
|
IsoValue(100, StopType.full),
|
||||||
|
IsoValue(200, StopType.full),
|
||||||
|
IsoValue(250, StopType.third),
|
||||||
|
IsoValue(400, StopType.full),
|
||||||
|
IsoValue(500, StopType.third),
|
||||||
|
IsoValue(800, StopType.full),
|
||||||
|
IsoValue(1600, StopType.full),
|
||||||
|
IsoValue(3200, StopType.full),
|
||||||
|
],
|
||||||
|
lensZoom: 50 / (Platform.isAndroid ? 24 : 26),
|
||||||
|
);
|
||||||
const _mockFilm = FilmExponential(id: '1', name: 'Ilford HP5+', iso: 400, exponent: 1.34);
|
const _mockFilm = FilmExponential(id: '1', name: 'Ilford HP5+', iso: 400, exponent: 1.34);
|
||||||
final Color _lightThemeColor = primaryColorsList[5];
|
final Color _lightThemeColor = primaryColorsList[5];
|
||||||
final Color _darkThemeColor = primaryColorsList[3];
|
final Color _darkThemeColor = primaryColorsList[3];
|
||||||
|
@ -109,6 +134,8 @@ void main() {
|
||||||
testWidgets('Generate light theme screenshots', (tester) async {
|
testWidgets('Generate light theme screenshots', (tester) async {
|
||||||
await mockSharedPrefs(theme: ThemeType.light, color: _lightThemeColor);
|
await mockSharedPrefs(theme: ThemeType.light, color: _lightThemeColor);
|
||||||
await tester.pumpApplication(
|
await tester.pumpApplication(
|
||||||
|
equipmentProfiles: [_mockEquipmentProfile].toTogglableMap(),
|
||||||
|
selectedEquipmentProfileId: _mockEquipmentProfile.id,
|
||||||
predefinedFilms: [_mockFilm].toTogglableMap(),
|
predefinedFilms: [_mockFilm].toTogglableMap(),
|
||||||
customFilms: {},
|
customFilms: {},
|
||||||
selectedFilmId: _mockFilm.id,
|
selectedFilmId: _mockFilm.id,
|
||||||
|
@ -159,6 +186,8 @@ void main() {
|
||||||
nd: photo.nd,
|
nd: photo.nd,
|
||||||
apertureValue: const ApertureValue(2.0, StopType.full),
|
apertureValue: const ApertureValue(2.0, StopType.full),
|
||||||
shutterSpeedValue: photo.shutterSpeedValue,
|
shutterSpeedValue: photo.shutterSpeedValue,
|
||||||
|
equipmentProfileId: _mockEquipmentProfile.id,
|
||||||
|
filmId: _mockFilm.id,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
await tester.tapDescendantTextOf<SettingsScreen>(S.current.logbook);
|
await tester.tapDescendantTextOf<SettingsScreen>(S.current.logbook);
|
||||||
|
|
Before Width: | Height: | Size: 466 KiB After Width: | Height: | Size: 469 KiB |
Before Width: | Height: | Size: 221 KiB After Width: | Height: | Size: 221 KiB |
Before Width: | Height: | Size: 737 KiB After Width: | Height: | Size: 572 KiB |
Before Width: | Height: | Size: 238 KiB After Width: | Height: | Size: 248 KiB |
Before Width: | Height: | Size: 230 KiB After Width: | Height: | Size: 229 KiB |
Before Width: | Height: | Size: 469 KiB After Width: | Height: | Size: 479 KiB |
Before Width: | Height: | Size: 224 KiB After Width: | Height: | Size: 224 KiB |
Before Width: | Height: | Size: 218 KiB After Width: | Height: | Size: 218 KiB |
Before Width: | Height: | Size: 343 KiB After Width: | Height: | Size: 346 KiB |
Before Width: | Height: | Size: 164 KiB After Width: | Height: | Size: 164 KiB |
Before Width: | Height: | Size: 535 KiB After Width: | Height: | Size: 416 KiB |
Before Width: | Height: | Size: 187 KiB After Width: | Height: | Size: 186 KiB |
Before Width: | Height: | Size: 339 KiB After Width: | Height: | Size: 346 KiB |
Before Width: | Height: | Size: 164 KiB After Width: | Height: | Size: 163 KiB |
Before Width: | Height: | Size: 508 KiB After Width: | Height: | Size: 512 KiB |
Before Width: | Height: | Size: 232 KiB After Width: | Height: | Size: 232 KiB |
Before Width: | Height: | Size: 792 KiB After Width: | Height: | Size: 617 KiB |
Before Width: | Height: | Size: 260 KiB After Width: | Height: | Size: 260 KiB |
Before Width: | Height: | Size: 499 KiB After Width: | Height: | Size: 506 KiB |
Before Width: | Height: | Size: 231 KiB After Width: | Height: | Size: 228 KiB |
|
@ -1,6 +1,8 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:lightmeter/data/geolocation_service.dart';
|
import 'package:lightmeter/data/geolocation_service.dart';
|
||||||
|
import 'package:lightmeter/providers/equipment_profile_provider.dart';
|
||||||
|
import 'package:lightmeter/providers/films_provider.dart';
|
||||||
import 'package:lightmeter/providers/logbook_photos_provider.dart';
|
import 'package:lightmeter/providers/logbook_photos_provider.dart';
|
||||||
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
|
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
|
||||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||||
|
@ -36,6 +38,13 @@ void main() {
|
||||||
when(() => storageService.deletePhoto(any<String>())).thenAnswer((_) async {});
|
when(() => storageService.deletePhoto(any<String>())).thenAnswer((_) async {});
|
||||||
when(() => storageService.getPhotos()).thenAnswer((_) => Future.value(_customPhotos));
|
when(() => storageService.getPhotos()).thenAnswer((_) => Future.value(_customPhotos));
|
||||||
|
|
||||||
|
when(() => storageService.selectedEquipmentProfileId).thenReturn('');
|
||||||
|
when(() => storageService.getEquipmentProfiles()).thenAnswer((_) => Future.value({}));
|
||||||
|
|
||||||
|
when(() => storageService.selectedFilmId).thenReturn(const FilmStub().id);
|
||||||
|
when(() => storageService.getPredefinedFilms()).thenAnswer((_) => Future.value({}));
|
||||||
|
when(() => storageService.getCustomFilms()).thenAnswer((_) => Future.value({}));
|
||||||
|
|
||||||
when(() => geolocationService.getCurrentPosition()).thenAnswer((_) => Future.value());
|
when(() => geolocationService.getCurrentPosition()).thenAnswer((_) => Future.value());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -47,12 +56,18 @@ void main() {
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
IAPProducts(
|
IAPProducts(
|
||||||
isPro: isPro,
|
isPro: isPro,
|
||||||
|
child: EquipmentProfilesProvider(
|
||||||
|
storageService: storageService,
|
||||||
|
child: FilmsProvider(
|
||||||
|
storageService: storageService,
|
||||||
child: LogbookPhotosProvider(
|
child: LogbookPhotosProvider(
|
||||||
storageService: storageService,
|
storageService: storageService,
|
||||||
geolocationService: geolocationService,
|
geolocationService: geolocationService,
|
||||||
child: const _Application(),
|
child: const _Application(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,7 +47,7 @@ void main() {
|
||||||
await pumpApplication(tester);
|
await pumpApplication(tester);
|
||||||
expectReadingValueContainerText(S.current.equipmentProfile);
|
expectReadingValueContainerText(S.current.equipmentProfile);
|
||||||
await tester.openAnimatedPicker<EquipmentProfilePicker>();
|
await tester.openAnimatedPicker<EquipmentProfilePicker>();
|
||||||
expect(find.byIcon(Icons.camera_outlined), findsOneWidget);
|
expect(find.byIcon(Icons.camera_alt_outlined), findsOneWidget);
|
||||||
expectDialogPickerText<EquipmentProfile>(S.current.equipmentProfile);
|
expectDialogPickerText<EquipmentProfile>(S.current.equipmentProfile);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
Before Width: | Height: | Size: 474 KiB After Width: | Height: | Size: 468 KiB |