mirror of
https://github.com/vodemn/m3_lightmeter.git
synced 2024-11-21 15:00:40 +00:00
ML-196 Allow to select equipment profile in use (#197)
* integrated `EquipmentProfilesStorageService` * implemented `EquipmentProfileEditScreen` * added equipment profiles screens to navigation * fixed tests * fixed splashscreen removal * replaced old `EquipmentProfilesScreen` * typo * use outlined icons * fixed storage mock for integration tests * recovered copy feature for profiles * added profile deletion to e2e test * added translations * added film translations * wip * add ability to toggle equipment profiles * lints * fixed tests * sync with iap rename * use `Toggleable` from resources * use iap 2.1.0 * use outlined edit icon
This commit is contained in:
parent
c66381f813
commit
30418a9cfd
41 changed files with 1234 additions and 937 deletions
|
@ -4,7 +4,8 @@ import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||||
|
|
||||||
export 'src/data/models/iap_product.dart';
|
export 'src/data/models/iap_product.dart';
|
||||||
export 'src/providers/iap_products_provider.dart';
|
export 'src/providers/iap_products_provider.dart';
|
||||||
export 'src/data/iap_storage_service.dart';
|
export 'src/data/equipment_profile_service.dart';
|
||||||
export 'src/data/films_storage_service.dart';
|
export 'src/data/films_storage_service.dart';
|
||||||
|
export 'src/data/iap_storage_service.dart';
|
||||||
|
|
||||||
const List<Film> films = [];
|
const List<Film> films = [];
|
||||||
|
|
29
iap/lib/src/data/equipment_profile_service.dart
Normal file
29
iap/lib/src/data/equipment_profile_service.dart
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||||
|
|
||||||
|
class EquipmentProfilesStorageService {
|
||||||
|
EquipmentProfilesStorageService();
|
||||||
|
|
||||||
|
Future<void> init() async {}
|
||||||
|
|
||||||
|
String get selectedEquipmentProfileId => '';
|
||||||
|
set selectedEquipmentProfileId(String id) {}
|
||||||
|
|
||||||
|
Future<void> addProfile(EquipmentProfile profile) async {}
|
||||||
|
|
||||||
|
Future<void> updateProfile({
|
||||||
|
required String id,
|
||||||
|
String? name,
|
||||||
|
List<IsoValue>? isoValues,
|
||||||
|
List<NdValue>? ndValues,
|
||||||
|
List<ApertureValue>? apertureValues,
|
||||||
|
List<ShutterSpeedValue>? shutterSpeedValues,
|
||||||
|
double? lensZoom,
|
||||||
|
bool? isUsed,
|
||||||
|
}) async {}
|
||||||
|
|
||||||
|
Future<void> deleteProfile(String id) async {}
|
||||||
|
|
||||||
|
Future<TogglableMap<EquipmentProfile>> getProfiles() async => {};
|
||||||
|
}
|
|
@ -1,16 +1,10 @@
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||||
|
|
||||||
typedef SelectableFilm<T extends Film> = ({T film, bool isUsed});
|
|
||||||
|
|
||||||
class FilmsStorageService {
|
class FilmsStorageService {
|
||||||
FilmsStorageService();
|
FilmsStorageService();
|
||||||
|
|
||||||
Future<void> init() async {}
|
Future<void> init() async {}
|
||||||
|
|
||||||
@visibleForTesting
|
|
||||||
Future<void> createTable(dynamic _) async {}
|
|
||||||
|
|
||||||
String get selectedFilmId => '';
|
String get selectedFilmId => '';
|
||||||
set selectedFilmId(String id) {}
|
set selectedFilmId(String id) {}
|
||||||
|
|
||||||
|
@ -22,11 +16,11 @@ class FilmsStorageService {
|
||||||
|
|
||||||
Future<void> deleteFilm(FilmExponential _) async {}
|
Future<void> deleteFilm(FilmExponential _) async {}
|
||||||
|
|
||||||
Future<Map<String, SelectableFilm<Film>>> getPredefinedFilms() async {
|
Future<TogglableMap<Film>> getPredefinedFilms() async {
|
||||||
return const {};
|
return const {};
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, SelectableFilm<FilmExponential>>> getCustomFilms() async {
|
Future<TogglableMap<FilmExponential>> getCustomFilms() async {
|
||||||
return const {};
|
return const {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.0.0
|
ref: v2.1.0
|
||||||
shared_preferences: 2.2.0
|
shared_preferences: 2.2.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
|
|
@ -48,8 +48,8 @@ void testE2E(String description) {
|
||||||
description,
|
description,
|
||||||
(tester) async {
|
(tester) async {
|
||||||
await tester.pumpApplication(
|
await tester.pumpApplication(
|
||||||
equipmentProfiles: [],
|
equipmentProfiles: {},
|
||||||
predefinedFilms: mockFilms.toFilmsMap(isUsed: true),
|
predefinedFilms: mockFilms.toTogglableMap(),
|
||||||
customFilms: {},
|
customFilms: {},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -58,27 +58,33 @@ void testE2E(String description) {
|
||||||
await tester.tapDescendantTextOf<SettingsScreen>(S.current.equipmentProfiles);
|
await tester.tapDescendantTextOf<SettingsScreen>(S.current.equipmentProfiles);
|
||||||
await tester.tap(find.byIcon(Icons.add_outlined).first);
|
await tester.tap(find.byIcon(Icons.add_outlined).first);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.setProfileName(mockEquipmentProfiles[0].name);
|
await tester.enterProfileName(mockEquipmentProfiles[0].name);
|
||||||
await tester.expandEquipmentProfileContainer(mockEquipmentProfiles[0].name);
|
await tester.setIsoValues(mockEquipmentProfiles[0].isoValues);
|
||||||
await tester.setIsoValues(0, mockEquipmentProfiles[0].isoValues);
|
await tester.setNdValues(mockEquipmentProfiles[0].ndValues);
|
||||||
await tester.setNdValues(0, mockEquipmentProfiles[0].ndValues);
|
await tester.setApertureValues(mockEquipmentProfiles[0].apertureValues);
|
||||||
await tester.setApertureValues(0, mockEquipmentProfiles[0].apertureValues);
|
await tester.setShutterSpeedValues(mockEquipmentProfiles[0].shutterSpeedValues);
|
||||||
await tester.setShutterSpeedValues(0, mockEquipmentProfiles[0].shutterSpeedValues);
|
await tester.setZoomValue(mockEquipmentProfiles[0].lensZoom);
|
||||||
await tester.setZoomValue(0, mockEquipmentProfiles[0].lensZoom);
|
|
||||||
expect(find.text('x1.91'), findsOneWidget);
|
expect(find.text('x1.91'), findsOneWidget);
|
||||||
expect(find.text('f/1.7 - f/16'), findsOneWidget);
|
expect(find.text('f/1.7 - f/16'), findsOneWidget);
|
||||||
expect(find.text('1/1000 - B'), findsOneWidget);
|
expect(find.text('1/1000 - B'), findsOneWidget);
|
||||||
|
await tester.saveEdits();
|
||||||
|
|
||||||
/// Create Praktica + Jupiter profile from Zenitar profile
|
/// Create Praktica + Jupiter profile from Zenitar profile
|
||||||
|
await tester.tap(find.byIcon(Icons.edit_outlined));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
await tester.tap(find.byIcon(Icons.copy_outlined).first);
|
await tester.tap(find.byIcon(Icons.copy_outlined).first);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.setProfileName(mockEquipmentProfiles[1].name);
|
await tester.enterProfileName(mockEquipmentProfiles[1].name);
|
||||||
await tester.expandEquipmentProfileContainer(mockEquipmentProfiles[1].name);
|
await tester.setApertureValues(mockEquipmentProfiles[1].apertureValues);
|
||||||
await tester.setApertureValues(1, mockEquipmentProfiles[1].apertureValues);
|
await tester.setZoomValue(mockEquipmentProfiles[1].lensZoom);
|
||||||
await tester.setZoomValue(1, mockEquipmentProfiles[1].lensZoom);
|
|
||||||
expect(find.text('x5.02'), findsOneWidget);
|
expect(find.text('x5.02'), findsOneWidget);
|
||||||
expect(find.text('f/3.5 - f/22'), findsOneWidget);
|
expect(find.text('f/3.5 - f/22'), findsOneWidget);
|
||||||
expect(find.text('1/1000 - B'), findsNWidgets(2));
|
expect(find.text('1/1000 - B'), findsNWidgets(1));
|
||||||
|
await tester.saveEdits();
|
||||||
|
|
||||||
|
/// Verify that both profiles were added and leave to the main screen
|
||||||
|
expect(find.text(mockEquipmentProfiles[0].name), findsOneWidget);
|
||||||
|
expect(find.text(mockEquipmentProfiles[1].name), findsOneWidget);
|
||||||
await tester.navigatorPop();
|
await tester.navigatorPop();
|
||||||
await tester.navigatorPop();
|
await tester.navigatorPop();
|
||||||
|
|
||||||
|
@ -142,45 +148,45 @@ void testE2E(String description) {
|
||||||
nd: 'None',
|
nd: 'None',
|
||||||
ev: mockPhotoEv100 + 1,
|
ev: mockPhotoEv100 + 1,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// 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.deleteEdits();
|
||||||
|
expect(find.text(mockEquipmentProfiles[0].name), findsNothing);
|
||||||
|
expect(find.text(mockEquipmentProfiles[1].name), findsOneWidget);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
extension EquipmentProfileActions on WidgetTester {
|
extension EquipmentProfileActions on WidgetTester {
|
||||||
Future<void> expandEquipmentProfileContainer(String name) async {
|
Future<void> enterProfileName(String name) async {
|
||||||
await tap(find.text(name));
|
|
||||||
await pump(Dimens.durationM);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> setProfileName(String name) async {
|
|
||||||
await enterText(find.byType(TextField), name);
|
await enterText(find.byType(TextField), name);
|
||||||
await pump();
|
await pump();
|
||||||
await tapSaveButton();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setIsoValues(int profileIndex, List<IsoValue> values) =>
|
Future<void> setIsoValues(List<IsoValue> values) =>
|
||||||
_openAndSetDialogFilterValues<IsoValue>(profileIndex, S.current.isoValues, values);
|
_openAndSetDialogFilterValues<IsoValue>(S.current.isoValues, values);
|
||||||
Future<void> setNdValues(int profileIndex, List<NdValue> values) =>
|
Future<void> setNdValues(List<NdValue> values) => _openAndSetDialogFilterValues<NdValue>(S.current.ndFilters, values);
|
||||||
_openAndSetDialogFilterValues<NdValue>(profileIndex, S.current.ndFilters, values);
|
|
||||||
Future<void> _openAndSetDialogFilterValues<T extends PhotographyValue>(
|
Future<void> _openAndSetDialogFilterValues<T extends PhotographyValue>(
|
||||||
int profileIndex,
|
|
||||||
String listTileTitle,
|
String listTileTitle,
|
||||||
List<T> valuesToSelect, {
|
List<T> valuesToSelect, {
|
||||||
bool deselectAll = true,
|
bool deselectAll = true,
|
||||||
}) async {
|
}) async {
|
||||||
await tap(find.text(listTileTitle).at(profileIndex));
|
await tap(find.text(listTileTitle));
|
||||||
await pumpAndSettle();
|
await pumpAndSettle();
|
||||||
await setDialogFilterValues(valuesToSelect, deselectAll: deselectAll);
|
await setDialogFilterValues(valuesToSelect, deselectAll: deselectAll);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setApertureValues(int profileIndex, List<ApertureValue> values) =>
|
Future<void> setApertureValues(List<ApertureValue> values) =>
|
||||||
_setDialogRangePickerValues<ApertureValue>(profileIndex, S.current.apertureValues, values);
|
_setDialogRangePickerValues<ApertureValue>(S.current.apertureValues, values);
|
||||||
|
|
||||||
Future<void> setShutterSpeedValues(int profileIndex, List<ShutterSpeedValue> values) =>
|
Future<void> setShutterSpeedValues(List<ShutterSpeedValue> values) =>
|
||||||
_setDialogRangePickerValues<ShutterSpeedValue>(profileIndex, S.current.shutterSpeedValues, values);
|
_setDialogRangePickerValues<ShutterSpeedValue>(S.current.shutterSpeedValues, values);
|
||||||
|
|
||||||
Future<void> setZoomValue(int profileIndex, double value) =>
|
Future<void> setZoomValue(double value) => _setDialogSliderPickerValue(S.current.lensZoom, value);
|
||||||
_setDialogSliderPickerValue(profileIndex, S.current.lensZoom, value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension on WidgetTester {
|
extension on WidgetTester {
|
||||||
|
@ -190,6 +196,16 @@ extension on WidgetTester {
|
||||||
await tapSelectButton();
|
await tapSelectButton();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> saveEdits() async {
|
||||||
|
await tap(find.byIcon(Icons.save_outlined));
|
||||||
|
await pumpAndSettle(Dimens.durationML);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteEdits() async {
|
||||||
|
await tap(find.byIcon(Icons.delete_outlined));
|
||||||
|
await pumpAndSettle(Dimens.durationML);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> setDialogFilterValues<T>(
|
Future<void> setDialogFilterValues<T>(
|
||||||
List<T> valuesToSelect, {
|
List<T> valuesToSelect, {
|
||||||
bool deselectAll = true,
|
bool deselectAll = true,
|
||||||
|
@ -212,11 +228,10 @@ extension on WidgetTester {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _setDialogRangePickerValues<T extends PhotographyValue>(
|
Future<void> _setDialogRangePickerValues<T extends PhotographyValue>(
|
||||||
int profileIndex,
|
|
||||||
String listTileTitle,
|
String listTileTitle,
|
||||||
List<T> valuesToSelect,
|
List<T> valuesToSelect,
|
||||||
) async {
|
) async {
|
||||||
await tap(find.text(listTileTitle).at(profileIndex));
|
await tap(find.text(listTileTitle));
|
||||||
await pumpAndSettle();
|
await pumpAndSettle();
|
||||||
|
|
||||||
final dialog = widget<DialogRangePicker<T>>(find.byType(DialogRangePicker<T>));
|
final dialog = widget<DialogRangePicker<T>>(find.byType(DialogRangePicker<T>));
|
||||||
|
@ -247,11 +262,10 @@ extension on WidgetTester {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _setDialogSliderPickerValue(
|
Future<void> _setDialogSliderPickerValue(
|
||||||
int profileIndex,
|
|
||||||
String listTileTitle,
|
String listTileTitle,
|
||||||
double value,
|
double value,
|
||||||
) async {
|
) async {
|
||||||
await tap(find.text(listTileTitle).at(profileIndex));
|
await tap(find.text(listTileTitle));
|
||||||
await pumpAndSettle();
|
await pumpAndSettle();
|
||||||
|
|
||||||
final sliderFinder = find.byType(Slider);
|
final sliderFinder = find.byType(Slider);
|
||||||
|
|
|
@ -5,28 +5,29 @@ 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';
|
||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
class _MockIAPStorageService extends Mock implements IAPStorageService {}
|
class _MockEquipmentProfilesStorageService extends Mock implements EquipmentProfilesStorageService {}
|
||||||
|
|
||||||
class _MockFilmsStorageService extends Mock implements FilmsStorageService {}
|
class _MockFilmsStorageService extends Mock implements FilmsStorageService {}
|
||||||
|
|
||||||
class MockIAPProviders extends StatefulWidget {
|
class MockIAPProviders extends StatefulWidget {
|
||||||
final List<EquipmentProfile>? equipmentProfiles;
|
final TogglableMap<EquipmentProfile> equipmentProfiles;
|
||||||
final String selectedEquipmentProfileId;
|
final String selectedEquipmentProfileId;
|
||||||
final Map<String, SelectableFilm<Film>> predefinedFilms;
|
final TogglableMap<Film> predefinedFilms;
|
||||||
final Map<String, SelectableFilm<FilmExponential>> customFilms;
|
final TogglableMap<FilmExponential> customFilms;
|
||||||
final String selectedFilmId;
|
final String selectedFilmId;
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
|
||||||
MockIAPProviders({
|
MockIAPProviders({
|
||||||
this.equipmentProfiles = const [],
|
TogglableMap<EquipmentProfile>? equipmentProfiles,
|
||||||
this.selectedEquipmentProfileId = '',
|
this.selectedEquipmentProfileId = '',
|
||||||
Map<String, SelectableFilm<Film>>? predefinedFilms,
|
TogglableMap<Film>? predefinedFilms,
|
||||||
Map<String, SelectableFilm<FilmExponential>>? customFilms,
|
TogglableMap<FilmExponential>? customFilms,
|
||||||
String? selectedFilmId,
|
String? selectedFilmId,
|
||||||
required this.child,
|
required this.child,
|
||||||
super.key,
|
super.key,
|
||||||
}) : predefinedFilms = predefinedFilms ?? mockFilms.toFilmsMap(),
|
}) : equipmentProfiles = equipmentProfiles ?? mockEquipmentProfiles.toTogglableMap(),
|
||||||
customFilms = customFilms ?? mockFilms.toFilmsMap(),
|
predefinedFilms = predefinedFilms ?? mockFilms.toTogglableMap(),
|
||||||
|
customFilms = customFilms ?? mockFilms.toTogglableMap(),
|
||||||
selectedFilmId = selectedFilmId ?? const FilmStub().id;
|
selectedFilmId = selectedFilmId ?? const FilmStub().id;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -34,15 +35,27 @@ class MockIAPProviders extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MockIAPProvidersState extends State<MockIAPProviders> {
|
class _MockIAPProvidersState extends State<MockIAPProviders> {
|
||||||
late final _MockIAPStorageService mockIAPStorageService;
|
late final _MockEquipmentProfilesStorageService mockEquipmentProfilesStorageService;
|
||||||
late final _MockFilmsStorageService mockFilmsStorageService;
|
late final _MockFilmsStorageService mockFilmsStorageService;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
mockIAPStorageService = _MockIAPStorageService();
|
registerFallbackValue(defaultEquipmentProfile);
|
||||||
when(() => mockIAPStorageService.equipmentProfiles).thenReturn(widget.equipmentProfiles ?? mockEquipmentProfiles);
|
mockEquipmentProfilesStorageService = _MockEquipmentProfilesStorageService();
|
||||||
when(() => mockIAPStorageService.selectedEquipmentProfileId).thenReturn(widget.selectedEquipmentProfileId);
|
when(() => mockEquipmentProfilesStorageService.init()).thenAnswer((_) async {});
|
||||||
|
when(() => mockEquipmentProfilesStorageService.getProfiles())
|
||||||
|
.thenAnswer((_) => Future.value(widget.equipmentProfiles));
|
||||||
|
when(() => mockEquipmentProfilesStorageService.selectedEquipmentProfileId)
|
||||||
|
.thenReturn(widget.selectedEquipmentProfileId);
|
||||||
|
when(() => mockEquipmentProfilesStorageService.addProfile(any<EquipmentProfile>())).thenAnswer((_) async {});
|
||||||
|
when(
|
||||||
|
() => mockEquipmentProfilesStorageService.updateProfile(
|
||||||
|
id: any<String>(named: 'id'),
|
||||||
|
name: any<String>(named: 'name'),
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async {});
|
||||||
|
when(() => mockEquipmentProfilesStorageService.deleteProfile(any<String>())).thenAnswer((_) async {});
|
||||||
|
|
||||||
mockFilmsStorageService = _MockFilmsStorageService();
|
mockFilmsStorageService = _MockFilmsStorageService();
|
||||||
when(() => mockFilmsStorageService.init()).thenAnswer((_) async {});
|
when(() => mockFilmsStorageService.init()).thenAnswer((_) async {});
|
||||||
|
@ -53,10 +66,10 @@ class _MockIAPProvidersState extends State<MockIAPProviders> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return EquipmentProfileProvider(
|
return EquipmentProfilesProvider(
|
||||||
storageService: mockIAPStorageService,
|
storageService: mockEquipmentProfilesStorageService,
|
||||||
child: FilmsProvider(
|
child: FilmsProvider(
|
||||||
filmsStorageService: mockFilmsStorageService,
|
storageService: mockFilmsStorageService,
|
||||||
child: widget.child,
|
child: widget.child,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -142,11 +155,6 @@ const mockFilms = [
|
||||||
_FilmMultiplying(id: '4', name: 'Mock film 4', iso: 1200, reciprocityMultiplier: 1.5),
|
_FilmMultiplying(id: '4', name: 'Mock film 4', iso: 1200, reciprocityMultiplier: 1.5),
|
||||||
];
|
];
|
||||||
|
|
||||||
extension FilmMapper on List<Film> {
|
|
||||||
Map<String, ({T film, bool isUsed})> toFilmsMap<T extends Film>({bool isUsed = true}) =>
|
|
||||||
Map.fromEntries(map((e) => MapEntry(e.id, (film: e as T, isUsed: isUsed))));
|
|
||||||
}
|
|
||||||
|
|
||||||
class _FilmMultiplying extends FilmExponential {
|
class _FilmMultiplying extends FilmExponential {
|
||||||
final double reciprocityMultiplier;
|
final double reciprocityMultiplier;
|
||||||
|
|
||||||
|
|
|
@ -20,10 +20,10 @@ const mockPhotoEv100 = 8.3;
|
||||||
extension WidgetTesterCommonActions on WidgetTester {
|
extension WidgetTesterCommonActions on WidgetTester {
|
||||||
Future<void> pumpApplication({
|
Future<void> pumpApplication({
|
||||||
IAPProductStatus productStatus = IAPProductStatus.purchased,
|
IAPProductStatus productStatus = IAPProductStatus.purchased,
|
||||||
List<EquipmentProfile>? equipmentProfiles,
|
TogglableMap<EquipmentProfile>? equipmentProfiles,
|
||||||
String selectedEquipmentProfileId = '',
|
String selectedEquipmentProfileId = '',
|
||||||
Map<String, SelectableFilm<Film>>? predefinedFilms,
|
TogglableMap<Film>? predefinedFilms,
|
||||||
Map<String, SelectableFilm<FilmExponential>>? customFilms,
|
TogglableMap<FilmExponential>? customFilms,
|
||||||
String selectedFilmId = '',
|
String selectedFilmId = '',
|
||||||
}) async {
|
}) async {
|
||||||
await pumpWidget(
|
await pumpWidget(
|
||||||
|
|
|
@ -7,6 +7,8 @@ import 'package:lightmeter/navigation/modal_route_args_parser.dart';
|
||||||
import 'package:lightmeter/navigation/routes.dart';
|
import 'package:lightmeter/navigation/routes.dart';
|
||||||
import 'package:lightmeter/platform_config.dart';
|
import 'package:lightmeter/platform_config.dart';
|
||||||
import 'package:lightmeter/providers/user_preferences_provider.dart';
|
import 'package:lightmeter/providers/user_preferences_provider.dart';
|
||||||
|
import 'package:lightmeter/screens/equipment_profile_edit/flow_equipment_profile_edit.dart';
|
||||||
|
import 'package:lightmeter/screens/equipment_profiles/screen_equipment_profiles.dart';
|
||||||
import 'package:lightmeter/screens/film_edit/flow_film_edit.dart';
|
import 'package:lightmeter/screens/film_edit/flow_film_edit.dart';
|
||||||
import 'package:lightmeter/screens/films/screen_films.dart';
|
import 'package:lightmeter/screens/films/screen_films.dart';
|
||||||
import 'package:lightmeter/screens/lightmeter_pro/screen_lightmeter_pro.dart';
|
import 'package:lightmeter/screens/lightmeter_pro/screen_lightmeter_pro.dart';
|
||||||
|
@ -49,8 +51,10 @@ class Application extends StatelessWidget {
|
||||||
routes: {
|
routes: {
|
||||||
NavigationRoutes.meteringScreen.name: (_) => const ReleaseNotesFlow(child: MeteringFlow()),
|
NavigationRoutes.meteringScreen.name: (_) => const ReleaseNotesFlow(child: MeteringFlow()),
|
||||||
NavigationRoutes.settingsScreen.name: (_) => const SettingsFlow(),
|
NavigationRoutes.settingsScreen.name: (_) => const SettingsFlow(),
|
||||||
|
NavigationRoutes.equipmentProfilesListScreen.name: (_) => const EquipmentProfilesScreen(),
|
||||||
|
NavigationRoutes.equipmentProfileEditScreen.name: (context) =>
|
||||||
|
EquipmentProfileEditFlow(args: context.routeArgs<EquipmentProfileEditArgs>()),
|
||||||
NavigationRoutes.filmsListScreen.name: (_) => const FilmsScreen(),
|
NavigationRoutes.filmsListScreen.name: (_) => const FilmsScreen(),
|
||||||
NavigationRoutes.filmAddScreen.name: (_) => const FilmEditFlow(args: FilmEditArgs()),
|
|
||||||
NavigationRoutes.filmEditScreen.name: (context) => FilmEditFlow(args: context.routeArgs<FilmEditArgs>()),
|
NavigationRoutes.filmEditScreen.name: (context) => FilmEditFlow(args: context.routeArgs<FilmEditArgs>()),
|
||||||
NavigationRoutes.proFeaturesScreen.name: (_) => LightmeterProScreen(),
|
NavigationRoutes.proFeaturesScreen.name: (_) => LightmeterProScreen(),
|
||||||
NavigationRoutes.timerScreen.name: (context) => TimerFlow(args: context.routeArgs<TimerFlowArgs>()),
|
NavigationRoutes.timerScreen.name: (context) => TimerFlow(args: context.routeArgs<TimerFlowArgs>()),
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
||||||
import 'package:lightmeter/data/analytics/analytics.dart';
|
import 'package:lightmeter/data/analytics/analytics.dart';
|
||||||
|
@ -34,11 +36,14 @@ class _ApplicationWrapperState extends State<ApplicationWrapper> {
|
||||||
? const RemoteConfigService(LightmeterAnalytics(api: LightmeterAnalyticsFirebase()))
|
? const RemoteConfigService(LightmeterAnalytics(api: LightmeterAnalyticsFirebase()))
|
||||||
: const MockRemoteConfigService();
|
: const MockRemoteConfigService();
|
||||||
|
|
||||||
late final IAPStorageService iapStorageService;
|
|
||||||
late final UserPreferencesService userPreferencesService;
|
late final UserPreferencesService userPreferencesService;
|
||||||
late final bool hasLightSensor;
|
late final bool hasLightSensor;
|
||||||
|
|
||||||
|
final equipmentProfilesStorageService = EquipmentProfilesStorageService();
|
||||||
|
final equipmentProfilesStorageServiceCompleter = Completer<void>();
|
||||||
|
|
||||||
final filmsStorageService = FilmsStorageService();
|
final filmsStorageService = FilmsStorageService();
|
||||||
|
final filmsStorageServiceCompleter = Completer<void>();
|
||||||
|
|
||||||
late final Future<void> _initFuture;
|
late final Future<void> _initFuture;
|
||||||
|
|
||||||
|
@ -46,6 +51,7 @@ class _ApplicationWrapperState extends State<ApplicationWrapper> {
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_initFuture = _initialize();
|
_initFuture = _initialize();
|
||||||
|
_removeSplashscreen();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -67,11 +73,12 @@ class _ApplicationWrapperState extends State<ApplicationWrapper> {
|
||||||
volumeEventsService: const VolumeEventsService(LocalPlatform()),
|
volumeEventsService: const VolumeEventsService(LocalPlatform()),
|
||||||
child: RemoteConfigProvider(
|
child: RemoteConfigProvider(
|
||||||
remoteConfigService: remoteConfigService,
|
remoteConfigService: remoteConfigService,
|
||||||
child: EquipmentProfileProvider(
|
child: EquipmentProfilesProvider(
|
||||||
storageService: iapStorageService,
|
storageService: equipmentProfilesStorageService,
|
||||||
|
onInitialized: equipmentProfilesStorageServiceCompleter.complete,
|
||||||
child: FilmsProvider(
|
child: FilmsProvider(
|
||||||
filmsStorageService: filmsStorageService,
|
storageService: filmsStorageService,
|
||||||
onInitialized: _onFilmsProviderInitialized,
|
onInitialized: filmsStorageServiceCompleter.complete,
|
||||||
child: UserPreferencesProvider(
|
child: UserPreferencesProvider(
|
||||||
hasLightSensor: hasLightSensor,
|
hasLightSensor: hasLightSensor,
|
||||||
userPreferencesService: userPreferencesService,
|
userPreferencesService: userPreferencesService,
|
||||||
|
@ -93,16 +100,20 @@ class _ApplicationWrapperState extends State<ApplicationWrapper> {
|
||||||
SharedPreferences.getInstance(),
|
SharedPreferences.getInstance(),
|
||||||
const LightSensorService(LocalPlatform()).hasSensor(),
|
const LightSensorService(LocalPlatform()).hasSensor(),
|
||||||
remoteConfigService.activeAndFetchFeatures(),
|
remoteConfigService.activeAndFetchFeatures(),
|
||||||
|
equipmentProfilesStorageService.init(),
|
||||||
filmsStorageService.init(),
|
filmsStorageService.init(),
|
||||||
]).then((value) {
|
]).then((value) {
|
||||||
final sharedPrefs = (value[0] as SharedPreferences?)!;
|
userPreferencesService = UserPreferencesService((value[0] as SharedPreferences?)!);
|
||||||
iapStorageService = IAPStorageService(sharedPrefs);
|
|
||||||
userPreferencesService = UserPreferencesService(sharedPrefs);
|
|
||||||
hasLightSensor = value[1] as bool? ?? false;
|
hasLightSensor = value[1] as bool? ?? false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onFilmsProviderInitialized() {
|
void _removeSplashscreen() {
|
||||||
FlutterNativeSplash.remove();
|
Future.wait([
|
||||||
|
equipmentProfilesStorageServiceCompleter.future,
|
||||||
|
filmsStorageServiceCompleter.future,
|
||||||
|
]).then((_) {
|
||||||
|
FlutterNativeSplash.remove();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -152,7 +152,6 @@
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
"films": "Films",
|
"films": "Films",
|
||||||
"filmsInUse": "Films in use",
|
"filmsInUse": "Films in use",
|
||||||
"filmsInUseDescription": "Select films which you use.",
|
|
||||||
"filmsCustom": "Custom films",
|
"filmsCustom": "Custom films",
|
||||||
"addFilmTitle": "Add film",
|
"addFilmTitle": "Add film",
|
||||||
"editFilmTitle": "Edit film",
|
"editFilmTitle": "Edit film",
|
||||||
|
@ -160,5 +159,7 @@
|
||||||
"filmFormulaExponential": "T=t^Rf",
|
"filmFormulaExponential": "T=t^Rf",
|
||||||
"filmFormulaExponentialRf": "Rf",
|
"filmFormulaExponentialRf": "Rf",
|
||||||
"filmFormulaExponentialRfPlaceholder": "1.3",
|
"filmFormulaExponentialRfPlaceholder": "1.3",
|
||||||
"name": "Name"
|
"name": "Name",
|
||||||
|
"addEquipmentProfileTitle": "Add equipment",
|
||||||
|
"editEquipmentProfileTitle": "Edit equipment"
|
||||||
}
|
}
|
|
@ -67,8 +67,6 @@
|
||||||
"equipmentProfile": "Profil de l'équipement",
|
"equipmentProfile": "Profil de l'équipement",
|
||||||
"equipmentProfiles": "Profils de l'équipement",
|
"equipmentProfiles": "Profils de l'équipement",
|
||||||
"tapToAdd": "Appuie pour ajouter",
|
"tapToAdd": "Appuie pour ajouter",
|
||||||
"filmsInUse": "Films en usage",
|
|
||||||
"filmsInUseDescription": "Sélectionnez les films que vous utilisez.",
|
|
||||||
"general": "Général",
|
"general": "Général",
|
||||||
"keepScreenOn": "Garder l'écran allumé",
|
"keepScreenOn": "Garder l'écran allumé",
|
||||||
"haptics": "Haptiques",
|
"haptics": "Haptiques",
|
||||||
|
@ -142,5 +140,17 @@
|
||||||
"tooltipUseLightSensor": "Utiliser un capteur de lumière",
|
"tooltipUseLightSensor": "Utiliser un capteur de lumière",
|
||||||
"tooltipUseCamera": "Utiliser la caméra",
|
"tooltipUseCamera": "Utiliser la caméra",
|
||||||
"tooltipOpenSettings": "Ouvrir les paramètres",
|
"tooltipOpenSettings": "Ouvrir les paramètres",
|
||||||
"exposurePair": "Paire d'exposition"
|
"exposurePair": "Paire d'exposition",
|
||||||
|
"films": "Films",
|
||||||
|
"filmsInUse": "Films en usage",
|
||||||
|
"filmsCustom": "Films personnalisés",
|
||||||
|
"addFilmTitle": "Ajouter un film",
|
||||||
|
"editFilmTitle": "Editer le film",
|
||||||
|
"filmFormula": "Formule",
|
||||||
|
"filmFormulaExponential": "T=t^Rf",
|
||||||
|
"filmFormulaExponentialRf": "Rf",
|
||||||
|
"filmFormulaExponentialRfPlaceholder": "1.3",
|
||||||
|
"name": "Titre",
|
||||||
|
"addEquipmentProfileTitle": "Ajouter un profil",
|
||||||
|
"editEquipmentProfileTitle": "Editer le profil"
|
||||||
}
|
}
|
|
@ -67,8 +67,6 @@
|
||||||
"equipmentProfile": "Оборудование",
|
"equipmentProfile": "Оборудование",
|
||||||
"equipmentProfiles": "Профили оборудования",
|
"equipmentProfiles": "Профили оборудования",
|
||||||
"tapToAdd": "Нажмите, чтобы добавить",
|
"tapToAdd": "Нажмите, чтобы добавить",
|
||||||
"filmsInUse": "Используемые плёнки",
|
|
||||||
"filmsInUseDescription": "Выберите плёнки, которыми вы пользуетесь.",
|
|
||||||
"general": "Общие",
|
"general": "Общие",
|
||||||
"keepScreenOn": "Запрет блокировки",
|
"keepScreenOn": "Запрет блокировки",
|
||||||
"haptics": "Вибрация",
|
"haptics": "Вибрация",
|
||||||
|
@ -141,5 +139,17 @@
|
||||||
"tooltipUseLightSensor": "Использовать датчик освещенности",
|
"tooltipUseLightSensor": "Использовать датчик освещенности",
|
||||||
"tooltipUseCamera": "Использовать камеру",
|
"tooltipUseCamera": "Использовать камеру",
|
||||||
"tooltipOpenSettings": "Открыть настройки",
|
"tooltipOpenSettings": "Открыть настройки",
|
||||||
"exposurePair": "Пара экспозиции"
|
"exposurePair": "Пара экспозиции",
|
||||||
|
"films": "Плёнки",
|
||||||
|
"filmsInUse": "Используемые плёнки",
|
||||||
|
"filmsCustom": "Пользовательские плёнки",
|
||||||
|
"addFilmTitle": "Добавить плёнку",
|
||||||
|
"editFilmTitle": "Редактировать плёнку",
|
||||||
|
"filmFormula": "Формула",
|
||||||
|
"filmFormulaExponential": "T=t^Rf",
|
||||||
|
"filmFormulaExponentialRf": "Rf",
|
||||||
|
"filmFormulaExponentialRfPlaceholder": "1.3",
|
||||||
|
"name": "Название",
|
||||||
|
"addEquipmentProfileTitle": "Добавить профиль",
|
||||||
|
"editEquipmentProfileTitle": "Редактировать профиль"
|
||||||
}
|
}
|
|
@ -67,8 +67,6 @@
|
||||||
"equipmentProfile": "设备配置",
|
"equipmentProfile": "设备配置",
|
||||||
"equipmentProfiles": "设备配置",
|
"equipmentProfiles": "设备配置",
|
||||||
"tapToAdd": "点击添加",
|
"tapToAdd": "点击添加",
|
||||||
"filmsInUse": "使用的胶片",
|
|
||||||
"filmsInUseDescription": "选择你使用的胶片",
|
|
||||||
"general": "通用",
|
"general": "通用",
|
||||||
"keepScreenOn": "保持屏幕常亮",
|
"keepScreenOn": "保持屏幕常亮",
|
||||||
"haptics": "震动",
|
"haptics": "震动",
|
||||||
|
@ -140,5 +138,16 @@
|
||||||
"tooltipUseLightSensor": "使用光线传感器",
|
"tooltipUseLightSensor": "使用光线传感器",
|
||||||
"tooltipUseCamera": "使用摄像头",
|
"tooltipUseCamera": "使用摄像头",
|
||||||
"tooltipOpenSettings": "打开设置",
|
"tooltipOpenSettings": "打开设置",
|
||||||
"exposurePair": "曝光对"
|
"exposurePair": "曝光对",
|
||||||
|
"films": "Films",
|
||||||
|
"filmsInUse": "使用的胶片",
|
||||||
|
"filmsCustom": "定制胶片",
|
||||||
|
"addFilmTitle": "添加胶片",
|
||||||
|
"editFilmTitle": "编辑胶片",
|
||||||
|
"filmFormula": "计算公式",
|
||||||
|
"filmFormulaExponential": "T=t^Rf",
|
||||||
|
"filmFormulaExponentialRf": "Rf",
|
||||||
|
"filmFormulaExponentialRfPlaceholder": "1.3",
|
||||||
|
"addEquipmentProfileTitle": "添加简介",
|
||||||
|
"editEquipmentProfileTitle": "编辑个人资料"
|
||||||
}
|
}
|
|
@ -1,8 +1,9 @@
|
||||||
enum NavigationRoutes {
|
enum NavigationRoutes {
|
||||||
meteringScreen,
|
meteringScreen,
|
||||||
settingsScreen,
|
settingsScreen,
|
||||||
|
equipmentProfilesListScreen,
|
||||||
|
equipmentProfileEditScreen,
|
||||||
filmsListScreen,
|
filmsListScreen,
|
||||||
filmAddScreen,
|
|
||||||
filmEditScreen,
|
filmEditScreen,
|
||||||
proFeaturesScreen,
|
proFeaturesScreen,
|
||||||
timerScreen,
|
timerScreen,
|
||||||
|
|
|
@ -1,30 +1,11 @@
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:lightmeter/utils/context_utils.dart';
|
import 'package:lightmeter/utils/context_utils.dart';
|
||||||
import 'package:lightmeter/utils/selectable_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';
|
||||||
import 'package:uuid/uuid.dart';
|
|
||||||
|
|
||||||
class EquipmentProfileProvider extends StatefulWidget {
|
class EquipmentProfilesProvider extends StatefulWidget {
|
||||||
final IAPStorageService storageService;
|
static const EquipmentProfile defaultProfile = EquipmentProfile(
|
||||||
final Widget child;
|
|
||||||
|
|
||||||
const EquipmentProfileProvider({
|
|
||||||
required this.storageService,
|
|
||||||
required this.child,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
static EquipmentProfileProviderState of(BuildContext context) {
|
|
||||||
return context.findAncestorStateOfType<EquipmentProfileProviderState>()!;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<EquipmentProfileProvider> createState() => EquipmentProfileProviderState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class EquipmentProfileProviderState extends State<EquipmentProfileProvider> {
|
|
||||||
static const EquipmentProfile _defaultProfile = EquipmentProfile(
|
|
||||||
id: '',
|
id: '',
|
||||||
name: '',
|
name: '',
|
||||||
apertureValues: ApertureValue.values,
|
apertureValues: ApertureValue.values,
|
||||||
|
@ -33,34 +14,89 @@ class EquipmentProfileProviderState extends State<EquipmentProfileProvider> {
|
||||||
isoValues: IsoValue.values,
|
isoValues: IsoValue.values,
|
||||||
);
|
);
|
||||||
|
|
||||||
List<EquipmentProfile> _customProfiles = [];
|
final EquipmentProfilesStorageService storageService;
|
||||||
|
final VoidCallback? onInitialized;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const EquipmentProfilesProvider({
|
||||||
|
required this.storageService,
|
||||||
|
this.onInitialized,
|
||||||
|
required this.child,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
static EquipmentProfilesProviderState of(BuildContext context) {
|
||||||
|
return context.findAncestorStateOfType<EquipmentProfilesProviderState>()!;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<EquipmentProfilesProvider> createState() => EquipmentProfilesProviderState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class EquipmentProfilesProviderState extends State<EquipmentProfilesProvider> {
|
||||||
|
final TogglableMap<EquipmentProfile> _customProfiles = {};
|
||||||
String _selectedId = '';
|
String _selectedId = '';
|
||||||
|
|
||||||
EquipmentProfile get _selectedProfile => _customProfiles.firstWhere(
|
EquipmentProfile get _selectedProfile =>
|
||||||
(e) => e.id == _selectedId,
|
_customProfiles[_selectedId]?.value ?? EquipmentProfilesProvider.defaultProfile;
|
||||||
orElse: () => _defaultProfile,
|
|
||||||
);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_selectedId = widget.storageService.selectedEquipmentProfileId;
|
_init();
|
||||||
_customProfiles = widget.storageService.equipmentProfiles;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return EquipmentProfiles(
|
return EquipmentProfiles(
|
||||||
values: [
|
profiles: context.isPro ? _customProfiles : {},
|
||||||
_defaultProfile,
|
selected: context.isPro ? _selectedProfile : EquipmentProfilesProvider.defaultProfile,
|
||||||
if (context.isPro) ..._customProfiles,
|
|
||||||
],
|
|
||||||
selected: context.isPro ? _selectedProfile : _defaultProfile,
|
|
||||||
child: widget.child,
|
child: widget.child,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void setProfile(EquipmentProfile data) {
|
Future<void> _init() async {
|
||||||
|
_selectedId = widget.storageService.selectedEquipmentProfileId;
|
||||||
|
_customProfiles.addAll(await widget.storageService.getProfiles());
|
||||||
|
_discardSelectedIfNotIncluded();
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
widget.onInitialized?.call();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addProfile(EquipmentProfile profile) async {
|
||||||
|
await widget.storageService.addProfile(profile);
|
||||||
|
_customProfiles[profile.id] = (value: profile, isUsed: true);
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateProfile(EquipmentProfile profile) async {
|
||||||
|
final oldProfile = _customProfiles[profile.id]!.value;
|
||||||
|
await widget.storageService.updateProfile(
|
||||||
|
id: profile.id,
|
||||||
|
name: oldProfile.name != profile.name ? profile.name : null,
|
||||||
|
apertureValues: oldProfile.apertureValues != profile.apertureValues ? profile.apertureValues : null,
|
||||||
|
shutterSpeedValues:
|
||||||
|
oldProfile.shutterSpeedValues != profile.shutterSpeedValues ? profile.shutterSpeedValues : null,
|
||||||
|
isoValues: oldProfile.isoValues != profile.isoValues ? profile.isoValues : null,
|
||||||
|
ndValues: oldProfile.ndValues != profile.ndValues ? profile.ndValues : null,
|
||||||
|
lensZoom: oldProfile.lensZoom != profile.lensZoom ? profile.lensZoom : null,
|
||||||
|
);
|
||||||
|
_customProfiles[profile.id] = (value: profile, isUsed: _customProfiles[profile.id]!.isUsed);
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteProfile(EquipmentProfile profile) async {
|
||||||
|
await widget.storageService.deleteProfile(profile.id);
|
||||||
|
if (profile.id == _selectedId) {
|
||||||
|
_selectedId = EquipmentProfilesProvider.defaultProfile.id;
|
||||||
|
widget.storageService.selectedEquipmentProfileId = EquipmentProfilesProvider.defaultProfile.id;
|
||||||
|
}
|
||||||
|
_customProfiles.remove(profile.id);
|
||||||
|
_discardSelectedIfNotIncluded();
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
void selectProfile(EquipmentProfile data) {
|
||||||
if (_selectedId != data.id) {
|
if (_selectedId != data.id) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedId = data.id;
|
_selectedId = data.id;
|
||||||
|
@ -69,62 +105,81 @@ class EquipmentProfileProviderState extends State<EquipmentProfileProvider> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a default equipment profile
|
Future<void> toggleProfile(EquipmentProfile profile, bool enabled) async {
|
||||||
void addProfile(String name, [EquipmentProfile? copyFrom]) {
|
if (_customProfiles.containsKey(profile.id)) {
|
||||||
_customProfiles.add(
|
_customProfiles[profile.id] = (value: profile, isUsed: enabled);
|
||||||
EquipmentProfile(
|
} else {
|
||||||
id: const Uuid().v1(),
|
return;
|
||||||
name: name,
|
|
||||||
apertureValues: copyFrom?.apertureValues ?? ApertureValue.values,
|
|
||||||
ndValues: copyFrom?.ndValues ?? NdValue.values,
|
|
||||||
shutterSpeedValues: copyFrom?.shutterSpeedValues ?? ShutterSpeedValue.values,
|
|
||||||
isoValues: copyFrom?.isoValues ?? IsoValue.values,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
_refreshSavedProfiles();
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateProfile(EquipmentProfile data) {
|
|
||||||
final indexToUpdate = _customProfiles.indexWhere((element) => element.id == data.id);
|
|
||||||
if (indexToUpdate >= 0) {
|
|
||||||
_customProfiles[indexToUpdate] = data;
|
|
||||||
_refreshSavedProfiles();
|
|
||||||
}
|
}
|
||||||
}
|
await widget.storageService.updateProfile(id: profile.id, isUsed: enabled);
|
||||||
|
_discardSelectedIfNotIncluded();
|
||||||
void deleteProfile(EquipmentProfile data) {
|
|
||||||
if (data.id == _selectedId) {
|
|
||||||
_selectedId = _defaultProfile.id;
|
|
||||||
widget.storageService.selectedEquipmentProfileId = _defaultProfile.id;
|
|
||||||
}
|
|
||||||
_customProfiles.remove(data);
|
|
||||||
_refreshSavedProfiles();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _refreshSavedProfiles() {
|
|
||||||
widget.storageService.equipmentProfiles = _customProfiles;
|
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _discardSelectedIfNotIncluded() {
|
||||||
|
if (_selectedId == EquipmentProfilesProvider.defaultProfile.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final isSelectedUsed = _customProfiles[_selectedId]?.isUsed ?? false;
|
||||||
|
if (!isSelectedUsed) {
|
||||||
|
_selectedId = EquipmentProfilesProvider.defaultProfile.id;
|
||||||
|
widget.storageService.selectedEquipmentProfileId = _selectedId;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class EquipmentProfiles extends SelectableInheritedModel<EquipmentProfile> {
|
enum _EquipmentProfilesModelAspect {
|
||||||
|
profiles,
|
||||||
|
profilesInUse,
|
||||||
|
selected,
|
||||||
|
}
|
||||||
|
|
||||||
|
class EquipmentProfiles extends InheritedModel<_EquipmentProfilesModelAspect> {
|
||||||
|
final TogglableMap<EquipmentProfile> profiles;
|
||||||
|
final EquipmentProfile selected;
|
||||||
|
|
||||||
const EquipmentProfiles({
|
const EquipmentProfiles({
|
||||||
super.key,
|
required this.profiles,
|
||||||
required super.values,
|
required this.selected,
|
||||||
required super.selected,
|
|
||||||
required super.child,
|
required super.child,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// [_defaultProfile] + profiles created by the user
|
/// _default + profiles create by the user
|
||||||
static List<EquipmentProfile> of(BuildContext context) {
|
static List<EquipmentProfile> of(BuildContext context) {
|
||||||
return InheritedModel.inheritFrom<EquipmentProfiles>(context, aspect: SelectableAspect.list)!.values;
|
final model =
|
||||||
|
InheritedModel.inheritFrom<EquipmentProfiles>(context, aspect: _EquipmentProfilesModelAspect.profiles)!;
|
||||||
|
return List<EquipmentProfile>.from(
|
||||||
|
[
|
||||||
|
EquipmentProfilesProvider.defaultProfile,
|
||||||
|
...model.profiles.values.map((p) => p.value),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<EquipmentProfile> inUseOf(BuildContext context) {
|
||||||
|
final model =
|
||||||
|
InheritedModel.inheritFrom<EquipmentProfiles>(context, aspect: _EquipmentProfilesModelAspect.profilesInUse)!;
|
||||||
|
return List<EquipmentProfile>.from(
|
||||||
|
[
|
||||||
|
EquipmentProfilesProvider.defaultProfile,
|
||||||
|
...model.profiles.values.where((p) => p.isUsed).map((p) => p.value),
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static EquipmentProfile selectedOf(BuildContext context) {
|
static EquipmentProfile selectedOf(BuildContext context) {
|
||||||
return InheritedModel.inheritFrom<EquipmentProfiles>(
|
return InheritedModel.inheritFrom<EquipmentProfiles>(context, aspect: _EquipmentProfilesModelAspect.selected)!
|
||||||
context,
|
|
||||||
aspect: SelectableAspect.selected,
|
|
||||||
)!
|
|
||||||
.selected;
|
.selected;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool updateShouldNotify(EquipmentProfiles _) => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool updateShouldNotifyDependent(EquipmentProfiles oldWidget, Set<_EquipmentProfilesModelAspect> dependencies) {
|
||||||
|
return (dependencies.contains(_EquipmentProfilesModelAspect.selected) && oldWidget.selected != selected) ||
|
||||||
|
((dependencies.contains(_EquipmentProfilesModelAspect.profiles) ||
|
||||||
|
dependencies.contains(_EquipmentProfilesModelAspect.profilesInUse)) &&
|
||||||
|
const DeepCollectionEquality().equals(oldWidget.profiles, profiles));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,12 +5,12 @@ 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';
|
||||||
|
|
||||||
class FilmsProvider extends StatefulWidget {
|
class FilmsProvider extends StatefulWidget {
|
||||||
final FilmsStorageService filmsStorageService;
|
final FilmsStorageService storageService;
|
||||||
final VoidCallback? onInitialized;
|
final VoidCallback? onInitialized;
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
|
||||||
const FilmsProvider({
|
const FilmsProvider({
|
||||||
required this.filmsStorageService,
|
required this.storageService,
|
||||||
this.onInitialized,
|
this.onInitialized,
|
||||||
required this.child,
|
required this.child,
|
||||||
super.key,
|
super.key,
|
||||||
|
@ -25,11 +25,11 @@ class FilmsProvider extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class FilmsProviderState extends State<FilmsProvider> {
|
class FilmsProviderState extends State<FilmsProvider> {
|
||||||
final Map<String, SelectableFilm<Film>> predefinedFilms = {};
|
final TogglableMap<Film> predefinedFilms = {};
|
||||||
final Map<String, SelectableFilm<FilmExponential>> customFilms = {};
|
final TogglableMap<FilmExponential> customFilms = {};
|
||||||
String _selectedId = '';
|
String _selectedId = '';
|
||||||
|
|
||||||
Film get _selectedFilm => customFilms[_selectedId]?.film ?? predefinedFilms[_selectedId]?.film ?? const FilmStub();
|
Film get _selectedFilm => customFilms[_selectedId]?.value ?? predefinedFilms[_selectedId]?.value ?? const FilmStub();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
@ -48,9 +48,9 @@ class FilmsProviderState extends State<FilmsProvider> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _init() async {
|
Future<void> _init() async {
|
||||||
_selectedId = widget.filmsStorageService.selectedFilmId;
|
_selectedId = widget.storageService.selectedFilmId;
|
||||||
predefinedFilms.addAll(await widget.filmsStorageService.getPredefinedFilms());
|
predefinedFilms.addAll(await widget.storageService.getPredefinedFilms());
|
||||||
customFilms.addAll(await widget.filmsStorageService.getCustomFilms());
|
customFilms.addAll(await widget.storageService.getCustomFilms());
|
||||||
_discardSelectedIfNotIncluded();
|
_discardSelectedIfNotIncluded();
|
||||||
if (mounted) setState(() {});
|
if (mounted) setState(() {});
|
||||||
widget.onInitialized?.call();
|
widget.onInitialized?.call();
|
||||||
|
@ -60,13 +60,13 @@ class FilmsProviderState extends State<FilmsProvider> {
|
||||||
|
|
||||||
Future<void> toggleFilm(Film film, bool enabled) async {
|
Future<void> toggleFilm(Film film, bool enabled) async {
|
||||||
if (predefinedFilms.containsKey(film.id)) {
|
if (predefinedFilms.containsKey(film.id)) {
|
||||||
predefinedFilms[film.id] = (film: film, isUsed: enabled);
|
predefinedFilms[film.id] = (value: film, isUsed: enabled);
|
||||||
} else if (customFilms.containsKey(film.id)) {
|
} else if (customFilms.containsKey(film.id)) {
|
||||||
customFilms[film.id] = (film: film as FilmExponential, isUsed: enabled);
|
customFilms[film.id] = (value: film as FilmExponential, isUsed: enabled);
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await widget.filmsStorageService.toggleFilm(film, enabled);
|
await widget.storageService.toggleFilm(film, enabled);
|
||||||
_discardSelectedIfNotIncluded();
|
_discardSelectedIfNotIncluded();
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
@ -74,7 +74,7 @@ class FilmsProviderState extends State<FilmsProvider> {
|
||||||
void selectFilm(Film film) {
|
void selectFilm(Film film) {
|
||||||
if (_selectedFilm != film) {
|
if (_selectedFilm != film) {
|
||||||
_selectedId = film.id;
|
_selectedId = film.id;
|
||||||
widget.filmsStorageService.selectedFilmId = _selectedId;
|
widget.storageService.selectedFilmId = _selectedId;
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -83,19 +83,19 @@ class FilmsProviderState extends State<FilmsProvider> {
|
||||||
|
|
||||||
Future<void> addCustomFilm(FilmExponential film) async {
|
Future<void> addCustomFilm(FilmExponential film) async {
|
||||||
// ignore: avoid_redundant_argument_values
|
// ignore: avoid_redundant_argument_values
|
||||||
await widget.filmsStorageService.addFilm(film, isUsed: true);
|
await widget.storageService.addFilm(film, isUsed: true);
|
||||||
customFilms[film.id] = (film: film, isUsed: true);
|
customFilms[film.id] = (value: film, isUsed: true);
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateCustomFilm(FilmExponential film) async {
|
Future<void> updateCustomFilm(FilmExponential film) async {
|
||||||
await widget.filmsStorageService.updateFilm(film);
|
await widget.storageService.updateFilm(film);
|
||||||
customFilms[film.id] = (film: film, isUsed: customFilms[film.id]!.isUsed);
|
customFilms[film.id] = (value: film, isUsed: customFilms[film.id]!.isUsed);
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> deleteCustomFilm(FilmExponential film) async {
|
Future<void> deleteCustomFilm(FilmExponential film) async {
|
||||||
await widget.filmsStorageService.deleteFilm(film);
|
await widget.storageService.deleteFilm(film);
|
||||||
customFilms.remove(film.id);
|
customFilms.remove(film.id);
|
||||||
_discardSelectedIfNotIncluded();
|
_discardSelectedIfNotIncluded();
|
||||||
setState(() {});
|
setState(() {});
|
||||||
|
@ -108,7 +108,7 @@ class FilmsProviderState extends State<FilmsProvider> {
|
||||||
final isSelectedUsed = predefinedFilms[_selectedId]?.isUsed ?? customFilms[_selectedId]?.isUsed ?? false;
|
final isSelectedUsed = predefinedFilms[_selectedId]?.isUsed ?? customFilms[_selectedId]?.isUsed ?? false;
|
||||||
if (!isSelectedUsed) {
|
if (!isSelectedUsed) {
|
||||||
_selectedId = const FilmStub().id;
|
_selectedId = const FilmStub().id;
|
||||||
widget.filmsStorageService.selectedFilmId = _selectedId;
|
widget.storageService.selectedFilmId = _selectedId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -121,10 +121,10 @@ enum _FilmsModelAspect {
|
||||||
}
|
}
|
||||||
|
|
||||||
class Films extends InheritedModel<_FilmsModelAspect> {
|
class Films extends InheritedModel<_FilmsModelAspect> {
|
||||||
final Map<String, SelectableFilm<Film>> predefinedFilms;
|
final TogglableMap<Film> predefinedFilms;
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
final Map<String, SelectableFilm<FilmExponential>> customFilms;
|
final TogglableMap<FilmExponential> customFilms;
|
||||||
final Film selected;
|
final Film selected;
|
||||||
|
|
||||||
const Films({
|
const Films({
|
||||||
|
@ -138,7 +138,7 @@ class Films extends InheritedModel<_FilmsModelAspect> {
|
||||||
return InheritedModel.inheritFrom<Films>(context, aspect: _FilmsModelAspect.predefinedFilms)!
|
return InheritedModel.inheritFrom<Films>(context, aspect: _FilmsModelAspect.predefinedFilms)!
|
||||||
.predefinedFilms
|
.predefinedFilms
|
||||||
.values
|
.values
|
||||||
.map((value) => value.film)
|
.map((value) => value.value)
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -146,7 +146,7 @@ class Films extends InheritedModel<_FilmsModelAspect> {
|
||||||
return InheritedModel.inheritFrom<Films>(context, aspect: _FilmsModelAspect.customFilms)!
|
return InheritedModel.inheritFrom<Films>(context, aspect: _FilmsModelAspect.customFilms)!
|
||||||
.customFilms
|
.customFilms
|
||||||
.values
|
.values
|
||||||
.map((value) => value.film)
|
.map((value) => value.value)
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,8 +155,8 @@ class Films extends InheritedModel<_FilmsModelAspect> {
|
||||||
final model = InheritedModel.inheritFrom<Films>(context, aspect: _FilmsModelAspect.filmsInUse)!;
|
final model = InheritedModel.inheritFrom<Films>(context, aspect: _FilmsModelAspect.filmsInUse)!;
|
||||||
return [
|
return [
|
||||||
const FilmStub(),
|
const FilmStub(),
|
||||||
...model.customFilms.values.where((e) => e.isUsed).map((e) => e.film),
|
...model.customFilms.values.where((e) => e.isUsed).map((e) => e.value),
|
||||||
...model.predefinedFilms.values.where((e) => e.isUsed).map((e) => e.film),
|
...model.predefinedFilms.values.where((e) => e.isUsed).map((e) => e.value),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,158 @@
|
||||||
|
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';
|
||||||
|
import 'package:lightmeter/screens/equipment_profile_edit/state_equipment_profile_edit.dart';
|
||||||
|
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
|
class EquipmentProfileEditBloc extends Bloc<EquipmentProfileEditEvent, EquipmentProfileEditState> {
|
||||||
|
final EquipmentProfilesProviderState profilesProvider;
|
||||||
|
final EquipmentProfile _originalEquipmentProfile;
|
||||||
|
EquipmentProfile _newEquipmentProfile;
|
||||||
|
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,
|
||||||
|
super(
|
||||||
|
EquipmentProfileEditState(
|
||||||
|
name: profile.name,
|
||||||
|
apertureValues: profile.apertureValues,
|
||||||
|
shutterSpeedValues: profile.shutterSpeedValues,
|
||||||
|
isoValues: profile.isoValues,
|
||||||
|
ndValues: profile.ndValues,
|
||||||
|
lensZoom: profile.lensZoom,
|
||||||
|
canSave: false,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
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 EquipmentProfileSaveEvent():
|
||||||
|
await _onSave(event, emit);
|
||||||
|
case EquipmentProfileCopyEvent():
|
||||||
|
await _onCopy(event, emit);
|
||||||
|
case EquipmentProfileDeleteEvent():
|
||||||
|
await _onDelete(event, emit);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onApertureValuesChanged(EquipmentProfileApertureValuesChangedEvent event, Emitter emit) async {
|
||||||
|
_newEquipmentProfile = _newEquipmentProfile.copyWith(apertureValues: event.apertureValues);
|
||||||
|
emit(state.copyWith(apertureValues: event.apertureValues));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onShutterSpeedValuesChanged(EquipmentProfileShutterSpeedValuesChangedEvent event, Emitter emit) async {
|
||||||
|
_newEquipmentProfile = _newEquipmentProfile.copyWith(shutterSpeedValues: event.shutterSpeedValues);
|
||||||
|
emit(state.copyWith(shutterSpeedValues: event.shutterSpeedValues));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onIsoValuesChanged(EquipmentProfileIsoValuesChangedEvent event, Emitter emit) async {
|
||||||
|
_newEquipmentProfile = _newEquipmentProfile.copyWith(isoValues: event.isoValues);
|
||||||
|
emit(state.copyWith(isoValues: event.isoValues));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onNdValuesChanged(EquipmentProfileNdValuesChangedEvent event, Emitter emit) async {
|
||||||
|
_newEquipmentProfile = _newEquipmentProfile.copyWith(ndValues: event.ndValues);
|
||||||
|
emit(state.copyWith(ndValues: event.ndValues));
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} 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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
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 && lensZoom != null && _newEquipmentProfile != _originalEquipmentProfile;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||||
|
|
||||||
|
sealed class EquipmentProfileEditEvent {
|
||||||
|
const EquipmentProfileEditEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
class EquipmentProfileNameChangedEvent extends EquipmentProfileEditEvent {
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
const EquipmentProfileNameChangedEvent(this.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
class EquipmentProfileIsoValuesChangedEvent extends EquipmentProfileEditEvent {
|
||||||
|
final List<IsoValue> isoValues;
|
||||||
|
|
||||||
|
const EquipmentProfileIsoValuesChangedEvent(this.isoValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
class EquipmentProfileNdValuesChangedEvent extends EquipmentProfileEditEvent {
|
||||||
|
final List<NdValue> ndValues;
|
||||||
|
|
||||||
|
const EquipmentProfileNdValuesChangedEvent(this.ndValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
class EquipmentProfileApertureValuesChangedEvent extends EquipmentProfileEditEvent {
|
||||||
|
final List<ApertureValue> apertureValues;
|
||||||
|
|
||||||
|
const EquipmentProfileApertureValuesChangedEvent(this.apertureValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
class EquipmentProfileShutterSpeedValuesChangedEvent extends EquipmentProfileEditEvent {
|
||||||
|
final List<ShutterSpeedValue> shutterSpeedValues;
|
||||||
|
|
||||||
|
const EquipmentProfileShutterSpeedValuesChangedEvent(this.shutterSpeedValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
class EquipmentProfileLensZoomChangedEvent extends EquipmentProfileEditEvent {
|
||||||
|
final double lensZoom;
|
||||||
|
|
||||||
|
const EquipmentProfileLensZoomChangedEvent(this.lensZoom);
|
||||||
|
}
|
||||||
|
|
||||||
|
class EquipmentProfileSaveEvent extends EquipmentProfileEditEvent {
|
||||||
|
const EquipmentProfileSaveEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
class EquipmentProfileCopyEvent extends EquipmentProfileEditEvent {
|
||||||
|
const EquipmentProfileCopyEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
class EquipmentProfileDeleteEvent extends EquipmentProfileEditEvent {
|
||||||
|
const EquipmentProfileDeleteEvent();
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
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/bloc_equipment_profile_edit.dart';
|
||||||
|
import 'package:lightmeter/screens/equipment_profile_edit/screen_equipment_profile_edit.dart';
|
||||||
|
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||||
|
|
||||||
|
enum EquipmentProfileEditType { add, edit }
|
||||||
|
|
||||||
|
class EquipmentProfileEditArgs {
|
||||||
|
final EquipmentProfileEditType editType;
|
||||||
|
final EquipmentProfile? profile;
|
||||||
|
|
||||||
|
const EquipmentProfileEditArgs({required this.editType, this.profile});
|
||||||
|
}
|
||||||
|
|
||||||
|
class EquipmentProfileEditFlow extends StatelessWidget {
|
||||||
|
final EquipmentProfileEditArgs args;
|
||||||
|
final bool _isEdit;
|
||||||
|
|
||||||
|
EquipmentProfileEditFlow({
|
||||||
|
required this.args,
|
||||||
|
super.key,
|
||||||
|
}) : _isEdit = args.editType == EquipmentProfileEditType.edit;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocProvider(
|
||||||
|
create: (_) => EquipmentProfileEditBloc(
|
||||||
|
EquipmentProfilesProvider.of(context),
|
||||||
|
profile: args.profile,
|
||||||
|
isEdit: _isEdit,
|
||||||
|
),
|
||||||
|
child: EquipmentProfileEditScreen(isEdit: _isEdit),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,257 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
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/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';
|
||||||
|
import 'package:lightmeter/screens/equipment_profile_edit/event_equipment_profile_edit.dart';
|
||||||
|
import 'package:lightmeter/screens/equipment_profile_edit/flow_equipment_profile_edit.dart';
|
||||||
|
import 'package:lightmeter/screens/equipment_profile_edit/state_equipment_profile_edit.dart';
|
||||||
|
import 'package:lightmeter/screens/shared/sliver_screen/screen_sliver.dart';
|
||||||
|
import 'package:lightmeter/screens/shared/text_field/widget_text_field.dart';
|
||||||
|
import 'package:lightmeter/utils/double_to_zoom.dart';
|
||||||
|
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||||
|
|
||||||
|
class EquipmentProfileEditScreen extends StatefulWidget {
|
||||||
|
final bool isEdit;
|
||||||
|
|
||||||
|
const EquipmentProfileEditScreen({
|
||||||
|
required this.isEdit,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<EquipmentProfileEditScreen> createState() => _EquipmentProfileEditScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EquipmentProfileEditScreenState extends State<EquipmentProfileEditScreen> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocConsumer<EquipmentProfileEditBloc, EquipmentProfileEditState>(
|
||||||
|
listenWhen: (previous, current) => previous.isLoading != current.isLoading,
|
||||||
|
listener: (context, state) {
|
||||||
|
if (state.isLoading) {
|
||||||
|
FocusScope.of(context).unfocus();
|
||||||
|
} else {
|
||||||
|
if (state.profileToCopy != null) {
|
||||||
|
Navigator.of(context).pushReplacementNamed(
|
||||||
|
NavigationRoutes.equipmentProfileEditScreen.name,
|
||||||
|
arguments: EquipmentProfileEditArgs(
|
||||||
|
editType: EquipmentProfileEditType.add,
|
||||||
|
profile: state.profileToCopy,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
buildWhen: (previous, current) => previous.isLoading != current.isLoading,
|
||||||
|
builder: (context, state) => IgnorePointer(
|
||||||
|
ignoring: state.isLoading,
|
||||||
|
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,
|
||||||
|
builder: (context, state) => IconButton(
|
||||||
|
onPressed: state.canSave
|
||||||
|
? () {
|
||||||
|
context.read<EquipmentProfileEditBloc>().add(const EquipmentProfileSaveEvent());
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
icon: const Icon(Icons.save_outlined),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (widget.isEdit)
|
||||||
|
BlocBuilder<EquipmentProfileEditBloc, EquipmentProfileEditState>(
|
||||||
|
buildWhen: (previous, current) => previous.canSave != current.canSave,
|
||||||
|
builder: (context, state) => IconButton(
|
||||||
|
onPressed: state.canSave
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
context.read<EquipmentProfileEditBloc>().add(const EquipmentProfileCopyEvent());
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.copy_outlined),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (widget.isEdit)
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
context.read<EquipmentProfileEditBloc>().add(const EquipmentProfileDeleteEvent());
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.delete_outlined),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
slivers: [
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(
|
||||||
|
Dimens.paddingM,
|
||||||
|
0,
|
||||||
|
Dimens.paddingM,
|
||||||
|
Dimens.paddingM,
|
||||||
|
),
|
||||||
|
child: Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: Dimens.paddingM),
|
||||||
|
child: Opacity(
|
||||||
|
opacity: state.isLoading ? Dimens.disabledOpacity : Dimens.enabledOpacity,
|
||||||
|
child: const Column(
|
||||||
|
children: [
|
||||||
|
_NameFieldBuilder(),
|
||||||
|
_IsoValuesListTileBuilder(),
|
||||||
|
_NdValuesListTileBuilder(),
|
||||||
|
_ApertureValuesListTileBuilder(),
|
||||||
|
_ShutterSpeedValuesListTileBuilder(),
|
||||||
|
_LensZoomListTileBuilder(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SliverToBoxAdapter(child: SizedBox(height: MediaQuery.paddingOf(context).bottom)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NameFieldBuilder extends StatelessWidget {
|
||||||
|
const _NameFieldBuilder();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocBuilder<EquipmentProfileEditBloc, EquipmentProfileEditState>(
|
||||||
|
builder: (context, state) => Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
left: Dimens.paddingM,
|
||||||
|
top: Dimens.paddingS / 2,
|
||||||
|
right: Dimens.paddingL,
|
||||||
|
bottom: Dimens.paddingS / 2,
|
||||||
|
),
|
||||||
|
child: LightmeterTextField(
|
||||||
|
initialValue: state.name,
|
||||||
|
autofocus: true,
|
||||||
|
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));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _IsoValuesListTileBuilder extends StatelessWidget {
|
||||||
|
const _IsoValuesListTileBuilder();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocBuilder<EquipmentProfileEditBloc, EquipmentProfileEditState>(
|
||||||
|
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,
|
||||||
|
onChanged: (value) {
|
||||||
|
context.read<EquipmentProfileEditBloc>().add(EquipmentProfileIsoValuesChangedEvent(value));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NdValuesListTileBuilder extends StatelessWidget {
|
||||||
|
const _NdValuesListTileBuilder();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocBuilder<EquipmentProfileEditBloc, EquipmentProfileEditState>(
|
||||||
|
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,
|
||||||
|
onChanged: (value) {
|
||||||
|
context.read<EquipmentProfileEditBloc>().add(EquipmentProfileNdValuesChangedEvent(value));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ApertureValuesListTileBuilder extends StatelessWidget {
|
||||||
|
const _ApertureValuesListTileBuilder();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocBuilder<EquipmentProfileEditBloc, EquipmentProfileEditState>(
|
||||||
|
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,
|
||||||
|
onChanged: (value) {
|
||||||
|
context.read<EquipmentProfileEditBloc>().add(EquipmentProfileApertureValuesChangedEvent(value));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ShutterSpeedValuesListTileBuilder extends StatelessWidget {
|
||||||
|
const _ShutterSpeedValuesListTileBuilder();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocBuilder<EquipmentProfileEditBloc, EquipmentProfileEditState>(
|
||||||
|
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,
|
||||||
|
trailingAdapter: (context, value) =>
|
||||||
|
value.value == 1 ? S.of(context).shutterSpeedManualShort : value.toString(),
|
||||||
|
dialogValueAdapter: (context, value) => value.value == 1 ? S.of(context).shutterSpeedManual : value.toString(),
|
||||||
|
onChanged: (value) {
|
||||||
|
context.read<EquipmentProfileEditBloc>().add(EquipmentProfileShutterSpeedValuesChangedEvent(value));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LensZoomListTileBuilder extends StatelessWidget {
|
||||||
|
const _LensZoomListTileBuilder();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocBuilder<EquipmentProfileEditBloc, EquipmentProfileEditState>(
|
||||||
|
builder: (context, state) => SliderPickerListTile(
|
||||||
|
icon: Icons.zoom_in_outlined,
|
||||||
|
title: S.of(context).lensZoom,
|
||||||
|
description: S.of(context).lensZoomDescription,
|
||||||
|
value: state.lensZoom,
|
||||||
|
range: const RangeValues(1, 7),
|
||||||
|
valueAdapter: (_, value) => value.toZoom(),
|
||||||
|
onChanged: (value) {
|
||||||
|
context.read<EquipmentProfileEditBloc>().add(EquipmentProfileLensZoomChangedEvent(value));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
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 bool canSave;
|
||||||
|
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.canSave,
|
||||||
|
this.isLoading = false,
|
||||||
|
this.profileToCopy,
|
||||||
|
});
|
||||||
|
|
||||||
|
EquipmentProfileEditState copyWith({
|
||||||
|
String? name,
|
||||||
|
List<ApertureValue>? apertureValues,
|
||||||
|
List<NdValue>? ndValues,
|
||||||
|
List<ShutterSpeedValue>? shutterSpeedValues,
|
||||||
|
List<IsoValue>? isoValues,
|
||||||
|
double? lensZoom,
|
||||||
|
bool? canSave,
|
||||||
|
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,
|
||||||
|
canSave: canSave ?? this.canSave,
|
||||||
|
isLoading: isLoading ?? this.isLoading,
|
||||||
|
profileToCopy: profileToCopy ?? this.profileToCopy,
|
||||||
|
);
|
||||||
|
}
|
110
lib/screens/equipment_profiles/screen_equipment_profiles.dart
Normal file
110
lib/screens/equipment_profiles/screen_equipment_profiles.dart
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:lightmeter/generated/l10n.dart';
|
||||||
|
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/shared/sliver_placeholder/widget_sliver_placeholder.dart';
|
||||||
|
import 'package:lightmeter/screens/shared/sliver_screen/screen_sliver.dart';
|
||||||
|
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||||
|
|
||||||
|
class EquipmentProfilesScreen extends StatefulWidget {
|
||||||
|
const EquipmentProfilesScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<EquipmentProfilesScreen> createState() => _EquipmentProfilesScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EquipmentProfilesScreenState extends State<EquipmentProfilesScreen> with SingleTickerProviderStateMixin {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SliverScreen(
|
||||||
|
title: Text(S.of(context).equipmentProfiles),
|
||||||
|
appBarActions: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: _addProfile,
|
||||||
|
icon: const Icon(Icons.add_outlined),
|
||||||
|
tooltip: S.of(context).tooltipAdd,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
slivers: [
|
||||||
|
if (EquipmentProfiles.of(context).length > 1)
|
||||||
|
_EquipmentProfilesListBuilder(
|
||||||
|
values: EquipmentProfiles.of(context).skip(1).toList(),
|
||||||
|
onEdit: _editProfile,
|
||||||
|
onCheckbox: EquipmentProfilesProvider.of(context).toggleProfile,
|
||||||
|
)
|
||||||
|
else
|
||||||
|
SliverPlaceholder(onTap: _addProfile),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _addProfile() {
|
||||||
|
Navigator.of(context).pushNamed(
|
||||||
|
NavigationRoutes.equipmentProfileEditScreen.name,
|
||||||
|
arguments: const EquipmentProfileEditArgs(editType: EquipmentProfileEditType.add),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _editProfile(EquipmentProfile profile) {
|
||||||
|
Navigator.of(context).pushNamed(
|
||||||
|
NavigationRoutes.equipmentProfileEditScreen.name,
|
||||||
|
arguments: EquipmentProfileEditArgs(
|
||||||
|
editType: EquipmentProfileEditType.edit,
|
||||||
|
profile: profile,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EquipmentProfilesListBuilder extends StatelessWidget {
|
||||||
|
final List<EquipmentProfile> values;
|
||||||
|
final void Function(EquipmentProfile film) onEdit;
|
||||||
|
final void Function(EquipmentProfile film, bool value) onCheckbox;
|
||||||
|
|
||||||
|
const _EquipmentProfilesListBuilder({
|
||||||
|
required this.values,
|
||||||
|
required this.onEdit,
|
||||||
|
required this.onCheckbox,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SliverList.builder(
|
||||||
|
itemCount: values.length,
|
||||||
|
itemBuilder: (_, index) => Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(
|
||||||
|
Dimens.paddingM,
|
||||||
|
index == 0 ? Dimens.paddingM : 0,
|
||||||
|
Dimens.paddingM,
|
||||||
|
index == values.length - 1 ? Dimens.paddingM + MediaQuery.paddingOf(context).bottom : 0.0,
|
||||||
|
),
|
||||||
|
child: Card(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(
|
||||||
|
top: index == 0 ? const Radius.circular(Dimens.borderRadiusL) : Radius.zero,
|
||||||
|
bottom: index == values.length - 1 ? const Radius.circular(Dimens.borderRadiusL) : Radius.zero,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
top: index == 0 ? Dimens.paddingM : 0.0,
|
||||||
|
bottom: index == values.length - 1 ? Dimens.paddingM : 0.0,
|
||||||
|
),
|
||||||
|
child: CheckboxListTile(
|
||||||
|
title: Text(values[index].name),
|
||||||
|
controlAffinity: ListTileControlAffinity.leading,
|
||||||
|
value: EquipmentProfiles.inUseOf(context).contains(values[index]),
|
||||||
|
onChanged: (value) => onCheckbox(values[index], value ?? false),
|
||||||
|
secondary: IconButton(
|
||||||
|
onPressed: () => onEdit(values[index]),
|
||||||
|
icon: const Icon(Icons.edit_outlined),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -48,7 +48,7 @@ class _FilmEditScreenState extends State<FilmEditScreen> {
|
||||||
context.read<FilmEditBloc>().add(const FilmEditSaveEvent());
|
context.read<FilmEditBloc>().add(const FilmEditSaveEvent());
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
icon: const Icon(Icons.save),
|
icon: const Icon(Icons.save_outlined),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (widget.isEdit)
|
if (widget.isEdit)
|
||||||
|
@ -56,7 +56,7 @@ class _FilmEditScreenState extends State<FilmEditScreen> {
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.read<FilmEditBloc>().add(const FilmEditDeleteEvent());
|
context.read<FilmEditBloc>().add(const FilmEditDeleteEvent());
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.delete),
|
icon: const Icon(Icons.delete_outlined),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
slivers: [
|
slivers: [
|
||||||
|
|
|
@ -78,7 +78,7 @@ class _FilmsScreenState extends State<FilmsScreen> with SingleTickerProviderStat
|
||||||
|
|
||||||
void _addFilm() {
|
void _addFilm() {
|
||||||
Navigator.of(context).pushNamed(
|
Navigator.of(context).pushNamed(
|
||||||
NavigationRoutes.filmAddScreen.name,
|
NavigationRoutes.filmEditScreen.name,
|
||||||
arguments: const FilmEditArgs(),
|
arguments: const FilmEditArgs(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -135,7 +135,7 @@ class _FilmsListBuilder<T extends Film> extends StatelessWidget {
|
||||||
secondary: onFilmEdit != null
|
secondary: onFilmEdit != null
|
||||||
? IconButton(
|
? IconButton(
|
||||||
onPressed: () => onFilmEdit!(films[index]),
|
onPressed: () => onFilmEdit!(films[index]),
|
||||||
icon: const Icon(Icons.edit),
|
icon: const Icon(Icons.edit_outlined),
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
|
|
|
@ -14,9 +14,9 @@ class EquipmentProfilePicker extends StatelessWidget {
|
||||||
icon: Icons.camera_outlined,
|
icon: Icons.camera_outlined,
|
||||||
title: S.of(context).equipmentProfile,
|
title: S.of(context).equipmentProfile,
|
||||||
selectedValue: EquipmentProfiles.selectedOf(context),
|
selectedValue: EquipmentProfiles.selectedOf(context),
|
||||||
values: EquipmentProfiles.of(context),
|
values: EquipmentProfiles.inUseOf(context),
|
||||||
itemTitleBuilder: (_, value) => Text(value.id.isEmpty ? S.of(context).none : value.name),
|
itemTitleBuilder: (_, value) => Text(value.id.isEmpty ? S.of(context).none : value.name),
|
||||||
onChanged: EquipmentProfileProvider.of(context).setProfile,
|
onChanged: EquipmentProfilesProvider.of(context).selectProfile,
|
||||||
closedChild: ReadingValueContainer.singleValue(
|
closedChild: ReadingValueContainer.singleValue(
|
||||||
value: ReadingValue(
|
value: ReadingValue(
|
||||||
label: S.of(context).equipmentProfile,
|
label: S.of(context).equipmentProfile,
|
||||||
|
|
|
@ -1,310 +0,0 @@
|
||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/scheduler.dart';
|
|
||||||
import 'package:lightmeter/generated/l10n.dart';
|
|
||||||
import 'package:lightmeter/res/dimens.dart';
|
|
||||||
import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/filter_list_tile/widget_list_tile_filter.dart';
|
|
||||||
import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/range_picker_list_tile/widget_list_tile_range_picker.dart';
|
|
||||||
import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/components/slider_picker_list_tile/widget_list_tile_slider_picker.dart';
|
|
||||||
import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_name_dialog/widget_dialog_equipment_profile_name.dart';
|
|
||||||
import 'package:lightmeter/utils/double_to_zoom.dart';
|
|
||||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
|
||||||
|
|
||||||
class EquipmentProfileContainer extends StatefulWidget {
|
|
||||||
final EquipmentProfile data;
|
|
||||||
final ValueChanged<EquipmentProfile> onUpdate;
|
|
||||||
final VoidCallback onCopy;
|
|
||||||
final VoidCallback onDelete;
|
|
||||||
final VoidCallback onExpand;
|
|
||||||
|
|
||||||
const EquipmentProfileContainer({
|
|
||||||
required this.data,
|
|
||||||
required this.onUpdate,
|
|
||||||
required this.onCopy,
|
|
||||||
required this.onDelete,
|
|
||||||
required this.onExpand,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<EquipmentProfileContainer> createState() => EquipmentProfileContainerState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class EquipmentProfileContainerState extends State<EquipmentProfileContainer> with TickerProviderStateMixin {
|
|
||||||
late EquipmentProfile _equipmentData = EquipmentProfile(
|
|
||||||
id: widget.data.id,
|
|
||||||
name: widget.data.name,
|
|
||||||
apertureValues: widget.data.apertureValues,
|
|
||||||
ndValues: widget.data.ndValues,
|
|
||||||
shutterSpeedValues: widget.data.shutterSpeedValues,
|
|
||||||
isoValues: widget.data.isoValues,
|
|
||||||
lensZoom: widget.data.lensZoom,
|
|
||||||
);
|
|
||||||
|
|
||||||
late final AnimationController _controller = AnimationController(
|
|
||||||
duration: Dimens.durationM,
|
|
||||||
vsync: this,
|
|
||||||
);
|
|
||||||
bool get _expanded => _controller.isCompleted;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didUpdateWidget(EquipmentProfileContainer oldWidget) {
|
|
||||||
super.didUpdateWidget(oldWidget);
|
|
||||||
_equipmentData = EquipmentProfile(
|
|
||||||
id: widget.data.id,
|
|
||||||
name: widget.data.name,
|
|
||||||
apertureValues: widget.data.apertureValues,
|
|
||||||
ndValues: widget.data.ndValues,
|
|
||||||
shutterSpeedValues: widget.data.shutterSpeedValues,
|
|
||||||
isoValues: widget.data.isoValues,
|
|
||||||
lensZoom: widget.data.lensZoom,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_controller.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Card(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: Dimens.paddingM),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
ListTile(
|
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM),
|
|
||||||
title: Row(
|
|
||||||
children: [
|
|
||||||
_AnimatedNameLeading(controller: _controller),
|
|
||||||
const SizedBox(width: Dimens.grid8),
|
|
||||||
Flexible(
|
|
||||||
child: Text(
|
|
||||||
_equipmentData.name,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
trailing: _AnimatedArrowButton(
|
|
||||||
controller: _controller,
|
|
||||||
onPressed: () => _expanded ? collapse() : expand(),
|
|
||||||
),
|
|
||||||
onTap: () => _expanded ? _showNameDialog() : expand(),
|
|
||||||
),
|
|
||||||
_AnimatedEquipmentListTiles(
|
|
||||||
controller: _controller,
|
|
||||||
equipmentData: _equipmentData,
|
|
||||||
onApertureValuesSelected: (value) {
|
|
||||||
_equipmentData = _equipmentData.copyWith(apertureValues: value);
|
|
||||||
widget.onUpdate(_equipmentData);
|
|
||||||
},
|
|
||||||
onIsoValuesSelecred: (value) {
|
|
||||||
_equipmentData = _equipmentData.copyWith(isoValues: value);
|
|
||||||
widget.onUpdate(_equipmentData);
|
|
||||||
},
|
|
||||||
onNdValuesSelected: (value) {
|
|
||||||
_equipmentData = _equipmentData.copyWith(ndValues: value);
|
|
||||||
widget.onUpdate(_equipmentData);
|
|
||||||
},
|
|
||||||
onShutterSpeedValuesSelected: (value) {
|
|
||||||
_equipmentData = _equipmentData.copyWith(shutterSpeedValues: value);
|
|
||||||
widget.onUpdate(_equipmentData);
|
|
||||||
},
|
|
||||||
onLensZoomChanged: (value) {
|
|
||||||
_equipmentData = _equipmentData.copyWith(lensZoom: value);
|
|
||||||
widget.onUpdate(_equipmentData);
|
|
||||||
},
|
|
||||||
onCopy: widget.onCopy,
|
|
||||||
onDelete: widget.onDelete,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showNameDialog() {
|
|
||||||
showDialog<String>(
|
|
||||||
context: context,
|
|
||||||
builder: (_) => EquipmentProfileNameDialog(initialValue: _equipmentData.name),
|
|
||||||
).then((value) {
|
|
||||||
if (value != null) {
|
|
||||||
_equipmentData = _equipmentData.copyWith(name: value);
|
|
||||||
widget.onUpdate(_equipmentData);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void expand() {
|
|
||||||
widget.onExpand();
|
|
||||||
_controller.forward();
|
|
||||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
|
||||||
Future.delayed(_controller.duration!).then((_) {
|
|
||||||
Scrollable.ensureVisible(
|
|
||||||
context,
|
|
||||||
alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd,
|
|
||||||
duration: _controller.duration!,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void collapse() {
|
|
||||||
_controller.reverse();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AnimatedNameLeading extends AnimatedWidget {
|
|
||||||
const _AnimatedNameLeading({required AnimationController controller}) : super(listenable: controller);
|
|
||||||
|
|
||||||
Animation<double> get _progress => listenable as Animation<double>;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Padding(
|
|
||||||
padding: EdgeInsets.only(right: _progress.value * Dimens.grid8),
|
|
||||||
child: Icon(
|
|
||||||
Icons.edit_outlined,
|
|
||||||
size: _progress.value * Dimens.grid24,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AnimatedArrowButton extends AnimatedWidget {
|
|
||||||
final VoidCallback onPressed;
|
|
||||||
|
|
||||||
const _AnimatedArrowButton({
|
|
||||||
required AnimationController controller,
|
|
||||||
required this.onPressed,
|
|
||||||
}) : super(listenable: controller);
|
|
||||||
|
|
||||||
Animation<double> get _progress => listenable as Animation<double>;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return IconButton(
|
|
||||||
onPressed: onPressed,
|
|
||||||
icon: Transform.rotate(
|
|
||||||
angle: _progress.value * pi,
|
|
||||||
child: const Icon(Icons.keyboard_arrow_down_outlined),
|
|
||||||
),
|
|
||||||
tooltip: _progress.value == 0 ? S.of(context).tooltipExpand : S.of(context).tooltipCollapse,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AnimatedEquipmentListTiles extends AnimatedWidget {
|
|
||||||
final EquipmentProfile equipmentData;
|
|
||||||
final ValueChanged<List<ApertureValue>> onApertureValuesSelected;
|
|
||||||
final ValueChanged<List<IsoValue>> onIsoValuesSelecred;
|
|
||||||
final ValueChanged<List<NdValue>> onNdValuesSelected;
|
|
||||||
final ValueChanged<List<ShutterSpeedValue>> onShutterSpeedValuesSelected;
|
|
||||||
final ValueChanged<double> onLensZoomChanged;
|
|
||||||
final VoidCallback onCopy;
|
|
||||||
final VoidCallback onDelete;
|
|
||||||
|
|
||||||
const _AnimatedEquipmentListTiles({
|
|
||||||
required AnimationController controller,
|
|
||||||
required this.equipmentData,
|
|
||||||
required this.onApertureValuesSelected,
|
|
||||||
required this.onIsoValuesSelecred,
|
|
||||||
required this.onNdValuesSelected,
|
|
||||||
required this.onShutterSpeedValuesSelected,
|
|
||||||
required this.onLensZoomChanged,
|
|
||||||
required this.onCopy,
|
|
||||||
required this.onDelete,
|
|
||||||
}) : super(listenable: controller);
|
|
||||||
|
|
||||||
Animation<double> get _progress => listenable as Animation<double>;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return SizedOverflowBox(
|
|
||||||
alignment: Alignment.topCenter,
|
|
||||||
size: Size(
|
|
||||||
double.maxFinite,
|
|
||||||
_progress.value * Dimens.grid56 * 6,
|
|
||||||
),
|
|
||||||
// https://github.com/gskinnerTeam/flutter-folio/pull/62
|
|
||||||
child: Opacity(
|
|
||||||
opacity: _progress.value,
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
FilterListTile<IsoValue>(
|
|
||||||
icon: Icons.iso_outlined,
|
|
||||||
title: S.of(context).isoValues,
|
|
||||||
description: S.of(context).isoValuesFilterDescription,
|
|
||||||
values: IsoValue.values,
|
|
||||||
selectedValues: equipmentData.isoValues,
|
|
||||||
onChanged: onIsoValuesSelecred,
|
|
||||||
),
|
|
||||||
FilterListTile<NdValue>(
|
|
||||||
icon: Icons.filter_b_and_w_outlined,
|
|
||||||
title: S.of(context).ndFilters,
|
|
||||||
description: S.of(context).ndFiltersFilterDescription,
|
|
||||||
values: NdValue.values,
|
|
||||||
selectedValues: equipmentData.ndValues,
|
|
||||||
onChanged: onNdValuesSelected,
|
|
||||||
),
|
|
||||||
RangePickerListTile<ApertureValue>(
|
|
||||||
icon: Icons.camera_outlined,
|
|
||||||
title: S.of(context).apertureValues,
|
|
||||||
description: S.of(context).apertureValuesFilterDescription,
|
|
||||||
values: ApertureValue.values,
|
|
||||||
selectedValues: equipmentData.apertureValues,
|
|
||||||
onChanged: onApertureValuesSelected,
|
|
||||||
),
|
|
||||||
RangePickerListTile<ShutterSpeedValue>(
|
|
||||||
icon: Icons.shutter_speed_outlined,
|
|
||||||
title: S.of(context).shutterSpeedValues,
|
|
||||||
description: S.of(context).shutterSpeedValuesFilterDescription,
|
|
||||||
values: ShutterSpeedValue.values,
|
|
||||||
selectedValues: equipmentData.shutterSpeedValues,
|
|
||||||
onChanged: onShutterSpeedValuesSelected,
|
|
||||||
trailingAdapter: (context, value) =>
|
|
||||||
value.value == 1 ? S.of(context).shutterSpeedManualShort : value.toString(),
|
|
||||||
dialogValueAdapter: (context, value) =>
|
|
||||||
value.value == 1 ? S.of(context).shutterSpeedManual : value.toString(),
|
|
||||||
),
|
|
||||||
SliderPickerListTile(
|
|
||||||
icon: Icons.zoom_in_outlined,
|
|
||||||
title: S.of(context).lensZoom,
|
|
||||||
description: S.of(context).lensZoomDescription,
|
|
||||||
value: equipmentData.lensZoom,
|
|
||||||
range: const RangeValues(1, 7),
|
|
||||||
onChanged: onLensZoomChanged,
|
|
||||||
valueAdapter: (_, value) => value.toZoom(),
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM),
|
|
||||||
trailing: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
onPressed: onCopy,
|
|
||||||
icon: const Icon(Icons.copy_outlined),
|
|
||||||
tooltip: S.of(context).tooltipCopy,
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
onPressed: onDelete,
|
|
||||||
icon: const Icon(Icons.delete_outlined),
|
|
||||||
tooltip: S.of(context).tooltipDelete,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,49 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:lightmeter/generated/l10n.dart';
|
|
||||||
import 'package:lightmeter/res/dimens.dart';
|
|
||||||
|
|
||||||
class EquipmentProfileNameDialog extends StatefulWidget {
|
|
||||||
final String initialValue;
|
|
||||||
|
|
||||||
const EquipmentProfileNameDialog({this.initialValue = '', super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<EquipmentProfileNameDialog> createState() => _EquipmentProfileNameDialogState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _EquipmentProfileNameDialogState extends State<EquipmentProfileNameDialog> {
|
|
||||||
late final _nameController = TextEditingController(text: widget.initialValue);
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_nameController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return AlertDialog(
|
|
||||||
icon: const Icon(Icons.edit_outlined),
|
|
||||||
titlePadding: Dimens.dialogIconTitlePadding,
|
|
||||||
title: Text(S.of(context).equipmentProfileName),
|
|
||||||
content: TextField(
|
|
||||||
autofocus: true,
|
|
||||||
controller: _nameController,
|
|
||||||
decoration: InputDecoration(hintText: S.of(context).equipmentProfileNameHint),
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: Navigator.of(context).pop,
|
|
||||||
child: Text(S.of(context).cancel),
|
|
||||||
),
|
|
||||||
ValueListenableBuilder(
|
|
||||||
valueListenable: _nameController,
|
|
||||||
builder: (_, value, __) => TextButton(
|
|
||||||
onPressed: value.text.isNotEmpty ? () => Navigator.of(context).pop(value.text) : null,
|
|
||||||
child: Text(S.of(context).save),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,128 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:lightmeter/generated/l10n.dart';
|
|
||||||
import 'package:lightmeter/providers/equipment_profile_provider.dart';
|
|
||||||
import 'package:lightmeter/res/dimens.dart';
|
|
||||||
import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart';
|
|
||||||
import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_name_dialog/widget_dialog_equipment_profile_name.dart';
|
|
||||||
import 'package:lightmeter/screens/shared/sliver_placeholder/widget_sliver_placeholder.dart';
|
|
||||||
import 'package:lightmeter/screens/shared/sliver_screen/screen_sliver.dart';
|
|
||||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
|
||||||
|
|
||||||
class EquipmentProfilesScreen extends StatefulWidget {
|
|
||||||
const EquipmentProfilesScreen({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<EquipmentProfilesScreen> createState() => _EquipmentProfilesScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _EquipmentProfilesScreenState extends State<EquipmentProfilesScreen> {
|
|
||||||
final Map<String, GlobalKey<EquipmentProfileContainerState>> keysMap = {};
|
|
||||||
int get profilesCount => keysMap.length;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didChangeDependencies() {
|
|
||||||
super.didChangeDependencies();
|
|
||||||
_updateProfilesKeys();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return SliverScreen(
|
|
||||||
title: Text(S.of(context).equipmentProfiles),
|
|
||||||
appBarActions: [
|
|
||||||
IconButton(
|
|
||||||
onPressed: _addProfile,
|
|
||||||
icon: const Icon(Icons.add_outlined),
|
|
||||||
tooltip: S.of(context).tooltipAdd,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
slivers: profilesCount == 1
|
|
||||||
? [SliverPlaceholder(onTap: _addProfile)]
|
|
||||||
: [
|
|
||||||
SliverList(
|
|
||||||
delegate: SliverChildBuilderDelegate(
|
|
||||||
(context, index) {
|
|
||||||
if (index == 0) {
|
|
||||||
// skip default profile
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
|
|
||||||
final profile = EquipmentProfiles.of(context)[index];
|
|
||||||
return Padding(
|
|
||||||
padding: EdgeInsets.fromLTRB(
|
|
||||||
Dimens.paddingM,
|
|
||||||
index == 0 ? Dimens.paddingM : 0,
|
|
||||||
Dimens.paddingM,
|
|
||||||
Dimens.paddingM,
|
|
||||||
),
|
|
||||||
child: EquipmentProfileContainer(
|
|
||||||
key: keysMap[profile.id],
|
|
||||||
data: profile,
|
|
||||||
onExpand: () => _keepExpandedAt(index),
|
|
||||||
onUpdate: _updateProfileAt,
|
|
||||||
onCopy: () => _addProfile(profile),
|
|
||||||
onDelete: () => _removeProfileAt(profile),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
childCount: EquipmentProfiles.of(context).length,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SliverToBoxAdapter(child: SizedBox(height: MediaQuery.paddingOf(context).bottom)),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _addProfile([EquipmentProfile? copyFrom]) {
|
|
||||||
showDialog<String>(
|
|
||||||
context: context,
|
|
||||||
builder: (_) => const EquipmentProfileNameDialog(),
|
|
||||||
).then((name) {
|
|
||||||
if (name != null) {
|
|
||||||
EquipmentProfileProvider.of(context).addProfile(name, copyFrom);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _updateProfileAt(EquipmentProfile data) {
|
|
||||||
EquipmentProfileProvider.of(context).updateProfile(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _removeProfileAt(EquipmentProfile data) {
|
|
||||||
EquipmentProfileProvider.of(context).deleteProfile(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _keepExpandedAt(int index) {
|
|
||||||
keysMap.values.toList().getRange(0, index).forEach((element) {
|
|
||||||
element.currentState?.collapse();
|
|
||||||
});
|
|
||||||
keysMap.values.toList().getRange(index + 1, profilesCount).forEach((element) {
|
|
||||||
element.currentState?.collapse();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void _updateProfilesKeys() {
|
|
||||||
final profiles = EquipmentProfiles.of(context);
|
|
||||||
if (profiles.length > keysMap.length) {
|
|
||||||
// profile added
|
|
||||||
final List<String> idsToAdd = [];
|
|
||||||
for (final profile in profiles) {
|
|
||||||
if (!keysMap.keys.contains(profile.id)) idsToAdd.add(profile.id);
|
|
||||||
}
|
|
||||||
for (final id in idsToAdd) {
|
|
||||||
keysMap[id] = GlobalKey<EquipmentProfileContainerState>(debugLabel: id);
|
|
||||||
}
|
|
||||||
idsToAdd.clear();
|
|
||||||
} else if (profiles.length < keysMap.length) {
|
|
||||||
// profile deleted
|
|
||||||
final List<String> idsToDelete = [];
|
|
||||||
for (final id in keysMap.keys) {
|
|
||||||
if (!profiles.any((p) => p.id == id)) idsToDelete.add(id);
|
|
||||||
}
|
|
||||||
idsToDelete.forEach(keysMap.remove);
|
|
||||||
idsToDelete.clear();
|
|
||||||
} else {
|
|
||||||
// profile updated, no need to updated keys
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +1,7 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:lightmeter/generated/l10n.dart';
|
import 'package:lightmeter/generated/l10n.dart';
|
||||||
import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart';
|
import 'package:lightmeter/navigation/routes.dart';
|
||||||
import 'package:lightmeter/screens/settings/components/shared/iap_list_tile/widget_list_tile_iap.dart';
|
import 'package:lightmeter/screens/settings/components/shared/iap_list_tile/widget_list_tile_iap.dart';
|
||||||
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
|
||||||
|
|
||||||
class EquipmentProfilesListTile extends StatelessWidget {
|
class EquipmentProfilesListTile extends StatelessWidget {
|
||||||
const EquipmentProfilesListTile({super.key});
|
const EquipmentProfilesListTile({super.key});
|
||||||
|
@ -13,9 +12,7 @@ class EquipmentProfilesListTile extends StatelessWidget {
|
||||||
leading: const Icon(Icons.camera_outlined),
|
leading: const Icon(Icons.camera_outlined),
|
||||||
title: Text(S.of(context).equipmentProfiles),
|
title: Text(S.of(context).equipmentProfiles),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.of(context).push<EquipmentProfile>(
|
Navigator.of(context).pushNamed(NavigationRoutes.equipmentProfilesListScreen.name);
|
||||||
MaterialPageRoute(builder: (_) => const EquipmentProfilesScreen()),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,7 +36,7 @@ class MeteringScreenLayoutListTile extends StatelessWidget {
|
||||||
},
|
},
|
||||||
onSave: (value) {
|
onSave: (value) {
|
||||||
if (!value[MeteringScreenLayoutFeature.equipmentProfiles]!) {
|
if (!value[MeteringScreenLayoutFeature.equipmentProfiles]!) {
|
||||||
EquipmentProfileProvider.of(context).setProfile(EquipmentProfiles.of(context).first);
|
EquipmentProfilesProvider.of(context).selectProfile(EquipmentProfiles.of(context).first);
|
||||||
}
|
}
|
||||||
if (!value[MeteringScreenLayoutFeature.filmPicker]!) {
|
if (!value[MeteringScreenLayoutFeature.filmPicker]!) {
|
||||||
FilmsProvider.of(context).selectFilm(const FilmStub());
|
FilmsProvider.of(context).selectFilm(const FilmStub());
|
||||||
|
|
|
@ -32,11 +32,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: v1.1.1
|
ref: v2.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.0.0
|
ref: v2.1.0
|
||||||
material_color_utilities: 0.5.0
|
material_color_utilities: 0.5.0
|
||||||
package_info_plus: 4.2.0
|
package_info_plus: 4.2.0
|
||||||
permission_handler: 10.4.3
|
permission_handler: 10.4.3
|
||||||
|
|
|
@ -15,10 +15,10 @@ 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/res/dimens.dart';
|
||||||
import 'package:lightmeter/res/theme.dart';
|
import 'package:lightmeter/res/theme.dart';
|
||||||
|
import 'package:lightmeter/screens/equipment_profiles/screen_equipment_profiles.dart';
|
||||||
import 'package:lightmeter/screens/metering/components/shared/exposure_pairs_list/widget_list_exposure_pairs.dart';
|
import 'package:lightmeter/screens/metering/components/shared/exposure_pairs_list/widget_list_exposure_pairs.dart';
|
||||||
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/iso_picker/widget_picker_iso.dart';
|
||||||
import 'package:lightmeter/screens/metering/screen_metering.dart';
|
import 'package:lightmeter/screens/metering/screen_metering.dart';
|
||||||
import 'package:lightmeter/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart';
|
|
||||||
import 'package:lightmeter/screens/settings/screen_settings.dart';
|
import 'package:lightmeter/screens/settings/screen_settings.dart';
|
||||||
import 'package:lightmeter/screens/shared/animated_circular_button/widget_button_circular_animated.dart';
|
import 'package:lightmeter/screens/shared/animated_circular_button/widget_button_circular_animated.dart';
|
||||||
import 'package:lightmeter/screens/timer/screen_timer.dart';
|
import 'package:lightmeter/screens/timer/screen_timer.dart';
|
||||||
|
@ -92,7 +92,7 @@ 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(
|
||||||
predefinedFilms: [_mockFilm].toFilmsMap(),
|
predefinedFilms: [_mockFilm].toTogglableMap(),
|
||||||
customFilms: {},
|
customFilms: {},
|
||||||
selectedFilmId: _mockFilm.id,
|
selectedFilmId: _mockFilm.id,
|
||||||
);
|
);
|
||||||
|
@ -132,7 +132,7 @@ void main() {
|
||||||
(tester) async {
|
(tester) async {
|
||||||
await mockSharedPrefs(theme: ThemeType.dark, color: _darkThemeColor);
|
await mockSharedPrefs(theme: ThemeType.dark, color: _darkThemeColor);
|
||||||
await tester.pumpApplication(
|
await tester.pumpApplication(
|
||||||
predefinedFilms: [_mockFilm].toFilmsMap(),
|
predefinedFilms: [_mockFilm].toTogglableMap(),
|
||||||
customFilms: {},
|
customFilms: {},
|
||||||
selectedFilmId: _mockFilm.id,
|
selectedFilmId: _mockFilm.id,
|
||||||
);
|
);
|
||||||
|
@ -157,7 +157,7 @@ void main() {
|
||||||
color: _lightThemeColor,
|
color: _lightThemeColor,
|
||||||
);
|
);
|
||||||
await tester.pumpApplication(
|
await tester.pumpApplication(
|
||||||
predefinedFilms: [_mockFilm].toFilmsMap(),
|
predefinedFilms: [_mockFilm].toTogglableMap(),
|
||||||
customFilms: {},
|
customFilms: {},
|
||||||
selectedFilmId: _mockFilm.id,
|
selectedFilmId: _mockFilm.id,
|
||||||
);
|
);
|
||||||
|
|
|
@ -108,7 +108,6 @@ class _GoldenTestApplicationMockState extends State<GoldenTestApplicationMock> {
|
||||||
],
|
],
|
||||||
child: _MockApplicationWrapper(
|
child: _MockApplicationWrapper(
|
||||||
child: MockIAPProviders(
|
child: MockIAPProviders(
|
||||||
equipmentProfiles: mockEquipmentProfiles,
|
|
||||||
selectedEquipmentProfileId: mockEquipmentProfiles.first.id,
|
selectedEquipmentProfileId: mockEquipmentProfiles.first.id,
|
||||||
selectedFilmId: mockFilms.first.id,
|
selectedFilmId: mockFilms.first.id,
|
||||||
child: Builder(
|
child: Builder(
|
||||||
|
|
|
@ -5,14 +5,28 @@ 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';
|
||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
class _MockIAPStorageService extends Mock implements IAPStorageService {}
|
class _MockEquipmentProfilesStorageService extends Mock implements EquipmentProfilesStorageService {}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
TestWidgetsFlutterBinding.ensureInitialized();
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
late _MockIAPStorageService storageService;
|
late _MockEquipmentProfilesStorageService storageService;
|
||||||
|
|
||||||
setUpAll(() {
|
setUpAll(() {
|
||||||
storageService = _MockIAPStorageService();
|
storageService = _MockEquipmentProfilesStorageService();
|
||||||
|
});
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
registerFallbackValue(_customProfiles.first);
|
||||||
|
when(() => storageService.addProfile(any<EquipmentProfile>())).thenAnswer((_) async {});
|
||||||
|
when(
|
||||||
|
() => storageService.updateProfile(
|
||||||
|
id: any<String>(named: 'id'),
|
||||||
|
name: any<String>(named: 'name'),
|
||||||
|
isUsed: any<bool>(named: 'isUsed'),
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async {});
|
||||||
|
when(() => storageService.deleteProfile(any<String>())).thenAnswer((_) async {});
|
||||||
|
when(() => storageService.getProfiles()).thenAnswer((_) => Future.value(_customProfiles.toTogglableMap()));
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDown(() {
|
tearDown(() {
|
||||||
|
@ -29,35 +43,40 @@ void main() {
|
||||||
price: '0.0\$',
|
price: '0.0\$',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: EquipmentProfileProvider(
|
child: EquipmentProfilesProvider(
|
||||||
storageService: storageService,
|
storageService: storageService,
|
||||||
child: const _Application(),
|
child: const _Application(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
}
|
}
|
||||||
|
|
||||||
void expectEquipmentProfilesCount(int count) {
|
void expectEquipmentProfilesCount(int count) {
|
||||||
expect(find.text('Equipment profiles count: $count'), findsOneWidget);
|
expect(find.text(_EquipmentProfilesCount.text(count)), findsOneWidget);
|
||||||
|
}
|
||||||
|
|
||||||
|
void expectEquipmentProfilesInUseCount(int count) {
|
||||||
|
expect(find.text(_EquipmentProfilesInUseCount.text(count)), findsOneWidget);
|
||||||
}
|
}
|
||||||
|
|
||||||
void expectSelectedEquipmentProfileName(String name) {
|
void expectSelectedEquipmentProfileName(String name) {
|
||||||
expect(find.text('Selected equipment profile: $name'), findsOneWidget);
|
expect(find.text(_SelectedEquipmentProfile.text(name)), findsOneWidget);
|
||||||
}
|
}
|
||||||
|
|
||||||
group(
|
group(
|
||||||
'EquipmentProfileProvider dependency on IAPProductStatus',
|
'EquipmentProfilesProvider dependency on IAPProductStatus',
|
||||||
() {
|
() {
|
||||||
setUp(() {
|
setUp(() {
|
||||||
when(() => storageService.selectedEquipmentProfileId).thenReturn(_customProfiles.first.id);
|
when(() => storageService.selectedEquipmentProfileId).thenReturn(_customProfiles.first.id);
|
||||||
when(() => storageService.equipmentProfiles).thenReturn(_customProfiles);
|
when(() => storageService.getProfiles()).thenAnswer((_) => Future.value(_customProfiles.toTogglableMap()));
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets(
|
testWidgets(
|
||||||
'IAPProductStatus.purchased - show all saved profiles',
|
'IAPProductStatus.purchased - show all saved profiles',
|
||||||
(tester) async {
|
(tester) async {
|
||||||
await pumpTestWidget(tester, IAPProductStatus.purchased);
|
await pumpTestWidget(tester, IAPProductStatus.purchased);
|
||||||
expectEquipmentProfilesCount(3);
|
expectEquipmentProfilesCount(_customProfiles.length + 1);
|
||||||
expectSelectedEquipmentProfileName(_customProfiles.first.name);
|
expectSelectedEquipmentProfileName(_customProfiles.first.name);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -82,203 +101,137 @@ void main() {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
group('EquipmentProfileProvider CRUD', () {
|
testWidgets(
|
||||||
testWidgets(
|
'toggleProfile',
|
||||||
'Add',
|
(tester) async {
|
||||||
(tester) async {
|
when(() => storageService.selectedEquipmentProfileId).thenReturn(_customProfiles.first.id);
|
||||||
when(() => storageService.equipmentProfiles).thenReturn([]);
|
await pumpTestWidget(tester, IAPProductStatus.purchased);
|
||||||
when(() => storageService.selectedEquipmentProfileId).thenReturn('');
|
expectEquipmentProfilesCount(_customProfiles.length + 1);
|
||||||
|
expectEquipmentProfilesInUseCount(_customProfiles.length + 1);
|
||||||
|
expectSelectedEquipmentProfileName(_customProfiles.first.name);
|
||||||
|
|
||||||
await pumpTestWidget(tester, IAPProductStatus.purchased);
|
await tester.equipmentProfilesProvider.toggleProfile(_customProfiles.first, false);
|
||||||
expectEquipmentProfilesCount(1);
|
await tester.pump();
|
||||||
expectSelectedEquipmentProfileName('');
|
expectEquipmentProfilesCount(_customProfiles.length + 1);
|
||||||
|
expectEquipmentProfilesInUseCount(_customProfiles.length + 1 - 1);
|
||||||
|
expectSelectedEquipmentProfileName('');
|
||||||
|
|
||||||
await tester.tap(find.byKey(_Application.addProfileButtonKey));
|
verify(() => storageService.updateProfile(id: _customProfiles.first.id, isUsed: false)).called(1);
|
||||||
await tester.pump();
|
verify(() => storageService.selectedEquipmentProfileId = '').called(1);
|
||||||
expectEquipmentProfilesCount(2);
|
},
|
||||||
expectSelectedEquipmentProfileName('');
|
);
|
||||||
|
|
||||||
verifyNever(() => storageService.selectedEquipmentProfileId = '');
|
testWidgets(
|
||||||
verify(() => storageService.equipmentProfiles = any<List<EquipmentProfile>>()).called(1);
|
'EquipmentProfilesProvider CRUD',
|
||||||
},
|
(tester) async {
|
||||||
);
|
when(() => storageService.getProfiles()).thenAnswer((_) async => {});
|
||||||
|
when(() => storageService.selectedEquipmentProfileId).thenReturn('');
|
||||||
|
|
||||||
testWidgets(
|
await pumpTestWidget(tester, IAPProductStatus.purchased);
|
||||||
'Add from',
|
expectEquipmentProfilesCount(1);
|
||||||
(tester) async {
|
expectSelectedEquipmentProfileName('');
|
||||||
when(() => storageService.equipmentProfiles).thenReturn(List.from(_customProfiles));
|
|
||||||
when(() => storageService.selectedEquipmentProfileId).thenReturn('');
|
|
||||||
|
|
||||||
await pumpTestWidget(tester, IAPProductStatus.purchased);
|
/// Add first profile and verify
|
||||||
expectEquipmentProfilesCount(3);
|
await tester.equipmentProfilesProvider.addProfile(_customProfiles.first);
|
||||||
expectSelectedEquipmentProfileName('');
|
await tester.pump();
|
||||||
|
expectEquipmentProfilesCount(2);
|
||||||
|
expectSelectedEquipmentProfileName('');
|
||||||
|
verify(() => storageService.addProfile(any<EquipmentProfile>())).called(1);
|
||||||
|
|
||||||
await tester.tap(find.byKey(_Application.addFromProfileButtonKey(_customProfiles[0].id)));
|
/// Add the other profiles and select the 1st one
|
||||||
await tester.pump();
|
for (final profile in _customProfiles.skip(1)) {
|
||||||
expectEquipmentProfilesCount(4);
|
await tester.equipmentProfilesProvider.addProfile(profile);
|
||||||
expectSelectedEquipmentProfileName('');
|
}
|
||||||
|
tester.equipmentProfilesProvider.selectProfile(_customProfiles.first);
|
||||||
|
await tester.pump();
|
||||||
|
expectEquipmentProfilesCount(1 + _customProfiles.length);
|
||||||
|
expectSelectedEquipmentProfileName(_customProfiles.first.name);
|
||||||
|
|
||||||
verifyNever(() => storageService.selectedEquipmentProfileId = '');
|
/// Edit the selected profile
|
||||||
verify(() => storageService.equipmentProfiles = any<List<EquipmentProfile>>()).called(1);
|
final updatedName = "${_customProfiles.first} updated";
|
||||||
},
|
await tester.equipmentProfilesProvider.updateProfile(_customProfiles.first.copyWith(name: updatedName));
|
||||||
);
|
await tester.pump();
|
||||||
|
expectEquipmentProfilesCount(1 + _customProfiles.length);
|
||||||
|
expectSelectedEquipmentProfileName(updatedName);
|
||||||
|
verify(() => storageService.updateProfile(id: _customProfiles.first.id, name: updatedName)).called(1);
|
||||||
|
|
||||||
testWidgets(
|
/// Delete a non-selected profile
|
||||||
'Edit selected',
|
await tester.equipmentProfilesProvider.deleteProfile(_customProfiles.last);
|
||||||
(tester) async {
|
await tester.pump();
|
||||||
when(() => storageService.equipmentProfiles).thenReturn(List.from(_customProfiles));
|
expectEquipmentProfilesCount(1 + _customProfiles.length - 1);
|
||||||
when(() => storageService.selectedEquipmentProfileId).thenReturn(_customProfiles[0].id);
|
expectSelectedEquipmentProfileName(updatedName);
|
||||||
|
verifyNever(() => storageService.selectedEquipmentProfileId = '');
|
||||||
|
verify(() => storageService.deleteProfile(_customProfiles.last.id)).called(1);
|
||||||
|
|
||||||
await pumpTestWidget(tester, IAPProductStatus.purchased);
|
/// Delete the selected profile
|
||||||
|
await tester.equipmentProfilesProvider.deleteProfile(_customProfiles.first);
|
||||||
|
await tester.pump();
|
||||||
|
expectEquipmentProfilesCount(1 + _customProfiles.length - 2);
|
||||||
|
expectSelectedEquipmentProfileName('');
|
||||||
|
verify(() => storageService.selectedEquipmentProfileId = '').called(1);
|
||||||
|
verify(() => storageService.deleteProfile(_customProfiles.first.id)).called(1);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Change the name & limit ISO values of the both added profiles
|
extension on WidgetTester {
|
||||||
await tester.tap(find.byKey(_Application.updateProfileButtonKey(_customProfiles[0].id)));
|
EquipmentProfilesProviderState get equipmentProfilesProvider {
|
||||||
await tester.pumpAndSettle();
|
final BuildContext context = element(find.byType(_Application));
|
||||||
expectEquipmentProfilesCount(3);
|
return EquipmentProfilesProvider.of(context);
|
||||||
expectSelectedEquipmentProfileName("${_customProfiles[0].name} updated");
|
}
|
||||||
|
|
||||||
verifyNever(() => storageService.selectedEquipmentProfileId = _customProfiles[0].id);
|
|
||||||
verify(() => storageService.equipmentProfiles = any<List<EquipmentProfile>>()).called(1);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
testWidgets(
|
|
||||||
'Delete selected',
|
|
||||||
(tester) async {
|
|
||||||
when(() => storageService.equipmentProfiles).thenReturn(List.from(_customProfiles));
|
|
||||||
when(() => storageService.selectedEquipmentProfileId).thenReturn(_customProfiles[0].id);
|
|
||||||
|
|
||||||
await pumpTestWidget(tester, IAPProductStatus.purchased);
|
|
||||||
expectEquipmentProfilesCount(3);
|
|
||||||
expectSelectedEquipmentProfileName(_customProfiles[0].name);
|
|
||||||
|
|
||||||
/// Delete the selected profile
|
|
||||||
await tester.tap(find.byKey(_Application.deleteProfileButtonKey(_customProfiles[0].id)));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
expectEquipmentProfilesCount(2);
|
|
||||||
expectSelectedEquipmentProfileName('');
|
|
||||||
|
|
||||||
verify(() => storageService.selectedEquipmentProfileId = '').called(1);
|
|
||||||
verify(() => storageService.equipmentProfiles = any<List<EquipmentProfile>>()).called(1);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
testWidgets(
|
|
||||||
'Delete not selected',
|
|
||||||
(tester) async {
|
|
||||||
when(() => storageService.equipmentProfiles).thenReturn(List.from(_customProfiles));
|
|
||||||
when(() => storageService.selectedEquipmentProfileId).thenReturn(_customProfiles[0].id);
|
|
||||||
|
|
||||||
await pumpTestWidget(tester, IAPProductStatus.purchased);
|
|
||||||
expectEquipmentProfilesCount(3);
|
|
||||||
expectSelectedEquipmentProfileName(_customProfiles[0].name);
|
|
||||||
|
|
||||||
/// Delete the not selected profile
|
|
||||||
await tester.tap(find.byKey(_Application.deleteProfileButtonKey(_customProfiles[1].id)));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
expectEquipmentProfilesCount(2);
|
|
||||||
expectSelectedEquipmentProfileName(_customProfiles[0].name);
|
|
||||||
|
|
||||||
verifyNever(() => storageService.selectedEquipmentProfileId = '');
|
|
||||||
verify(() => storageService.equipmentProfiles = any<List<EquipmentProfile>>()).called(1);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
testWidgets(
|
|
||||||
'Select',
|
|
||||||
(tester) async {
|
|
||||||
when(() => storageService.equipmentProfiles).thenReturn(List.from(_customProfiles));
|
|
||||||
when(() => storageService.selectedEquipmentProfileId).thenReturn('');
|
|
||||||
|
|
||||||
await pumpTestWidget(tester, IAPProductStatus.purchased);
|
|
||||||
expectEquipmentProfilesCount(3);
|
|
||||||
expectSelectedEquipmentProfileName('');
|
|
||||||
|
|
||||||
/// Select the 1st custom profile
|
|
||||||
await tester.tap(find.byKey(_Application.setProfileButtonKey(_customProfiles[0].id)));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
expectEquipmentProfilesCount(3);
|
|
||||||
expectSelectedEquipmentProfileName(_customProfiles[0].name);
|
|
||||||
|
|
||||||
verify(() => storageService.selectedEquipmentProfileId = _customProfiles[0].id).called(1);
|
|
||||||
verifyNever(() => storageService.equipmentProfiles = any<List<EquipmentProfile>>());
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _Application extends StatelessWidget {
|
class _Application extends StatelessWidget {
|
||||||
const _Application();
|
const _Application();
|
||||||
|
|
||||||
static ValueKey get addProfileButtonKey => const ValueKey('addProfileButtonKey');
|
|
||||||
static ValueKey addFromProfileButtonKey(String id) => ValueKey('addFromProfileButtonKey$id');
|
|
||||||
static ValueKey setProfileButtonKey(String id) => ValueKey('setProfileButtonKey$id');
|
|
||||||
static ValueKey updateProfileButtonKey(String id) => ValueKey('updateProfileButtonKey$id');
|
|
||||||
static ValueKey deleteProfileButtonKey(String id) => ValueKey('deleteProfileButtonKey$id');
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialApp(
|
return const MaterialApp(
|
||||||
home: Scaffold(
|
home: Scaffold(
|
||||||
appBar: AppBar(title: const Text('IAPProviders test')),
|
|
||||||
body: Center(
|
body: Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Text("Equipment profiles count: ${EquipmentProfiles.of(context).length}"),
|
_EquipmentProfilesCount(),
|
||||||
Text("Selected equipment profile: ${EquipmentProfiles.selectedOf(context).name}"),
|
_EquipmentProfilesInUseCount(),
|
||||||
ElevatedButton(
|
_SelectedEquipmentProfile(),
|
||||||
key: addProfileButtonKey,
|
|
||||||
onPressed: () {
|
|
||||||
EquipmentProfileProvider.of(context).addProfile('Test added');
|
|
||||||
},
|
|
||||||
child: const Text("Add"),
|
|
||||||
),
|
|
||||||
...EquipmentProfiles.of(context).map((e) => _equipmentProfilesCrudRow(context, e)),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Widget _equipmentProfilesCrudRow(BuildContext context, EquipmentProfile profile) {
|
class _EquipmentProfilesCount extends StatelessWidget {
|
||||||
return Row(
|
static String text(int count) => "Profiles count: $count";
|
||||||
children: [
|
|
||||||
ElevatedButton(
|
const _EquipmentProfilesCount();
|
||||||
key: setProfileButtonKey(profile.id),
|
|
||||||
onPressed: () {
|
@override
|
||||||
EquipmentProfileProvider.of(context).setProfile(profile);
|
Widget build(BuildContext context) {
|
||||||
},
|
return Text(text(EquipmentProfiles.of(context).length));
|
||||||
child: const Text("Set"),
|
}
|
||||||
),
|
}
|
||||||
ElevatedButton(
|
|
||||||
key: addFromProfileButtonKey(profile.id),
|
class _EquipmentProfilesInUseCount extends StatelessWidget {
|
||||||
onPressed: () {
|
static String text(int count) => "Profiles in use count: $count";
|
||||||
EquipmentProfileProvider.of(context).addProfile('Test from ${profile.name}', profile);
|
|
||||||
},
|
const _EquipmentProfilesInUseCount();
|
||||||
child: const Text("Add from"),
|
|
||||||
),
|
@override
|
||||||
ElevatedButton(
|
Widget build(BuildContext context) {
|
||||||
key: updateProfileButtonKey(profile.id),
|
return Text(text(EquipmentProfiles.inUseOf(context).length));
|
||||||
onPressed: () {
|
}
|
||||||
EquipmentProfileProvider.of(context).updateProfile(
|
}
|
||||||
profile.copyWith(
|
|
||||||
name: '${profile.name} updated',
|
class _SelectedEquipmentProfile extends StatelessWidget {
|
||||||
isoValues: _customProfiles.first.isoValues,
|
static String text(String name) => "Selected profile: $name}";
|
||||||
),
|
|
||||||
);
|
const _SelectedEquipmentProfile();
|
||||||
},
|
|
||||||
child: const Text("Update"),
|
@override
|
||||||
),
|
Widget build(BuildContext context) {
|
||||||
ElevatedButton(
|
return Text(text(EquipmentProfiles.selectedOf(context).name));
|
||||||
key: deleteProfileButtonKey(profile.id),
|
|
||||||
onPressed: () {
|
|
||||||
EquipmentProfileProvider.of(context).deleteProfile(profile);
|
|
||||||
},
|
|
||||||
child: const Text("Delete"),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,25 +9,24 @@ class _MockFilmsStorageService extends Mock implements FilmsStorageService {}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
TestWidgetsFlutterBinding.ensureInitialized();
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
late _MockFilmsStorageService mockFilmsStorageService;
|
late _MockFilmsStorageService storageService;
|
||||||
|
|
||||||
setUpAll(() {
|
setUpAll(() {
|
||||||
mockFilmsStorageService = _MockFilmsStorageService();
|
storageService = _MockFilmsStorageService();
|
||||||
});
|
});
|
||||||
|
|
||||||
setUp(() {
|
setUp(() {
|
||||||
registerFallbackValue(mockCustomFilms.first);
|
registerFallbackValue(mockCustomFilms.first);
|
||||||
when(() => mockFilmsStorageService.toggleFilm(any<Film>(), any<bool>())).thenAnswer((_) async {});
|
when(() => storageService.toggleFilm(any<Film>(), any<bool>())).thenAnswer((_) async {});
|
||||||
when(() => mockFilmsStorageService.addFilm(any<FilmExponential>())).thenAnswer((_) async {});
|
when(() => storageService.addFilm(any<FilmExponential>())).thenAnswer((_) async {});
|
||||||
when(() => mockFilmsStorageService.updateFilm(any<FilmExponential>())).thenAnswer((_) async {});
|
when(() => storageService.updateFilm(any<FilmExponential>())).thenAnswer((_) async {});
|
||||||
when(() => mockFilmsStorageService.deleteFilm(any<FilmExponential>())).thenAnswer((_) async {});
|
when(() => storageService.deleteFilm(any<FilmExponential>())).thenAnswer((_) async {});
|
||||||
when(() => mockFilmsStorageService.getPredefinedFilms())
|
when(() => storageService.getPredefinedFilms()).thenAnswer((_) => Future.value(mockPredefinedFilms.toFilmsMap()));
|
||||||
.thenAnswer((_) => Future.value(mockPredefinedFilms.toFilmsMap()));
|
when(() => storageService.getCustomFilms()).thenAnswer((_) => Future.value(mockCustomFilms.toFilmsMap()));
|
||||||
when(() => mockFilmsStorageService.getCustomFilms()).thenAnswer((_) => Future.value(mockCustomFilms.toFilmsMap()));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDown(() {
|
tearDown(() {
|
||||||
reset(mockFilmsStorageService);
|
reset(storageService);
|
||||||
});
|
});
|
||||||
|
|
||||||
Future<void> pumpTestWidget(WidgetTester tester, IAPProductStatus productStatus) async {
|
Future<void> pumpTestWidget(WidgetTester tester, IAPProductStatus productStatus) async {
|
||||||
|
@ -41,7 +40,7 @@ void main() {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: FilmsProvider(
|
child: FilmsProvider(
|
||||||
filmsStorageService: mockFilmsStorageService,
|
storageService: storageService,
|
||||||
child: const _Application(),
|
child: const _Application(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -69,11 +68,10 @@ void main() {
|
||||||
'FilmsProvider dependency on IAPProductStatus',
|
'FilmsProvider dependency on IAPProductStatus',
|
||||||
() {
|
() {
|
||||||
setUp(() {
|
setUp(() {
|
||||||
when(() => mockFilmsStorageService.selectedFilmId).thenReturn(mockPredefinedFilms.first.id);
|
when(() => storageService.selectedFilmId).thenReturn(mockPredefinedFilms.first.id);
|
||||||
when(() => mockFilmsStorageService.getPredefinedFilms())
|
when(() => storageService.getPredefinedFilms())
|
||||||
.thenAnswer((_) => Future.value(mockPredefinedFilms.toFilmsMap()));
|
.thenAnswer((_) => Future.value(mockPredefinedFilms.toFilmsMap()));
|
||||||
when(() => mockFilmsStorageService.getCustomFilms())
|
when(() => storageService.getCustomFilms()).thenAnswer((_) => Future.value(mockCustomFilms.toFilmsMap()));
|
||||||
.thenAnswer((_) => Future.value(mockCustomFilms.toFilmsMap()));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets(
|
testWidgets(
|
||||||
|
@ -117,7 +115,7 @@ void main() {
|
||||||
testWidgets(
|
testWidgets(
|
||||||
'toggle predefined film',
|
'toggle predefined film',
|
||||||
(tester) async {
|
(tester) async {
|
||||||
when(() => mockFilmsStorageService.selectedFilmId).thenReturn(mockPredefinedFilms.first.id);
|
when(() => storageService.selectedFilmId).thenReturn(mockPredefinedFilms.first.id);
|
||||||
await pumpTestWidget(tester, IAPProductStatus.purchased);
|
await pumpTestWidget(tester, IAPProductStatus.purchased);
|
||||||
expectPredefinedFilmsCount(mockPredefinedFilms.length);
|
expectPredefinedFilmsCount(mockPredefinedFilms.length);
|
||||||
expectCustomFilmsCount(mockCustomFilms.length);
|
expectCustomFilmsCount(mockCustomFilms.length);
|
||||||
|
@ -131,15 +129,15 @@ void main() {
|
||||||
expectFilmsInUseCount(mockPredefinedFilms.length - 1 + mockCustomFilms.length + 1);
|
expectFilmsInUseCount(mockPredefinedFilms.length - 1 + mockCustomFilms.length + 1);
|
||||||
expectSelectedFilmName('');
|
expectSelectedFilmName('');
|
||||||
|
|
||||||
verify(() => mockFilmsStorageService.toggleFilm(mockPredefinedFilms.first, false)).called(1);
|
verify(() => storageService.toggleFilm(mockPredefinedFilms.first, false)).called(1);
|
||||||
verify(() => mockFilmsStorageService.selectedFilmId = '').called(1);
|
verify(() => storageService.selectedFilmId = '').called(1);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
testWidgets(
|
testWidgets(
|
||||||
'toggle custom film',
|
'toggle custom film',
|
||||||
(tester) async {
|
(tester) async {
|
||||||
when(() => mockFilmsStorageService.selectedFilmId).thenReturn(mockCustomFilms.first.id);
|
when(() => storageService.selectedFilmId).thenReturn(mockCustomFilms.first.id);
|
||||||
await pumpTestWidget(tester, IAPProductStatus.purchased);
|
await pumpTestWidget(tester, IAPProductStatus.purchased);
|
||||||
expectPredefinedFilmsCount(mockPredefinedFilms.length);
|
expectPredefinedFilmsCount(mockPredefinedFilms.length);
|
||||||
expectCustomFilmsCount(mockCustomFilms.length);
|
expectCustomFilmsCount(mockCustomFilms.length);
|
||||||
|
@ -153,8 +151,8 @@ void main() {
|
||||||
expectFilmsInUseCount(mockPredefinedFilms.length - 1 + mockCustomFilms.length + 1);
|
expectFilmsInUseCount(mockPredefinedFilms.length - 1 + mockCustomFilms.length + 1);
|
||||||
expectSelectedFilmName('');
|
expectSelectedFilmName('');
|
||||||
|
|
||||||
verify(() => mockFilmsStorageService.toggleFilm(mockCustomFilms.first, false)).called(1);
|
verify(() => storageService.toggleFilm(mockCustomFilms.first, false)).called(1);
|
||||||
verify(() => mockFilmsStorageService.selectedFilmId = '').called(1);
|
verify(() => storageService.selectedFilmId = '').called(1);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -163,7 +161,7 @@ void main() {
|
||||||
testWidgets(
|
testWidgets(
|
||||||
'selectFilm',
|
'selectFilm',
|
||||||
(tester) async {
|
(tester) async {
|
||||||
when(() => mockFilmsStorageService.selectedFilmId).thenReturn('');
|
when(() => storageService.selectedFilmId).thenReturn('');
|
||||||
await pumpTestWidget(tester, IAPProductStatus.purchased);
|
await pumpTestWidget(tester, IAPProductStatus.purchased);
|
||||||
expectSelectedFilmName('');
|
expectSelectedFilmName('');
|
||||||
|
|
||||||
|
@ -175,16 +173,16 @@ void main() {
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expectSelectedFilmName(mockCustomFilms.first.name);
|
expectSelectedFilmName(mockCustomFilms.first.name);
|
||||||
|
|
||||||
verify(() => mockFilmsStorageService.selectedFilmId = mockPredefinedFilms.first.id).called(1);
|
verify(() => storageService.selectedFilmId = mockPredefinedFilms.first.id).called(1);
|
||||||
verify(() => mockFilmsStorageService.selectedFilmId = mockCustomFilms.first.id).called(1);
|
verify(() => storageService.selectedFilmId = mockCustomFilms.first.id).called(1);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
testWidgets(
|
testWidgets(
|
||||||
'Custom film CRUD',
|
'Custom film CRUD',
|
||||||
(tester) async {
|
(tester) async {
|
||||||
when(() => mockFilmsStorageService.selectedFilmId).thenReturn('');
|
when(() => storageService.selectedFilmId).thenReturn('');
|
||||||
when(() => mockFilmsStorageService.getCustomFilms()).thenAnswer((_) => Future.value({}));
|
when(() => storageService.getCustomFilms()).thenAnswer((_) => Future.value({}));
|
||||||
await pumpTestWidget(tester, IAPProductStatus.purchased);
|
await pumpTestWidget(tester, IAPProductStatus.purchased);
|
||||||
expectPredefinedFilmsCount(mockPredefinedFilms.length);
|
expectPredefinedFilmsCount(mockPredefinedFilms.length);
|
||||||
expectCustomFilmsCount(0);
|
expectCustomFilmsCount(0);
|
||||||
|
@ -199,16 +197,16 @@ void main() {
|
||||||
expectCustomFilmsCount(1);
|
expectCustomFilmsCount(1);
|
||||||
expectFilmsInUseCount(mockPredefinedFilms.length + 1 + 1);
|
expectFilmsInUseCount(mockPredefinedFilms.length + 1 + 1);
|
||||||
expectSelectedFilmName(mockCustomFilms.first.name);
|
expectSelectedFilmName(mockCustomFilms.first.name);
|
||||||
verify(() => mockFilmsStorageService.addFilm(mockCustomFilms.first)).called(1);
|
verify(() => storageService.addFilm(mockCustomFilms.first)).called(1);
|
||||||
verify(() => mockFilmsStorageService.toggleFilm(mockCustomFilms.first, true)).called(1);
|
verify(() => storageService.toggleFilm(mockCustomFilms.first, true)).called(1);
|
||||||
verify(() => mockFilmsStorageService.selectedFilmId = mockCustomFilms.first.id).called(1);
|
verify(() => storageService.selectedFilmId = mockCustomFilms.first.id).called(1);
|
||||||
|
|
||||||
const editedFilmName = 'Edited custom film 2x';
|
const editedFilmName = 'Edited custom film 2x';
|
||||||
final editedFilm = mockCustomFilms.first.copyWith(name: editedFilmName);
|
final editedFilm = mockCustomFilms.first.copyWith(name: editedFilmName);
|
||||||
await tester.filmsProvider.updateCustomFilm(editedFilm);
|
await tester.filmsProvider.updateCustomFilm(editedFilm);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expectSelectedFilmName(editedFilm.name);
|
expectSelectedFilmName(editedFilm.name);
|
||||||
verify(() => mockFilmsStorageService.updateFilm(editedFilm)).called(1);
|
verify(() => storageService.updateFilm(editedFilm)).called(1);
|
||||||
|
|
||||||
await tester.filmsProvider.deleteCustomFilm(editedFilm);
|
await tester.filmsProvider.deleteCustomFilm(editedFilm);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
@ -216,8 +214,8 @@ void main() {
|
||||||
expectCustomFilmsCount(0);
|
expectCustomFilmsCount(0);
|
||||||
expectFilmsInUseCount(mockPredefinedFilms.length + 0 + 1);
|
expectFilmsInUseCount(mockPredefinedFilms.length + 0 + 1);
|
||||||
expectSelectedFilmName('');
|
expectSelectedFilmName('');
|
||||||
verify(() => mockFilmsStorageService.deleteFilm(editedFilm)).called(1);
|
verify(() => storageService.deleteFilm(editedFilm)).called(1);
|
||||||
verify(() => mockFilmsStorageService.selectedFilmId = '').called(1);
|
verify(() => storageService.selectedFilmId = '').called(1);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -307,6 +305,6 @@ const mockCustomFilms = [
|
||||||
];
|
];
|
||||||
|
|
||||||
extension on List<Film> {
|
extension on List<Film> {
|
||||||
Map<String, ({T film, bool isUsed})> toFilmsMap<T extends Film>() =>
|
Map<String, ({T value, bool isUsed})> toFilmsMap<T extends Film>() =>
|
||||||
Map.fromEntries(map((e) => MapEntry(e.id, (film: e as T, isUsed: true))));
|
Map.fromEntries(map((e) => MapEntry(e.id, (value: e as T, isUsed: true))));
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,15 +10,15 @@ import 'package:mocktail/mocktail.dart';
|
||||||
import '../../../../../application_mock.dart';
|
import '../../../../../application_mock.dart';
|
||||||
import 'utils.dart';
|
import 'utils.dart';
|
||||||
|
|
||||||
class _MockIAPStorageService extends Mock implements IAPStorageService {}
|
class _MockEquipmentProfilesStorageService extends Mock implements EquipmentProfilesStorageService {}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
late final _MockIAPStorageService mockIAPStorageService;
|
late final _MockEquipmentProfilesStorageService storageService;
|
||||||
|
|
||||||
setUpAll(() {
|
setUpAll(() {
|
||||||
mockIAPStorageService = _MockIAPStorageService();
|
storageService = _MockEquipmentProfilesStorageService();
|
||||||
when(() => mockIAPStorageService.equipmentProfiles).thenReturn(_mockEquipmentProfiles);
|
when(() => storageService.getProfiles()).thenAnswer((_) async => _mockEquipmentProfiles.toTogglableMap());
|
||||||
when(() => mockIAPStorageService.selectedEquipmentProfileId).thenReturn('');
|
when(() => storageService.selectedEquipmentProfileId).thenReturn('');
|
||||||
});
|
});
|
||||||
|
|
||||||
Future<void> pumpApplication(WidgetTester tester) async {
|
Future<void> pumpApplication(WidgetTester tester) async {
|
||||||
|
@ -31,8 +31,8 @@ void main() {
|
||||||
price: '0.0\$',
|
price: '0.0\$',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: EquipmentProfileProvider(
|
child: EquipmentProfilesProvider(
|
||||||
storageService: mockIAPStorageService,
|
storageService: storageService,
|
||||||
child: const WidgetTestApplicationMock(
|
child: const WidgetTestApplicationMock(
|
||||||
child: Row(children: [Expanded(child: EquipmentProfilePicker())]),
|
child: Row(children: [Expanded(child: EquipmentProfilePicker())]),
|
||||||
),
|
),
|
||||||
|
@ -59,7 +59,7 @@ void main() {
|
||||||
testWidgets(
|
testWidgets(
|
||||||
'None',
|
'None',
|
||||||
(tester) async {
|
(tester) async {
|
||||||
when(() => mockIAPStorageService.selectedEquipmentProfileId).thenReturn('');
|
when(() => storageService.selectedEquipmentProfileId).thenReturn('');
|
||||||
await pumpApplication(tester);
|
await pumpApplication(tester);
|
||||||
expectReadingValueContainerText(S.current.none);
|
expectReadingValueContainerText(S.current.none);
|
||||||
await tester.openAnimatedPicker<EquipmentProfilePicker>();
|
await tester.openAnimatedPicker<EquipmentProfilePicker>();
|
||||||
|
@ -70,7 +70,7 @@ void main() {
|
||||||
testWidgets(
|
testWidgets(
|
||||||
'Praktica + Zenitar',
|
'Praktica + Zenitar',
|
||||||
(tester) async {
|
(tester) async {
|
||||||
when(() => mockIAPStorageService.selectedEquipmentProfileId).thenReturn(_mockEquipmentProfiles.first.id);
|
when(() => storageService.selectedEquipmentProfileId).thenReturn(_mockEquipmentProfiles.first.id);
|
||||||
await pumpApplication(tester);
|
await pumpApplication(tester);
|
||||||
expectReadingValueContainerText(_mockEquipmentProfiles.first.name);
|
expectReadingValueContainerText(_mockEquipmentProfiles.first.name);
|
||||||
await tester.openAnimatedPicker<EquipmentProfilePicker>();
|
await tester.openAnimatedPicker<EquipmentProfilePicker>();
|
||||||
|
@ -79,6 +79,18 @@ void main() {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'Equipment profile picker shows only profiles in use',
|
||||||
|
(tester) async {
|
||||||
|
when(() => storageService.getProfiles())
|
||||||
|
.thenAnswer((_) async => _mockEquipmentProfiles.skip(1).toList().toTogglableMap());
|
||||||
|
await pumpApplication(tester);
|
||||||
|
await tester.openAnimatedPicker<EquipmentProfilePicker>();
|
||||||
|
expectRadioListTile<EquipmentProfile>(S.current.none, isSelected: true);
|
||||||
|
expectRadioListTile<EquipmentProfile>(_mockEquipmentProfiles[1].name);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final _mockEquipmentProfiles = [
|
final _mockEquipmentProfiles = [
|
||||||
|
|
|
@ -18,7 +18,7 @@ void main() {
|
||||||
setUpAll(() {
|
setUpAll(() {
|
||||||
mockFilmsStorageService = _MockFilmsStorageService();
|
mockFilmsStorageService = _MockFilmsStorageService();
|
||||||
when(() => mockFilmsStorageService.getPredefinedFilms()).thenAnswer(
|
when(() => mockFilmsStorageService.getPredefinedFilms()).thenAnswer(
|
||||||
(_) => Future.value(Map.fromEntries(_films.map((e) => MapEntry(e.id, (film: e, isUsed: true))))),
|
(_) => Future.value(Map.fromEntries(_films.map((e) => MapEntry(e.id, (value: e, isUsed: true))))),
|
||||||
);
|
);
|
||||||
when(() => mockFilmsStorageService.getCustomFilms()).thenAnswer(
|
when(() => mockFilmsStorageService.getCustomFilms()).thenAnswer(
|
||||||
(_) => Future.value({}),
|
(_) => Future.value({}),
|
||||||
|
@ -36,7 +36,7 @@ void main() {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: FilmsProvider(
|
child: FilmsProvider(
|
||||||
filmsStorageService: mockFilmsStorageService,
|
storageService: mockFilmsStorageService,
|
||||||
child: const WidgetTestApplicationMock(
|
child: const WidgetTestApplicationMock(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
|
|
|
@ -8,14 +8,26 @@ import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
import '../../../function_mock.dart';
|
import '../../../function_mock.dart';
|
||||||
|
|
||||||
class _MockIAPStorageService extends Mock implements IAPStorageService {}
|
class _MockEquipmentProfilesStorageService extends Mock implements EquipmentProfilesStorageService {}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
TestWidgetsFlutterBinding.ensureInitialized();
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
final storageService = _MockIAPStorageService();
|
final storageService = _MockEquipmentProfilesStorageService();
|
||||||
final equipmentProfileProviderKey = GlobalKey<EquipmentProfileProviderState>();
|
|
||||||
final onDidChangeDependencies = MockValueChanged<EquipmentProfile>();
|
final onDidChangeDependencies = MockValueChanged<EquipmentProfile>();
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
registerFallbackValue(_customProfiles.first);
|
||||||
|
when(() => storageService.addProfile(any<EquipmentProfile>())).thenAnswer((_) async {});
|
||||||
|
when(
|
||||||
|
() => storageService.updateProfile(
|
||||||
|
id: any<String>(named: 'id'),
|
||||||
|
name: any<String>(named: 'name'),
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async {});
|
||||||
|
when(() => storageService.deleteProfile(any<String>())).thenAnswer((_) async {});
|
||||||
|
when(() => storageService.getProfiles()).thenAnswer((_) => Future.value(_customProfiles.toTogglableMap()));
|
||||||
|
});
|
||||||
|
|
||||||
tearDown(() {
|
tearDown(() {
|
||||||
reset(onDidChangeDependencies);
|
reset(onDidChangeDependencies);
|
||||||
reset(storageService);
|
reset(storageService);
|
||||||
|
@ -31,8 +43,7 @@ void main() {
|
||||||
price: '0.0\$',
|
price: '0.0\$',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: EquipmentProfileProvider(
|
child: EquipmentProfilesProvider(
|
||||||
key: equipmentProfileProviderKey,
|
|
||||||
storageService: storageService,
|
storageService: storageService,
|
||||||
child: MaterialApp(
|
child: MaterialApp(
|
||||||
home: EquipmentProfileListener(
|
home: EquipmentProfileListener(
|
||||||
|
@ -48,11 +59,10 @@ void main() {
|
||||||
testWidgets(
|
testWidgets(
|
||||||
'Trigger `onDidChangeDependencies` by selecting a new profile',
|
'Trigger `onDidChangeDependencies` by selecting a new profile',
|
||||||
(tester) async {
|
(tester) async {
|
||||||
when(() => storageService.equipmentProfiles).thenReturn(List.from(_customProfiles));
|
|
||||||
when(() => storageService.selectedEquipmentProfileId).thenReturn('');
|
when(() => storageService.selectedEquipmentProfileId).thenReturn('');
|
||||||
await pumpTestWidget(tester);
|
await pumpTestWidget(tester);
|
||||||
|
|
||||||
equipmentProfileProviderKey.currentState!.setProfile(_customProfiles[0]);
|
tester.equipmentProfilesProvider.selectProfile(_customProfiles[0]);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
verify(() => onDidChangeDependencies.onChanged(_customProfiles[0])).called(1);
|
verify(() => onDidChangeDependencies.onChanged(_customProfiles[0])).called(1);
|
||||||
},
|
},
|
||||||
|
@ -61,18 +71,17 @@ void main() {
|
||||||
testWidgets(
|
testWidgets(
|
||||||
'Trigger `onDidChangeDependencies` by updating the selected profile',
|
'Trigger `onDidChangeDependencies` by updating the selected profile',
|
||||||
(tester) async {
|
(tester) async {
|
||||||
when(() => storageService.equipmentProfiles).thenReturn(List.from(_customProfiles));
|
|
||||||
when(() => storageService.selectedEquipmentProfileId).thenReturn(_customProfiles[0].id);
|
when(() => storageService.selectedEquipmentProfileId).thenReturn(_customProfiles[0].id);
|
||||||
await pumpTestWidget(tester);
|
await pumpTestWidget(tester);
|
||||||
|
|
||||||
final updatedProfile1 = _customProfiles[0].copyWith(name: 'Test 1 updated');
|
final updatedProfile1 = _customProfiles[0].copyWith(name: 'Test 1 updated');
|
||||||
equipmentProfileProviderKey.currentState!.updateProfile(updatedProfile1);
|
await tester.equipmentProfilesProvider.updateProfile(updatedProfile1);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
verify(() => onDidChangeDependencies.onChanged(updatedProfile1)).called(1);
|
verify(() => onDidChangeDependencies.onChanged(updatedProfile1)).called(1);
|
||||||
|
|
||||||
/// Verify that updating the not selected profile doesn't trigger the callback
|
/// Verify that updating the not selected profile doesn't trigger the callback
|
||||||
final updatedProfile2 = _customProfiles[1].copyWith(name: 'Test 2 updated');
|
final updatedProfile2 = _customProfiles[1].copyWith(name: 'Test 2 updated');
|
||||||
equipmentProfileProviderKey.currentState!.updateProfile(updatedProfile2);
|
await tester.equipmentProfilesProvider.updateProfile(updatedProfile2);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
verifyNever(() => onDidChangeDependencies.onChanged(updatedProfile2));
|
verifyNever(() => onDidChangeDependencies.onChanged(updatedProfile2));
|
||||||
},
|
},
|
||||||
|
@ -81,18 +90,24 @@ void main() {
|
||||||
testWidgets(
|
testWidgets(
|
||||||
"Don't trigger `onDidChangeDependencies` by updating the unselected profile",
|
"Don't trigger `onDidChangeDependencies` by updating the unselected profile",
|
||||||
(tester) async {
|
(tester) async {
|
||||||
when(() => storageService.equipmentProfiles).thenReturn(List.from(_customProfiles));
|
|
||||||
when(() => storageService.selectedEquipmentProfileId).thenReturn(_customProfiles[0].id);
|
when(() => storageService.selectedEquipmentProfileId).thenReturn(_customProfiles[0].id);
|
||||||
await pumpTestWidget(tester);
|
await pumpTestWidget(tester);
|
||||||
|
|
||||||
final updatedProfile2 = _customProfiles[1].copyWith(name: 'Test 2 updated');
|
final updatedProfile2 = _customProfiles[1].copyWith(name: 'Test 2 updated');
|
||||||
equipmentProfileProviderKey.currentState!.updateProfile(updatedProfile2);
|
await tester.equipmentProfilesProvider.updateProfile(updatedProfile2);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
verifyNever(() => onDidChangeDependencies.onChanged(updatedProfile2));
|
verifyNever(() => onDidChangeDependencies.onChanged(updatedProfile2));
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension on WidgetTester {
|
||||||
|
EquipmentProfilesProviderState get equipmentProfilesProvider {
|
||||||
|
final BuildContext context = element(find.byType(MaterialApp));
|
||||||
|
return EquipmentProfilesProvider.of(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final List<EquipmentProfile> _customProfiles = [
|
final List<EquipmentProfile> _customProfiles = [
|
||||||
const EquipmentProfile(
|
const EquipmentProfile(
|
||||||
id: '1',
|
id: '1',
|
||||||
|
|
Loading…
Reference in a new issue