ML-16 [Android] Implement incident light metering (#17)

* wip

* rename

* wip

* rename

* fixed camera screen layout

* omit camera measure on startup

* added calibration for light sensor

* save evsource

* Update widget_button_measure.dart

* fixed iOS init

* hide light sensor calibration on ios

* cleanup
This commit is contained in:
Vadim 2023-01-29 19:57:47 +03:00 committed by GitHub
parent 179008ff77
commit 9ffb5112c1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
54 changed files with 1230 additions and 753 deletions

View file

@ -33,7 +33,7 @@ The list of features that the old lightmeter app has and that have to be impleme
- [x] ISO selecting - [x] ISO selecting
- [ ] Reciprocity for different films - [ ] Reciprocity for different films
- [x] Reflected light metering - [x] Reflected light metering
- [ ] Incident light metering - [x] Incident light metering
### Adjust ### Adjust
- [x] Light sources EV calibration - [x] Light sources EV calibration

View file

@ -1,22 +1,24 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:light_sensor/light_sensor.dart';
import 'package:lightmeter/data/haptics_service.dart'; import 'package:lightmeter/data/haptics_service.dart';
import 'package:lightmeter/data/models/ev_source_type.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'data/light_sensor_service.dart';
import 'data/permissions_service.dart'; import 'data/permissions_service.dart';
import 'data/shared_prefs_service.dart'; import 'data/shared_prefs_service.dart';
import 'environment.dart'; import 'environment.dart';
import 'generated/l10n.dart'; import 'generated/l10n.dart';
import 'providers/ev_source_type_provider.dart';
import 'res/theme.dart'; import 'res/theme.dart';
import 'screens/metering/flow_metering.dart'; import 'screens/metering/flow_metering.dart';
import 'screens/settings/flow_settings.dart'; import 'screens/settings/flow_settings.dart';
import 'utils/stop_type_provider.dart'; import 'utils/stop_type_provider.dart';
final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();
class Application extends StatelessWidget { class Application extends StatelessWidget {
final Environment env; final Environment env;
@ -24,19 +26,23 @@ class Application extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FutureBuilder<SharedPreferences>( return FutureBuilder(
future: SharedPreferences.getInstance(), future: Future.wait([
SharedPreferences.getInstance(),
Platform.isAndroid ? LightSensor.hasSensor : Future.value(false),
]),
builder: (_, snapshot) { builder: (_, snapshot) {
if (snapshot.data != null) { if (snapshot.data != null) {
return MultiProvider( return MultiProvider(
providers: [ providers: [
Provider.value(value: env), Provider.value(value: env.copyWith(hasLightSensor: snapshot.data![1] as bool)),
Provider.value(value: EvSourceType.camera), Provider(create: (_) => UserPreferencesService(snapshot.data![0] as SharedPreferences)),
Provider(create: (_) => UserPreferencesService(snapshot.data!)),
Provider(create: (_) => const HapticsService()), Provider(create: (_) => const HapticsService()),
Provider(create: (_) => PermissionsService()), Provider(create: (_) => PermissionsService()),
Provider(create: (_) => const LightSensorService()),
], ],
child: StopTypeProvider( child: StopTypeProvider(
child: EvSourceTypeProvider(
child: ThemeProvider( child: ThemeProvider(
builder: (context, _) { builder: (context, _) {
final systemIconsBrightness = ThemeData.estimateBrightnessForColor( final systemIconsBrightness = ThemeData.estimateBrightnessForColor(
@ -75,6 +81,7 @@ class Application extends StatelessWidget {
}, },
), ),
), ),
),
); );
} }
return const SizedBox(); return const SizedBox();

View file

@ -0,0 +1,9 @@
import 'package:light_sensor/light_sensor.dart';
class LightSensorService {
const LightSensorService();
Future<bool> hasSensor() async => await LightSensor.hasSensor ?? false;
Stream<int> luxStream() => LightSensor.lightSensorStream;
}

View file

@ -1,14 +1,17 @@
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'models/ev_source_type.dart';
import 'models/photography_values/iso_value.dart'; import 'models/photography_values/iso_value.dart';
import 'models/photography_values/nd_value.dart'; import 'models/photography_values/nd_value.dart';
import 'models/theme_type.dart'; import 'models/theme_type.dart';
class UserPreferencesService { class UserPreferencesService {
static const _isoKey = "iso"; static const _isoKey = "iso";
static const _ndFilterKey = "nd"; static const _ndFilterKey = "ndFilter";
static const _evSourceTypeKey = "evSourceType";
static const _cameraEvCalibrationKey = "cameraEvCalibration"; static const _cameraEvCalibrationKey = "cameraEvCalibration";
static const _lightSensorEvCalibrationKey = "lightSensorEvCalibration";
static const _hapticsKey = "haptics"; static const _hapticsKey = "haptics";
static const _themeTypeKey = "themeType"; static const _themeTypeKey = "themeType";
@ -24,12 +27,18 @@ class UserPreferencesService {
NdValue get ndFilter => ndValues.firstWhere((v) => v.value == (_sharedPreferences.getInt(_ndFilterKey) ?? 0)); NdValue get ndFilter => ndValues.firstWhere((v) => v.value == (_sharedPreferences.getInt(_ndFilterKey) ?? 0));
set ndFilter(NdValue value) => _sharedPreferences.setInt(_ndFilterKey, value.value); set ndFilter(NdValue value) => _sharedPreferences.setInt(_ndFilterKey, value.value);
EvSourceType get evSourceType => EvSourceType.values[_sharedPreferences.getInt(_evSourceTypeKey) ?? 0];
set evSourceType(EvSourceType value) => _sharedPreferences.setInt(_evSourceTypeKey, value.index);
bool get haptics => _sharedPreferences.getBool(_hapticsKey) ?? false; bool get haptics => _sharedPreferences.getBool(_hapticsKey) ?? false;
set haptics(bool value) => _sharedPreferences.setBool(_hapticsKey, value); set haptics(bool value) => _sharedPreferences.setBool(_hapticsKey, value);
double get cameraEvCalibration => _sharedPreferences.getDouble(_cameraEvCalibrationKey) ?? 0.0; double get cameraEvCalibration => _sharedPreferences.getDouble(_cameraEvCalibrationKey) ?? 0.0;
set cameraEvCalibration(double value) => _sharedPreferences.setDouble(_cameraEvCalibrationKey, value); set cameraEvCalibration(double value) => _sharedPreferences.setDouble(_cameraEvCalibrationKey, value);
double get lightSensorEvCalibration => _sharedPreferences.getDouble(_lightSensorEvCalibrationKey) ?? 0.0;
set lightSensorEvCalibration(double value) => _sharedPreferences.setDouble(_lightSensorEvCalibrationKey, value);
ThemeType get themeType => ThemeType.values[_sharedPreferences.getInt(_themeTypeKey) ?? 0]; ThemeType get themeType => ThemeType.values[_sharedPreferences.getInt(_themeTypeKey) ?? 0];
set themeType(ThemeType value) => _sharedPreferences.setInt(_themeTypeKey, value.index); set themeType(ThemeType value) => _sharedPreferences.setInt(_themeTypeKey, value.index);

View file

@ -3,19 +3,31 @@ class Environment {
final String issuesReportUrl; final String issuesReportUrl;
final String contactEmail; final String contactEmail;
final bool hasLightSensor;
const Environment({ const Environment({
required this.sourceCodeUrl, required this.sourceCodeUrl,
required this.issuesReportUrl, required this.issuesReportUrl,
required this.contactEmail, required this.contactEmail,
this.hasLightSensor = false,
}); });
const Environment.dev() const Environment.dev()
: sourceCodeUrl = 'https://github.com/vodemn/m3_lightmeter', : sourceCodeUrl = 'https://github.com/vodemn/m3_lightmeter',
issuesReportUrl = 'https://github.com/vodemn/m3_lightmeter/issues', issuesReportUrl = 'https://github.com/vodemn/m3_lightmeter/issues',
contactEmail = 'contact.vodemn@gmail.com'; contactEmail = 'contact.vodemn@gmail.com',
hasLightSensor = false;
const Environment.prod() const Environment.prod()
: sourceCodeUrl = 'https://github.com/vodemn/m3_lightmeter', : sourceCodeUrl = 'https://github.com/vodemn/m3_lightmeter',
issuesReportUrl = 'https://github.com/vodemn/m3_lightmeter/issues', issuesReportUrl = 'https://github.com/vodemn/m3_lightmeter/issues',
contactEmail = 'contact.vodemn@gmail.com'; contactEmail = 'contact.vodemn@gmail.com',
hasLightSensor = false;
Environment copyWith({bool? hasLightSensor}) => Environment(
sourceCodeUrl: sourceCodeUrl,
issuesReportUrl: issuesReportUrl,
contactEmail: contactEmail,
hasLightSensor: hasLightSensor ?? this.hasLightSensor,
);
} }

View file

@ -1,16 +1,22 @@
import 'dart:io';
import 'package:lightmeter/data/haptics_service.dart'; import 'package:lightmeter/data/haptics_service.dart';
import 'package:lightmeter/data/light_sensor_service.dart';
import 'package:lightmeter/data/shared_prefs_service.dart'; import 'package:lightmeter/data/shared_prefs_service.dart';
class MeteringInteractor { class MeteringInteractor {
final UserPreferencesService _userPreferencesService; final UserPreferencesService _userPreferencesService;
final HapticsService _hapticsService; final HapticsService _hapticsService;
final LightSensorService _lightSensorService;
const MeteringInteractor( const MeteringInteractor(
this._userPreferencesService, this._userPreferencesService,
this._hapticsService, this._hapticsService,
this._lightSensorService,
); );
double get cameraEvCalibration => _userPreferencesService.cameraEvCalibration; double get cameraEvCalibration => _userPreferencesService.cameraEvCalibration;
double get lightSensorEvCalibration => _userPreferencesService.lightSensorEvCalibration;
bool get isHapticsEnabled => _userPreferencesService.haptics; bool get isHapticsEnabled => _userPreferencesService.haptics;
@ -25,4 +31,14 @@ class MeteringInteractor {
} }
void enableHaptics(bool enable) => _userPreferencesService.haptics = enable; void enableHaptics(bool enable) => _userPreferencesService.haptics = enable;
Future<bool> hasAmbientLightSensor() async {
if (Platform.isAndroid) {
return _lightSensorService.hasSensor();
} else {
return false;
}
}
Stream<int> luxStream() => _lightSensorService.luxStream();
} }

View file

@ -13,6 +13,9 @@ class SettingsInteractor {
double get cameraEvCalibration => _userPreferencesService.cameraEvCalibration; double get cameraEvCalibration => _userPreferencesService.cameraEvCalibration;
void setCameraEvCalibration(double value) => _userPreferencesService.cameraEvCalibration = value; void setCameraEvCalibration(double value) => _userPreferencesService.cameraEvCalibration = value;
double get lightSensorEvCalibration => _userPreferencesService.lightSensorEvCalibration;
void setLightSensorEvCalibration(double value) => _userPreferencesService.lightSensorEvCalibration = value;
bool get isHapticsEnabled => _userPreferencesService.haptics; bool get isHapticsEnabled => _userPreferencesService.haptics;
/// Executes vibration if haptics are enabled in settings /// Executes vibration if haptics are enabled in settings

View file

@ -28,8 +28,10 @@
"halfStops": "1/2", "halfStops": "1/2",
"thirdStops": "1/3", "thirdStops": "1/3",
"calibration": "Calibration", "calibration": "Calibration",
"calibrationMessage": "The accuracy of the readings measured by this application depends entirely on the hardware of the device. Therefore, consider testing this application and setting up an EV calibration value that will give you the desired measurement results.", "calibrationMessage": "The accuracy of the readings measured by this application depends entirely on the hardware of the device. Therefore, consider testing this application and setting up EV calibration values that will give you the desired measurement results.",
"calibrationMessageCameraOnly": "The accuracy of the readings measured by this application depends entirely on the rear camera of the device. Therefore, consider testing this application and setting up an EV calibration value that will give you the desired measurement results.",
"camera": "Camera", "camera": "Camera",
"lightSensor": "Light sensor",
"general": "General", "general": "General",
"haptics": "Haptics", "haptics": "Haptics",
"theme": "Theme", "theme": "Theme",

View file

@ -0,0 +1,65 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/ev_source_type.dart';
import 'package:lightmeter/data/shared_prefs_service.dart';
import 'package:lightmeter/environment.dart';
import 'package:provider/provider.dart';
class EvSourceTypeProvider extends StatefulWidget {
final Widget child;
const EvSourceTypeProvider({required this.child, super.key});
static EvSourceTypeProviderState of(BuildContext context) {
return context.findAncestorStateOfType<EvSourceTypeProviderState>()!;
}
@override
State<EvSourceTypeProvider> createState() => EvSourceTypeProviderState();
}
class EvSourceTypeProviderState extends State<EvSourceTypeProvider> {
late final ValueNotifier<EvSourceType> valueListenable;
@override
void initState() {
super.initState();
final evSourceType = context.read<UserPreferencesService>().evSourceType;
valueListenable = ValueNotifier(
evSourceType == EvSourceType.sensor && !context.read<Environment>().hasLightSensor
? EvSourceType.camera
: evSourceType,
);
}
@override
void dispose() {
valueListenable.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: valueListenable,
builder: (_, value, child) => Provider.value(
value: value,
child: child,
),
child: widget.child,
);
}
void toggleType() {
switch (valueListenable.value) {
case EvSourceType.camera:
if (context.read<Environment>().hasLightSensor) {
valueListenable.value = EvSourceType.sensor;
}
break;
case EvSourceType.sensor:
valueListenable.value = EvSourceType.camera;
break;
}
context.read<UserPreferencesService>().evSourceType = valueListenable.value;
}
}

View file

@ -21,6 +21,10 @@ class Dimens {
static const Duration durationML = Duration(milliseconds: 250); static const Duration durationML = Duration(milliseconds: 250);
static const Duration durationL = Duration(milliseconds: 300); static const Duration durationL = Duration(milliseconds: 300);
// TopBar
/// Probably this is a bad practice, but with text size locked, the height is always 212
static const double readingContainerHeight = 212;
// `CenteredSlider` // `CenteredSlider`
static const double cameraSliderTrackHeight = grid4; static const double cameraSliderTrackHeight = grid4;
static const double cameraSliderTrackRadius = cameraSliderTrackHeight / 2; static const double cameraSliderTrackRadius = cameraSliderTrackHeight / 2;

View file

@ -6,7 +6,9 @@ import 'state_communication_metering.dart';
class MeteringCommunicationBloc class MeteringCommunicationBloc
extends Bloc<MeteringCommunicationEvent, MeteringCommunicationState> { extends Bloc<MeteringCommunicationEvent, MeteringCommunicationState> {
MeteringCommunicationBloc() : super(const InitState()) { MeteringCommunicationBloc() : super(const InitState()) {
on<MeasureEvent>((_, emit) => emit(const MeasureState())); // `MeasureState` is not const, so that `Bloc` treats each state as new and updates state stream
// ignore: prefer_const_constructors
on<MeasureEvent>((_, emit) => emit(MeasureState()));
on<MeasuredEvent>((event, emit) => emit(MeasuredState(event.ev100))); on<MeasuredEvent>((event, emit) => emit(MeasuredState(event.ev100)));
} }
} }

View file

@ -36,6 +36,11 @@ class _MeteringMeasureButtonState extends State<MeteringMeasureButton> {
_isPressed = false; _isPressed = false;
}); });
}, },
onTapCancel: () {
setState(() {
_isPressed = false;
});
},
child: DecoratedBox( child: DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(widget.size / 2), borderRadius: BorderRadius.circular(widget.size / 2),

View file

@ -1,14 +1,18 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/ev_source_type.dart';
import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/res/dimens.dart';
import 'package:provider/provider.dart';
import 'components/widget_button_measure.dart'; import 'components/widget_button_measure.dart';
import 'components/widget_button_secondary.dart'; import 'components/widget_button_secondary.dart';
class MeteringBottomControls extends StatelessWidget { class MeteringBottomControls extends StatelessWidget {
final VoidCallback? onSwitchEvSourceType;
final VoidCallback onMeasure; final VoidCallback onMeasure;
final VoidCallback onSettings; final VoidCallback onSettings;
const MeteringBottomControls({ const MeteringBottomControls({
required this.onSwitchEvSourceType,
required this.onMeasure, required this.onMeasure,
required this.onSettings, required this.onSettings,
super.key, super.key,
@ -30,6 +34,16 @@ class MeteringBottomControls extends StatelessWidget {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
if (onSwitchEvSourceType != null)
Expanded(
child: MeteringSecondaryButton(
onPressed: onSwitchEvSourceType!,
icon: context.watch<EvSourceType>() != EvSourceType.camera
? Icons.camera_rear
: Icons.wb_incandescent,
),
)
else
const Spacer(), const Spacer(),
MeteringMeasureButton( MeteringMeasureButton(
onTap: onMeasure, onTap: onMeasure,

View file

@ -1,94 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/metering/ev_source/camera/bloc_camera.dart';
import 'package:lightmeter/screens/metering/ev_source/camera/event_camera.dart';
import 'package:lightmeter/screens/metering/ev_source/camera/state_camera.dart';
import 'package:lightmeter/screens/shared/centered_slider/widget_slider_centered.dart';
import 'package:lightmeter/utils/to_string_signed.dart';
class CameraExposureSlider extends StatelessWidget {
const CameraExposureSlider({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<CameraBloc, CameraState>(
builder: (context, state) {
if (state is CameraActiveState) {
return Column(
children: [
IconButton(
icon: const Icon(Icons.sync),
onPressed: state.currentExposureOffset != 0.0
? () => context.read<CameraBloc>().add(const ExposureOffsetChangedEvent(0.0))
: null,
),
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: Dimens.grid8),
child: _Ruler(state.minExposureOffset, state.maxExposureOffset),
),
CenteredSlider(
isVertical: true,
icon: const Icon(Icons.light_mode),
value: state.currentExposureOffset,
min: state.minExposureOffset,
max: state.maxExposureOffset,
onChanged: (value) {
context.read<CameraBloc>().add(ExposureOffsetChangedEvent(value));
},
),
],
),
),
],
);
}
return const SizedBox();
},
);
}
}
class _Ruler extends StatelessWidget {
final double min;
final double max;
const _Ruler(this.min, this.max);
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.generate(
(max - min + 1).toInt(),
(index) {
final bool showValue = index % 2 == 0.0 || index == 0.0;
return Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (showValue)
Text(
(index + min).toStringSigned(),
style: Theme.of(context).textTheme.bodyLarge,
),
const SizedBox(width: Dimens.grid8),
ColoredBox(
color: Theme.of(context).colorScheme.onBackground,
child: SizedBox(
height: 1,
width: showValue ? Dimens.grid16 : Dimens.grid8,
),
),
const SizedBox(width: Dimens.grid8),
],
);
},
).reversed.toList(),
);
}
}

View file

@ -1,31 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/screens/metering/ev_source/camera/bloc_camera.dart';
import 'package:lightmeter/screens/metering/ev_source/camera/event_camera.dart';
import 'package:lightmeter/screens/metering/ev_source/camera/state_camera.dart';
import '../../../shared/centered_slider/widget_slider_centered.dart';
class CameraZoomSlider extends StatelessWidget {
const CameraZoomSlider({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<CameraBloc, CameraState>(
builder: (context, state) {
if (state is CameraActiveState) {
return CenteredSlider(
icon: const Icon(Icons.search),
value: state.currentZoom,
min: state.minZoom,
max: state.maxZoom,
onChanged: (value) {
context.read<CameraBloc>().add(ZoomChangedEvent(value));
},
);
}
return const SizedBox();
},
);
}
}

View file

@ -7,7 +7,7 @@ import 'package:exif/exif.dart';
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/interactors/metering_interactor.dart'; import 'package:lightmeter/interactors/metering_interactor.dart';
import 'package:lightmeter/screens/metering/ev_source/ev_source_bloc.dart'; import 'package:lightmeter/screens/metering/components/shared/ev_source_base/bloc_base_ev_source.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/communication/event_communication_metering.dart' import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart'
as communication_event; as communication_event;
@ -15,10 +15,10 @@ import 'package:lightmeter/screens/metering/communication/state_communication_me
as communication_states; as communication_states;
import 'package:lightmeter/utils/log_2.dart'; import 'package:lightmeter/utils/log_2.dart';
import 'event_camera.dart'; import 'event_container_camera.dart';
import 'state_camera.dart'; import 'state_container_camera.dart';
class CameraBloc extends EvSourceBloc<CameraEvent, CameraState> { class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraContainerState> {
final MeteringInteractor _meteringInteractor; final MeteringInteractor _meteringInteractor;
late final _WidgetsBindingObserver _observer; late final _WidgetsBindingObserver _observer;
CameraController? _cameraController; CameraController? _cameraController;
@ -33,9 +33,9 @@ class CameraBloc extends EvSourceBloc<CameraEvent, CameraState> {
double _exposureStep = 0.0; double _exposureStep = 0.0;
double _currentExposureOffset = 0.0; double _currentExposureOffset = 0.0;
CameraBloc( CameraContainerBloc(
MeteringCommunicationBloc communicationBloc,
this._meteringInteractor, this._meteringInteractor,
MeteringCommunicationBloc communicationBloc,
) : super( ) : super(
communicationBloc, communicationBloc,
const CameraInitState(), const CameraInitState(),
@ -54,8 +54,8 @@ class CameraBloc extends EvSourceBloc<CameraEvent, CameraState> {
@override @override
Future<void> close() async { Future<void> close() async {
WidgetsBinding.instance.removeObserver(_observer); WidgetsBinding.instance.removeObserver(_observer);
_cameraController?.dispose(); unawaited(_cameraController?.dispose());
super.close(); return super.close();
} }
@override @override
@ -110,11 +110,6 @@ class CameraBloc extends EvSourceBloc<CameraEvent, CameraState> {
emit(CameraInitializedState(_cameraController!)); emit(CameraInitializedState(_cameraController!));
_emitActiveState(emit); _emitActiveState(emit);
_takePhoto().then((ev100) {
if (ev100 != null) {
communicationBloc.add(communication_event.MeasuredEvent(ev100));
}
});
} catch (e) { } catch (e) {
emit(const CameraErrorState()); emit(const CameraErrorState());
} }
@ -139,11 +134,9 @@ class CameraBloc extends EvSourceBloc<CameraEvent, CameraState> {
void _emitActiveState(Emitter emit) { void _emitActiveState(Emitter emit) {
emit(CameraActiveState( emit(CameraActiveState(
minZoom: _zoomRange!.start, zoomRange: _zoomRange!,
maxZoom: _zoomRange!.end,
currentZoom: _currentZoom, currentZoom: _currentZoom,
minExposureOffset: _exposureOffsetRange!.start, exposureOffsetRange: _exposureOffsetRange!,
maxExposureOffset: _exposureOffsetRange!.end,
exposureOffsetStep: _exposureStep, exposureOffsetStep: _exposureStep,
currentExposureOffset: _currentExposureOffset, currentExposureOffset: _currentExposureOffset,
)); ));

View file

@ -0,0 +1,91 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/shared/centered_slider/widget_slider_centered.dart';
import 'package:lightmeter/utils/to_string_signed.dart';
class ExposureOffsetSlider extends StatelessWidget {
final RangeValues range;
final double value;
final ValueChanged<double> onChanged;
const ExposureOffsetSlider({
required this.range,
required this.value,
required this.onChanged,
super.key,
});
@override
Widget build(BuildContext context) {
return Column(
children: [
IconButton(
icon: const Icon(Icons.sync),
onPressed: value != 0.0 ? () => onChanged(0.0) : null,
),
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: Dimens.grid8),
child: _Ruler(
range.start,
range.end,
),
),
CenteredSlider(
isVertical: true,
icon: const Icon(Icons.light_mode),
value: value,
min: range.start,
max: range.end,
onChanged: onChanged,
),
],
),
),
],
);
}
}
class _Ruler extends StatelessWidget {
final double min;
final double max;
const _Ruler(this.min, this.max);
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.generate(
(max - min + 1).toInt(),
(index) {
final bool showValue = index % 2 == 0.0 || index == 0.0;
return Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (showValue)
Text(
(index + min).toStringSigned(),
style: Theme.of(context).textTheme.bodyLarge,
),
const SizedBox(width: Dimens.grid8),
ColoredBox(
color: Theme.of(context).colorScheme.onBackground,
child: SizedBox(
height: 1,
width: showValue ? Dimens.grid16 : Dimens.grid8,
),
),
const SizedBox(width: Dimens.grid8),
],
);
},
).reversed.toList(),
);
}
}

View file

@ -0,0 +1,26 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/screens/shared/centered_slider/widget_slider_centered.dart';
class ZoomSlider extends StatelessWidget {
final RangeValues range;
final double value;
final ValueChanged<double> onChanged;
const ZoomSlider({
required this.range,
required this.value,
required this.onChanged,
super.key,
});
@override
Widget build(BuildContext context) {
return CenteredSlider(
icon: const Icon(Icons.search),
value: value,
min: range.start,
max: range.end,
onChanged: onChanged,
);
}
}

View file

@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/res/dimens.dart';
import 'components/exposure_offset_slider/widget_slider_exposure_offset.dart';
import 'components/zoom_slider/widget_slider_zoom.dart';
class CameraControls extends StatelessWidget {
final RangeValues exposureOffsetRange;
final double exposureOffsetValue;
final ValueChanged<double> onExposureOffsetChanged;
final RangeValues zoomRange;
final double zoomValue;
final ValueChanged<double> onZoomChanged;
const CameraControls({
required this.exposureOffsetRange,
required this.exposureOffsetValue,
required this.onExposureOffsetChanged,
required this.zoomRange,
required this.zoomValue,
required this.onZoomChanged,
super.key,
});
@override
Widget build(BuildContext context) {
return Column(
children: [
Expanded(
child: ExposureOffsetSlider(
range: exposureOffsetRange,
value: exposureOffsetValue,
onChanged: onExposureOffsetChanged,
),
),
const SizedBox(height: Dimens.grid24),
ZoomSlider(
range: zoomRange,
value: zoomValue,
onChanged: onZoomChanged,
),
],
);
}
}

View file

@ -0,0 +1,47 @@
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class CameraView extends StatelessWidget {
final CameraController controller;
const CameraView({required this.controller, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final value = controller.value;
return ValueListenableBuilder<CameraValue>(
valueListenable: controller,
builder: (_, __, ___) => AspectRatio(
aspectRatio: _isLandscape(value) ? value.aspectRatio : (1 / value.aspectRatio),
child: RotatedBox(
quarterTurns: _getQuarterTurns(value),
child: controller.buildPreview(),
),
),
);
}
bool _isLandscape(CameraValue value) {
return <DeviceOrientation>[DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]
.contains(_getApplicableOrientation(value));
}
int _getQuarterTurns(CameraValue value) {
final Map<DeviceOrientation, int> turns = <DeviceOrientation, int>{
DeviceOrientation.portraitUp: 0,
DeviceOrientation.landscapeRight: 1,
DeviceOrientation.portraitDown: 2,
DeviceOrientation.landscapeLeft: 3,
};
return turns[_getApplicableOrientation(value)]!;
}
DeviceOrientation _getApplicableOrientation(CameraValue value) {
return value.isRecordingVideo
? value.recordingOrientation!
: (value.previewPauseOrientation ??
value.lockedCaptureOrientation ??
value.deviceOrientation);
}
}

View file

@ -0,0 +1,23 @@
abstract class CameraContainerEvent {
const CameraContainerEvent();
}
class InitializeEvent extends CameraContainerEvent {
const InitializeEvent();
}
class ZoomChangedEvent extends CameraContainerEvent {
final double value;
const ZoomChangedEvent(this.value);
}
class ExposureOffsetChangedEvent extends CameraContainerEvent {
final double value;
const ExposureOffsetChangedEvent(this.value);
}
class ExposureOffsetResetEvent extends CameraContainerEvent {
const ExposureOffsetResetEvent();
}

View file

@ -0,0 +1,50 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/data/models/exposure_pair.dart';
import 'package:lightmeter/data/models/photography_values/iso_value.dart';
import 'package:lightmeter/data/models/photography_values/nd_value.dart';
import 'package:lightmeter/interactors/metering_interactor.dart';
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
import 'bloc_container_camera.dart';
import 'widget_container_camera.dart';
class CameraContainerProvider extends StatelessWidget {
final ExposurePair? fastest;
final ExposurePair? slowest;
final IsoValue iso;
final NdValue nd;
final ValueChanged<IsoValue> onIsoChanged;
final ValueChanged<NdValue> onNdChanged;
final List<ExposurePair> exposurePairs;
const CameraContainerProvider({
required this.fastest,
required this.slowest,
required this.iso,
required this.nd,
required this.onIsoChanged,
required this.onNdChanged,
required this.exposurePairs,
super.key,
});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => CameraContainerBloc(
context.read<MeteringInteractor>(),
context.read<MeteringCommunicationBloc>(),
),
child: CameraContainer(
fastest: fastest,
slowest: slowest,
iso: iso,
nd: nd,
onIsoChanged: onIsoChanged,
onNdChanged: onNdChanged,
exposurePairs: exposurePairs,
),
);
}
}

View file

@ -0,0 +1,40 @@
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
abstract class CameraContainerState {
const CameraContainerState();
}
class CameraInitState extends CameraContainerState {
const CameraInitState();
}
class CameraLoadingState extends CameraContainerState {
const CameraLoadingState();
}
class CameraInitializedState extends CameraContainerState {
final CameraController controller;
const CameraInitializedState(this.controller);
}
class CameraActiveState extends CameraContainerState {
final RangeValues zoomRange;
final double currentZoom;
final RangeValues exposureOffsetRange;
final double? exposureOffsetStep;
final double currentExposureOffset;
const CameraActiveState({
required this.zoomRange,
required this.currentZoom,
required this.exposureOffsetRange,
required this.exposureOffsetStep,
required this.currentExposureOffset,
});
}
class CameraErrorState extends CameraContainerState {
const CameraErrorState();
}

View file

@ -0,0 +1,159 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/data/models/exposure_pair.dart';
import 'package:lightmeter/data/models/photography_values/iso_value.dart';
import 'package:lightmeter/data/models/photography_values/nd_value.dart';
import 'package:lightmeter/platform_config.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/metering/components/camera_container/components/camera_view/widget_camera_view.dart';
import 'package:lightmeter/screens/metering/components/shared/exposure_pairs_list/widget_list_exposure_pairs.dart';
import 'package:lightmeter/screens/metering/components/shared/metering_top_bar/widget_top_bar_metering.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/widget_container_readings.dart';
import 'bloc_container_camera.dart';
import 'components/camera_controls/widget_camera_controls.dart';
import 'event_container_camera.dart';
import 'state_container_camera.dart';
class CameraContainer extends StatelessWidget {
final ExposurePair? fastest;
final ExposurePair? slowest;
final IsoValue iso;
final NdValue nd;
final ValueChanged<IsoValue> onIsoChanged;
final ValueChanged<NdValue> onNdChanged;
final List<ExposurePair> exposurePairs;
const CameraContainer({
required this.fastest,
required this.slowest,
required this.iso,
required this.nd,
required this.onIsoChanged,
required this.onNdChanged,
required this.exposurePairs,
super.key,
});
@override
Widget build(BuildContext context) {
final topBarOverflow = Dimens.readingContainerHeight -
((MediaQuery.of(context).size.width - Dimens.grid8 - 2 * Dimens.paddingM) / 2) /
PlatformConfig.cameraPreviewAspectRatio;
return Column(
children: [
MeteringTopBar(
readingsContainer: ReadingsContainer(
fastest: fastest,
slowest: slowest,
iso: iso,
nd: nd,
onIsoChanged: onIsoChanged,
onNdChanged: onNdChanged,
),
appendixHeight: topBarOverflow,
preview: const _CameraViewBuilder(),
),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM),
child: _MiddleContentWrapper(
topBarOverflow: topBarOverflow,
leftContent: ExposurePairsList(exposurePairs),
rightContent: const _CameraControlsBuilder(),
),
),
),
],
);
}
}
class _CameraViewBuilder extends StatelessWidget {
const _CameraViewBuilder({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return AspectRatio(
aspectRatio: PlatformConfig.cameraPreviewAspectRatio,
child: Center(
child: BlocBuilder<CameraContainerBloc, CameraContainerState>(
buildWhen: (previous, current) => current is CameraInitializedState,
builder: (context, state) => state is CameraInitializedState
? CameraView(controller: state.controller)
: const ColoredBox(color: Colors.black),
),
),
);
}
}
class _CameraControlsBuilder extends StatelessWidget {
const _CameraControlsBuilder();
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: Dimens.paddingM),
child: BlocBuilder<CameraContainerBloc, CameraContainerState>(
builder: (context, state) => AnimatedSwitcher(
duration: Dimens.durationS,
child: state is CameraActiveState
? CameraControls(
exposureOffsetRange: state.exposureOffsetRange,
exposureOffsetValue: state.currentExposureOffset,
onExposureOffsetChanged: (value) {
context.read<CameraContainerBloc>().add(ExposureOffsetChangedEvent(value));
},
zoomRange: state.zoomRange,
zoomValue: state.currentZoom,
onZoomChanged: (value) {
context.read<CameraContainerBloc>().add(ZoomChangedEvent(value));
},
)
: const SizedBox.shrink(),
),
),
);
}
}
class _MiddleContentWrapper extends StatelessWidget {
final double topBarOverflow;
final Widget leftContent;
final Widget rightContent;
const _MiddleContentWrapper({
required this.topBarOverflow,
required this.leftContent,
required this.rightContent,
});
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) => OverflowBox(
alignment: Alignment.bottomCenter,
maxHeight: constraints.maxHeight + topBarOverflow.abs(),
maxWidth: constraints.maxWidth,
child: Row(
children: [
Expanded(
child: Padding(
padding: EdgeInsets.only(top: topBarOverflow >= 0 ? topBarOverflow : 0),
child: leftContent,
),
),
const SizedBox(width: Dimens.grid8),
Expanded(
child: Padding(
padding: EdgeInsets.only(top: topBarOverflow <= 0 ? -topBarOverflow : 0),
child: rightContent,
),
),
],
),
),
);
}
}

