diff --git a/.vscode/launch.json b/.vscode/launch.json index 40dbfac..a222516 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -82,5 +82,20 @@ ], "program": "${workspaceFolder}/lib/main_dev.dart", }, + { + "name": "dev-simulator", + "request": "launch", + "type": "dart", + "flutterMode": "debug", + "args": [ + "--flavor", + "dev", + "--dart-define", + "cameraPreviewAspectRatio=240/320", + "--dart-define", + "cameraStubImage=assets/camera_stub_image.jpg" + ], + "program": "${workspaceFolder}/lib/main_dev.dart", + }, ], } \ No newline at end of file diff --git a/lib/application.dart b/lib/application.dart index 68dd44a..15eba3a 100644 --- a/lib/application.dart +++ b/lib/application.dart @@ -3,6 +3,7 @@ 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/platform_config.dart'; import 'package:lightmeter/providers/user_preferences_provider.dart'; import 'package:lightmeter/screens/metering/flow_metering.dart'; import 'package:lightmeter/screens/settings/flow_settings.dart'; @@ -17,13 +18,13 @@ class Application extends StatelessWidget { return AnnotatedRegion( value: SystemUiOverlayStyle( statusBarColor: Colors.transparent, - statusBarBrightness: - systemIconsBrightness == Brightness.light ? Brightness.dark : Brightness.light, + statusBarBrightness: systemIconsBrightness == Brightness.light ? Brightness.dark : Brightness.light, statusBarIconBrightness: systemIconsBrightness, systemNavigationBarColor: Colors.transparent, systemNavigationBarIconBrightness: systemIconsBrightness, ), child: MaterialApp( + debugShowCheckedModeBanner: !PlatformConfig.isTest, theme: theme, locale: Locale(UserPreferencesProvider.localeOf(context).intlName), localizationsDelegates: const [ diff --git a/lib/platform_config.dart b/lib/platform_config.dart index def5a80..d06223e 100644 --- a/lib/platform_config.dart +++ b/lib/platform_config.dart @@ -7,4 +7,6 @@ class PlatformConfig { } static String get cameraStubImage => const String.fromEnvironment('cameraStubImage'); + + static bool get isTest => cameraStubImage.isNotEmpty; } 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 2e6b051..3b5596b 100644 --- a/lib/screens/metering/components/camera_container/bloc_container_camera.dart +++ b/lib/screens/metering/components/camera_container/bloc_container_camera.dart @@ -4,7 +4,6 @@ import 'dart:io'; import 'dart:math' as math; import 'package:camera/camera.dart'; -import 'package:exif/exif.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -17,7 +16,9 @@ import 'package:lightmeter/screens/metering/components/camera_container/event_co import 'package:lightmeter/screens/metering/components/camera_container/models/camera_error_type.dart'; import 'package:lightmeter/screens/metering/components/camera_container/state_container_camera.dart'; import 'package:lightmeter/screens/metering/components/shared/ev_source_base/bloc_base_ev_source.dart'; -import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; +import 'package:lightmeter/utils/ev_from_bytes.dart'; + +part 'mock_bloc_container_camera.dart'; class CameraContainerBloc extends EvSourceBlocBase { final MeteringInteractor _meteringInteractor; @@ -213,33 +214,15 @@ 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 bytes = await file.readAsBytes(); + Directory(file.path).deleteSync(recursive: true); - late final Uint8List bytes; - if (PlatformConfig.cameraStubImage.isNotEmpty) { - bytes = (await rootBundle.load(PlatformConfig.cameraStubImage)).buffer.asUint8List(); - } else { - 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); - bytes = await file.readAsBytes(); - Directory(file.path).deleteSync(recursive: true); - } - - final tags = await readExifFromBytes(bytes); - final iso = double.tryParse("${tags["EXIF ISOSpeedRatings"]}"); - final apertureValueRatio = (tags["EXIF FNumber"]?.values as IfdRatios?)?.ratios.first; - final speedValueRatio = (tags["EXIF ExposureTime"]?.values as IfdRatios?)?.ratios.first; - if (iso == null || apertureValueRatio == null || speedValueRatio == null) { - log('Error parsing EXIF: ${tags.keys}'); - return null; - } - - final aperture = apertureValueRatio.numerator / apertureValueRatio.denominator; - final speed = speedValueRatio.numerator / speedValueRatio.denominator; - - return log2(math.pow(aperture, 2)) - log2(speed) - log2(iso / 100); + return await evFromImage(bytes); } catch (e) { log(e.toString()); return null; diff --git a/lib/screens/metering/components/camera_container/mock_bloc_container_camera.dart b/lib/screens/metering/components/camera_container/mock_bloc_container_camera.dart new file mode 100644 index 0000000..f915a52 --- /dev/null +++ b/lib/screens/metering/components/camera_container/mock_bloc_container_camera.dart @@ -0,0 +1,80 @@ +part of 'bloc_container_camera.dart'; + +class MockCameraContainerBloc extends CameraContainerBloc { + MockCameraContainerBloc( + super._meteringInteractor, + super.communicationBloc, + ); + + @override + Future _onRequestPermission(_, Emitter emit) async { + add(const InitializeEvent()); + } + + @override + Future _onOpenAppSettings(_, Emitter emit) async { + _meteringInteractor.openAppSettings(); + } + + @override + Future _onInitialize(_, Emitter emit) async { + emit(const CameraLoadingState()); + try { + _cameraController = CameraController( + const CameraDescription(name: '0', lensDirection: CameraLensDirection.back, sensorOrientation: 0), + ResolutionPreset.low, + enableAudio: false, + ); + + _zoomRange = const RangeValues(1, 6); + _currentZoom = _zoomRange!.start; + + _exposureOffsetRange = const RangeValues(-4, 4); + _exposureStep = 0.1; + _currentExposureOffset = 0.0; + + emit(CameraInitializedState(_cameraController!)); + + _emitActiveState(emit); + } catch (e) { + emit(const CameraErrorState(CameraErrorType.other)); + } + } + + @override + Future _onZoomChanged(ZoomChangedEvent event, Emitter emit) async { + if (event.value >= _zoomRange!.start && event.value <= _zoomRange!.end) { + _currentZoom = event.value; + _emitActiveState(emit); + } + } + + @override + Future _onExposureOffsetChanged(ExposureOffsetChangedEvent event, Emitter emit) async { + _currentExposureOffset = event.value; + _emitActiveState(emit); + } + + @override + Future _onExposureOffsetResetEvent(ExposureOffsetResetEvent event, Emitter emit) async { + _meteringInteractor.quickVibration(); + add(const ExposureOffsetChangedEvent(0)); + } + + @override + Future _onExposureSpotChangedEvent(ExposureSpotChangedEvent event, Emitter emit) async {} + + @override + bool get _canTakePhoto => PlatformConfig.cameraStubImage.isNotEmpty; + + @override + Future _takePhoto() async { + try { + final bytes = (await rootBundle.load(PlatformConfig.cameraStubImage)).buffer.asUint8List(); + return await evFromImage(bytes); + } catch (e) { + log(e.toString()); + return null; + } + } +} diff --git a/lib/screens/metering/components/camera_container/provider_container_camera.dart b/lib/screens/metering/components/camera_container/provider_container_camera.dart index 1d6d8c0..80be814 100644 --- a/lib/screens/metering/components/camera_container/provider_container_camera.dart +++ b/lib/screens/metering/components/camera_container/provider_container_camera.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:lightmeter/data/models/exposure_pair.dart'; +import 'package:lightmeter/platform_config.dart'; import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart'; import 'package:lightmeter/screens/metering/components/camera_container/bloc_container_camera.dart'; import 'package:lightmeter/screens/metering/components/camera_container/event_container_camera.dart'; @@ -30,12 +31,18 @@ class CameraContainerProvider extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocProvider( + return BlocProvider( lazy: false, - create: (context) => CameraContainerBloc( - MeteringInteractorProvider.of(context), - context.read(), - )..add(const RequestPermissionEvent()), + create: (context) => (PlatformConfig.cameraStubImage.isNotEmpty + ? MockCameraContainerBloc( + MeteringInteractorProvider.of(context), + context.read(), + ) + : CameraContainerBloc( + MeteringInteractorProvider.of(context), + context.read(), + )) + ..add(const RequestPermissionEvent()), child: CameraContainer( fastest: fastest, slowest: slowest, diff --git a/lib/utils/ev_from_bytes.dart b/lib/utils/ev_from_bytes.dart new file mode 100644 index 0000000..c7d12ea --- /dev/null +++ b/lib/utils/ev_from_bytes.dart @@ -0,0 +1,27 @@ +import 'dart:developer'; +import 'dart:math' as math; +import 'dart:typed_data'; + +import 'package:exif/exif.dart'; +import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; + +Future evFromImage(Uint8List bytes) async { + try { + final tags = await readExifFromBytes(bytes); + final iso = double.tryParse("${tags["EXIF ISOSpeedRatings"]}"); + final apertureValueRatio = (tags["EXIF FNumber"]?.values as IfdRatios?)?.ratios.first; + final speedValueRatio = (tags["EXIF ExposureTime"]?.values as IfdRatios?)?.ratios.first; + if (iso == null || apertureValueRatio == null || speedValueRatio == null) { + log('Error parsing EXIF: ${tags.keys}'); + return null; + } + + final aperture = apertureValueRatio.numerator / apertureValueRatio.denominator; + final speed = speedValueRatio.numerator / speedValueRatio.denominator; + + return log2(math.pow(aperture, 2)) - log2(speed) - log2(iso / 100); + } catch (e) { + log(e.toString()); + return null; + } +}