Skip to content

Commit

Permalink
ML-191 Add an ability to add a generic film, that will accept a formu…
Browse files Browse the repository at this point in the history
…la (#195)

* sync with resources

* separated `ExpandableSectionList` as widget

* fixed generic type

* implemented `FilmsScreen` (wip)

* made `SliverScreen` title a widget

* [`FilmEditScreen`] wip

* [`FilmEditScreen`] added validation

* fixed title overflow for `SliverScreen`

* [`FilmEditScreen`] separated add and edit blocs

* [`FilmEditScreen`] split into separate components

* added bottom widget to `SliverScreen`

* implemented films list tabs fo `FilmsScreen`

* added films screen to navigation

* replaced explicit routes names with enum values

* implemented CRUD for custom films

* added placeholder for empty custom films list

* added `FilmsStorageService`

* fixed unit tests

* fixed integration tests

* lint

* fixed golden tests

* added iap stub methods

* added custom films to features list

* use 2.0.0 resouces

* fixed film picket tests

* migrated to iap 1.0.1

* autofocus film name field

* wait for the film to edited

* migrated to iap 1.1.0

* typo

* wait for storage initialization

* migrated to iap 1.1.1

* fixed films initialization

* added conditions to films model `updateShouldNotifyDependent`

* typo

* fixed select film discard notify

* covered films model `updateShouldNotifyDependent`
  • Loading branch information
vodemn authored Nov 3, 2024
1 parent d938be6 commit c66381f
Show file tree
Hide file tree
Showing 54 changed files with 1,766 additions and 392 deletions.
1 change: 1 addition & 0 deletions iap/lib/m3_lightmeter_iap.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
export 'src/data/models/iap_product.dart';
export 'src/providers/iap_products_provider.dart';
export 'src/data/iap_storage_service.dart';
export 'src/data/films_storage_service.dart';

const List<Film> films = [];
32 changes: 32 additions & 0 deletions iap/lib/src/data/films_storage_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import 'package:flutter/foundation.dart';
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';

typedef SelectableFilm<T extends Film> = ({T film, bool isUsed});

class FilmsStorageService {
FilmsStorageService();

Future<void> init() async {}

@visibleForTesting
Future<void> createTable(dynamic _) async {}

String get selectedFilmId => '';
set selectedFilmId(String id) {}

Future<void> addFilm(FilmExponential _, {bool isUsed = true}) async {}

Future<void> updateFilm(FilmExponential _) async {}

Future<void> toggleFilm(Film _, bool __) async {}

Future<void> deleteFilm(FilmExponential _) async {}

Future<Map<String, SelectableFilm<Film>>> getPredefinedFilms() async {
return const {};
}

Future<Map<String, SelectableFilm<FilmExponential>>> getCustomFilms() async {
return const {};
}
}
6 changes: 0 additions & 6 deletions iap/lib/src/data/iap_storage_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,4 @@ class IAPStorageService {

List<EquipmentProfile> get equipmentProfiles => [];
set equipmentProfiles(List<EquipmentProfile> profiles) {}

Film get selectedFilm => const Film.other();
set selectedFilm(Film value) {}

List<Film> get filmsInUse => [];
set filmsInUse(List<Film> profiles) {}
}
7 changes: 3 additions & 4 deletions iap/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
name: m3_lightmeter_iap
description: IAP stubs for the M3 Lightmeter app.
version: 0.2.0
version: 1.0.0
publish_to: 'none'

environment:
sdk: '>=2.19.2 <3.0.0'
flutter: ">=1.17.0"
sdk: ">=3.0.0 <4.0.0"

dependencies:
flutter:
sdk: flutter
m3_lightmeter_resources:
git:
url: "https://github.com/vodemn/m3_lightmeter_resources"
ref: v1.4.0
ref: v2.0.0
shared_preferences: 2.2.0

dev_dependencies:
Expand Down
11 changes: 5 additions & 6 deletions integration_test/e2e_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,11 @@ void testE2E(String description) {
testWidgets(
description,
(tester) async {
await tester.pumpApplication(equipmentProfiles: [], filmsInUse: []);
await tester.pumpApplication(
equipmentProfiles: [],
predefinedFilms: mockFilms.toFilmsMap(isUsed: true),
customFilms: {},
);

/// Create Praktica + Zenitar profile from scratch
await tester.openSettings();
Expand Down Expand Up @@ -76,11 +80,6 @@ void testE2E(String description) {
expect(find.text('f/3.5 - f/22'), findsOneWidget);
expect(find.text('1/1000 - B'), findsNWidgets(2));
await tester.navigatorPop();

/// Select some films
await tester.tap(find.text(S.current.filmsInUse));
await tester.pumpAndSettle();
await tester.setDialogFilterValues<Film>([mockFilms[0], mockFilms[1]], deselectAll: false);
await tester.navigatorPop();

/// Select some initial settings according to the selected gear and film
Expand Down
2 changes: 1 addition & 1 deletion integration_test/metering_screen_layout_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ void testToggleLayoutFeatures(String description) {
testWidgets(
'Film picker',
(tester) async {
await tester.pumpApplication(selectedFilm: mockFilms.first);
await tester.pumpApplication(selectedFilmId: mockFilms.first.id);
await tester.takePhoto();
expectPickerTitle<FilmPicker>(mockFilms.first.name);
expectExtremeExposurePairs('f/1.0 - 1/320', 'f/45 - 12"');
Expand Down
80 changes: 63 additions & 17 deletions integration_test/mocks/paid_features_mock.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,49 +7,56 @@ import 'package:mocktail/mocktail.dart';

class _MockIAPStorageService extends Mock implements IAPStorageService {}

class _MockFilmsStorageService extends Mock implements FilmsStorageService {}

class MockIAPProviders extends StatefulWidget {
final List<EquipmentProfile>? equipmentProfiles;
final String selectedEquipmentProfileId;
final List<Film> availableFilms;
final List<Film> filmsInUse;
final Film selectedFilm;
final Map<String, SelectableFilm<Film>> predefinedFilms;
final Map<String, SelectableFilm<FilmExponential>> customFilms;
final String selectedFilmId;
final Widget child;

const MockIAPProviders({
MockIAPProviders({
this.equipmentProfiles = const [],
this.selectedEquipmentProfileId = '',
List<Film>? availableFilms,
List<Film>? filmsInUse,
this.selectedFilm = const Film.other(),
Map<String, SelectableFilm<Film>>? predefinedFilms,
Map<String, SelectableFilm<FilmExponential>>? customFilms,
String? selectedFilmId,
required this.child,
super.key,
}) : availableFilms = availableFilms ?? mockFilms,
filmsInUse = filmsInUse ?? mockFilms;
}) : predefinedFilms = predefinedFilms ?? mockFilms.toFilmsMap(),
customFilms = customFilms ?? mockFilms.toFilmsMap(),
selectedFilmId = selectedFilmId ?? const FilmStub().id;

@override
State<MockIAPProviders> createState() => _MockIAPProvidersState();
}

class _MockIAPProvidersState extends State<MockIAPProviders> {
late final _MockIAPStorageService mockIAPStorageService;
late final _MockFilmsStorageService mockFilmsStorageService;

@override
void initState() {
super.initState();
mockIAPStorageService = _MockIAPStorageService();
when(() => mockIAPStorageService.equipmentProfiles).thenReturn(widget.equipmentProfiles ?? mockEquipmentProfiles);
when(() => mockIAPStorageService.selectedEquipmentProfileId).thenReturn(widget.selectedEquipmentProfileId);
when(() => mockIAPStorageService.filmsInUse).thenReturn(widget.filmsInUse);
when(() => mockIAPStorageService.selectedFilm).thenReturn(widget.selectedFilm);

mockFilmsStorageService = _MockFilmsStorageService();
when(() => mockFilmsStorageService.init()).thenAnswer((_) async {});
when(() => mockFilmsStorageService.getPredefinedFilms()).thenAnswer((_) => Future.value(widget.predefinedFilms));
when(() => mockFilmsStorageService.getCustomFilms()).thenAnswer((_) => Future.value(widget.customFilms));
when(() => mockFilmsStorageService.selectedFilmId).thenReturn(widget.selectedFilmId);
}

@override
Widget build(BuildContext context) {
return EquipmentProfileProvider(
storageService: mockIAPStorageService,
child: FilmsProvider(
storageService: mockIAPStorageService,
availableFilms: widget.availableFilms,
filmsStorageService: mockFilmsStorageService,
child: widget.child,
),
);
Expand Down Expand Up @@ -128,13 +135,52 @@ final mockEquipmentProfiles = [
),
];

const mockFilms = [_MockFilm(100, 2), _MockFilm(400, 2), _MockFilm(3, 800), _MockFilm(400, 1.5)];
const mockFilms = [
_FilmMultiplying(id: '1', name: 'Mock film 1', iso: 100, reciprocityMultiplier: 2),
_FilmMultiplying(id: '2', name: 'Mock film 2', iso: 400, reciprocityMultiplier: 2),
_FilmMultiplying(id: '3', name: 'Mock film 3', iso: 800, reciprocityMultiplier: 3),
_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 _MockFilm extends Film {
class _FilmMultiplying extends FilmExponential {
final double reciprocityMultiplier;

const _MockFilm(int iso, this.reciprocityMultiplier) : super('Mock film $iso x$reciprocityMultiplier', iso);
const _FilmMultiplying({
String? id,
required String name,
required super.iso,
required this.reciprocityMultiplier,
}) : super(id: id ?? name, name: 'Mock film $iso x$reciprocityMultiplier', exponent: 1);

@override
ShutterSpeedValue reciprocityFailure(ShutterSpeedValue shutterSpeed) {
if (shutterSpeed.isFraction) {
return shutterSpeed;
} else {
return ShutterSpeedValue(
shutterSpeed.rawValue * reciprocityMultiplier,
shutterSpeed.isFraction,
shutterSpeed.stopType,
);
}
}

@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other.runtimeType != runtimeType) return false;
return other is _FilmMultiplying &&
other.id == id &&
other.name == name &&
other.iso == iso &&
other.reciprocityMultiplier == reciprocityMultiplier;
}

@override
double reciprocityFormula(double t) => t * reciprocityMultiplier;
int get hashCode => Object.hash(id, name, iso, reciprocityMultiplier, runtimeType);
}
12 changes: 6 additions & 6 deletions integration_test/utils/widget_tester_actions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ extension WidgetTesterCommonActions on WidgetTester {
IAPProductStatus productStatus = IAPProductStatus.purchased,
List<EquipmentProfile>? equipmentProfiles,
String selectedEquipmentProfileId = '',
List<Film>? availableFilms,
List<Film>? filmsInUse,
Film selectedFilm = const Film.other(),
Map<String, SelectableFilm<Film>>? predefinedFilms,
Map<String, SelectableFilm<FilmExponential>>? customFilms,
String selectedFilmId = '',
}) async {
await pumpWidget(
MockIAPProductsProvider(
Expand All @@ -34,9 +34,9 @@ extension WidgetTesterCommonActions on WidgetTester {
child: MockIAPProviders(
equipmentProfiles: equipmentProfiles,
selectedEquipmentProfileId: selectedEquipmentProfileId,
availableFilms: availableFilms,
filmsInUse: filmsInUse,
selectedFilm: selectedFilm,
predefinedFilms: predefinedFilms,
customFilms: customFilms,
selectedFilmId: selectedFilmId,
child: const Application(),
),
),
Expand Down
17 changes: 12 additions & 5 deletions lib/application.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ import 'package:flutter/services.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:lightmeter/data/models/supported_locale.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/navigation/modal_route_args_parser.dart';
import 'package:lightmeter/navigation/routes.dart';
import 'package:lightmeter/platform_config.dart';
import 'package:lightmeter/providers/user_preferences_provider.dart';
import 'package:lightmeter/screens/film_edit/flow_film_edit.dart';
import 'package:lightmeter/screens/films/screen_films.dart';
import 'package:lightmeter/screens/lightmeter_pro/screen_lightmeter_pro.dart';
import 'package:lightmeter/screens/metering/flow_metering.dart';
import 'package:lightmeter/screens/settings/flow_settings.dart';
Expand Down Expand Up @@ -41,12 +45,15 @@ class Application extends StatelessWidget {
data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
child: child!,
),
initialRoute: "metering",
initialRoute: NavigationRoutes.meteringScreen.name,
routes: {
"metering": (_) => const ReleaseNotesFlow(child: MeteringFlow()),
"settings": (_) => const SettingsFlow(),
"lightmeterPro": (_) => LightmeterProScreen(),
"timer": (context) => TimerFlow(args: ModalRoute.of(context)!.settings.arguments! as TimerFlowArgs),
NavigationRoutes.meteringScreen.name: (_) => const ReleaseNotesFlow(child: MeteringFlow()),
NavigationRoutes.settingsScreen.name: (_) => const SettingsFlow(),
NavigationRoutes.filmsListScreen.name: (_) => const FilmsScreen(),
NavigationRoutes.filmAddScreen.name: (_) => const FilmEditFlow(args: FilmEditArgs()),
NavigationRoutes.filmEditScreen.name: (context) => FilmEditFlow(args: context.routeArgs<FilmEditArgs>()),
NavigationRoutes.proFeaturesScreen.name: (_) => LightmeterProScreen(),
NavigationRoutes.timerScreen.name: (context) => TimerFlow(args: context.routeArgs<TimerFlowArgs>()),
},
),
);
Expand Down
Loading

0 comments on commit c66381f

Please sign in to comment.