import 'dart:async'; import 'dart:math'; import 'package:bloc_concurrency/bloc_concurrency.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:lightmeter/data/models/exposure_pair.dart'; import 'package:lightmeter/data/models/film.dart'; import 'package:lightmeter/interactors/metering_interactor.dart'; import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart'; import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart' as communication_events; import 'package:lightmeter/screens/metering/communication/state_communication_metering.dart' as communication_states; import 'package:lightmeter/screens/metering/event_metering.dart'; import 'package:lightmeter/screens/metering/state_metering.dart'; import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; class MeteringBloc extends Bloc { final MeteringCommunicationBloc _communicationBloc; final MeteringInteractor _meteringInteractor; late final StreamSubscription _communicationSubscription; List get _apertureValues => _equipmentProfileData.apertureValues.whereStopType(stopType); List get _shutterSpeedValues => _equipmentProfileData.shutterSpeedValues.whereStopType(stopType); EquipmentProfileData _equipmentProfileData; StopType stopType; @visibleForTesting late IsoValue iso = _meteringInteractor.iso; @visibleForTesting late NdValue nd = _meteringInteractor.ndFilter; @visibleForTesting late Film film = _meteringInteractor.film; @visibleForTesting double? ev100; MeteringBloc( this._communicationBloc, this._meteringInteractor, this._equipmentProfileData, this.stopType, ) : super( MeteringDataState( ev: null, film: _meteringInteractor.film, iso: _meteringInteractor.iso, nd: _meteringInteractor.ndFilter, exposurePairs: const [], continuousMetering: false, ), ) { _communicationSubscription = _communicationBloc.stream .where((state) => state is communication_states.ScreenState) .map((state) => state as communication_states.ScreenState) .listen(onCommunicationState); on(_onEquipmentProfileChanged); on(_onStopTypeChanged); on(_onFilmChanged); on(_onIsoChanged); on(_onNdChanged); on(_onMeasure, transformer: droppable()); on(_onMeasured); on(_onMeasureError); } @override Future close() async { await _communicationSubscription.cancel(); return super.close(); } @visibleForTesting void onCommunicationState(communication_states.ScreenState communicationState) { if (communicationState is communication_states.MeasuredState) { _handleEv100( communicationState.ev100, continuousMetering: communicationState is communication_states.MeteringInProgressState, ); } } void _onStopTypeChanged(StopTypeChangedEvent event, Emitter emit) { if (stopType != event.stopType) { stopType = event.stopType; _updateMeasurements(); } } void _onEquipmentProfileChanged(EquipmentProfileChangedEvent event, Emitter emit) { _equipmentProfileData = event.equipmentProfileData; bool willUpdateMeasurements = false; /// Update selected ISO value, if selected equipment profile /// doesn't contain currently selected value if (!event.equipmentProfileData.isoValues.any((v) => iso.value == v.value)) { _meteringInteractor.iso = event.equipmentProfileData.isoValues.first; iso = event.equipmentProfileData.isoValues.first; willUpdateMeasurements &= true; } /// The same for ND filter if (!event.equipmentProfileData.ndValues.any((v) => nd.value == v.value)) { _meteringInteractor.ndFilter = event.equipmentProfileData.ndValues.first; nd = event.equipmentProfileData.ndValues.first; willUpdateMeasurements &= true; } if (willUpdateMeasurements) { _updateMeasurements(); } } void _onFilmChanged(FilmChangedEvent event, Emitter emit) { if (film.name != event.data.name) { film = event.data; _meteringInteractor.film = event.data; _film = event.data; /// If user selects 'Other' film we preserve currently selected ISO /// and therefore only discard reciprocity formula if (iso.value != event.data.iso && event.data != const Film.other()) { final newIso = IsoValue.values.firstWhere( (e) => e.value == event.data.iso, orElse: () => iso, ); _meteringInteractor.iso = newIso; iso = newIso; } _updateMeasurements(); } } void _onIsoChanged(IsoChangedEvent event, Emitter emit) { /// Discard currently selected film even if ISO is the same, /// because, for example, Fomapan 400 and any Ilford 400 /// have different reciprocity formulas _meteringInteractor.film = Film.values.first; _film = Film.values.first; if (iso != event.isoValue) { _meteringInteractor.iso = event.isoValue; iso = event.isoValue; _updateMeasurements(); } } void _onNdChanged(NdChangedEvent event, Emitter emit) { if (nd != event.ndValue) { _meteringInteractor.ndFilter = event.ndValue; nd = event.ndValue; _updateMeasurements(); } } void _onMeasure(MeasureEvent _, Emitter emit) { _meteringInteractor.quickVibration(); _communicationBloc.add(const communication_events.MeasureEvent()); emit( LoadingState( film: film, iso: iso, nd: nd, ), ); } void _updateMeasurements() => _handleEv100(ev100, continuousMetering: false); void _handleEv100(double? ev100, {required bool continuousMetering}) { if (ev100 == null || ev100.isNaN || ev100.isInfinite) { add(MeasureErrorEvent(continuousMetering: continuousMetering)); } else { add(MeasuredEvent(ev100, continuousMetering: continuousMetering)); } } void _onMeasured(MeasuredEvent event, Emitter emit) { _meteringInteractor.responseVibration(); ev100 = event.ev100; final ev = event.ev100 + log2(iso.value / 100) - nd.stopReduction; emit( MeteringDataState( ev: ev, film: film, iso: iso, nd: nd, exposurePairs: buildExposureValues(ev), continuousMetering: event.continuousMetering, ), ); } void _onMeasureError(MeasureErrorEvent event, Emitter emit) { _meteringInteractor.errorVibration(); ev100 = null; emit( MeteringDataState( ev: null, film: film, iso: iso, nd: nd, exposurePairs: const [], continuousMetering: event.continuousMetering, ), ); } @visibleForTesting List buildExposureValues(double ev) { if (ev.isNaN || ev.isInfinite) { return List.empty(); } /// Depending on the `stopType` the exposure pairs list length is multiplied by 1,2 or 3 final int evSteps = (ev * (stopType.index + 1)).round(); /// Basically we use 1" shutter speed as an anchor point for building the exposure pairs list. /// But user can exclude this value from the list using custom equipment profile. /// So we have to restore the index of the anchor value. const ShutterSpeedValue anchorShutterSpeed = ShutterSpeedValue(1, false, StopType.full); int anchorIndex = _shutterSpeedValues.indexOf(anchorShutterSpeed); if (anchorIndex < 0) { final filteredFullList = ShutterSpeedValue.values.whereStopType(stopType); final customListStartIndex = filteredFullList.indexOf(_shutterSpeedValues.first); final fullListAnchor = filteredFullList.indexOf(anchorShutterSpeed); if (customListStartIndex < fullListAnchor) { /// This means, that user excluded anchor value at the end, /// i.e. all shutter speed values are shorter than 1". anchorIndex = fullListAnchor - customListStartIndex; } else { /// In case user excludes anchor value at the start, /// we can do no adjustment. } } final int evOffset = anchorIndex - evSteps; late final int apertureOffset; late final int shutterSpeedOffset; if (evOffset >= 0) { apertureOffset = 0; shutterSpeedOffset = evOffset; } else { apertureOffset = -evOffset; shutterSpeedOffset = 0; } final int itemsCount = min( _apertureValues.length + shutterSpeedOffset, _shutterSpeedValues.length + apertureOffset, ) - max(apertureOffset, shutterSpeedOffset); if (itemsCount < 0) { return List.empty(); } return List.generate( itemsCount, (index) => ExposurePair( _apertureValues[index + apertureOffset], film.reciprocityFailure(_shutterSpeedValues[index + shutterSpeedOffset]), ), growable: false, ); } }