From b13acedebd693d3e9c82e036e143a5019c658e9d Mon Sep 17 00:00:00 2001 From: Vadim <44135514+vodemn@users.noreply.github.com> Date: Mon, 10 Jul 2023 17:49:34 +0200 Subject: [PATCH 01/12] ML-62 Interactors tests (#87) * removed redundant `UserPreferencesService` from `MeteringBloc` * wip * post-merge fixes * `MeasureEvent` tests * `MeasureEvent` tests revision * `MeasureEvent` tests added timeout * added stubs for other `MeteringBloc` events * rewritten `MeteringBloc` logic * wip * `IsoChangedEvent` tests * refined `IsoChangedEvent` tests * `NdChangedEvent` tests * `FilmChangedEvent` tests * `MeteringCommunicationBloc` tests * added test run to ci * overriden `==` for `MeasuredState` * `LuxMeteringEvent` tests * refined `LuxMeteringEvent` tests * rename * wip * wip * `InitializeEvent`/`DeinitializeEvent` tests * clamp minZoomLevel * fixed `MeteringCommunicationBloc` tests * wip * `ZoomChangedEvent` tests * `ExposureOffsetChangedEvent`/`ExposureOffsetResetEvent` tests * renamed test groups * added test coverage script * improved `CameraContainerBloc` test coverage * `EquipmentProfileChangedEvent` tests * verify response vibration * fixed running all tests * `MeteringCommunicationBloc` equality tests * `CameraContainerBloc` equality tests * removed generated code from coverage * `MeteringScreenLayoutFeature` tests * `SupportedLocale` tests * `Film` tests * `CaffeineService` tests * `UserPreferencesService` tests (wip) * `LightSensorService` tests (wip) * `migrateOldKeys()` tests * ignore currently unused getters & setters * gradle upgrade * `reset(sharedPreferences);` calls count * typo * `MeteringInteractor` tests * `SettingsInteractor` tests (wip) * `MeteringInteractor` tests (wip) * `SettingsInteractor` tests --- analysis_options.yaml | 1 - lib/interactors/metering_interactor.dart | 6 +- lib/interactors/settings_interactor.dart | 2 +- .../bloc_container_camera.dart | 2 +- lib/screens/metering/flow_metering.dart | 2 +- .../interactors/metering_interactor_test.dart | 274 ++++++++++++++++++ .../interactors/settings_interactor_test.dart | 205 +++++++++++++ .../camera/bloc_container_camera_test.dart | 12 +- 8 files changed, 492 insertions(+), 12 deletions(-) create mode 100644 test/interactors/metering_interactor_test.dart create mode 100644 test/interactors/settings_interactor_test.dart diff --git a/analysis_options.yaml b/analysis_options.yaml index c8e8d30..7f980c4 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,6 +1,5 @@ include: package:lint/strict.yaml - linter: rules: use_setters_to_change_properties: false diff --git a/lib/interactors/metering_interactor.dart b/lib/interactors/metering_interactor.dart index 34b03ec..7ea25ac 100644 --- a/lib/interactors/metering_interactor.dart +++ b/lib/interactors/metering_interactor.dart @@ -25,7 +25,9 @@ class MeteringInteractor { this._permissionsService, this._lightSensorService, this._volumeEventsService, - ) { + ); + + void initialize() { if (_userPreferencesService.caffeine) { _caffeineService.keepScreenOn(true); } @@ -69,7 +71,7 @@ class MeteringInteractor { .then((value) => value == PermissionStatus.granted); } - Future requestPermission() async { + Future requestCameraPermission() async { return _permissionsService .requestCameraPermission() .then((value) => value == PermissionStatus.granted); diff --git a/lib/interactors/settings_interactor.dart b/lib/interactors/settings_interactor.dart index 4eeb8b9..8f74dd2 100644 --- a/lib/interactors/settings_interactor.dart +++ b/lib/interactors/settings_interactor.dart @@ -41,8 +41,8 @@ class SettingsInteractor { VolumeAction get volumeAction => _userPreferencesService.volumeAction; Future setVolumeAction(VolumeAction value) async { - await _volumeEventsService.setVolumeHandling(value != VolumeAction.none); _userPreferencesService.volumeAction = value; + await _volumeEventsService.setVolumeHandling(value != VolumeAction.none); } bool get isHapticsEnabled => _userPreferencesService.haptics; diff --git a/lib/screens/metering/components/camera_container/bloc_container_camera.dart b/lib/screens/metering/components/camera_container/bloc_container_camera.dart index 7f00256..cafce13 100644 --- a/lib/screens/metering/components/camera_container/bloc_container_camera.dart +++ b/lib/screens/metering/components/camera_container/bloc_container_camera.dart @@ -92,7 +92,7 @@ class CameraContainerBloc extends EvSourceBlocBase _onRequestPermission(_, Emitter emit) async { - final hasPermission = await _meteringInteractor.requestPermission(); + final hasPermission = await _meteringInteractor.requestCameraPermission(); if (!hasPermission) { emit(const CameraErrorState(CameraErrorType.permissionNotGranted)); } else { diff --git a/lib/screens/metering/flow_metering.dart b/lib/screens/metering/flow_metering.dart index 780f537..1caef02 100644 --- a/lib/screens/metering/flow_metering.dart +++ b/lib/screens/metering/flow_metering.dart @@ -31,7 +31,7 @@ class _MeteringFlowState extends State { context.get(), context.get(), context.get(), - ), + )..initialize(), child: InheritedWidgetBase( data: VolumeKeysNotifier(context.get()), child: MultiBlocProvider( diff --git a/test/interactors/metering_interactor_test.dart b/test/interactors/metering_interactor_test.dart new file mode 100644 index 0000000..bd6a169 --- /dev/null +++ b/test/interactors/metering_interactor_test.dart @@ -0,0 +1,274 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:lightmeter/data/caffeine_service.dart'; +import 'package:lightmeter/data/haptics_service.dart'; +import 'package:lightmeter/data/light_sensor_service.dart'; +import 'package:lightmeter/data/models/film.dart'; +import 'package:lightmeter/data/models/volume_action.dart'; +import 'package:lightmeter/data/permissions_service.dart'; +import 'package:lightmeter/data/shared_prefs_service.dart'; +import 'package:lightmeter/data/volume_events_service.dart'; +import 'package:lightmeter/interactors/metering_interactor.dart'; +import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:permission_handler/permission_handler.dart'; + +class _MockUserPreferencesService extends Mock implements UserPreferencesService {} + +class _MockCaffeineService extends Mock implements CaffeineService {} + +class _MockHapticsService extends Mock implements HapticsService {} + +class _MockPermissionsService extends Mock implements PermissionsService {} + +class _MockLightSensorService extends Mock implements LightSensorService {} + +class _MockVolumeEventsService extends Mock implements VolumeEventsService {} + +void main() { + late _MockUserPreferencesService mockUserPreferencesService; + late _MockCaffeineService mockCaffeineService; + late _MockHapticsService mockHapticsService; + late _MockPermissionsService mockPermissionsService; + late _MockLightSensorService mockLightSensorService; + late _MockVolumeEventsService mockVolumeEventsService; + + late MeteringInteractor interactor; + + setUp(() { + mockUserPreferencesService = _MockUserPreferencesService(); + mockCaffeineService = _MockCaffeineService(); + mockHapticsService = _MockHapticsService(); + mockPermissionsService = _MockPermissionsService(); + mockLightSensorService = _MockLightSensorService(); + mockVolumeEventsService = _MockVolumeEventsService(); + + interactor = MeteringInteractor( + mockUserPreferencesService, + mockCaffeineService, + mockHapticsService, + mockPermissionsService, + mockLightSensorService, + mockVolumeEventsService, + ); + }); + + group( + 'Initalization', + () { + test('caffeine - true', () async { + when(() => mockUserPreferencesService.caffeine).thenReturn(true); + when(() => mockCaffeineService.keepScreenOn(true)).thenAnswer((_) async => true); + when(() => mockUserPreferencesService.volumeAction).thenReturn(VolumeAction.shutter); + when(() => mockVolumeEventsService.setVolumeHandling(true)).thenAnswer((_) async => true); + interactor.initialize(); + verify(() => mockUserPreferencesService.caffeine).called(1); + verify(() => mockCaffeineService.keepScreenOn(true)).called(1); + verify(() => mockVolumeEventsService.setVolumeHandling(true)).called(1); + }); + + test('caffeine - false', () async { + when(() => mockUserPreferencesService.caffeine).thenReturn(false); + when(() => mockCaffeineService.keepScreenOn(false)).thenAnswer((_) async => false); + when(() => mockUserPreferencesService.volumeAction).thenReturn(VolumeAction.shutter); + when(() => mockVolumeEventsService.setVolumeHandling(true)).thenAnswer((_) async => true); + interactor.initialize(); + verify(() => mockUserPreferencesService.caffeine).called(1); + verifyNever(() => mockCaffeineService.keepScreenOn(false)); + verify(() => mockVolumeEventsService.setVolumeHandling(true)).called(1); + }); + }, + ); + + group( + 'Calibration', + () { + test('cameraEvCalibration', () async { + when(() => mockUserPreferencesService.cameraEvCalibration).thenReturn(0.0); + expect(interactor.cameraEvCalibration, 0.0); + verify(() => mockUserPreferencesService.cameraEvCalibration).called(1); + }); + + test('lightSensorEvCalibration', () async { + when(() => mockUserPreferencesService.lightSensorEvCalibration).thenReturn(0.0); + expect(interactor.lightSensorEvCalibration, 0.0); + verify(() => mockUserPreferencesService.lightSensorEvCalibration).called(1); + }); + }, + ); + + group( + 'Equipment', + () { + test('iso - get', () async { + when(() => mockUserPreferencesService.iso).thenReturn(IsoValue.values.first); + expect(interactor.iso, IsoValue.values.first); + verify(() => mockUserPreferencesService.iso).called(1); + }); + + test('iso - set', () async { + when(() => mockUserPreferencesService.iso = IsoValue.values.first) + .thenReturn(IsoValue.values.first); + interactor.iso = IsoValue.values.first; + verify(() => mockUserPreferencesService.iso = IsoValue.values.first).called(1); + }); + + test('ndFilter - get', () async { + when(() => mockUserPreferencesService.ndFilter).thenReturn(NdValue.values.first); + expect(interactor.ndFilter, NdValue.values.first); + verify(() => mockUserPreferencesService.ndFilter).called(1); + }); + + test('ndFilter - set', () async { + when(() => mockUserPreferencesService.ndFilter = NdValue.values.first) + .thenReturn(NdValue.values.first); + interactor.ndFilter = NdValue.values.first; + verify(() => mockUserPreferencesService.ndFilter = NdValue.values.first).called(1); + }); + + test('film - get', () async { + when(() => mockUserPreferencesService.film).thenReturn(Film.values.first); + expect(interactor.film, Film.values.first); + verify(() => mockUserPreferencesService.film).called(1); + }); + + test('film - set', () async { + when(() => mockUserPreferencesService.film = Film.values.first) + .thenReturn(Film.values.first); + interactor.film = Film.values.first; + verify(() => mockUserPreferencesService.film = Film.values.first).called(1); + }); + }, + ); + + group( + 'Volume action', + () { + test('volumeAction - VolumeAction.shutter', () async { + when(() => mockUserPreferencesService.volumeAction).thenReturn(VolumeAction.shutter); + expect(interactor.volumeAction, VolumeAction.shutter); + verify(() => mockUserPreferencesService.volumeAction).called(1); + }); + + test('volumeAction - VolumeAction.none', () async { + when(() => mockUserPreferencesService.volumeAction).thenReturn(VolumeAction.none); + expect(interactor.volumeAction, VolumeAction.none); + verify(() => mockUserPreferencesService.volumeAction).called(1); + }); + }, + ); + + group( + 'Haptics', + () { + test('isHapticsEnabled', () async { + when(() => mockUserPreferencesService.haptics).thenReturn(true); + expect(interactor.isHapticsEnabled, true); + verify(() => mockUserPreferencesService.haptics).called(1); + }); + + test('quickVibration() - true', () async { + when(() => mockUserPreferencesService.haptics).thenReturn(true); + when(() => mockHapticsService.quickVibration()).thenAnswer((_) async {}); + interactor.quickVibration(); + verify(() => mockUserPreferencesService.haptics).called(1); + verify(() => mockHapticsService.quickVibration()).called(1); + }); + + test('quickVibration() - false', () async { + when(() => mockUserPreferencesService.haptics).thenReturn(false); + when(() => mockHapticsService.quickVibration()).thenAnswer((_) async {}); + interactor.quickVibration(); + verify(() => mockUserPreferencesService.haptics).called(1); + verifyNever(() => mockHapticsService.quickVibration()); + }); + + test('responseVibration() - true', () async { + when(() => mockUserPreferencesService.haptics).thenReturn(true); + when(() => mockHapticsService.responseVibration()).thenAnswer((_) async {}); + interactor.responseVibration(); + verify(() => mockUserPreferencesService.haptics).called(1); + verify(() => mockHapticsService.responseVibration()).called(1); + }); + + test('responseVibration() - false', () async { + when(() => mockUserPreferencesService.haptics).thenReturn(false); + when(() => mockHapticsService.responseVibration()).thenAnswer((_) async {}); + interactor.responseVibration(); + verify(() => mockUserPreferencesService.haptics).called(1); + verifyNever(() => mockHapticsService.responseVibration()); + }); + + test('errorVibration() - true', () async { + when(() => mockUserPreferencesService.haptics).thenReturn(true); + when(() => mockHapticsService.errorVibration()).thenAnswer((_) async {}); + interactor.errorVibration(); + verify(() => mockUserPreferencesService.haptics).called(1); + verify(() => mockHapticsService.errorVibration()).called(1); + }); + + test('errorVibration() - false', () async { + when(() => mockUserPreferencesService.haptics).thenReturn(false); + when(() => mockHapticsService.errorVibration()).thenAnswer((_) async {}); + interactor.errorVibration(); + verify(() => mockUserPreferencesService.haptics).called(1); + verifyNever(() => mockHapticsService.errorVibration()); + }); + }, + ); + + group( + 'Permissions', + () { + test('checkCameraPermission() - granted', () async { + when(() => mockPermissionsService.checkCameraPermission()) + .thenAnswer((_) async => PermissionStatus.granted); + expectLater(interactor.checkCameraPermission(), completion(true)); + verify(() => mockPermissionsService.checkCameraPermission()).called(1); + }); + + test('checkCameraPermission() - denied', () async { + when(() => mockPermissionsService.checkCameraPermission()) + .thenAnswer((_) async => PermissionStatus.denied); + expectLater(interactor.checkCameraPermission(), completion(false)); + verify(() => mockPermissionsService.checkCameraPermission()).called(1); + }); + + test('requestCameraPermission() - granted', () async { + when(() => mockPermissionsService.requestCameraPermission()) + .thenAnswer((_) async => PermissionStatus.granted); + expectLater(interactor.requestCameraPermission(), completion(true)); + verify(() => mockPermissionsService.requestCameraPermission()).called(1); + }); + + test('requestCameraPermission() - denied', () async { + when(() => mockPermissionsService.requestCameraPermission()) + .thenAnswer((_) async => PermissionStatus.denied); + expectLater(interactor.requestCameraPermission(), completion(false)); + verify(() => mockPermissionsService.requestCameraPermission()).called(1); + }); + }, + ); + + group( + 'Haptics', + () { + test('hasAmbientLightSensor() - true', () async { + when(() => mockLightSensorService.hasSensor()).thenAnswer((_) async => true); + expectLater(interactor.hasAmbientLightSensor(), completion(true)); + verify(() => mockLightSensorService.hasSensor()).called(1); + }); + + test('hasAmbientLightSensor() - false', () async { + when(() => mockLightSensorService.hasSensor()).thenAnswer((_) async => false); + expectLater(interactor.hasAmbientLightSensor(), completion(false)); + verify(() => mockLightSensorService.hasSensor()).called(1); + }); + + test('luxStream()', () async { + when(() => mockLightSensorService.luxStream()).thenAnswer((_) => const Stream.empty()); + expect(interactor.luxStream(), const Stream.empty()); + verify(() => mockLightSensorService.luxStream()).called(1); + }); + }, + ); +} diff --git a/test/interactors/settings_interactor_test.dart b/test/interactors/settings_interactor_test.dart new file mode 100644 index 0000000..03437dc --- /dev/null +++ b/test/interactors/settings_interactor_test.dart @@ -0,0 +1,205 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:lightmeter/data/caffeine_service.dart'; +import 'package:lightmeter/data/haptics_service.dart'; +import 'package:lightmeter/data/models/volume_action.dart'; +import 'package:lightmeter/data/shared_prefs_service.dart'; +import 'package:lightmeter/data/volume_events_service.dart'; +import 'package:lightmeter/interactors/settings_interactor.dart'; +import 'package:mocktail/mocktail.dart'; + +class _MockUserPreferencesService extends Mock implements UserPreferencesService {} + +class _MockCaffeineService extends Mock implements CaffeineService {} + +class _MockHapticsService extends Mock implements HapticsService {} + +class _MockVolumeEventsService extends Mock implements VolumeEventsService {} + +void main() { + late _MockUserPreferencesService mockUserPreferencesService; + late _MockCaffeineService mockCaffeineService; + late _MockHapticsService mockHapticsService; + late _MockVolumeEventsService mockVolumeEventsService; + + late SettingsInteractor interactor; + + setUp(() { + mockUserPreferencesService = _MockUserPreferencesService(); + mockCaffeineService = _MockCaffeineService(); + mockHapticsService = _MockHapticsService(); + mockVolumeEventsService = _MockVolumeEventsService(); + + interactor = SettingsInteractor( + mockUserPreferencesService, + mockCaffeineService, + mockHapticsService, + mockVolumeEventsService, + ); + }); + + group( + 'Calibration', + () { + test('cameraEvCalibration - get', () async { + when(() => mockUserPreferencesService.cameraEvCalibration).thenReturn(0.0); + expect(interactor.cameraEvCalibration, 0.0); + verify(() => mockUserPreferencesService.cameraEvCalibration).called(1); + }); + + test('cameraEvCalibration - set', () async { + when(() => mockUserPreferencesService.cameraEvCalibration = 0.0).thenReturn(0.0); + interactor.setCameraEvCalibration(0.0); + verify(() => mockUserPreferencesService.cameraEvCalibration = 0.0).called(1); + }); + + test('lightSensorEvCalibration - get', () async { + when(() => mockUserPreferencesService.lightSensorEvCalibration).thenReturn(0.0); + expect(interactor.lightSensorEvCalibration, 0.0); + verify(() => mockUserPreferencesService.lightSensorEvCalibration).called(1); + }); + + test('lightSensorEvCalibration - set', () async { + when(() => mockUserPreferencesService.lightSensorEvCalibration = 0.0).thenReturn(0.0); + interactor.setLightSensorEvCalibration(0.0); + verify(() => mockUserPreferencesService.lightSensorEvCalibration = 0.0).called(1); + }); + }, + ); + + group( + 'Caffeine', + () { + test('isCaffeineEnabled', () async { + when(() => mockUserPreferencesService.caffeine).thenReturn(true); + expect(interactor.isCaffeineEnabled, true); + verify(() => mockUserPreferencesService.caffeine).called(1); + }); + + test('enableCaffeine(true)', () async { + when(() => mockCaffeineService.keepScreenOn(true)).thenAnswer((_) async => true); + when(() => mockUserPreferencesService.caffeine = true).thenReturn(true); + await interactor.enableCaffeine(true); + verify(() => mockCaffeineService.keepScreenOn(true)).called(1); + verify(() => mockUserPreferencesService.caffeine = true).called(1); + }); + }, + ); + + group( + 'Volume action', + () { + test('disableVolumeHandling()', () async { + when(() => mockVolumeEventsService.setVolumeHandling(false)).thenAnswer((_) async => false); + expectLater(interactor.disableVolumeHandling(), isA>()); + verify(() => mockVolumeEventsService.setVolumeHandling(false)).called(1); + }); + + test('restoreVolumeHandling() - VolumeAction.shutter', () async { + when(() => mockUserPreferencesService.volumeAction).thenReturn(VolumeAction.shutter); + when(() => mockVolumeEventsService.setVolumeHandling(true)).thenAnswer((_) async => true); + expectLater(interactor.restoreVolumeHandling(), isA>()); + verify(() => mockUserPreferencesService.volumeAction).called(1); + verify(() => mockVolumeEventsService.setVolumeHandling(true)).called(1); + }); + + test('restoreVolumeHandling() - VolumeAction.none', () async { + when(() => mockUserPreferencesService.volumeAction).thenReturn(VolumeAction.none); + when(() => mockVolumeEventsService.setVolumeHandling(false)).thenAnswer((_) async => false); + expectLater(interactor.restoreVolumeHandling(), isA>()); + verify(() => mockUserPreferencesService.volumeAction).called(1); + verify(() => mockVolumeEventsService.setVolumeHandling(false)).called(1); + }); + + test('volumeAction - VolumeAction.shutter', () async { + when(() => mockUserPreferencesService.volumeAction).thenReturn(VolumeAction.shutter); + expect(interactor.volumeAction, VolumeAction.shutter); + verify(() => mockUserPreferencesService.volumeAction).called(1); + }); + + test('volumeAction - VolumeAction.none', () async { + when(() => mockUserPreferencesService.volumeAction).thenReturn(VolumeAction.none); + expect(interactor.volumeAction, VolumeAction.none); + verify(() => mockUserPreferencesService.volumeAction).called(1); + }); + + test('setVolumeAction(VolumeAction.shutter)', () async { + when(() => mockUserPreferencesService.volumeAction = VolumeAction.shutter) + .thenReturn(VolumeAction.shutter); + when(() => mockVolumeEventsService.setVolumeHandling(true)).thenAnswer((_) async => true); + expectLater(interactor.setVolumeAction(VolumeAction.shutter), isA>()); + verify(() => mockVolumeEventsService.setVolumeHandling(true)).called(1); + verify(() => mockUserPreferencesService.volumeAction = VolumeAction.shutter).called(1); + }); + + test('setVolumeAction(VolumeAction.none)', () async { + when(() => mockUserPreferencesService.volumeAction = VolumeAction.none) + .thenReturn(VolumeAction.none); + when(() => mockVolumeEventsService.setVolumeHandling(false)).thenAnswer((_) async => false); + expectLater(interactor.setVolumeAction(VolumeAction.none), isA>()); + verify(() => mockVolumeEventsService.setVolumeHandling(false)).called(1); + verify(() => mockUserPreferencesService.volumeAction = VolumeAction.none).called(1); + }); + }, + ); + + group( + 'Haptics', + () { + test('isHapticsEnabled', () async { + when(() => mockUserPreferencesService.haptics).thenReturn(true); + expect(interactor.isHapticsEnabled, true); + verify(() => mockUserPreferencesService.haptics).called(1); + }); + + test('enableHaptics() - true', () async { + when(() => mockUserPreferencesService.haptics = true).thenReturn(true); + when(() => mockUserPreferencesService.haptics).thenReturn(true); + when(() => mockHapticsService.quickVibration()).thenAnswer((_) async {}); + interactor.enableHaptics(true); + verify(() => mockUserPreferencesService.haptics).called(1); + verify(() => mockHapticsService.quickVibration()).called(1); + }); + + test('enableHaptics() - false', () async { + when(() => mockUserPreferencesService.haptics = false).thenReturn(false); + when(() => mockUserPreferencesService.haptics).thenReturn(false); + when(() => mockHapticsService.quickVibration()).thenAnswer((_) async {}); + interactor.enableHaptics(false); + verify(() => mockUserPreferencesService.haptics).called(1); + verifyNever(() => mockHapticsService.quickVibration()); + }); + + test('quickVibration() - true', () async { + when(() => mockUserPreferencesService.haptics).thenReturn(true); + when(() => mockHapticsService.quickVibration()).thenAnswer((_) async {}); + interactor.quickVibration(); + verify(() => mockUserPreferencesService.haptics).called(1); + verify(() => mockHapticsService.quickVibration()).called(1); + }); + + test('quickVibration() - false', () async { + when(() => mockUserPreferencesService.haptics).thenReturn(false); + when(() => mockHapticsService.quickVibration()).thenAnswer((_) async {}); + interactor.quickVibration(); + verify(() => mockUserPreferencesService.haptics).called(1); + verifyNever(() => mockHapticsService.quickVibration()); + }); + + test('responseVibration() - true', () async { + when(() => mockUserPreferencesService.haptics).thenReturn(true); + when(() => mockHapticsService.responseVibration()).thenAnswer((_) async {}); + interactor.responseVibration(); + verify(() => mockUserPreferencesService.haptics).called(1); + verify(() => mockHapticsService.responseVibration()).called(1); + }); + + test('responseVibration() - false', () async { + when(() => mockUserPreferencesService.haptics).thenReturn(false); + when(() => mockHapticsService.responseVibration()).thenAnswer((_) async {}); + interactor.responseVibration(); + verify(() => mockUserPreferencesService.haptics).called(1); + verifyNever(() => mockHapticsService.responseVibration()); + }); + }, + ); +} diff --git a/test/screens/metering/components/camera/bloc_container_camera_test.dart b/test/screens/metering/components/camera/bloc_container_camera_test.dart index fa08edf..d54c0a5 100644 --- a/test/screens/metering/components/camera/bloc_container_camera_test.dart +++ b/test/screens/metering/components/camera/bloc_container_camera_test.dart @@ -140,11 +140,11 @@ void main() { 'Request denied', build: () => bloc, setUp: () { - when(() => meteringInteractor.requestPermission()).thenAnswer((_) async => false); + when(() => meteringInteractor.requestCameraPermission()).thenAnswer((_) async => false); }, act: (bloc) => bloc.add(const RequestPermissionEvent()), verify: (_) { - verify(() => meteringInteractor.requestPermission()).called(1); + verify(() => meteringInteractor.requestCameraPermission()).called(1); }, expect: () => [ isA() @@ -156,12 +156,12 @@ void main() { 'Request granted -> check denied', build: () => bloc, setUp: () { - when(() => meteringInteractor.requestPermission()).thenAnswer((_) async => true); + when(() => meteringInteractor.requestCameraPermission()).thenAnswer((_) async => true); when(() => meteringInteractor.checkCameraPermission()).thenAnswer((_) async => false); }, act: (bloc) => bloc.add(const RequestPermissionEvent()), verify: (_) { - verify(() => meteringInteractor.requestPermission()).called(1); + verify(() => meteringInteractor.requestCameraPermission()).called(1); verify(() => meteringInteractor.checkCameraPermission()).called(1); }, expect: () => [ @@ -175,12 +175,12 @@ void main() { 'Request granted -> check granted', build: () => bloc, setUp: () { - when(() => meteringInteractor.requestPermission()).thenAnswer((_) async => true); + when(() => meteringInteractor.requestCameraPermission()).thenAnswer((_) async => true); when(() => meteringInteractor.checkCameraPermission()).thenAnswer((_) async => true); }, act: (bloc) => bloc.add(const RequestPermissionEvent()), verify: (_) { - verify(() => meteringInteractor.requestPermission()).called(1); + verify(() => meteringInteractor.requestCameraPermission()).called(1); verify(() => meteringInteractor.checkCameraPermission()).called(1); }, expect: () => initializedStateSequence, From dbf1f09eb68e1b0e9ec817a500f4695f186288bb Mon Sep 17 00:00:00 2001 From: Vadim Date: Mon, 24 Jul 2023 09:08:37 +0200 Subject: [PATCH 02/12] Renamed `EquipmentProfileData` -> `EquipmentProfile` --- lib/data/models/supported_locale.dart | 4 +++- lib/data/shared_prefs_service.dart | 4 ++-- lib/providers/equipment_profile_provider.dart | 21 +++++++++---------- .../widget_container_readings.dart | 2 +- lib/screens/metering/event_metering.dart | 2 +- lib/screens/metering/screen_metering.dart | 1 - .../widget_container_equipment_profile.dart | 10 ++++----- .../screen_equipment_profile.dart | 2 +- .../widget_list_tile_equipment_profiles.dart | 2 +- test/screens/metering/bloc_metering_test.dart | 2 +- 10 files changed, 25 insertions(+), 25 deletions(-) diff --git a/lib/data/models/supported_locale.dart b/lib/data/models/supported_locale.dart index e5f6dcb..445a30c 100644 --- a/lib/data/models/supported_locale.dart +++ b/lib/data/models/supported_locale.dart @@ -1,4 +1,4 @@ -enum SupportedLocale { en, fr, ru } +enum SupportedLocale { en, fr, ru, cn } extension SupportedLocaleExtension on SupportedLocale { String get intlName => toString().replaceAll("SupportedLocale.", ""); @@ -11,6 +11,8 @@ extension SupportedLocaleExtension on SupportedLocale { return 'Français'; case SupportedLocale.ru: return 'Русский'; + case SupportedLocale.cn: + return '<--->'; } } } diff --git a/lib/data/shared_prefs_service.dart b/lib/data/shared_prefs_service.dart index 76f9045..8105d5c 100644 --- a/lib/data/shared_prefs_service.dart +++ b/lib/data/shared_prefs_service.dart @@ -150,6 +150,6 @@ class UserPreferencesService { String get selectedEquipmentProfileId => ''; // coverage:ignore-line set selectedEquipmentProfileId(String id) {} // coverage:ignore-line - List get equipmentProfiles => []; // coverage:ignore-line - set equipmentProfiles(List profiles) {} // coverage:ignore-line + List get equipmentProfiles => []; // coverage:ignore-line + set equipmentProfiles(List profiles) {} // coverage:ignore-line } diff --git a/lib/providers/equipment_profile_provider.dart b/lib/providers/equipment_profile_provider.dart index c5ef82d..85c9377 100644 --- a/lib/providers/equipment_profile_provider.dart +++ b/lib/providers/equipment_profile_provider.dart @@ -4,8 +4,7 @@ import 'package:lightmeter/utils/inherited_generics.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; import 'package:uuid/uuid.dart'; -typedef EquipmentProfiles = List; -typedef EquipmentProfile = EquipmentProfileData; +typedef EquipmentProfiles = List; class EquipmentProfileProvider extends StatefulWidget { final Widget child; @@ -21,7 +20,7 @@ class EquipmentProfileProvider extends StatefulWidget { } class EquipmentProfileProviderState extends State { - static const EquipmentProfileData _defaultProfile = EquipmentProfileData( + static const EquipmentProfile _defaultProfile = EquipmentProfile( id: '', name: '', apertureValues: ApertureValue.values, @@ -30,10 +29,10 @@ class EquipmentProfileProviderState extends State { isoValues: IsoValue.values, ); - List _customProfiles = []; + List _customProfiles = []; String _selectedId = ''; - EquipmentProfileData get _selectedProfile => _customProfiles.firstWhere( + EquipmentProfile get _selectedProfile => _customProfiles.firstWhere( (e) => e.id == _selectedId, orElse: () { context.get().selectedEquipmentProfileId = _defaultProfile.id; @@ -50,16 +49,16 @@ class EquipmentProfileProviderState extends State { @override Widget build(BuildContext context) { - return InheritedWidgetBase>( + return InheritedWidgetBase>( data: [_defaultProfile] + _customProfiles, - child: InheritedWidgetBase( + child: InheritedWidgetBase( data: _selectedProfile, child: widget.child, ), ); } - void setProfile(EquipmentProfileData data) { + void setProfile(EquipmentProfile data) { setState(() { _selectedId = data.id; }); @@ -69,7 +68,7 @@ class EquipmentProfileProviderState extends State { /// Creates a default equipment profile void addProfile(String name) { _customProfiles.add( - EquipmentProfileData( + EquipmentProfile( id: const Uuid().v1(), name: name, apertureValues: ApertureValue.values, @@ -81,7 +80,7 @@ class EquipmentProfileProviderState extends State { _refreshSavedProfiles(); } - void updateProdile(EquipmentProfileData data) { + void updateProdile(EquipmentProfile data) { final indexToUpdate = _customProfiles.indexWhere((element) => element.id == data.id); if (indexToUpdate >= 0) { _customProfiles[indexToUpdate] = data; @@ -89,7 +88,7 @@ class EquipmentProfileProviderState extends State { } } - void deleteProfile(EquipmentProfileData data) { + void deleteProfile(EquipmentProfile data) { _customProfiles.remove(data); _refreshSavedProfiles(); } diff --git a/lib/screens/metering/components/shared/readings_container/widget_container_readings.dart b/lib/screens/metering/components/shared/readings_container/widget_container_readings.dart index 09f8bce..0893305 100644 --- a/lib/screens/metering/components/shared/readings_container/widget_container_readings.dart +++ b/lib/screens/metering/components/shared/readings_container/widget_container_readings.dart @@ -105,7 +105,7 @@ class _EquipmentProfilePicker extends StatelessWidget { @override Widget build(BuildContext context) { - return AnimatedDialogPicker( + return AnimatedDialogPicker( icon: Icons.camera, title: S.of(context).equipmentProfile, selectedValue: context.listen(), diff --git a/lib/screens/metering/event_metering.dart b/lib/screens/metering/event_metering.dart index 852b5e4..bf3a22d 100644 --- a/lib/screens/metering/event_metering.dart +++ b/lib/screens/metering/event_metering.dart @@ -6,7 +6,7 @@ sealed class MeteringEvent { } class EquipmentProfileChangedEvent extends MeteringEvent { - final EquipmentProfileData equipmentProfileData; + final EquipmentProfile equipmentProfileData; const EquipmentProfileChangedEvent(this.equipmentProfileData); } diff --git a/lib/screens/metering/screen_metering.dart b/lib/screens/metering/screen_metering.dart index 4fb1e85..bbfebfc 100644 --- a/lib/screens/metering/screen_metering.dart +++ b/lib/screens/metering/screen_metering.dart @@ -7,7 +7,6 @@ import 'package:lightmeter/data/models/exposure_pair.dart'; import 'package:lightmeter/data/models/film.dart'; import 'package:lightmeter/data/models/metering_screen_layout_config.dart'; import 'package:lightmeter/environment.dart'; -import 'package:lightmeter/providers/equipment_profile_provider.dart'; import 'package:lightmeter/providers/ev_source_type_provider.dart'; import 'package:lightmeter/screens/metering/bloc_metering.dart'; import 'package:lightmeter/screens/metering/components/bottom_controls/provider_bottom_controls.dart'; diff --git a/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart b/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart index b240611..1d9b7bf 100644 --- a/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart +++ b/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/components/equipment_profile_container/widget_container_equipment_profile.dart @@ -8,8 +8,8 @@ import 'package:lightmeter/screens/settings/components/metering/components/equip import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; class EquipmentProfileContainer extends StatefulWidget { - final EquipmentProfileData data; - final ValueChanged onUpdate; + final EquipmentProfile data; + final ValueChanged onUpdate; final VoidCallback onDelete; final VoidCallback onExpand; @@ -27,7 +27,7 @@ class EquipmentProfileContainer extends StatefulWidget { class EquipmentProfileContainerState extends State with TickerProviderStateMixin { - late EquipmentProfileData _equipmentData = EquipmentProfileData( + late EquipmentProfile _equipmentData = EquipmentProfile( id: widget.data.id, name: widget.data.name, apertureValues: widget.data.apertureValues, @@ -45,7 +45,7 @@ class EquipmentProfileContainerState extends State @override void didUpdateWidget(EquipmentProfileContainer oldWidget) { super.didUpdateWidget(oldWidget); - _equipmentData = EquipmentProfileData( + _equipmentData = EquipmentProfile( id: widget.data.id, name: widget.data.name, apertureValues: widget.data.apertureValues, @@ -195,7 +195,7 @@ class _AnimatedArrowButton extends AnimatedWidget { } class _AnimatedEquipmentListTiles extends AnimatedWidget { - final EquipmentProfileData equipmentData; + final EquipmentProfile equipmentData; final ValueChanged> onApertureValuesSelected; final ValueChanged> onIsoValuesSelecred; final ValueChanged> onNdValuesSelected; diff --git a/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart b/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart index 25ca59a..4872d79 100644 --- a/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart +++ b/lib/screens/settings/components/metering/components/equipment_profiles/components/equipment_profile_screen/screen_equipment_profile.dart @@ -84,7 +84,7 @@ class _EquipmentProfilesScreenState extends State { }); } - void _updateProfileAt(EquipmentProfileData data, int index) { + void _updateProfileAt(EquipmentProfile data, int index) { EquipmentProfileProvider.of(context).updateProdile(data); } diff --git a/lib/screens/settings/components/metering/components/equipment_profiles/widget_list_tile_equipment_profiles.dart b/lib/screens/settings/components/metering/components/equipment_profiles/widget_list_tile_equipment_profiles.dart index 13cb6f0..d1a6ef3 100644 --- a/lib/screens/settings/components/metering/components/equipment_profiles/widget_list_tile_equipment_profiles.dart +++ b/lib/screens/settings/components/metering/components/equipment_profiles/widget_list_tile_equipment_profiles.dart @@ -12,7 +12,7 @@ class EquipmentProfilesListTile extends StatelessWidget { leading: const Icon(Icons.camera), title: Text(S.of(context).equipmentProfiles), onTap: () { - Navigator.of(context).push( + Navigator.of(context).push( MaterialPageRoute(builder: (_) => const EquipmentProfilesScreen()), ); }, diff --git a/test/screens/metering/bloc_metering_test.dart b/test/screens/metering/bloc_metering_test.dart index 3593c91..45b9291 100644 --- a/test/screens/metering/bloc_metering_test.dart +++ b/test/screens/metering/bloc_metering_test.dart @@ -495,7 +495,7 @@ void main() { group( '`EquipmentProfileChangedEvent`', () { - final reducedProfile = EquipmentProfileData( + final reducedProfile = EquipmentProfile( id: '0', name: 'Reduced', apertureValues: ApertureValue.values, From bb9b023fa7c7d3e788f1b87cac39921de99c01e6 Mon Sep 17 00:00:00 2001 From: ScaredCube <74418739+ScaredCube@users.noreply.github.com> Date: Mon, 24 Jul 2023 15:35:30 +0800 Subject: [PATCH 03/12] Add Chinese language support (#91) * Add Chinese language support * Update intl_cn.arb * Fixed some bugs * Add Chinese language support * renamed `cn` to `zh` --------- Co-authored-by: Vadim <44135514+vodemn@users.noreply.github.com> Co-authored-by: Vadim --- lib/data/models/supported_locale.dart | 6 +- lib/l10n/intl_zh.arb | 88 +++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 3 deletions(-) create mode 100644 lib/l10n/intl_zh.arb diff --git a/lib/data/models/supported_locale.dart b/lib/data/models/supported_locale.dart index 445a30c..0d93a50 100644 --- a/lib/data/models/supported_locale.dart +++ b/lib/data/models/supported_locale.dart @@ -1,4 +1,4 @@ -enum SupportedLocale { en, fr, ru, cn } +enum SupportedLocale { en, fr, ru, zh } extension SupportedLocaleExtension on SupportedLocale { String get intlName => toString().replaceAll("SupportedLocale.", ""); @@ -11,8 +11,8 @@ extension SupportedLocaleExtension on SupportedLocale { return 'Français'; case SupportedLocale.ru: return 'Русский'; - case SupportedLocale.cn: - return '<--->'; + case SupportedLocale.zh: + return '简体中文'; } } } diff --git a/lib/l10n/intl_zh.arb b/lib/l10n/intl_zh.arb new file mode 100644 index 0000000..b011254 --- /dev/null +++ b/lib/l10n/intl_zh.arb @@ -0,0 +1,88 @@ +{ + "@@locale": "zh", + "fastestExposurePair": "最快曝光组合", + "slowestExposurePair": "最慢曝光组合", + "ev": "EV", + "evValue": "{value} EV", + "@evValue": { + "placeholders": { + "value": { + "type": "String" + } + } + }, + "iso": "ISO", + "filmSpeed": "胶片感光度", + "nd": "ND", + "ndFilterFactor": "ND 滤镜系数", + "noExposurePairs": "所选设置没有曝光配对", + "noCamerasDetected": "您的设备似乎没有连接到任何摄像头", + "noCameraPermission": "未获得摄像头权限", + "otherCameraError": "连接摄像头时发生错误", + "none": "无", + "cancel": "取消", + "select": "选择", + "save": "保存", + "settings": "设置", + "metering": "测量", + "fractionalStops": "分数位", + "showFractionalStops": "显示分数位", + "halfStops": "1/2", + "thirdStops": "1/3", + "calibration": "校准", + "calibrationMessage": "此应用测量读数的准确性完全取决于设备的硬件。因此,请考虑测试此应用并手动设置 EV 校准,以获得准确的测量结果。", + "calibrationMessageCameraOnly": "此应用程序测量读数的准确性完全取决于设备的后置摄像头。因此,请考虑测试此应用并手动设置 EV 校准,以获得准确的测量结果。", + "camera": "摄像头", + "lightSensor": "光传感器", + "meteringScreenLayout": "布局", + "meteringScreenLayoutHint": "隐藏不需要的元素,以免浪费曝光列表空间", + "meteringScreenFeatureExtremeExposurePairs": "最快 & 最慢曝光组合", + "meteringScreenFeatureFilmPicker": "胶片选择", + "film": "胶片", + "equipment": "设备", + "equipmentProfileName": "设备配置名称", + "equipmentProfileNameHint": "Praktica MTL5B", + "equipmentProfileAllValues": "全部", + "apertureValues": "光圈值", + "apertureValuesFilterDescription": "选择要显示的光圈值范围。这通常由您使用的镜头决定。", + "ndFilters": "ND 滤镜", + "ndFiltersFilterDescription": "选择要显示的 ND 滤镜。这些可能是您最常用的 ND 滤镜,也可能是适合您镜头的滤光镜。", + "shutterSpeedValues": "快门速度", + "shutterSpeedValuesFilterDescription": "选择要显示的快门速度范围。这通常由您使用的相机机身决定。", + "isoValues": "ISO", + "isoValuesFilterDescription": "选择要显示的 ISO。这些值可能是您最常用的值,也可能是相机支持的值。", + "equipmentProfile": "设备配置", + "equipmentProfiles": "设备配置", + "general": "通用", + "keepScreenOn": "保持屏幕常亮", + "haptics": "震动", + "volumeKeysAction": "音量键快门", + "language": "语言", + "chooseLanguage": "选择语言", + "theme": "主题", + "chooseTheme": "选择主题", + "themeLight": "亮色", + "themeDark": "暗色", + "themeSystemDefault": "跟随系统", + "dynamicColor": "动态颜色", + "primaryColor": "主题颜色", + "choosePrimaryColor": "选择主题颜色", + "about": "关于", + "sourceCode": "源代码", + "reportIssue": "报告问题", + "writeEmail": "Email", + "youDontHaveMailApp": "您没有安装任何邮件App。", + "copyEmail": "复制电子邮件", + "version": "Version", + "versionNumber": "{version} ({buildNumber})", + "@versionNumber": { + "placeholders": { + "version": { + "type": "String" + }, + "buildNumber": { + "type": "String" + } + } + } +} From dd5f551fd2d35ba2c9c192a969caff43894a675e Mon Sep 17 00:00:00 2001 From: vodemn Date: Mon, 24 Jul 2023 07:54:38 +0000 Subject: [PATCH 04/12] Version bump --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 222ac3c..cad178c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: lightmeter description: A new Flutter project. publish_to: "none" -version: 0.12.0+31 +version: 0.12.1+32 environment: sdk: ">=3.0.0 <4.0.0" From b02b50bac349f2fa1c23e15b9739040948198edc Mon Sep 17 00:00:00 2001 From: ScaredCube <74418739+ScaredCube@users.noreply.github.com> Date: Mon, 24 Jul 2023 18:16:35 +0800 Subject: [PATCH 05/12] Fixed Chinese translation (#93) --- lib/l10n/intl_zh.arb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/l10n/intl_zh.arb b/lib/l10n/intl_zh.arb index b011254..b931906 100644 --- a/lib/l10n/intl_zh.arb +++ b/lib/l10n/intl_zh.arb @@ -25,8 +25,8 @@ "save": "保存", "settings": "设置", "metering": "测量", - "fractionalStops": "分数位", - "showFractionalStops": "显示分数位", + "fractionalStops": "EV 步进值", + "showFractionalStops": "显示 EV 步进值", "halfStops": "1/2", "thirdStops": "1/3", "calibration": "校准", @@ -46,7 +46,7 @@ "apertureValues": "光圈值", "apertureValuesFilterDescription": "选择要显示的光圈值范围。这通常由您使用的镜头决定。", "ndFilters": "ND 滤镜", - "ndFiltersFilterDescription": "选择要显示的 ND 滤镜。这些可能是您最常用的 ND 滤镜,也可能是适合您镜头的滤光镜。", + "ndFiltersFilterDescription": "选择要显示的 ND 滤镜系数。这些可能是您最常用的 ND 滤镜,也可能是适合您镜头的滤光镜。", "shutterSpeedValues": "快门速度", "shutterSpeedValuesFilterDescription": "选择要显示的快门速度范围。这通常由您使用的相机机身决定。", "isoValues": "ISO", From 119e079554016718e53a6b87a86d1fce5d238506 Mon Sep 17 00:00:00 2001 From: vodemn Date: Mon, 24 Jul 2023 11:04:23 +0000 Subject: [PATCH 06/12] Version bump --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index cad178c..1f8ed64 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: lightmeter description: A new Flutter project. publish_to: "none" -version: 0.12.1+32 +version: 0.12.2+33 environment: sdk: ">=3.0.0 <4.0.0" From 40c670ad3033809ca996456c353998aa6cab672f Mon Sep 17 00:00:00 2001 From: Vadim <44135514+vodemn@users.noreply.github.com> Date: Tue, 25 Jul 2023 17:31:01 +0200 Subject: [PATCH 07/12] Updated README Build section (#94) * Update README.md * Set exact Flutter version for workflows * Added stub `DefaultFirebaseOptions` * Fixed `rm` * Removed `rm` * Update .gitignore * Added readable name to ci workflow * Build -> Development * Update ci.yml --- .github/workflows/cd_dev.yml | 1 + .github/workflows/cd_prod.yml | 1 + .github/workflows/ci.yml | 30 ++++------------ README.md | 32 ++++++++++++++--- lib/firebase_options.dart | 68 +++++++++++++++++++++++++++++++++++ lib/main_prod.dart | 8 ++++- 6 files changed, 112 insertions(+), 28 deletions(-) create mode 100644 lib/firebase_options.dart diff --git a/.github/workflows/cd_dev.yml b/.github/workflows/cd_dev.yml index d90d0fb..e0e6a25 100644 --- a/.github/workflows/cd_dev.yml +++ b/.github/workflows/cd_dev.yml @@ -71,6 +71,7 @@ jobs: uses: subosito/flutter-action@v2 with: channel: "stable" + flutter-version: '3.10.0' - name: Prepare flutter project run: | diff --git a/.github/workflows/cd_prod.yml b/.github/workflows/cd_prod.yml index 9ff3d6e..9e45644 100644 --- a/.github/workflows/cd_prod.yml +++ b/.github/workflows/cd_prod.yml @@ -73,6 +73,7 @@ jobs: uses: subosito/flutter-action@v2 with: channel: "stable" + flutter-version: '3.10.0' - name: Prepare flutter project run: | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e8956f6..9022170 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,18 +12,12 @@ on: branches: ["main"] jobs: - build: + analyze_and_test: + name: Analyze & test runs-on: macos-11 timeout-minutes: 10 steps: - - uses: shaunco/ssh-agent@git-repo-mapping - with: - ssh-private-key: | - ${{ secrets.M3_LIGHTMETER_IAP_KEY }} - repo-mappings: | - github.com/vodemn/m3_lightmeter_iap - - uses: actions/checkout@v3 with: submodules: recursive @@ -31,23 +25,13 @@ jobs: - uses: subosito/flutter-action@v2 with: channel: "stable" + flutter-version: '3.10.0' - - name: Check flutter version - run: flutter --version - - - name: Install dependencies - run: flutter pub get - - - name: Generate intl - run: flutter pub run intl_utils:generate - - - name: Restore firebase_options.dart - env: - FIREBASE_OPTIONS: ${{ secrets.FIREBASE_OPTIONS }} + - name: Prepare flutter project run: | - FIREBASE_OPTIONS_PATH=$RUNNER_TEMP/firebase_options.dart - echo -n "$FIREBASE_OPTIONS" | base64 --decode --output $FIREBASE_OPTIONS_PATH - cp $FIREBASE_OPTIONS_PATH ./lib + flutter --version + flutter pub get + flutter pub run intl_utils:generate - name: Analyze project source run: flutter analyze lib --fatal-infos diff --git a/README.md b/README.md index 0b4dae8..2fef498 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ - [Table of contents](#table-of-contents) - [Backstory](#backstory) - [Screenshots](#screenshots) -- [Build](#build) +- [Development](#development) - [Contribution](#contribution) - [iOS Limitations](#ios-limitations) @@ -27,22 +27,46 @@ Without further delay behold my new Lightmeter app inspired by Material You (a.k

-# Build +# Development -As part of this project is private, you will be able to run this app from the _main_dev.dart_ file (i.e. --flavor dev). Also to avoid fatal errors the _main_prod.dart_ file is excluded from analysis. +### 1. Install Flutter + +To build this app you need to install Flutter 3.10.0 stable. [How to install](https://docs.flutter.dev/get-started/install). + +### 2. (Optional) Install Firebase + +Out of the box Firebase Crashlytics won't work. If you want to add Crashlytics to your local build please follow [this guide](https://firebase.google.com/docs/flutter/setup). + +### 3. Get packages + +Fetch all the neccessary dependencies and generate translation files by running the following commands: +```console +flutter pub get +flutter pub run intl_utils:generate +``` + +### 4. Build + +You can build an apk by running the following command from the root of the repository: +```console +flutter build apk --release --flavor $FLAVOR --dart-define cameraPreviewAspectRatio=2/3 -t lib/main_$FLAVOR.dart +``` +Just replace `$FLAVOR` with `dev` or `prod`. # Contribution To report a bug or suggest a new feature open a new [issue](https://github.com/vodemn/m3_lightmeter/issues). -In case you want to help develop this project you need to follow this [style guide](doc/style_guide.md). +In case you want to help develop this project feel free to open a Pull Request, but you need to follow this [style guide](doc/style_guide.md). # iOS Limitations A list of features, that Android version of the app has and that iOS does not. ## Incident light metering + Apple does not provide API for reading Lux stream form the ambient light sensor. Lux can be calculated based on front camera image stream, but this would be a reflected light. So there is no way incident light metering can be implemented on iOS. ## Volume buttons action + This can be [implemented](https://stackoverflow.com/questions/70161271/ios-override-hardware-volume-buttons-same-as-zello) but the app will be rejected due to [2.5.9](https://developer.apple.com/app-store/review/guidelines/#software-requirements) \ No newline at end of file diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart new file mode 100644 index 0000000..a8a32c7 --- /dev/null +++ b/lib/firebase_options.dart @@ -0,0 +1,68 @@ +// File generated by FlutterFire CLI. +// ignore_for_file: lines_longer_than_80_chars, avoid_classes_with_only_static_members +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' show defaultTargetPlatform, kIsWeb, TargetPlatform; + +/// Default [FirebaseOptions] for use with your Firebase apps. +/// +/// Example: +/// ```dart +/// import 'firebase_options.dart'; +/// // ... +/// await Firebase.initializeApp( +/// options: DefaultFirebaseOptions.currentPlatform, +/// ); +/// ``` +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for web - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + case TargetPlatform.iOS: + return ios; + case TargetPlatform.macOS: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for macos - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.windows: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for windows - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.linux: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for linux - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static const FirebaseOptions android = FirebaseOptions( + apiKey: '', + appId: '', + messagingSenderId: '', + projectId: '', + storageBucket: '', + ); + + static const FirebaseOptions ios = FirebaseOptions( + apiKey: '', + appId: '', + messagingSenderId: '', + projectId: '', + storageBucket: '', + iosClientId: '', + iosBundleId: '', + ); +} diff --git a/lib/main_prod.dart b/lib/main_prod.dart index a47d421..fc1700a 100644 --- a/lib/main_prod.dart +++ b/lib/main_prod.dart @@ -1,3 +1,5 @@ +import 'dart:developer'; + import 'package:flutter/material.dart'; import 'package:lightmeter/application.dart'; import 'package:lightmeter/environment.dart'; @@ -5,6 +7,10 @@ import 'package:lightmeter/firebase.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); - await initializeFirebase(); + try { + await initializeFirebase(); + } catch (e) { + log(e.toString()); + } runApp(const Application(Environment.prod())); } From 6a9036ce5eed82cd312334ff698411bb85be6333 Mon Sep 17 00:00:00 2001 From: Vadim Date: Tue, 1 Aug 2023 12:58:43 +0200 Subject: [PATCH 08/12] Camera is taking too long to take a picture --- .../components/camera_container/bloc_container_camera.dart | 1 + pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/screens/metering/components/camera_container/bloc_container_camera.dart b/lib/screens/metering/components/camera_container/bloc_container_camera.dart index cafce13..fee9899 100644 --- a/lib/screens/metering/components/camera_container/bloc_container_camera.dart +++ b/lib/screens/metering/components/camera_container/bloc_container_camera.dart @@ -129,6 +129,7 @@ class CameraContainerBloc extends EvSourceBlocBase([ _cameraController!.getMinZoomLevel(), diff --git a/pubspec.yaml b/pubspec.yaml index 1f8ed64..0015829 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -9,7 +9,7 @@ environment: dependencies: app_settings: 4.2.0 bloc_concurrency: 0.2.2 - camera: 0.10.5 + camera: 0.10.5+2 clipboard: 0.1.3 dynamic_color: 1.6.5 exif: 3.1.4 From 50c2460f16494b69fc60a9c2eea632ca2273cdd8 Mon Sep 17 00:00:00 2001 From: vodemn Date: Tue, 1 Aug 2023 11:08:55 +0000 Subject: [PATCH 09/12] Version bump --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 0015829..e2c7b6d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: lightmeter description: A new Flutter project. publish_to: "none" -version: 0.12.2+33 +version: 0.12.3+34 environment: sdk: ">=3.0.0 <4.0.0" From c12cfb16976b69c19affc4f802852c936741f845 Mon Sep 17 00:00:00 2001 From: Vadim Date: Thu, 3 Aug 2023 22:46:01 +0200 Subject: [PATCH 10/12] Lock & release focus when taking a picture --- .../components/camera_container/bloc_container_camera.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/screens/metering/components/camera_container/bloc_container_camera.dart b/lib/screens/metering/components/camera_container/bloc_container_camera.dart index fee9899..9c8658d 100644 --- a/lib/screens/metering/components/camera_container/bloc_container_camera.dart +++ b/lib/screens/metering/components/camera_container/bloc_container_camera.dart @@ -129,7 +129,6 @@ class CameraContainerBloc extends EvSourceBlocBase([ _cameraController!.getMinZoomLevel(), @@ -206,7 +205,13 @@ class CameraContainerBloc extends EvSourceBlocBase _takePhoto() async { try { + // https://github.com/flutter/flutter/issues/84957#issuecomment-1661155095 + await _cameraController!.setFocusMode(FocusMode.locked); + await _cameraController!.setExposureMode(ExposureMode.locked); final file = await _cameraController!.takePicture(); + await _cameraController!.setFocusMode(FocusMode.auto); + await _cameraController!.setExposureMode(ExposureMode.auto); + final Uint8List bytes = await file.readAsBytes(); Directory(file.path).deleteSync(recursive: true); From 6e1aaf5acf0d707d6853048d07cda87a037457c2 Mon Sep 17 00:00:00 2001 From: vodemn Date: Thu, 3 Aug 2023 20:54:33 +0000 Subject: [PATCH 11/12] Version bump --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index e2c7b6d..315e72f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: lightmeter description: A new Flutter project. publish_to: "none" -version: 0.12.3+34 +version: 0.12.4+35 environment: sdk: ">=3.0.0 <4.0.0" From 8a71c8db13dbbd6b380b36091ec8d27bd15bd0d9 Mon Sep 17 00:00:00 2001 From: Vadim Date: Fri, 4 Aug 2023 16:17:40 +0200 Subject: [PATCH 12/12] Added switch animations to `MeteringScreen` --- lib/res/dimens.dart | 1 + .../widget_placeholder_camera_view.dart | 5 +- .../widget_container_camera.dart | 26 ++-- .../widget_list_exposure_pairs.dart | 112 +++++++++--------- .../widget_container_reading_value.dart | 15 ++- 5 files changed, 82 insertions(+), 77 deletions(-) diff --git a/lib/res/dimens.dart b/lib/res/dimens.dart index 1eefdc1..a1bb780 100644 --- a/lib/res/dimens.dart +++ b/lib/res/dimens.dart @@ -25,6 +25,7 @@ class Dimens { static const Duration durationM = Duration(milliseconds: 200); static const Duration durationML = Duration(milliseconds: 250); static const Duration durationL = Duration(milliseconds: 300); + static const Duration switchDuration = Duration(milliseconds: 100); static const double enabledOpacity = 1.0; static const double disabledOpacity = 0.38; diff --git a/lib/screens/metering/components/camera_container/components/camera_view_placeholder/widget_placeholder_camera_view.dart b/lib/screens/metering/components/camera_container/components/camera_view_placeholder/widget_placeholder_camera_view.dart index 058da74..a2914d2 100644 --- a/lib/screens/metering/components/camera_container/components/camera_view_placeholder/widget_placeholder_camera_view.dart +++ b/lib/screens/metering/components/camera_container/components/camera_view_placeholder/widget_placeholder_camera_view.dart @@ -10,10 +10,9 @@ class CameraViewPlaceholder extends StatelessWidget { @override Widget build(BuildContext context) { return Card( + color: error != null ? null : Colors.black, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(Dimens.borderRadiusM)), - child: Center( - child: error != null ? const Icon(Icons.no_photography) : const CircularProgressIndicator(), - ), + child: Center(child: error != null ? const Icon(Icons.no_photography) : null), ); } } diff --git a/lib/screens/metering/components/camera_container/widget_container_camera.dart b/lib/screens/metering/components/camera_container/widget_container_camera.dart index 2f8ef45..7ac43eb 100644 --- a/lib/screens/metering/components/camera_container/widget_container_camera.dart +++ b/lib/screens/metering/components/camera_container/widget_container_camera.dart @@ -110,17 +110,17 @@ class _CameraViewBuilder extends StatelessWidget { return AspectRatio( aspectRatio: PlatformConfig.cameraPreviewAspectRatio, child: BlocBuilder( - buildWhen: (previous, current) => - current is CameraLoadingState || - current is CameraInitializedState || - current is CameraErrorState, - builder: (context, state) { - if (state is CameraInitializedState) { - return Center(child: CameraView(controller: state.controller)); - } else { - return CameraViewPlaceholder(error: state is CameraErrorState ? state.error : null); - } - }, + buildWhen: (previous, current) => current is! CameraActiveState, + builder: (context, state) => Center( + child: AnimatedSwitcher( + duration: Dimens.durationM, + child: switch (state) { + CameraInitializedState() => CameraView(controller: state.controller), + CameraErrorState() => CameraViewPlaceholder(error: state.error), + _ => const CameraViewPlaceholder(error: null), + }, + ), + ), ), ); } @@ -161,11 +161,11 @@ class _CameraControlsBuilder extends StatelessWidget { }, ); } else { - child = const SizedBox.shrink(); + child = const Column(children: [Expanded(child: SizedBox.shrink())],); } return AnimatedSwitcher( - duration: Dimens.durationS, + duration: Dimens.switchDuration, child: child, ); }, diff --git a/lib/screens/metering/components/shared/exposure_pairs_list/widget_list_exposure_pairs.dart b/lib/screens/metering/components/shared/exposure_pairs_list/widget_list_exposure_pairs.dart index f2496b2..71b293e 100644 --- a/lib/screens/metering/components/shared/exposure_pairs_list/widget_list_exposure_pairs.dart +++ b/lib/screens/metering/components/shared/exposure_pairs_list/widget_list_exposure_pairs.dart @@ -12,70 +12,72 @@ class ExposurePairsList extends StatelessWidget { @override Widget build(BuildContext context) { - if (exposurePairs.isEmpty) { - return const EmptyExposurePairsList(); - } - return Stack( - alignment: Alignment.center, - children: [ - Positioned.fill( - child: ListView.builder( - key: ValueKey(exposurePairs.hashCode), - padding: const EdgeInsets.symmetric(vertical: Dimens.paddingL), - itemCount: exposurePairs.length, - itemBuilder: (_, index) => Stack( + return AnimatedSwitcher( + duration: Dimens.switchDuration, + child: exposurePairs.isEmpty + ? const EmptyExposurePairsList() + : Stack( alignment: Alignment.center, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: Align( - alignment: Alignment.centerLeft, - child: ExposurePairsListItem( - exposurePairs[index].aperture, - tickOnTheLeft: false, + Positioned.fill( + child: ListView.builder( + key: ValueKey(exposurePairs.hashCode), + padding: const EdgeInsets.symmetric(vertical: Dimens.paddingL), + itemCount: exposurePairs.length, + itemBuilder: (_, index) => Stack( + alignment: Alignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Align( + alignment: Alignment.centerLeft, + child: ExposurePairsListItem( + exposurePairs[index].aperture, + tickOnTheLeft: false, + ), + ), + ), + Expanded( + child: Align( + alignment: Alignment.centerLeft, + child: ExposurePairsListItem( + exposurePairs[index].shutterSpeed, + tickOnTheLeft: true, + ), + ), + ), + ], ), - ), - ), - Expanded( - child: Align( - alignment: Alignment.centerLeft, - child: ExposurePairsListItem( - exposurePairs[index].shutterSpeed, - tickOnTheLeft: true, + Positioned( + top: 0, + bottom: 0, + child: LayoutBuilder( + builder: (context, constraints) => Align( + alignment: index == 0 + ? Alignment.bottomCenter + : (index == exposurePairs.length - 1 + ? Alignment.topCenter + : Alignment.center), + child: SizedBox( + height: index == 0 || index == exposurePairs.length - 1 + ? constraints.maxHeight / 2 + : constraints.maxHeight, + child: ColoredBox( + color: Theme.of(context).colorScheme.onBackground, + child: const SizedBox(width: 1), + ), + ), + ), + ), ), - ), - ), - ], - ), - Positioned( - top: 0, - bottom: 0, - child: LayoutBuilder( - builder: (context, constraints) => Align( - alignment: index == 0 - ? Alignment.bottomCenter - : (index == exposurePairs.length - 1 - ? Alignment.topCenter - : Alignment.center), - child: SizedBox( - height: index == 0 || index == exposurePairs.length - 1 - ? constraints.maxHeight / 2 - : constraints.maxHeight, - child: ColoredBox( - color: Theme.of(context).colorScheme.onBackground, - child: const SizedBox(width: 1), - ), - ), + ], ), ), ), ], ), - ), - ), - ], ); } } diff --git a/lib/screens/metering/components/shared/readings_container/components/reading_value_container/widget_container_reading_value.dart b/lib/screens/metering/components/shared/readings_container/components/reading_value_container/widget_container_reading_value.dart index b40f666..3254456 100644 --- a/lib/screens/metering/components/shared/readings_container/components/reading_value_container/widget_container_reading_value.dart +++ b/lib/screens/metering/components/shared/readings_container/components/reading_value_container/widget_container_reading_value.dart @@ -72,12 +72,15 @@ class _ReadingValueBuilder extends StatelessWidget { softWrap: false, ), const SizedBox(height: Dimens.grid4), - Text( - reading.value, - style: textTheme.titleMedium?.copyWith(color: textColor), - maxLines: 1, - overflow: TextOverflow.ellipsis, - softWrap: false, + AnimatedSwitcher( + duration: Dimens.switchDuration, + child: Text( + reading.value, + style: textTheme.titleMedium?.copyWith(color: textColor), + maxLines: 1, + overflow: TextOverflow.ellipsis, + softWrap: false, + ), ) ], );