m3_lightmeter/lib/screens/metering/screen_metering.dart

237 lines
8.6 KiB
Dart
Raw Permalink Normal View History

import 'dart:math';
2022-10-24 20:25:38 +00:00
import 'package:flutter/material.dart';
2022-10-29 18:02:45 +00:00
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/data/models/ev_source_type.dart';
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/providers/services_provider.dart';
import 'package:lightmeter/providers/user_preferences_provider.dart';
import 'package:lightmeter/screens/metering/bloc_metering.dart';
import 'package:lightmeter/screens/metering/components/bottom_controls/provider_bottom_controls.dart';
import 'package:lightmeter/screens/metering/components/camera_container/provider_container_camera.dart';
import 'package:lightmeter/screens/metering/components/light_sensor_container/provider_container_light_sensor.dart';
import 'package:lightmeter/screens/metering/event_metering.dart';
import 'package:lightmeter/screens/metering/state_metering.dart';
import 'package:lightmeter/screens/metering/utils/listener_metering_layout_feature.dart';
import 'package:lightmeter/screens/metering/utils/listsner_equipment_profiles.dart';
import 'package:m3_lightmeter_iap/m3_lightmeter_iap.dart';
ML-42 Implement equipment profiles creating (#45) * added Equipment section placeholder * get iso & nd values from equipment profile * use photography values from remote repo * removed equipment section * wip * moved `EquipmentProfileProvider` from iap repo * wip * moved equipment profiles screen from iap * improved equipment profiles screen * mock add/delete * collapse on expand * add profile with name * show selected values count (wip) * fixed profile update * cleanup * Update pubspec.yaml * made `AnimatedDialogPicker` more generic * switched to local `Dimens` * fixed `MeteringTopBarShape` * rename * animated `EquipmentProfileContainer` * added default equipment profile * change equipment profile name via dialog * fixed profile selection * filter equipment profile update/delete * removed `enabled` param from settings section * non-null `EquipmentProfile` * fixed duplicate GlobalKeys * animated equipment list * Update ci.yml * fixed shutter speed anchor issue * autofocus * added firebase to project * save/restore equipment profiles * unified `SliverList` * added SSH key to iap repo * Update ci.yml * ci recursive submodules * try full url * Revert "try full url" This reverts commit a9b692b60ea5b2e88188a5d497467708becb4a02. * restore firebase_options.dart * changed runner to macos * restore options earlier * removed problematic file from analysis :) * removed launch_app * textoverflow * implemented `DialogRangePicker` * add iap repo to cd * typo * added workflow_dispatch to crowdin push * removed `equipmentProfileValuesCount` from intl * fr & ru translations * style * removed iap
2023-03-30 19:24:18 +00:00
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
2022-10-24 20:25:38 +00:00
class MeteringScreen extends StatelessWidget {
const MeteringScreen({super.key});
2022-10-24 20:25:38 +00:00
@override
Widget build(BuildContext context) {
return _InheritedListeners(
child: Scaffold(
backgroundColor: Theme.of(context).colorScheme.background,
body: Column(
children: [
Expanded(
child: BlocBuilder<MeteringBloc, MeteringState>(
builder: (_, state) => MeteringContainerBuidler(
ev: state is MeteringDataState ? state.ev : null,
film: state.film,
iso: state.iso,
nd: state.nd,
onFilmChanged: (value) =>
context.read<MeteringBloc>().add(FilmChangedEvent(value)),
onIsoChanged: (value) => context.read<MeteringBloc>().add(IsoChangedEvent(value)),
onNdChanged: (value) => context.read<MeteringBloc>().add(NdChangedEvent(value)),
),
),
),
BlocBuilder<MeteringBloc, MeteringState>(
builder: (context, state) => MeteringBottomControlsProvider(
ev: state is MeteringDataState ? state.ev : null,
isMetering: state.isMetering,
onSwitchEvSourceType: ServicesProvider.of(context).environment.hasLightSensor
? UserPreferencesProvider.of(context).toggleEvSourceType
: null,
onMeasure: () => context.read<MeteringBloc>().add(const MeasureEvent()),
onSettings: () {
context.read<MeteringBloc>().add(const SettingsOpenedEvent());
Navigator.pushNamed(context, 'settings').then((value) {
context.read<MeteringBloc>().add(const SettingsClosedEvent());
});
},
),
),
],
),
),
);
}
}
class _InheritedListeners extends StatelessWidget {
final Widget child;
2023-01-26 15:03:48 +00:00
const _InheritedListeners({required this.child});
2022-10-24 20:25:38 +00:00
@override
Widget build(BuildContext context) {
return EquipmentProfileListener(
onDidChangeDependencies: (value) {
context.read<MeteringBloc>().add(EquipmentProfileChangedEvent(value));
},
child: MeteringScreenLayoutFeatureListener(
feature: MeteringScreenLayoutFeature.filmPicker,
onDidChangeDependencies: (value) {
if (!value) {
context.read<MeteringBloc>().add(const FilmChangedEvent(Film.other()));
}
},
child: MeteringScreenLayoutFeatureListener(
feature: MeteringScreenLayoutFeature.equipmentProfiles,
onDidChangeDependencies: (value) {
if (!value) {
EquipmentProfileProvider.of(context).setProfile(EquipmentProfiles.of(context).first);
}
},
child: child,
),
2022-10-24 20:25:38 +00:00
),
);
}
}
class MeteringContainerBuidler extends StatelessWidget {
final double? ev;
final Film film;
final IsoValue iso;
final NdValue nd;
final ValueChanged<Film> onFilmChanged;
final ValueChanged<IsoValue> onIsoChanged;
final ValueChanged<NdValue> onNdChanged;
const MeteringContainerBuidler({
required this.ev,
required this.film,
required this.iso,
required this.nd,
required this.onFilmChanged,
required this.onIsoChanged,
required this.onNdChanged,
});
@override
Widget build(BuildContext context) {
final exposurePairs = ev != null
? buildExposureValues(
ev!,
UserPreferencesProvider.stopTypeOf(context),
EquipmentProfiles.selectedOf(context),
film,
)
: <ExposurePair>[];
final fastest = exposurePairs.isNotEmpty ? exposurePairs.first : null;
final slowest = exposurePairs.isNotEmpty ? exposurePairs.last : null;
// Doubled build here when switching evSourceType. As new source bloc fires a new state on init
return UserPreferencesProvider.evSourceTypeOf(context) == EvSourceType.camera
? CameraContainerProvider(
fastest: fastest,
slowest: slowest,
film: film,
iso: iso,
nd: nd,
onFilmChanged: onFilmChanged,
onIsoChanged: onIsoChanged,
onNdChanged: onNdChanged,
exposurePairs: exposurePairs,
)
: LightSensorContainerProvider(
fastest: fastest,
slowest: slowest,
film: film,
iso: iso,
nd: nd,
onFilmChanged: onFilmChanged,
onIsoChanged: onIsoChanged,
onNdChanged: onNdChanged,
exposurePairs: exposurePairs,
);
}
@visibleForTesting
static List<ExposurePair> buildExposureValues(
double ev,
StopType stopType,
EquipmentProfile equipmentProfile,
Film film,
) {
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();
final apertureValues = ApertureValue.values.whereStopType(stopType);
final shutterSpeedValues = ShutterSpeedValue.values.whereStopType(stopType);
/// 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 anchorShutterSpeed = ShutterSpeedValue(1, false, StopType.full);
final int anchorIndex = shutterSpeedValues.indexOf(anchorShutterSpeed);
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();
}
final exposurePairs = List.generate(
itemsCount,
(index) => ExposurePair(
apertureValues[index + apertureOffset],
film.reciprocityFailure(shutterSpeedValues[index + shutterSpeedOffset]),
),
growable: false,
);
/// Full equipment profile, nothing to cut
if (equipmentProfile.id == "") {
return exposurePairs;
}
final equipmentApertureValues = equipmentProfile.apertureValues.whereStopType(stopType);
final equipmentShutterSpeedValues = equipmentProfile.shutterSpeedValues.whereStopType(stopType);
final startCutEV = max(
exposurePairs.first.aperture.difference(equipmentApertureValues.first),
exposurePairs.first.shutterSpeed.difference(equipmentShutterSpeedValues.first),
);
final endCutEV = max(
equipmentApertureValues.last.difference(exposurePairs.last.aperture),
equipmentShutterSpeedValues.last.difference(exposurePairs.last.shutterSpeed),
);
final startCut = (startCutEV * (stopType.index + 1)).round().clamp(0, itemsCount);
final endCut = (endCutEV * (stopType.index + 1)).round().clamp(0, itemsCount);
if (startCut > itemsCount - endCut) {
return const [];
}
return exposurePairs.sublist(startCut, itemsCount - endCut);
}
}