View file

@ -0,0 +1,48 @@
import 'dart:async';
import 'package:lightmeter/interactors/metering_interactor.dart';
import 'package:lightmeter/screens/metering/components/shared/ev_source_base/bloc_base_ev_source.dart';
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart'
as communication_event;
import 'package:lightmeter/screens/metering/communication/state_communication_metering.dart'
as communication_states;
import 'package:lightmeter/utils/log_2.dart';
import 'event_container_light_sensor.dart';
import 'state_container_light_sensor.dart';
class LightSensorContainerBloc
extends EvSourceBlocBase<LightSensorContainerEvent, LightSensorContainerState> {
final MeteringInteractor _meteringInteractor;
StreamSubscription<int>? _luxSubscriptions;
LightSensorContainerBloc(
this._meteringInteractor,
MeteringCommunicationBloc communicationBloc,
) : super(
communicationBloc,
const LightSensorInitState(),
);
@override
void onCommunicationState(communication_states.SourceState communicationState) {
if (communicationState is communication_states.MeasureState) {
if (_luxSubscriptions == null) {
_luxSubscriptions = _meteringInteractor.luxStream().listen((event) {
communicationBloc.add(communication_event.MeasuredEvent(
log2(event.toDouble() / 2.5) + _meteringInteractor.lightSensorEvCalibration,
));
});
} else {
_luxSubscriptions?.cancel().then((_) => _luxSubscriptions = null);
}
}
}
@override
Future<void> close() async {
_luxSubscriptions?.cancel().then((_) => _luxSubscriptions = null);
return super.close();
}
}

View file

@ -0,0 +1,3 @@
abstract class LightSensorContainerEvent {
const LightSensorContainerEvent();
}

View file

@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/data/models/exposure_pair.dart';
import 'package:lightmeter/data/models/photography_values/iso_value.dart';
import 'package:lightmeter/data/models/photography_values/nd_value.dart';
import 'package:lightmeter/interactors/metering_interactor.dart';
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
import 'bloc_container_light_sensor.dart';
import 'widget_container_light_sensor.dart';
class LightSensorContainerProvider extends StatelessWidget {
final ExposurePair? fastest;
final ExposurePair? slowest;
final IsoValue iso;
final NdValue nd;
final ValueChanged<IsoValue> onIsoChanged;
final ValueChanged<NdValue> onNdChanged;
final List<ExposurePair> exposurePairs;
const LightSensorContainerProvider({
required this.fastest,
required this.slowest,
required this.iso,
required this.nd,
required this.onIsoChanged,
required this.onNdChanged,
required this.exposurePairs,
super.key,
});
@override
Widget build(BuildContext context) {
return BlocProvider(
lazy: false,
create: (context) => LightSensorContainerBloc(
context.read<MeteringInteractor>(),
context.read<MeteringCommunicationBloc>(),
),
child: LightSensorContainer(
fastest: fastest,
slowest: slowest,
iso: iso,
nd: nd,
onIsoChanged: onIsoChanged,
onNdChanged: onNdChanged,
exposurePairs: exposurePairs,
),
);
}
}

View file

@ -0,0 +1,7 @@
abstract class LightSensorContainerState {
const LightSensorContainerState();
}
class LightSensorInitState extends LightSensorContainerState {
const LightSensorInitState();
}

View file

@ -0,0 +1,53 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/exposure_pair.dart';
import 'package:lightmeter/data/models/photography_values/iso_value.dart';
import 'package:lightmeter/data/models/photography_values/nd_value.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/metering/components/shared/exposure_pairs_list/widget_list_exposure_pairs.dart';
import 'package:lightmeter/screens/metering/components/shared/metering_top_bar/widget_top_bar_metering.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/widget_container_readings.dart';
class LightSensorContainer extends StatelessWidget {
final ExposurePair? fastest;
final ExposurePair? slowest;
final IsoValue iso;
final NdValue nd;
final ValueChanged<IsoValue> onIsoChanged;
final ValueChanged<NdValue> onNdChanged;
final List<ExposurePair> exposurePairs;
const LightSensorContainer({
required this.fastest,
required this.slowest,
required this.iso,
required this.nd,
required this.onIsoChanged,
required this.onNdChanged,
required this.exposurePairs,
super.key,
});
@override
Widget build(BuildContext context) {
return Column(
children: [
MeteringTopBar(
readingsContainer: ReadingsContainer(
fastest: fastest,
slowest: slowest,
iso: iso,
nd: nd,
onIsoChanged: onIsoChanged,
onNdChanged: onNdChanged,
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM),
child: ExposurePairsList(exposurePairs),
),
),
],
);
}
}

View file

@ -4,11 +4,11 @@ import 'package:lightmeter/screens/metering/communication/bloc_communication_met
import 'package:lightmeter/screens/metering/communication/state_communication_metering.dart' import 'package:lightmeter/screens/metering/communication/state_communication_metering.dart'
as communication_states; as communication_states;
abstract class EvSourceBloc<E, S> extends Bloc<E, S> { abstract class EvSourceBlocBase<E, S> extends Bloc<E, S> {
final MeteringCommunicationBloc communicationBloc; final MeteringCommunicationBloc communicationBloc;
late final StreamSubscription<communication_states.SourceState> _communicationSubscription; late final StreamSubscription<communication_states.SourceState> _communicationSubscription;
EvSourceBloc(this.communicationBloc, super.initialState) { EvSourceBlocBase(this.communicationBloc, super.initialState) {
_communicationSubscription = communicationBloc.stream _communicationSubscription = communicationBloc.stream
.where((event) => event is communication_states.SourceState) .where((event) => event is communication_states.SourceState)
.map((event) => event as communication_states.SourceState) .map((event) => event as communication_states.SourceState)
@ -18,7 +18,7 @@ abstract class EvSourceBloc<E, S> extends Bloc<E, S> {
@override @override
Future<void> close() async { Future<void> close() async {
await _communicationSubscription.cancel(); await _communicationSubscription.cancel();
super.close(); return super.close();
} }
void onCommunicationState(communication_states.SourceState communicationState); void onCommunicationState(communication_states.SourceState communicationState);

View file

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/exposure_pair.dart'; import 'package:lightmeter/data/models/exposure_pair.dart';
import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/res/dimens.dart';
import 'components/widget_item_list_exposure_pairs.dart'; import 'components/exposure_pairs_list_item/widget_item_list_exposure_pairs.dart';
class ExposurePairsList extends StatelessWidget { class ExposurePairsList extends StatelessWidget {
final List<ExposurePair> exposurePairs; final List<ExposurePair> exposurePairs;

View file

@ -3,7 +3,7 @@ import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/res/dimens.dart';
class TopBarShape extends CustomPainter { class MeteringTopBarShape extends CustomPainter {
final Color color; final Color color;
/// The appendix is on the left side /// The appendix is on the left side
@ -22,7 +22,7 @@ class TopBarShape extends CustomPainter {
final double appendixHeight; final double appendixHeight;
final double appendixWidth; final double appendixWidth;
TopBarShape({ MeteringTopBarShape({
required this.color, required this.color,
required this.appendixHeight, required this.appendixHeight,
required this.appendixWidth, required this.appendixWidth,
@ -38,8 +38,8 @@ class TopBarShape extends CustomPainter {
RRect.fromLTRBAndCorners( RRect.fromLTRBAndCorners(
0, 0,
0, 0,
0, size.width,
0, size.height,
bottomLeft: circularRadius, bottomLeft: circularRadius,
bottomRight: circularRadius, bottomRight: circularRadius,
), ),

View file

@ -0,0 +1,56 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/metering/components/shared/readings_container/widget_container_readings.dart';
import 'shape_top_bar_metering.dart';
class MeteringTopBar extends StatelessWidget {
final ReadingsContainer readingsContainer;
final double appendixHeight;
final Widget? preview;
const MeteringTopBar({
required this.readingsContainer,
this.preview,
this.appendixHeight = 0.0,
super.key,
});
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: MeteringTopBarShape(
color: Theme.of(context).colorScheme.surface,
appendixWidth: appendixHeight > 0
? MediaQuery.of(context).size.width / 2 - Dimens.grid8 + Dimens.paddingM
: MediaQuery.of(context).size.width / 2 + Dimens.grid8 - Dimens.paddingM,
appendixHeight: appendixHeight,
),
child: Padding(
padding: const EdgeInsets.all(Dimens.paddingM),
child: SafeArea(
bottom: false,
child: MediaQuery(
data: MediaQuery.of(context),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Expanded(child: readingsContainer),
if (preview != null) ...[
const SizedBox(width: Dimens.grid8),
Expanded(
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(Dimens.borderRadiusM)),
child: preview,
),
),
]
],
),
),
),
),
);
}
}

View file

@ -7,7 +7,7 @@ typedef DialogPickerItemBuilder<T extends PhotographyValue> = Widget Function(Bu
typedef DialogPickerEvDifferenceBuilder<T extends PhotographyValue> = String Function( typedef DialogPickerEvDifferenceBuilder<T extends PhotographyValue> = String Function(
T selected, T other); T selected, T other);
class MeteringScreenDialogPicker<T extends PhotographyValue> extends StatefulWidget { class PhotographyValuePickerDialog<T extends PhotographyValue> extends StatefulWidget {
final String title; final String title;
final String subtitle; final String subtitle;
final T initialValue; final T initialValue;
@ -17,7 +17,7 @@ class MeteringScreenDialogPicker<T extends PhotographyValue> extends StatefulWid
final VoidCallback onCancel; final VoidCallback onCancel;
final ValueChanged onSelect; final ValueChanged onSelect;
const MeteringScreenDialogPicker({ const PhotographyValuePickerDialog({
required this.title, required this.title,
required this.subtitle, required this.subtitle,
required this.initialValue, required this.initialValue,
@ -30,11 +30,11 @@ class MeteringScreenDialogPicker<T extends PhotographyValue> extends StatefulWid
}); });
@override @override
State<MeteringScreenDialogPicker<T>> createState() => _MeteringScreenDialogPickerState<T>(); State<PhotographyValuePickerDialog<T>> createState() => _PhotographyValuePickerDialogState<T>();
} }
class _MeteringScreenDialogPickerState<T extends PhotographyValue> class _PhotographyValuePickerDialogState<T extends PhotographyValue>
extends State<MeteringScreenDialogPicker<T>> { extends State<PhotographyValuePickerDialog<T>> {
late T _selectedValue = widget.initialValue; late T _selectedValue = widget.initialValue;
final _scrollController = ScrollController(); final _scrollController = ScrollController();

View file

@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/photography_values/photography_value.dart';
import 'components/animated_dialog/widget_dialog_animated.dart';
import 'components/photography_value_picker_dialog/widget_dialog_picker_photography_value.dart';
class AnimatedDialogPicker<T extends PhotographyValue> extends StatelessWidget {
final _key = GlobalKey<AnimatedDialogState>();
final String title;
final String subtitle;
final T selectedValue;
final List<T> values;
final DialogPickerItemBuilder<T> itemTitleBuilder;
final DialogPickerEvDifferenceBuilder<T> evDifferenceBuilder;
final ValueChanged<T> onChanged;
final Widget closedChild;
AnimatedDialogPicker({
required this.title,
required this.subtitle,
required this.selectedValue,
required this.values,
required this.itemTitleBuilder,
required this.evDifferenceBuilder,
required this.onChanged,
required this.closedChild,
super.key,
});
@override
Widget build(BuildContext context) {
return AnimatedDialog(
key: _key,
closedChild: closedChild,
openedChild: PhotographyValuePickerDialog<T>(
title: title,
subtitle: subtitle,
initialValue: selectedValue,
values: values,
itemTitleBuilder: itemTitleBuilder,
evDifferenceBuilder: evDifferenceBuilder,
onCancel: () {
_key.currentState?.close();
},
onSelect: (value) {
_key.currentState?.close().then((_) => onChanged(value));
},
),
);
}
}

View file

@ -68,11 +68,17 @@ class _ReadingValueBuilder extends StatelessWidget {
Text( Text(
reading.label, reading.label,
style: textTheme.labelMedium?.copyWith(color: textColor), style: textTheme.labelMedium?.copyWith(color: textColor),
maxLines: 1,
overflow: TextOverflow.visible,
softWrap: false,
), ),
const SizedBox(height: Dimens.grid4), const SizedBox(height: Dimens.grid4),
Text( Text(
reading.value, reading.value,
style: textTheme.titleMedium?.copyWith(color: textColor), style: textTheme.titleMedium?.copyWith(color: textColor),
maxLines: 1,
overflow: TextOverflow.visible,
softWrap: false,
), ),
], ],
); );

View file

@ -0,0 +1,128 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/exposure_pair.dart';
import 'package:lightmeter/data/models/photography_values/iso_value.dart';
import 'package:lightmeter/data/models/photography_values/nd_value.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/res/dimens.dart';
import 'components/animated_dialog_picker/widget_dialog_animated_picker.dart';
import 'components/reading_value_container/widget_container_reading_value.dart';
/// Contains a column of fastest & slowest exposure pairs + a row of ISO and ND pickers
class ReadingsContainer extends StatelessWidget {
final ExposurePair? fastest;
final ExposurePair? slowest;
final IsoValue iso;
final NdValue nd;
final ValueChanged<IsoValue> onIsoChanged;
final ValueChanged<NdValue> onNdChanged;
const ReadingsContainer({
required this.fastest,
required this.slowest,
required this.iso,
required this.nd,
required this.onIsoChanged,
required this.onNdChanged,
super.key,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
ReadingValueContainer(
values: [
ReadingValue(
label: S.of(context).fastestExposurePair,
value: fastest != null ? fastest!.toString() : '-',
),
ReadingValue(
label: S.of(context).slowestExposurePair,
value: fastest != null ? slowest!.toString() : '-',
),
],
),
const _InnerPadding(),
Row(
children: [
Expanded(
child: _IsoValueTile(
value: iso,
onChanged: onIsoChanged,
),
),
const _InnerPadding(),
Expanded(
child: _NdValueTile(
value: nd,
onChanged: onNdChanged,
),
),
],
)
],
);
}
}
class _InnerPadding extends SizedBox {
const _InnerPadding() : super(height: Dimens.grid8, width: Dimens.grid8);
}
class _IsoValueTile extends StatelessWidget {
final IsoValue value;
final ValueChanged<IsoValue> onChanged;
const _IsoValueTile({required this.value, required this.onChanged});
@override
Widget build(BuildContext context) {
return AnimatedDialogPicker<IsoValue>(
title: S.of(context).iso,
subtitle: S.of(context).filmSpeed,
selectedValue: value,
values: isoValues,
itemTitleBuilder: (_, value) => Text(value.value.toString()),
// using ascending order, because increase in film speed rises EV
evDifferenceBuilder: (selected, other) => selected.toStringDifference(other),
onChanged: onChanged,
closedChild: ReadingValueContainer.singleValue(
value: ReadingValue(
label: S.of(context).iso,
value: value.value.toString(),
),
),
);
}
}
class _NdValueTile extends StatelessWidget {
final NdValue value;
final ValueChanged<NdValue> onChanged;
const _NdValueTile({required this.value, required this.onChanged});
@override
Widget build(BuildContext context) {
return AnimatedDialogPicker<NdValue>(
title: S.of(context).nd,
subtitle: S.of(context).ndFilterFactor,
selectedValue: value,
values: ndValues,
itemTitleBuilder: (_, value) => Text(
value.value == 0 ? S.of(context).none : value.value.toString(),
),
// using descending order, because ND filter darkens image & lowers EV
evDifferenceBuilder: (selected, other) => other.toStringDifference(selected),
onChanged: onChanged,
closedChild: ReadingValueContainer.singleValue(
value: ReadingValue(
label: S.of(context).iso,
value: value.value.toString(),
),
),
);
}
}

View file

@ -1,62 +0,0 @@
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/platform_config.dart';
import 'package:lightmeter/screens/metering/ev_source/camera/bloc_camera.dart';
import 'package:lightmeter/screens/metering/ev_source/camera/state_camera.dart';
class CameraView extends StatelessWidget {
const CameraView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return AspectRatio(
aspectRatio: PlatformConfig.cameraPreviewAspectRatio,
child: Center(
child: BlocBuilder<CameraBloc, CameraState>(
buildWhen: (previous, current) => current is CameraInitializedState,
builder: (context, state) {
if (state is CameraInitializedState) {
final value = state.controller.value;
return ValueListenableBuilder<CameraValue>(
valueListenable: state.controller,
builder: (_, __, ___) => AspectRatio(
aspectRatio: _isLandscape(value) ? value.aspectRatio : (1 / value.aspectRatio),
child: RotatedBox(
quarterTurns: _getQuarterTurns(value),
child: state.controller.buildPreview(),
),
),
);
}
return const ColoredBox(color: Colors.black);
},
),
),
);
}
bool _isLandscape(CameraValue value) {
return <DeviceOrientation>[DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]
.contains(_getApplicableOrientation(value));
}
int _getQuarterTurns(CameraValue value) {
final Map<DeviceOrientation, int> turns = <DeviceOrientation, int>{
DeviceOrientation.portraitUp: 0,
DeviceOrientation.landscapeRight: 1,
DeviceOrientation.portraitDown: 2,
DeviceOrientation.landscapeLeft: 3,
};
return turns[_getApplicableOrientation(value)]!;
}
DeviceOrientation _getApplicableOrientation(CameraValue value) {
return value.isRecordingVideo
? value.recordingOrientation!
: (value.previewPauseOrientation ??
value.lockedCaptureOrientation ??
value.deviceOrientation);
}
}

View file

@ -1,32 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class SizeRenderWidget extends SingleChildRenderObjectWidget {
final ValueChanged<Size>? onLayout;
const SizeRenderWidget({
super.key,
super.child,
this.onLayout,
});
@override
SizeRenderBox createRenderObject(BuildContext context) => SizeRenderBox(onLayout: onLayout);
}
class SizeRenderBox extends RenderProxyBox {
final ValueChanged<Size>? onLayout;
SizeRenderBox({this.onLayout});
@override
void performLayout() {
if (child != null) {
child!.layout(constraints, parentUsesSize: true);
size = child!.size;
} else {
size = computeSizeForNoChild(constraints);
}
onLayout?.call(size);
}
}

View file

@ -1,233 +0,0 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/platform_config.dart';
import 'package:lightmeter/screens/metering/components/topbar/shape_topbar.dart';
import 'package:lightmeter/screens/metering/components/topbar/components/widget_size_render.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/data/models/exposure_pair.dart';
import 'package:lightmeter/data/models/photography_values/iso_value.dart';
import 'package:lightmeter/data/models/photography_values/nd_value.dart';
import 'package:lightmeter/data/models/photography_values/photography_value.dart';
import 'package:lightmeter/res/dimens.dart';
import 'components/widget_camera_preview.dart';
import 'components/shared/widget_dialog_animated.dart';
import 'components/widget_dialog_picker.dart';
import 'components/container_reading_value.dart';
class MeteringTopBar extends StatefulWidget {
final ExposurePair? fastest;
final ExposurePair? slowest;
final double ev;
final IsoValue iso;
final NdValue nd;
final ValueChanged<IsoValue> onIsoChanged;
final ValueChanged<NdValue> onNdChanged;
final ValueChanged<double> onCutoutLayout;
const MeteringTopBar({
required this.fastest,
required this.slowest,
required this.ev,
required this.iso,
required this.nd,
required this.onIsoChanged,
required this.onNdChanged,
required this.onCutoutLayout,
super.key,
});
@override
State<MeteringTopBar> createState() => _MeteringTopBarState();
}
class _MeteringTopBarState extends State<MeteringTopBar> {
double stepHeight = 0.0;
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: TopBarShape(
color: Theme.of(context).colorScheme.surface,
appendixWidth: stepHeight > 0
? MediaQuery.of(context).size.width / 2 - Dimens.grid8 + Dimens.paddingM
: MediaQuery.of(context).size.width / 2 + Dimens.grid8 - Dimens.paddingM,
appendixHeight: stepHeight,
),
child: Padding(
padding: const EdgeInsets.all(Dimens.paddingM),
child: SafeArea(
bottom: false,
child: MediaQuery(
data: MediaQuery.of(context),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: SizeRenderWidget(
onLayout: (size) => _onReadingsLayout(size.height),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
ReadingValueContainer(
values: [
ReadingValue(
label: S.of(context).fastestExposurePair,
value: widget.fastest != null ? widget.fastest!.toString() : '-',
),
ReadingValue(
label: S.of(context).slowestExposurePair,
value: widget.fastest != null ? widget.slowest!.toString() : '-',
),
],
),
/*
const _InnerPadding(),
ReadingValueContainer.singleValue(
value: ReadingValue(
label: 'EV',
value: ev.toStringAsFixed(1),
),
),
*/
const _InnerPadding(),
Row(
children: [
Expanded(
child: _IsoValueTile(
value: widget.iso,
onChanged: widget.onIsoChanged,
),
),
const _InnerPadding(),
Expanded(
child: _NdValueTile(
value: widget.nd,
onChanged: widget.onNdChanged,
),
),
],
)
],
),
),
),
const _InnerPadding(),
const Expanded(
child: ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(Dimens.borderRadiusM)),
child: CameraView(),
),
),
],
),
),
),
),
);
}
void _onReadingsLayout(double readingsSectionHeight) {
stepHeight = readingsSectionHeight -
((MediaQuery.of(context).size.width - Dimens.grid8 - 2 * Dimens.paddingM) / 2) /
PlatformConfig.cameraPreviewAspectRatio;
widget.onCutoutLayout(stepHeight);
}
}
class _InnerPadding extends SizedBox {
const _InnerPadding() : super(height: Dimens.grid8, width: Dimens.grid8);
}
class _IsoValueTile extends StatelessWidget {
final IsoValue value;
final ValueChanged<IsoValue> onChanged;
const _IsoValueTile({required this.value, required this.onChanged});
@override
Widget build(BuildContext context) {
return _AnimatedDialogPicker<IsoValue>(
title: S.of(context).iso,
subtitle: S.of(context).filmSpeed,
selectedValue: value,
values: isoValues,
itemTitleBuilder: (_, value) => Text(value.value.toString()),
// using ascending order, because increase in film speed rises EV
evDifferenceBuilder: (selected, other) => selected.toStringDifference(other),
onChanged: onChanged,
);
}
}
class _NdValueTile extends StatelessWidget {
final NdValue value;
final ValueChanged<NdValue> onChanged;
const _NdValueTile({required this.value, required this.onChanged});
@override
Widget build(BuildContext context) {
return _AnimatedDialogPicker<NdValue>(
title: S.of(context).nd,
subtitle: S.of(context).ndFilterFactor,
selectedValue: value,
values: ndValues,
itemTitleBuilder: (_, value) => Text(
value.value == 0 ? S.of(context).none : value.value.toString(),
),
// using descending order, because ND filter darkens image & lowers EV
evDifferenceBuilder: (selected, other) => other.toStringDifference(selected),
onChanged: onChanged,
);
}
}
class _AnimatedDialogPicker<T extends PhotographyValue> extends StatelessWidget {
final _key = GlobalKey<AnimatedDialogState>();
final String title;
final String subtitle;
final T selectedValue;
final List<T> values;
final DialogPickerItemBuilder<T> itemTitleBuilder;
final DialogPickerEvDifferenceBuilder<T> evDifferenceBuilder;
final ValueChanged<T> onChanged;
_AnimatedDialogPicker({
required this.title,
required this.subtitle,
required this.selectedValue,
required this.values,
required this.itemTitleBuilder,
required this.evDifferenceBuilder,
required this.onChanged,
}) : super();
@override
Widget build(BuildContext context) {
return AnimatedDialog(
key: _key,
closedChild: ReadingValueContainer.singleValue(
value: ReadingValue(
label: title,
value: selectedValue.value.toString(),
),
),
openedChild: MeteringScreenDialogPicker<T>(
title: title,
subtitle: subtitle,
initialValue: selectedValue,
values: values,
itemTitleBuilder: itemTitleBuilder,
evDifferenceBuilder: evDifferenceBuilder,
onCancel: () {
_key.currentState?.close();
},
onSelect: (value) {
_key.currentState?.close().then((_) => onChanged(value));
},
),
);
}
}

View file

@ -1,23 +0,0 @@
abstract class CameraEvent {
const CameraEvent();
}
class InitializeEvent extends CameraEvent {
const InitializeEvent();
}
class ZoomChangedEvent extends CameraEvent {
final double value;
const ZoomChangedEvent(this.value);
}
class ExposureOffsetChangedEvent extends CameraEvent {
final double value;
const ExposureOffsetChangedEvent(this.value);
}
class ExposureOffsetResetEvent extends CameraEvent {
const ExposureOffsetResetEvent();
}

View file

@ -1,43 +0,0 @@
import 'package:camera/camera.dart';
abstract class CameraState {
const CameraState();
}
class CameraInitState extends CameraState {
const CameraInitState();
}
class CameraLoadingState extends CameraState {
const CameraLoadingState();
}
class CameraInitializedState extends CameraState {
final CameraController controller;
const CameraInitializedState(this.controller);
}
class CameraActiveState extends CameraState {
final double minZoom;
final double maxZoom;
final double currentZoom;
final double minExposureOffset;
final double maxExposureOffset;
final double? exposureOffsetStep;
final double currentExposureOffset;
const CameraActiveState({
required this.minZoom,
required this.maxZoom,
required this.currentZoom,
required this.minExposureOffset,
required this.maxExposureOffset,
required this.exposureOffsetStep,
required this.currentExposureOffset,
});
}
class CameraErrorState extends CameraState {
const CameraErrorState();
}

View file

@ -1,27 +0,0 @@
import 'dart:math';
import 'package:lightmeter/screens/metering/ev_source/ev_source_bloc.dart';
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart'
as communication_event;
import 'package:lightmeter/screens/metering/communication/state_communication_metering.dart'
as communication_states;
import 'event_random_ev.dart';
import 'state_random_ev.dart';
class RandomEvBloc extends EvSourceBloc<RandomEvEvent, RandomEvState> {
final random = Random();
RandomEvBloc(MeteringCommunicationBloc communicationBloc)
: super(
communicationBloc,
RandomEvState(Random().nextDouble() * 15),
);
@override
void onCommunicationState(communication_states.SourceState communicationState) {
if (communicationState is communication_states.MeasureState) {
communicationBloc.add(communication_event.MeasuredEvent(random.nextDouble() * 15));
}
}
}

View file

@ -1,3 +0,0 @@
abstract class RandomEvEvent {
const RandomEvEvent();
}

View file

@ -1,5 +0,0 @@
class RandomEvState {
final double ev;
const RandomEvState(this.ev);
}

View file

@ -1,27 +1,31 @@
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/haptics_service.dart'; import 'package:lightmeter/data/haptics_service.dart';
import 'package:lightmeter/data/models/ev_source_type.dart'; import 'package:lightmeter/data/light_sensor_service.dart';
import 'package:lightmeter/data/models/photography_values/photography_value.dart'; import 'package:lightmeter/data/models/photography_values/photography_value.dart';
import 'package:lightmeter/data/shared_prefs_service.dart'; import 'package:lightmeter/data/shared_prefs_service.dart';
import 'package:lightmeter/interactors/metering_interactor.dart'; import 'package:lightmeter/interactors/metering_interactor.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'ev_source/camera/bloc_camera.dart';
import 'ev_source/random_ev/bloc_random_ev.dart';
import 'bloc_metering.dart'; import 'bloc_metering.dart';
import 'communication/bloc_communication_metering.dart'; import 'communication/bloc_communication_metering.dart';
import 'screen_metering.dart'; import 'screen_metering.dart';
class MeteringFlow extends StatelessWidget { class MeteringFlow extends StatefulWidget {
const MeteringFlow({super.key}); const MeteringFlow({super.key});
@override
State<MeteringFlow> createState() => _MeteringFlowState();
}
class _MeteringFlowState extends State<MeteringFlow> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Provider( return Provider(
create: (context) => MeteringInteractor( create: (context) => MeteringInteractor(
context.read<UserPreferencesService>(), context.read<UserPreferencesService>(),
context.read<HapticsService>(), context.read<HapticsService>(),
context.read<LightSensorService>(),
), ),
child: MultiBlocProvider( child: MultiBlocProvider(
providers: [ providers: [
@ -34,17 +38,6 @@ class MeteringFlow extends StatelessWidget {
context.read<StopType>(), context.read<StopType>(),
), ),
), ),
BlocProvider(
create: (context) => CameraBloc(
context.read<MeteringCommunicationBloc>(),
context.read<MeteringInteractor>(),
),
),
if (context.read<EvSourceType>() == EvSourceType.sensor)
BlocProvider(
lazy: false,
create: (context) => RandomEvBloc(context.read<MeteringCommunicationBloc>()),
),
], ],
child: const MeteringScreen(), child: const MeteringScreen(),
), ),

View file

@ -1,13 +1,13 @@
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/ev_source_type.dart';
import 'package:lightmeter/data/models/photography_values/photography_value.dart'; import 'package:lightmeter/data/models/photography_values/photography_value.dart';
import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/environment.dart';
import 'package:lightmeter/providers/ev_source_type_provider.dart';
import 'components/bottom_controls/widget_bottom_controls.dart'; import 'components/bottom_controls/widget_bottom_controls.dart';
import 'components/camera/widget_exposure_slider.dart'; import 'components/camera_container/provider_container_camera.dart';
import 'components/camera/widget_zoom_camera.dart'; import 'components/light_sensor_container/provider_container_light_sensor.dart';
import 'components/exposure_pairs_list/widget_list_exposure_pairs.dart';
import 'components/topbar/widget_topbar.dart';
import 'bloc_metering.dart'; import 'bloc_metering.dart';
import 'event_metering.dart'; import 'event_metering.dart';
import 'state_metering.dart'; import 'state_metering.dart';
@ -20,8 +20,6 @@ class MeteringScreen extends StatefulWidget {
} }
class _MeteringScreenState extends State<MeteringScreen> { class _MeteringScreenState extends State<MeteringScreen> {
double topBarOverflow = 0.0;
MeteringBloc get _bloc => context.read<MeteringBloc>(); MeteringBloc get _bloc => context.read<MeteringBloc>();
@override @override
@ -34,85 +32,40 @@ class _MeteringScreenState extends State<MeteringScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: Theme.of(context).colorScheme.background, backgroundColor: Theme.of(context).colorScheme.background,
body: BlocBuilder<MeteringBloc, MeteringState>( body: Column(
builder: (context, state) => Column(
children: [ children: [
MeteringTopBar( Expanded(
child: BlocBuilder<MeteringBloc, MeteringState>(
builder: (context, state) => context.watch<EvSourceType>() == EvSourceType.camera
? CameraContainerProvider(
fastest: state.fastest, fastest: state.fastest,
slowest: state.slowest, slowest: state.slowest,
ev: state.ev,
iso: state.iso, iso: state.iso,
nd: state.nd, nd: state.nd,
onIsoChanged: (value) => _bloc.add(IsoChangedEvent(value)), onIsoChanged: (value) => _bloc.add(IsoChangedEvent(value)),
onNdChanged: (value) => _bloc.add(NdChangedEvent(value)), onNdChanged: (value) => _bloc.add(NdChangedEvent(value)),
onCutoutLayout: (value) => topBarOverflow = value, exposurePairs: state.exposurePairs,
), )
Expanded( : LightSensorContainerProvider(
child: Padding( fastest: state.fastest,
padding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM), slowest: state.slowest,
child: _MiddleContentWrapper( iso: state.iso,
topBarOverflow: topBarOverflow, nd: state.nd,
leftContent: ExposurePairsList(state.exposurePairs), onIsoChanged: (value) => _bloc.add(IsoChangedEvent(value)),
rightContent: Padding( onNdChanged: (value) => _bloc.add(NdChangedEvent(value)),
padding: const EdgeInsets.symmetric(vertical: Dimens.paddingM), exposurePairs: state.exposurePairs,
child: Column(
children: const [
Expanded(child: CameraExposureSlider()),
SizedBox(height: Dimens.grid24),
CameraZoomSlider(),
],
),
),
), ),
), ),
), ),
MeteringBottomControls( MeteringBottomControls(
onSwitchEvSourceType: context.read<Environment>().hasLightSensor
? EvSourceTypeProvider.of(context).toggleType
: null,
onMeasure: () => _bloc.add(const MeasureEvent()), onMeasure: () => _bloc.add(const MeasureEvent()),
onSettings: () => Navigator.pushNamed(context, 'settings'), onSettings: () => Navigator.pushNamed(context, 'settings'),
), ),
], ],
), ),
),
);
}
}
class _MiddleContentWrapper extends StatelessWidget {
final double topBarOverflow;
final Widget leftContent;
final Widget rightContent;
const _MiddleContentWrapper({
required this.topBarOverflow,
required this.leftContent,
required this.rightContent,
});
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) => OverflowBox(
alignment: Alignment.bottomCenter,
maxHeight: constraints.maxHeight + topBarOverflow.abs(),
maxWidth: constraints.maxWidth,
child: Row(
children: [
Expanded(
child: Padding(
padding: EdgeInsets.only(top: topBarOverflow >= 0 ? topBarOverflow : 0),
child: leftContent,
),
),
const SizedBox(width: Dimens.grid8),
Expanded(
child: Padding(
padding: EdgeInsets.only(top: topBarOverflow <= 0 ? -topBarOverflow : 0),
child: rightContent,
),
),
],
),
),
); );
} }
} }

View file

@ -8,26 +8,47 @@ class CalibrationDialogBloc extends Bloc<CalibrationDialogEvent, CalibrationDial
final SettingsInteractor _settingsInteractor; final SettingsInteractor _settingsInteractor;
late double _cameraEvCalibration = _settingsInteractor.cameraEvCalibration; late double _cameraEvCalibration = _settingsInteractor.cameraEvCalibration;
late double _lightSensorEvCalibration = _settingsInteractor.lightSensorEvCalibration;
CalibrationDialogBloc(this._settingsInteractor) CalibrationDialogBloc(this._settingsInteractor)
: super(CalibrationDialogState(_settingsInteractor.cameraEvCalibration)) { : super(
CalibrationDialogState(
_settingsInteractor.cameraEvCalibration,
_settingsInteractor.lightSensorEvCalibration,
),
) {
on<CameraEvCalibrationChangedEvent>(_onCameraEvCalibrationChanged); on<CameraEvCalibrationChangedEvent>(_onCameraEvCalibrationChanged);
on<CameraEvCalibrationResetEvent>(_onCameraEvCalibrationReset); on<CameraEvCalibrationResetEvent>(_onCameraEvCalibrationReset);
on<LightSensorEvCalibrationChangedEvent>(_onLightSensorEvCalibrationChanged);
on<LightSensorEvCalibrationResetEvent>(_onLightSensorEvCalibrationReset);
on<SaveCalibrationDialogEvent>(_onSaveCalibration); on<SaveCalibrationDialogEvent>(_onSaveCalibration);
} }
void _onCameraEvCalibrationChanged(CameraEvCalibrationChangedEvent event, Emitter emit) { void _onCameraEvCalibrationChanged(CameraEvCalibrationChangedEvent event, Emitter emit) {
_cameraEvCalibration = event.value; _cameraEvCalibration = event.value;
emit(CalibrationDialogState(event.value)); emit(CalibrationDialogState(_cameraEvCalibration, _lightSensorEvCalibration));
} }
void _onCameraEvCalibrationReset(CameraEvCalibrationResetEvent event, Emitter emit) { void _onCameraEvCalibrationReset(CameraEvCalibrationResetEvent event, Emitter emit) {
_settingsInteractor.quickVibration(); _settingsInteractor.quickVibration();
_cameraEvCalibration = 0; _cameraEvCalibration = 0;
emit(CalibrationDialogState(_cameraEvCalibration)); emit(CalibrationDialogState(_cameraEvCalibration, _lightSensorEvCalibration));
}
void _onLightSensorEvCalibrationChanged(
LightSensorEvCalibrationChangedEvent event, Emitter emit) {
_lightSensorEvCalibration = event.value;
emit(CalibrationDialogState(_cameraEvCalibration, _lightSensorEvCalibration));
}
void _onLightSensorEvCalibrationReset(LightSensorEvCalibrationResetEvent event, Emitter emit) {
_settingsInteractor.quickVibration();
_lightSensorEvCalibration = 0;
emit(CalibrationDialogState(_cameraEvCalibration, _lightSensorEvCalibration));
} }
void _onSaveCalibration(SaveCalibrationDialogEvent event, __) { void _onSaveCalibration(SaveCalibrationDialogEvent event, __) {
_settingsInteractor.setCameraEvCalibration(_cameraEvCalibration); _settingsInteractor.setCameraEvCalibration(_cameraEvCalibration);
_settingsInteractor.setLightSensorEvCalibration(_lightSensorEvCalibration);
} }
} }

View file

@ -12,6 +12,16 @@ class CameraEvCalibrationResetEvent extends CalibrationDialogEvent {
const CameraEvCalibrationResetEvent(); const CameraEvCalibrationResetEvent();
} }
class LightSensorEvCalibrationChangedEvent extends CalibrationDialogEvent {
final double value;
const LightSensorEvCalibrationChangedEvent(this.value);
}
class LightSensorEvCalibrationResetEvent extends CalibrationDialogEvent {
const LightSensorEvCalibrationResetEvent();
}
class SaveCalibrationDialogEvent extends CalibrationDialogEvent { class SaveCalibrationDialogEvent extends CalibrationDialogEvent {
const SaveCalibrationDialogEvent(); const SaveCalibrationDialogEvent();
} }

View file

@ -1,5 +1,9 @@
class CalibrationDialogState { class CalibrationDialogState {
final double cameraEvCalibration; final double cameraEvCalibration;
final double lightSensorEvCalibration;
const CalibrationDialogState(this.cameraEvCalibration); const CalibrationDialogState(
this.cameraEvCalibration,
this.lightSensorEvCalibration,
);
} }

View file

@ -1,5 +1,6 @@
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/environment.dart';
import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/settings/components/calibration/components/calibration_dialog/event_dialog_calibration.dart'; import 'package:lightmeter/screens/settings/components/calibration/components/calibration_dialog/event_dialog_calibration.dart';
@ -9,18 +10,12 @@ import 'package:lightmeter/utils/to_string_signed.dart';
import 'bloc_dialog_calibration.dart'; import 'bloc_dialog_calibration.dart';
import 'state_dialog_calibration.dart'; import 'state_dialog_calibration.dart';
class CalibrationDialog extends StatefulWidget { class CalibrationDialog extends StatelessWidget {
const CalibrationDialog({super.key}); const CalibrationDialog({super.key});
@override
State<CalibrationDialog> createState() => _CalibrationDialogState();
}
class _CalibrationDialogState extends State<CalibrationDialog> {
CalibrationDialogBloc get bloc => context.read<CalibrationDialogBloc>();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final bool hasLightSensor = context.read<Environment>().hasLightSensor;
return AlertDialog( return AlertDialog(
titlePadding: const EdgeInsets.fromLTRB( titlePadding: const EdgeInsets.fromLTRB(
Dimens.paddingL, Dimens.paddingL,
@ -30,17 +25,44 @@ class _CalibrationDialogState extends State<CalibrationDialog> {
), ),
title: Text(S.of(context).calibration), title: Text(S.of(context).calibration),
contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingL), contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingL),
content: BlocBuilder<CalibrationDialogBloc, CalibrationDialogState>( content: SingleChildScrollView(
builder: (context, state) => Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text(S.of(context).calibrationMessage), Text(
hasLightSensor
? S.of(context).calibrationMessage
: S.of(context).calibrationMessageCameraOnly,
),
const SizedBox(height: Dimens.grid16), const SizedBox(height: Dimens.grid16),
_CalibrationUnit( BlocBuilder<CalibrationDialogBloc, CalibrationDialogState>(
buildWhen: (previous, current) =>
previous.cameraEvCalibration != current.cameraEvCalibration,
builder: (context, state) => _CalibrationUnit(
title: S.of(context).camera, title: S.of(context).camera,
value: state.cameraEvCalibration, value: state.cameraEvCalibration,
onChanged: (value) => bloc.add(CameraEvCalibrationChangedEvent(value)), onChanged: (value) => context
onReset: () => bloc.add(const CameraEvCalibrationResetEvent()), .read<CalibrationDialogBloc>()
.add(CameraEvCalibrationChangedEvent(value)),
onReset: () => context
.read<CalibrationDialogBloc>()
.add(const CameraEvCalibrationResetEvent()),
),
),
if (hasLightSensor)
BlocBuilder<CalibrationDialogBloc, CalibrationDialogState>(
buildWhen: (previous, current) =>
previous.lightSensorEvCalibration != current.lightSensorEvCalibration,
builder: (context, state) => _CalibrationUnit(
title: S.of(context).lightSensor,
value: state.lightSensorEvCalibration,
onChanged: (value) => context
.read<CalibrationDialogBloc>()
.add(LightSensorEvCalibrationChangedEvent(value)),
onReset: () => context
.read<CalibrationDialogBloc>()
.add(const LightSensorEvCalibrationResetEvent()),
),
), ),
], ],
), ),
@ -58,7 +80,7 @@ class _CalibrationDialogState extends State<CalibrationDialog> {
), ),
TextButton( TextButton(
onPressed: () { onPressed: () {
bloc.add(const SaveCalibrationDialogEvent()); context.read<CalibrationDialogBloc>().add(const SaveCalibrationDialogEvent());
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
child: Text(S.of(context).save), child: Text(S.of(context).save),

View file

@ -17,6 +17,7 @@ dependencies:
sdk: flutter sdk: flutter
intl: 0.17.0 intl: 0.17.0
intl_utils: 2.8.1 intl_utils: 2.8.1
light_sensor: 2.0.2
material_color_utilities: 0.2.0 material_color_utilities: 0.2.0
package_info_plus: 3.0.2 package_info_plus: 3.0.2
permission_handler: 10.2.0 permission_handler: 10.2.0