mirror of
https://github.com/vodemn/m3_lightmeter.git
synced 2024-11-22 15:30:59 +00:00
implemented MockCameraContainerBloc
to stub camera on simulator
This commit is contained in:
parent
55b0e52d7f
commit
184ef3a916
7 changed files with 150 additions and 35 deletions
15
.vscode/launch.json
vendored
15
.vscode/launch.json
vendored
|
@ -82,5 +82,20 @@
|
||||||
],
|
],
|
||||||
"program": "${workspaceFolder}/lib/main_dev.dart",
|
"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",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
|
@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
import 'package:lightmeter/data/models/supported_locale.dart';
|
import 'package:lightmeter/data/models/supported_locale.dart';
|
||||||
import 'package:lightmeter/generated/l10n.dart';
|
import 'package:lightmeter/generated/l10n.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/metering/flow_metering.dart';
|
import 'package:lightmeter/screens/metering/flow_metering.dart';
|
||||||
import 'package:lightmeter/screens/settings/flow_settings.dart';
|
import 'package:lightmeter/screens/settings/flow_settings.dart';
|
||||||
|
@ -17,13 +18,13 @@ class Application extends StatelessWidget {
|
||||||
return AnnotatedRegion(
|
return AnnotatedRegion(
|
||||||
value: SystemUiOverlayStyle(
|
value: SystemUiOverlayStyle(
|
||||||
statusBarColor: Colors.transparent,
|
statusBarColor: Colors.transparent,
|
||||||
statusBarBrightness:
|
statusBarBrightness: systemIconsBrightness == Brightness.light ? Brightness.dark : Brightness.light,
|
||||||
systemIconsBrightness == Brightness.light ? Brightness.dark : Brightness.light,
|
|
||||||
statusBarIconBrightness: systemIconsBrightness,
|
statusBarIconBrightness: systemIconsBrightness,
|
||||||
systemNavigationBarColor: Colors.transparent,
|
systemNavigationBarColor: Colors.transparent,
|
||||||
systemNavigationBarIconBrightness: systemIconsBrightness,
|
systemNavigationBarIconBrightness: systemIconsBrightness,
|
||||||
),
|
),
|
||||||
child: MaterialApp(
|
child: MaterialApp(
|
||||||
|
debugShowCheckedModeBanner: !PlatformConfig.isTest,
|
||||||
theme: theme,
|
theme: theme,
|
||||||
locale: Locale(UserPreferencesProvider.localeOf(context).intlName),
|
locale: Locale(UserPreferencesProvider.localeOf(context).intlName),
|
||||||
localizationsDelegates: const [
|
localizationsDelegates: const [
|
||||||
|
|
|
@ -7,4 +7,6 @@ class PlatformConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
static String get cameraStubImage => const String.fromEnvironment('cameraStubImage');
|
static String get cameraStubImage => const String.fromEnvironment('cameraStubImage');
|
||||||
|
|
||||||
|
static bool get isTest => cameraStubImage.isNotEmpty;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ import 'dart:io';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:camera/camera.dart';
|
import 'package:camera/camera.dart';
|
||||||
import 'package:exif/exif.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.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/models/camera_error_type.dart';
|
||||||
import 'package:lightmeter/screens/metering/components/camera_container/state_container_camera.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: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<CameraContainerEvent, CameraContainerState> {
|
class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraContainerState> {
|
||||||
final MeteringInteractor _meteringInteractor;
|
final MeteringInteractor _meteringInteractor;
|
||||||
|
@ -213,33 +214,15 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
|
||||||
Future<double?> _takePhoto() async {
|
Future<double?> _takePhoto() async {
|
||||||
try {
|
try {
|
||||||
// https://github.com/flutter/flutter/issues/84957#issuecomment-1661155095
|
// 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;
|
return await evFromImage(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);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log(e.toString());
|
log(e.toString());
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
part of 'bloc_container_camera.dart';
|
||||||
|
|
||||||
|
class MockCameraContainerBloc extends CameraContainerBloc {
|
||||||
|
MockCameraContainerBloc(
|
||||||
|
super._meteringInteractor,
|
||||||
|
super.communicationBloc,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> _onRequestPermission(_, Emitter emit) async {
|
||||||
|
add(const InitializeEvent());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> _onOpenAppSettings(_, Emitter emit) async {
|
||||||
|
_meteringInteractor.openAppSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> _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<void> _onZoomChanged(ZoomChangedEvent event, Emitter emit) async {
|
||||||
|
if (event.value >= _zoomRange!.start && event.value <= _zoomRange!.end) {
|
||||||
|
_currentZoom = event.value;
|
||||||
|
_emitActiveState(emit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> _onExposureOffsetChanged(ExposureOffsetChangedEvent event, Emitter emit) async {
|
||||||
|
_currentExposureOffset = event.value;
|
||||||
|
_emitActiveState(emit);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> _onExposureOffsetResetEvent(ExposureOffsetResetEvent event, Emitter emit) async {
|
||||||
|
_meteringInteractor.quickVibration();
|
||||||
|
add(const ExposureOffsetChangedEvent(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> _onExposureSpotChangedEvent(ExposureSpotChangedEvent event, Emitter emit) async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get _canTakePhoto => PlatformConfig.cameraStubImage.isNotEmpty;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<double?> _takePhoto() async {
|
||||||
|
try {
|
||||||
|
final bytes = (await rootBundle.load(PlatformConfig.cameraStubImage)).buffer.asUint8List();
|
||||||
|
return await evFromImage(bytes);
|
||||||
|
} catch (e) {
|
||||||
|
log(e.toString());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:lightmeter/data/models/exposure_pair.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/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/bloc_container_camera.dart';
|
||||||
import 'package:lightmeter/screens/metering/components/camera_container/event_container_camera.dart';
|
import 'package:lightmeter/screens/metering/components/camera_container/event_container_camera.dart';
|
||||||
|
@ -30,12 +31,18 @@ class CameraContainerProvider extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider(
|
return BlocProvider<CameraContainerBloc>(
|
||||||
lazy: false,
|
lazy: false,
|
||||||
create: (context) => CameraContainerBloc(
|
create: (context) => (PlatformConfig.cameraStubImage.isNotEmpty
|
||||||
MeteringInteractorProvider.of(context),
|
? MockCameraContainerBloc(
|
||||||
context.read<MeteringCommunicationBloc>(),
|
MeteringInteractorProvider.of(context),
|
||||||
)..add(const RequestPermissionEvent()),
|
context.read<MeteringCommunicationBloc>(),
|
||||||
|
)
|
||||||
|
: CameraContainerBloc(
|
||||||
|
MeteringInteractorProvider.of(context),
|
||||||
|
context.read<MeteringCommunicationBloc>(),
|
||||||
|
))
|
||||||
|
..add(const RequestPermissionEvent()),
|
||||||
child: CameraContainer(
|
child: CameraContainer(
|
||||||
fastest: fastest,
|
fastest: fastest,
|
||||||
slowest: slowest,
|
slowest: slowest,
|
||||||
|
|
27
lib/utils/ev_from_bytes.dart
Normal file
27
lib/utils/ev_from_bytes.dart
Normal file
|
@ -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<double?> 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;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue