From 9ffb5112c1eb6411cd4766c9b4fb45bc06e71fd6 Mon Sep 17 00:00:00 2001 From: Vadim <44135514+vodemn@users.noreply.github.com> Date: Sun, 29 Jan 2023 19:57:47 +0300 Subject: [PATCH] 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 --- README.md | 2 +- lib/application.dart | 93 +++---- lib/data/light_sensor_service.dart | 9 + lib/data/shared_prefs_service.dart | 11 +- lib/environment.dart | 16 +- lib/interactors/metering_interactor.dart | 16 ++ lib/interactors/settings_interactor.dart | 3 + lib/l10n/intl_en.arb | 4 +- lib/providers/ev_source_type_provider.dart | 65 +++++ lib/res/dimens.dart | 4 + .../bloc_communication_metering.dart | 4 +- .../components/widget_button_measure.dart | 5 + .../widget_bottom_controls.dart | 16 +- .../camera/widget_exposure_slider.dart | 94 ------- .../components/camera/widget_zoom_camera.dart | 31 --- .../bloc_container_camera.dart} | 27 +- .../widget_slider_exposure_offset.dart | 91 +++++++ .../zoom_slider/widget_slider_zoom.dart | 26 ++ .../widget_camera_controls.dart | 46 ++++ .../camera_view/widget_camera_view.dart | 47 ++++ .../event_container_camera.dart | 23 ++ .../provider_container_camera.dart | 50 ++++ .../state_container_camera.dart | 40 +++ .../widget_container_camera.dart | 159 ++++++++++++ .../bloc_container_light_sensor.dart | 48 ++++ .../event_container_light_sensor.dart | 3 + .../provider_container_light_sensor.dart | 51 ++++ .../state_container_light_sensor.dart | 7 + .../widget_container_light_sensor.dart | 53 ++++ .../ev_source_base/bloc_base_ev_source.dart} | 6 +- .../widget_item_list_exposure_pairs.dart | 0 .../widget_list_exposure_pairs.dart | 2 +- .../shape_top_bar_metering.dart} | 8 +- .../widget_top_bar_metering.dart | 56 +++++ .../widget_dialog_animated.dart | 0 ...dget_dialog_picker_photography_value.dart} | 10 +- .../widget_dialog_animated_picker.dart | 51 ++++ .../widget_container_reading_value.dart} | 6 + .../widget_container_readings.dart | 128 ++++++++++ .../components/widget_camera_preview.dart | 62 ----- .../topbar/components/widget_size_render.dart | 32 --- .../components/topbar/widget_topbar.dart | 233 ------------------ .../ev_source/camera/event_camera.dart | 23 -- .../ev_source/camera/state_camera.dart | 43 ---- .../ev_source/random_ev/bloc_random_ev.dart | 27 -- .../ev_source/random_ev/event_random_ev.dart | 3 - .../ev_source/random_ev/state_random_ev.dart | 5 - lib/screens/metering/flow_metering.dart | 23 +- lib/screens/metering/screen_metering.dart | 119 +++------ .../bloc_dialog_calibration.dart | 27 +- .../event_dialog_calibration.dart | 10 + .../state_dialog_calibration.dart | 6 +- .../widget_dialog_calibration.dart | 58 +++-- pubspec.yaml | 1 + 54 files changed, 1230 insertions(+), 753 deletions(-) create mode 100644 lib/data/light_sensor_service.dart create mode 100644 lib/providers/ev_source_type_provider.dart delete mode 100644 lib/screens/metering/components/camera/widget_exposure_slider.dart delete mode 100644 lib/screens/metering/components/camera/widget_zoom_camera.dart rename lib/screens/metering/{ev_source/camera/bloc_camera.dart => components/camera_container/bloc_container_camera.dart} (91%) create mode 100644 lib/screens/metering/components/camera_container/components/camera_controls/components/exposure_offset_slider/widget_slider_exposure_offset.dart create mode 100644 lib/screens/metering/components/camera_container/components/camera_controls/components/zoom_slider/widget_slider_zoom.dart create mode 100644 lib/screens/metering/components/camera_container/components/camera_controls/widget_camera_controls.dart create mode 100644 lib/screens/metering/components/camera_container/components/camera_view/widget_camera_view.dart create mode 100644 lib/screens/metering/components/camera_container/event_container_camera.dart create mode 100644 lib/screens/metering/components/camera_container/provider_container_camera.dart create mode 100644 lib/screens/metering/components/camera_container/state_container_camera.dart create mode 100644 lib/screens/metering/components/camera_container/widget_container_camera.dart create mode 100644 lib/screens/metering/components/light_sensor_container/bloc_container_light_sensor.dart create mode 100644 lib/screens/metering/components/light_sensor_container/event_container_light_sensor.dart create mode 100644 lib/screens/metering/components/light_sensor_container/provider_container_light_sensor.dart create mode 100644 lib/screens/metering/components/light_sensor_container/state_container_light_sensor.dart create mode 100644 lib/screens/metering/components/light_sensor_container/widget_container_light_sensor.dart rename lib/screens/metering/{ev_source/ev_source_bloc.dart => components/shared/ev_source_base/bloc_base_ev_source.dart} (85%) rename lib/screens/metering/components/{exposure_pairs_list/components => shared/exposure_pairs_list/components/exposure_pairs_list_item}/widget_item_list_exposure_pairs.dart (100%) rename lib/screens/metering/components/{ => shared}/exposure_pairs_list/widget_list_exposure_pairs.dart (97%) rename lib/screens/metering/components/{topbar/shape_topbar.dart => shared/metering_top_bar/shape_top_bar_metering.dart} (96%) create mode 100644 lib/screens/metering/components/shared/metering_top_bar/widget_top_bar_metering.dart rename lib/screens/metering/components/{topbar/components/shared => shared/readings_container/components/animated_dialog_picker/components/animated_dialog}/widget_dialog_animated.dart (100%) rename lib/screens/metering/components/{topbar/components/widget_dialog_picker.dart => shared/readings_container/components/animated_dialog_picker/components/photography_value_picker_dialog/widget_dialog_picker_photography_value.dart} (93%) create mode 100644 lib/screens/metering/components/shared/readings_container/components/animated_dialog_picker/widget_dialog_animated_picker.dart rename lib/screens/metering/components/{topbar/components/container_reading_value.dart => shared/readings_container/components/reading_value_container/widget_container_reading_value.dart} (91%) create mode 100644 lib/screens/metering/components/shared/readings_container/widget_container_readings.dart delete mode 100644 lib/screens/metering/components/topbar/components/widget_camera_preview.dart delete mode 100644 lib/screens/metering/components/topbar/components/widget_size_render.dart delete mode 100644 lib/screens/metering/components/topbar/widget_topbar.dart delete mode 100644 lib/screens/metering/ev_source/camera/event_camera.dart delete mode 100644 lib/screens/metering/ev_source/camera/state_camera.dart delete mode 100644 lib/screens/metering/ev_source/random_ev/bloc_random_ev.dart delete mode 100644 lib/screens/metering/ev_source/random_ev/event_random_ev.dart delete mode 100644 lib/screens/metering/ev_source/random_ev/state_random_ev.dart diff --git a/README.md b/README.md index 051ec60..d31d67c 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ The list of features that the old lightmeter app has and that have to be impleme - [x] ISO selecting - [ ] Reciprocity for different films - [x] Reflected light metering -- [ ] Incident light metering +- [x] Incident light metering ### Adjust - [x] Light sources EV calibration diff --git a/lib/application.dart b/lib/application.dart index b68b8ca..583fc18 100644 --- a/lib/application.dart +++ b/lib/application.dart @@ -1,22 +1,24 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter/services.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/models/ev_source_type.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'data/light_sensor_service.dart'; import 'data/permissions_service.dart'; import 'data/shared_prefs_service.dart'; import 'environment.dart'; import 'generated/l10n.dart'; +import 'providers/ev_source_type_provider.dart'; import 'res/theme.dart'; import 'screens/metering/flow_metering.dart'; import 'screens/settings/flow_settings.dart'; import 'utils/stop_type_provider.dart'; -final RouteObserver routeObserver = RouteObserver(); - class Application extends StatelessWidget { final Environment env; @@ -24,55 +26,60 @@ class Application extends StatelessWidget { @override Widget build(BuildContext context) { - return FutureBuilder( - future: SharedPreferences.getInstance(), + return FutureBuilder( + future: Future.wait([ + SharedPreferences.getInstance(), + Platform.isAndroid ? LightSensor.hasSensor : Future.value(false), + ]), builder: (_, snapshot) { if (snapshot.data != null) { return MultiProvider( providers: [ - Provider.value(value: env), - Provider.value(value: EvSourceType.camera), - Provider(create: (_) => UserPreferencesService(snapshot.data!)), + Provider.value(value: env.copyWith(hasLightSensor: snapshot.data![1] as bool)), + Provider(create: (_) => UserPreferencesService(snapshot.data![0] as SharedPreferences)), Provider(create: (_) => const HapticsService()), Provider(create: (_) => PermissionsService()), + Provider(create: (_) => const LightSensorService()), ], child: StopTypeProvider( - child: ThemeProvider( - builder: (context, _) { - final systemIconsBrightness = ThemeData.estimateBrightnessForColor( - context.watch().colorScheme.onSurface, - ); - return AnnotatedRegion( - value: SystemUiOverlayStyle( - statusBarColor: Colors.transparent, - statusBarBrightness: systemIconsBrightness == Brightness.light - ? Brightness.dark - : Brightness.light, - statusBarIconBrightness: systemIconsBrightness, - systemNavigationBarColor: context.watch().colorScheme.surface, - systemNavigationBarIconBrightness: systemIconsBrightness, - ), - child: MaterialApp( - theme: context.watch(), - localizationsDelegates: const [ - S.delegate, - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: S.delegate.supportedLocales, - builder: (context, child) => MediaQuery( - data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0), - child: child!, + child: EvSourceTypeProvider( + child: ThemeProvider( + builder: (context, _) { + final systemIconsBrightness = ThemeData.estimateBrightnessForColor( + context.watch().colorScheme.onSurface, + ); + return AnnotatedRegion( + value: SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarBrightness: systemIconsBrightness == Brightness.light + ? Brightness.dark + : Brightness.light, + statusBarIconBrightness: systemIconsBrightness, + systemNavigationBarColor: context.watch().colorScheme.surface, + systemNavigationBarIconBrightness: systemIconsBrightness, ), - initialRoute: "metering", - routes: { - "metering": (context) => const MeteringFlow(), - "settings": (context) => const SettingsFlow(), - }, - ), - ); - }, + child: MaterialApp( + theme: context.watch(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: S.delegate.supportedLocales, + builder: (context, child) => MediaQuery( + data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0), + child: child!, + ), + initialRoute: "metering", + routes: { + "metering": (context) => const MeteringFlow(), + "settings": (context) => const SettingsFlow(), + }, + ), + ); + }, + ), ), ), ); diff --git a/lib/data/light_sensor_service.dart b/lib/data/light_sensor_service.dart new file mode 100644 index 0000000..2eadebd --- /dev/null +++ b/lib/data/light_sensor_service.dart @@ -0,0 +1,9 @@ +import 'package:light_sensor/light_sensor.dart'; + +class LightSensorService { + const LightSensorService(); + + Future hasSensor() async => await LightSensor.hasSensor ?? false; + + Stream luxStream() => LightSensor.lightSensorStream; +} diff --git a/lib/data/shared_prefs_service.dart b/lib/data/shared_prefs_service.dart index 686c0d1..5665469 100644 --- a/lib/data/shared_prefs_service.dart +++ b/lib/data/shared_prefs_service.dart @@ -1,14 +1,17 @@ import 'package:shared_preferences/shared_preferences.dart'; +import 'models/ev_source_type.dart'; import 'models/photography_values/iso_value.dart'; import 'models/photography_values/nd_value.dart'; import 'models/theme_type.dart'; class UserPreferencesService { static const _isoKey = "iso"; - static const _ndFilterKey = "nd"; + static const _ndFilterKey = "ndFilter"; + static const _evSourceTypeKey = "evSourceType"; static const _cameraEvCalibrationKey = "cameraEvCalibration"; + static const _lightSensorEvCalibrationKey = "lightSensorEvCalibration"; static const _hapticsKey = "haptics"; static const _themeTypeKey = "themeType"; @@ -24,12 +27,18 @@ class UserPreferencesService { NdValue get ndFilter => ndValues.firstWhere((v) => v.value == (_sharedPreferences.getInt(_ndFilterKey) ?? 0)); 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; set haptics(bool value) => _sharedPreferences.setBool(_hapticsKey, value); double get cameraEvCalibration => _sharedPreferences.getDouble(_cameraEvCalibrationKey) ?? 0.0; 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]; set themeType(ThemeType value) => _sharedPreferences.setInt(_themeTypeKey, value.index); diff --git a/lib/environment.dart b/lib/environment.dart index 6b65396..7f93bb2 100644 --- a/lib/environment.dart +++ b/lib/environment.dart @@ -3,19 +3,31 @@ class Environment { final String issuesReportUrl; final String contactEmail; + final bool hasLightSensor; + const Environment({ required this.sourceCodeUrl, required this.issuesReportUrl, required this.contactEmail, + this.hasLightSensor = false, }); const Environment.dev() : sourceCodeUrl = 'https://github.com/vodemn/m3_lightmeter', issuesReportUrl = 'https://github.com/vodemn/m3_lightmeter/issues', - contactEmail = 'contact.vodemn@gmail.com'; + contactEmail = 'contact.vodemn@gmail.com', + hasLightSensor = false; const Environment.prod() : sourceCodeUrl = 'https://github.com/vodemn/m3_lightmeter', 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, + ); } diff --git a/lib/interactors/metering_interactor.dart b/lib/interactors/metering_interactor.dart index 6a58905..e58446c 100644 --- a/lib/interactors/metering_interactor.dart +++ b/lib/interactors/metering_interactor.dart @@ -1,16 +1,22 @@ +import 'dart:io'; + import 'package:lightmeter/data/haptics_service.dart'; +import 'package:lightmeter/data/light_sensor_service.dart'; import 'package:lightmeter/data/shared_prefs_service.dart'; class MeteringInteractor { final UserPreferencesService _userPreferencesService; final HapticsService _hapticsService; + final LightSensorService _lightSensorService; const MeteringInteractor( this._userPreferencesService, this._hapticsService, + this._lightSensorService, ); double get cameraEvCalibration => _userPreferencesService.cameraEvCalibration; + double get lightSensorEvCalibration => _userPreferencesService.lightSensorEvCalibration; bool get isHapticsEnabled => _userPreferencesService.haptics; @@ -25,4 +31,14 @@ class MeteringInteractor { } void enableHaptics(bool enable) => _userPreferencesService.haptics = enable; + + Future hasAmbientLightSensor() async { + if (Platform.isAndroid) { + return _lightSensorService.hasSensor(); + } else { + return false; + } + } + + Stream luxStream() => _lightSensorService.luxStream(); } diff --git a/lib/interactors/settings_interactor.dart b/lib/interactors/settings_interactor.dart index 62c7d6d..d19edfe 100644 --- a/lib/interactors/settings_interactor.dart +++ b/lib/interactors/settings_interactor.dart @@ -13,6 +13,9 @@ class SettingsInteractor { double get cameraEvCalibration => _userPreferencesService.cameraEvCalibration; void setCameraEvCalibration(double value) => _userPreferencesService.cameraEvCalibration = value; + double get lightSensorEvCalibration => _userPreferencesService.lightSensorEvCalibration; + void setLightSensorEvCalibration(double value) => _userPreferencesService.lightSensorEvCalibration = value; + bool get isHapticsEnabled => _userPreferencesService.haptics; /// Executes vibration if haptics are enabled in settings diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 08d98ef..e7c3eb8 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -28,8 +28,10 @@ "halfStops": "1/2", "thirdStops": "1/3", "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", + "lightSensor": "Light sensor", "general": "General", "haptics": "Haptics", "theme": "Theme", diff --git a/lib/providers/ev_source_type_provider.dart b/lib/providers/ev_source_type_provider.dart new file mode 100644 index 0000000..8c71d03 --- /dev/null +++ b/lib/providers/ev_source_type_provider.dart @@ -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()!; + } + + @override + State createState() => EvSourceTypeProviderState(); +} + +class EvSourceTypeProviderState extends State { + late final ValueNotifier valueListenable; + + @override + void initState() { + super.initState(); + final evSourceType = context.read().evSourceType; + valueListenable = ValueNotifier( + evSourceType == EvSourceType.sensor && !context.read().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().hasLightSensor) { + valueListenable.value = EvSourceType.sensor; + } + break; + case EvSourceType.sensor: + valueListenable.value = EvSourceType.camera; + break; + } + context.read().evSourceType = valueListenable.value; + } +} diff --git a/lib/res/dimens.dart b/lib/res/dimens.dart index cd80939..0a5b2e4 100644 --- a/lib/res/dimens.dart +++ b/lib/res/dimens.dart @@ -21,6 +21,10 @@ class Dimens { static const Duration durationML = Duration(milliseconds: 250); 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` static const double cameraSliderTrackHeight = grid4; static const double cameraSliderTrackRadius = cameraSliderTrackHeight / 2; diff --git a/lib/screens/metering/communication/bloc_communication_metering.dart b/lib/screens/metering/communication/bloc_communication_metering.dart index b317c01..14fb5ce 100644 --- a/lib/screens/metering/communication/bloc_communication_metering.dart +++ b/lib/screens/metering/communication/bloc_communication_metering.dart @@ -6,7 +6,9 @@ import 'state_communication_metering.dart'; class MeteringCommunicationBloc extends Bloc { MeteringCommunicationBloc() : super(const InitState()) { - on((_, 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((_, emit) => emit(MeasureState())); on((event, emit) => emit(MeasuredState(event.ev100))); } } diff --git a/lib/screens/metering/components/bottom_controls/components/widget_button_measure.dart b/lib/screens/metering/components/bottom_controls/components/widget_button_measure.dart index 77f0a13..cfecb86 100644 --- a/lib/screens/metering/components/bottom_controls/components/widget_button_measure.dart +++ b/lib/screens/metering/components/bottom_controls/components/widget_button_measure.dart @@ -36,6 +36,11 @@ class _MeteringMeasureButtonState extends State { _isPressed = false; }); }, + onTapCancel: () { + setState(() { + _isPressed = false; + }); + }, child: DecoratedBox( decoration: BoxDecoration( borderRadius: BorderRadius.circular(widget.size / 2), diff --git a/lib/screens/metering/components/bottom_controls/widget_bottom_controls.dart b/lib/screens/metering/components/bottom_controls/widget_bottom_controls.dart index 3bf00e3..8b2d7d8 100644 --- a/lib/screens/metering/components/bottom_controls/widget_bottom_controls.dart +++ b/lib/screens/metering/components/bottom_controls/widget_bottom_controls.dart @@ -1,14 +1,18 @@ import 'package:flutter/material.dart'; +import 'package:lightmeter/data/models/ev_source_type.dart'; import 'package:lightmeter/res/dimens.dart'; +import 'package:provider/provider.dart'; import 'components/widget_button_measure.dart'; import 'components/widget_button_secondary.dart'; class MeteringBottomControls extends StatelessWidget { + final VoidCallback? onSwitchEvSourceType; final VoidCallback onMeasure; final VoidCallback onSettings; const MeteringBottomControls({ + required this.onSwitchEvSourceType, required this.onMeasure, required this.onSettings, super.key, @@ -30,7 +34,17 @@ class MeteringBottomControls extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - const Spacer(), + if (onSwitchEvSourceType != null) + Expanded( + child: MeteringSecondaryButton( + onPressed: onSwitchEvSourceType!, + icon: context.watch() != EvSourceType.camera + ? Icons.camera_rear + : Icons.wb_incandescent, + ), + ) + else + const Spacer(), MeteringMeasureButton( onTap: onMeasure, ), diff --git a/lib/screens/metering/components/camera/widget_exposure_slider.dart b/lib/screens/metering/components/camera/widget_exposure_slider.dart deleted file mode 100644 index 0f6a1ec..0000000 --- a/lib/screens/metering/components/camera/widget_exposure_slider.dart +++ /dev/null @@ -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( - builder: (context, state) { - if (state is CameraActiveState) { - return Column( - children: [ - IconButton( - icon: const Icon(Icons.sync), - onPressed: state.currentExposureOffset != 0.0 - ? () => context.read().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().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(), - ); - } -} diff --git a/lib/screens/metering/components/camera/widget_zoom_camera.dart b/lib/screens/metering/components/camera/widget_zoom_camera.dart deleted file mode 100644 index ba7b4bb..0000000 --- a/lib/screens/metering/components/camera/widget_zoom_camera.dart +++ /dev/null @@ -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( - 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().add(ZoomChangedEvent(value)); - }, - ); - } - return const SizedBox(); - }, - ); - } -} diff --git a/lib/screens/metering/ev_source/camera/bloc_camera.dart b/lib/screens/metering/components/camera_container/bloc_container_camera.dart similarity index 91% rename from lib/screens/metering/ev_source/camera/bloc_camera.dart rename to lib/screens/metering/components/camera_container/bloc_container_camera.dart index 4bd4409..34d4fb5 100644 --- a/lib/screens/metering/ev_source/camera/bloc_camera.dart +++ b/lib/screens/metering/components/camera_container/bloc_container_camera.dart @@ -7,7 +7,7 @@ import 'package:exif/exif.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.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/event_communication_metering.dart' as communication_event; @@ -15,10 +15,10 @@ import 'package:lightmeter/screens/metering/communication/state_communication_me as communication_states; import 'package:lightmeter/utils/log_2.dart'; -import 'event_camera.dart'; -import 'state_camera.dart'; +import 'event_container_camera.dart'; +import 'state_container_camera.dart'; -class CameraBloc extends EvSourceBloc { +class CameraContainerBloc extends EvSourceBlocBase { final MeteringInteractor _meteringInteractor; late final _WidgetsBindingObserver _observer; CameraController? _cameraController; @@ -33,9 +33,9 @@ class CameraBloc extends EvSourceBloc { double _exposureStep = 0.0; double _currentExposureOffset = 0.0; - CameraBloc( - MeteringCommunicationBloc communicationBloc, + CameraContainerBloc( this._meteringInteractor, + MeteringCommunicationBloc communicationBloc, ) : super( communicationBloc, const CameraInitState(), @@ -54,8 +54,8 @@ class CameraBloc extends EvSourceBloc { @override Future close() async { WidgetsBinding.instance.removeObserver(_observer); - _cameraController?.dispose(); - super.close(); + unawaited(_cameraController?.dispose()); + return super.close(); } @override @@ -110,11 +110,6 @@ class CameraBloc extends EvSourceBloc { emit(CameraInitializedState(_cameraController!)); _emitActiveState(emit); - _takePhoto().then((ev100) { - if (ev100 != null) { - communicationBloc.add(communication_event.MeasuredEvent(ev100)); - } - }); } catch (e) { emit(const CameraErrorState()); } @@ -139,11 +134,9 @@ class CameraBloc extends EvSourceBloc { void _emitActiveState(Emitter emit) { emit(CameraActiveState( - minZoom: _zoomRange!.start, - maxZoom: _zoomRange!.end, + zoomRange: _zoomRange!, currentZoom: _currentZoom, - minExposureOffset: _exposureOffsetRange!.start, - maxExposureOffset: _exposureOffsetRange!.end, + exposureOffsetRange: _exposureOffsetRange!, exposureOffsetStep: _exposureStep, currentExposureOffset: _currentExposureOffset, )); diff --git a/lib/screens/metering/components/camera_container/components/camera_controls/components/exposure_offset_slider/widget_slider_exposure_offset.dart b/lib/screens/metering/components/camera_container/components/camera_controls/components/exposure_offset_slider/widget_slider_exposure_offset.dart new file mode 100644 index 0000000..87539ee --- /dev/null +++ b/lib/screens/metering/components/camera_container/components/camera_controls/components/exposure_offset_slider/widget_slider_exposure_offset.dart @@ -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 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(), + ); + } +} diff --git a/lib/screens/metering/components/camera_container/components/camera_controls/components/zoom_slider/widget_slider_zoom.dart b/lib/screens/metering/components/camera_container/components/camera_controls/components/zoom_slider/widget_slider_zoom.dart new file mode 100644 index 0000000..e3619df --- /dev/null +++ b/lib/screens/metering/components/camera_container/components/camera_controls/components/zoom_slider/widget_slider_zoom.dart @@ -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 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, + ); + } +} diff --git a/lib/screens/metering/components/camera_container/components/camera_controls/widget_camera_controls.dart b/lib/screens/metering/components/camera_container/components/camera_controls/widget_camera_controls.dart new file mode 100644 index 0000000..57be1d6 --- /dev/null +++ b/lib/screens/metering/components/camera_container/components/camera_controls/widget_camera_controls.dart @@ -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 onExposureOffsetChanged; + + final RangeValues zoomRange; + final double zoomValue; + final ValueChanged 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, + ), + ], + ); + } +} diff --git a/lib/screens/metering/components/camera_container/components/camera_view/widget_camera_view.dart b/lib/screens/metering/components/camera_container/components/camera_view/widget_camera_view.dart new file mode 100644 index 0000000..845d9af --- /dev/null +++ b/lib/screens/metering/components/camera_container/components/camera_view/widget_camera_view.dart @@ -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( + 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.landscapeLeft, DeviceOrientation.landscapeRight] + .contains(_getApplicableOrientation(value)); + } + + int _getQuarterTurns(CameraValue value) { + final Map turns = { + 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); + } +} diff --git a/lib/screens/metering/components/camera_container/event_container_camera.dart b/lib/screens/metering/components/camera_container/event_container_camera.dart new file mode 100644 index 0000000..9467a5c --- /dev/null +++ b/lib/screens/metering/components/camera_container/event_container_camera.dart @@ -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(); +} diff --git a/lib/screens/metering/components/camera_container/provider_container_camera.dart b/lib/screens/metering/components/camera_container/provider_container_camera.dart new file mode 100644 index 0000000..4b16363 --- /dev/null +++ b/lib/screens/metering/components/camera_container/provider_container_camera.dart @@ -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 onIsoChanged; + final ValueChanged onNdChanged; + final List 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(), + context.read(), + ), + child: CameraContainer( + fastest: fastest, + slowest: slowest, + iso: iso, + nd: nd, + onIsoChanged: onIsoChanged, + onNdChanged: onNdChanged, + exposurePairs: exposurePairs, + ), + ); + } +} diff --git a/lib/screens/metering/components/camera_container/state_container_camera.dart b/lib/screens/metering/components/camera_container/state_container_camera.dart new file mode 100644 index 0000000..ccc54bc --- /dev/null +++ b/lib/screens/metering/components/camera_container/state_container_camera.dart @@ -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(); +} diff --git a/lib/screens/metering/components/camera_container/widget_container_camera.dart b/lib/screens/metering/components/camera_container/widget_container_camera.dart new file mode 100644 index 0000000..5d46616 --- /dev/null +++ b/lib/screens/metering/components/camera_container/widget_container_camera.dart @@ -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 onIsoChanged; + final ValueChanged onNdChanged; + final List 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( + 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( + builder: (context, state) => AnimatedSwitcher( + duration: Dimens.durationS, + child: state is CameraActiveState + ? CameraControls( + exposureOffsetRange: state.exposureOffsetRange, + exposureOffsetValue: state.currentExposureOffset, + onExposureOffsetChanged: (value) { + context.read().add(ExposureOffsetChangedEvent(value)); + }, + zoomRange: state.zoomRange, + zoomValue: state.currentZoom, + onZoomChanged: (value) { + context.read().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, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/screens/metering/components/light_sensor_container/bloc_container_light_sensor.dart b/lib/screens/metering/components/light_sensor_container/bloc_container_light_sensor.dart new file mode 100644 index 0000000..f5ea16d --- /dev/null +++ b/lib/screens/metering/components/light_sensor_container/bloc_container_light_sensor.dart @@ -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 { + final MeteringInteractor _meteringInteractor; + + StreamSubscription? _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 close() async { + _luxSubscriptions?.cancel().then((_) => _luxSubscriptions = null); + return super.close(); + } +} diff --git a/lib/screens/metering/components/light_sensor_container/event_container_light_sensor.dart b/lib/screens/metering/components/light_sensor_container/event_container_light_sensor.dart new file mode 100644 index 0000000..6c6cbf6 --- /dev/null +++ b/lib/screens/metering/components/light_sensor_container/event_container_light_sensor.dart @@ -0,0 +1,3 @@ +abstract class LightSensorContainerEvent { + const LightSensorContainerEvent(); +} diff --git a/lib/screens/metering/components/light_sensor_container/provider_container_light_sensor.dart b/lib/screens/metering/components/light_sensor_container/provider_container_light_sensor.dart new file mode 100644 index 0000000..5d75647 --- /dev/null +++ b/lib/screens/metering/components/light_sensor_container/provider_container_light_sensor.dart @@ -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 onIsoChanged; + final ValueChanged onNdChanged; + final List 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(), + context.read(), + ), + child: LightSensorContainer( + fastest: fastest, + slowest: slowest, + iso: iso, + nd: nd, + onIsoChanged: onIsoChanged, + onNdChanged: onNdChanged, + exposurePairs: exposurePairs, + ), + ); + } +} diff --git a/lib/screens/metering/components/light_sensor_container/state_container_light_sensor.dart b/lib/screens/metering/components/light_sensor_container/state_container_light_sensor.dart new file mode 100644 index 0000000..810be0c --- /dev/null +++ b/lib/screens/metering/components/light_sensor_container/state_container_light_sensor.dart @@ -0,0 +1,7 @@ +abstract class LightSensorContainerState { + const LightSensorContainerState(); +} + +class LightSensorInitState extends LightSensorContainerState { + const LightSensorInitState(); +} diff --git a/lib/screens/metering/components/light_sensor_container/widget_container_light_sensor.dart b/lib/screens/metering/components/light_sensor_container/widget_container_light_sensor.dart new file mode 100644 index 0000000..1bec2da --- /dev/null +++ b/lib/screens/metering/components/light_sensor_container/widget_container_light_sensor.dart @@ -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 onIsoChanged; + final ValueChanged onNdChanged; + final List 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), + ), + ), + ], + ); + } +} diff --git a/lib/screens/metering/ev_source/ev_source_bloc.dart b/lib/screens/metering/components/shared/ev_source_base/bloc_base_ev_source.dart similarity index 85% rename from lib/screens/metering/ev_source/ev_source_bloc.dart rename to lib/screens/metering/components/shared/ev_source_base/bloc_base_ev_source.dart index eac25d8..5d8f9c2 100644 --- a/lib/screens/metering/ev_source/ev_source_bloc.dart +++ b/lib/screens/metering/components/shared/ev_source_base/bloc_base_ev_source.dart @@ -4,11 +4,11 @@ import 'package:lightmeter/screens/metering/communication/bloc_communication_met import 'package:lightmeter/screens/metering/communication/state_communication_metering.dart' as communication_states; -abstract class EvSourceBloc extends Bloc { +abstract class EvSourceBlocBase extends Bloc { final MeteringCommunicationBloc communicationBloc; late final StreamSubscription _communicationSubscription; - EvSourceBloc(this.communicationBloc, super.initialState) { + EvSourceBlocBase(this.communicationBloc, super.initialState) { _communicationSubscription = communicationBloc.stream .where((event) => event is communication_states.SourceState) .map((event) => event as communication_states.SourceState) @@ -18,7 +18,7 @@ abstract class EvSourceBloc extends Bloc { @override Future close() async { await _communicationSubscription.cancel(); - super.close(); + return super.close(); } void onCommunicationState(communication_states.SourceState communicationState); diff --git a/lib/screens/metering/components/exposure_pairs_list/components/widget_item_list_exposure_pairs.dart b/lib/screens/metering/components/shared/exposure_pairs_list/components/exposure_pairs_list_item/widget_item_list_exposure_pairs.dart similarity index 100% rename from lib/screens/metering/components/exposure_pairs_list/components/widget_item_list_exposure_pairs.dart rename to lib/screens/metering/components/shared/exposure_pairs_list/components/exposure_pairs_list_item/widget_item_list_exposure_pairs.dart diff --git a/lib/screens/metering/components/exposure_pairs_list/widget_list_exposure_pairs.dart b/lib/screens/metering/components/shared/exposure_pairs_list/widget_list_exposure_pairs.dart similarity index 97% rename from lib/screens/metering/components/exposure_pairs_list/widget_list_exposure_pairs.dart rename to lib/screens/metering/components/shared/exposure_pairs_list/widget_list_exposure_pairs.dart index 4bca49c..df758af 100644 --- a/lib/screens/metering/components/exposure_pairs_list/widget_list_exposure_pairs.dart +++ b/lib/screens/metering/components/shared/exposure_pairs_list/widget_list_exposure_pairs.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/data/models/exposure_pair.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 { final List exposurePairs; diff --git a/lib/screens/metering/components/topbar/shape_topbar.dart b/lib/screens/metering/components/shared/metering_top_bar/shape_top_bar_metering.dart similarity index 96% rename from lib/screens/metering/components/topbar/shape_topbar.dart rename to lib/screens/metering/components/shared/metering_top_bar/shape_top_bar_metering.dart index a4c7c8f..90d6718 100644 --- a/lib/screens/metering/components/topbar/shape_topbar.dart +++ b/lib/screens/metering/components/shared/metering_top_bar/shape_top_bar_metering.dart @@ -3,7 +3,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:lightmeter/res/dimens.dart'; -class TopBarShape extends CustomPainter { +class MeteringTopBarShape extends CustomPainter { final Color color; /// The appendix is on the left side @@ -22,7 +22,7 @@ class TopBarShape extends CustomPainter { final double appendixHeight; final double appendixWidth; - TopBarShape({ + MeteringTopBarShape({ required this.color, required this.appendixHeight, required this.appendixWidth, @@ -38,8 +38,8 @@ class TopBarShape extends CustomPainter { RRect.fromLTRBAndCorners( 0, 0, - 0, - 0, + size.width, + size.height, bottomLeft: circularRadius, bottomRight: circularRadius, ), diff --git a/lib/screens/metering/components/shared/metering_top_bar/widget_top_bar_metering.dart b/lib/screens/metering/components/shared/metering_top_bar/widget_top_bar_metering.dart new file mode 100644 index 0000000..f9b195a --- /dev/null +++ b/lib/screens/metering/components/shared/metering_top_bar/widget_top_bar_metering.dart @@ -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, + ), + ), + ] + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/screens/metering/components/topbar/components/shared/widget_dialog_animated.dart b/lib/screens/metering/components/shared/readings_container/components/animated_dialog_picker/components/animated_dialog/widget_dialog_animated.dart similarity index 100% rename from lib/screens/metering/components/topbar/components/shared/widget_dialog_animated.dart rename to lib/screens/metering/components/shared/readings_container/components/animated_dialog_picker/components/animated_dialog/widget_dialog_animated.dart diff --git a/lib/screens/metering/components/topbar/components/widget_dialog_picker.dart b/lib/screens/metering/components/shared/readings_container/components/animated_dialog_picker/components/photography_value_picker_dialog/widget_dialog_picker_photography_value.dart similarity index 93% rename from lib/screens/metering/components/topbar/components/widget_dialog_picker.dart rename to lib/screens/metering/components/shared/readings_container/components/animated_dialog_picker/components/photography_value_picker_dialog/widget_dialog_picker_photography_value.dart index 9f8efb4..3cb04bc 100644 --- a/lib/screens/metering/components/topbar/components/widget_dialog_picker.dart +++ b/lib/screens/metering/components/shared/readings_container/components/animated_dialog_picker/components/photography_value_picker_dialog/widget_dialog_picker_photography_value.dart @@ -7,7 +7,7 @@ typedef DialogPickerItemBuilder = Widget Function(Bu typedef DialogPickerEvDifferenceBuilder = String Function( T selected, T other); -class MeteringScreenDialogPicker extends StatefulWidget { +class PhotographyValuePickerDialog extends StatefulWidget { final String title; final String subtitle; final T initialValue; @@ -17,7 +17,7 @@ class MeteringScreenDialogPicker extends StatefulWid final VoidCallback onCancel; final ValueChanged onSelect; - const MeteringScreenDialogPicker({ + const PhotographyValuePickerDialog({ required this.title, required this.subtitle, required this.initialValue, @@ -30,11 +30,11 @@ class MeteringScreenDialogPicker extends StatefulWid }); @override - State> createState() => _MeteringScreenDialogPickerState(); + State> createState() => _PhotographyValuePickerDialogState(); } -class _MeteringScreenDialogPickerState - extends State> { +class _PhotographyValuePickerDialogState + extends State> { late T _selectedValue = widget.initialValue; final _scrollController = ScrollController(); diff --git a/lib/screens/metering/components/shared/readings_container/components/animated_dialog_picker/widget_dialog_animated_picker.dart b/lib/screens/metering/components/shared/readings_container/components/animated_dialog_picker/widget_dialog_animated_picker.dart new file mode 100644 index 0000000..0d729a7 --- /dev/null +++ b/lib/screens/metering/components/shared/readings_container/components/animated_dialog_picker/widget_dialog_animated_picker.dart @@ -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 extends StatelessWidget { + final _key = GlobalKey(); + final String title; + final String subtitle; + final T selectedValue; + final List values; + final DialogPickerItemBuilder itemTitleBuilder; + final DialogPickerEvDifferenceBuilder evDifferenceBuilder; + final ValueChanged 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( + title: title, + subtitle: subtitle, + initialValue: selectedValue, + values: values, + itemTitleBuilder: itemTitleBuilder, + evDifferenceBuilder: evDifferenceBuilder, + onCancel: () { + _key.currentState?.close(); + }, + onSelect: (value) { + _key.currentState?.close().then((_) => onChanged(value)); + }, + ), + ); + } +} diff --git a/lib/screens/metering/components/topbar/components/container_reading_value.dart b/lib/screens/metering/components/shared/readings_container/components/reading_value_container/widget_container_reading_value.dart similarity index 91% rename from lib/screens/metering/components/topbar/components/container_reading_value.dart rename to lib/screens/metering/components/shared/readings_container/components/reading_value_container/widget_container_reading_value.dart index 66ed30f..d1c55c7 100644 --- a/lib/screens/metering/components/topbar/components/container_reading_value.dart +++ b/lib/screens/metering/components/shared/readings_container/components/reading_value_container/widget_container_reading_value.dart @@ -68,11 +68,17 @@ class _ReadingValueBuilder extends StatelessWidget { Text( reading.label, style: textTheme.labelMedium?.copyWith(color: textColor), + maxLines: 1, + overflow: TextOverflow.visible, + softWrap: false, ), const SizedBox(height: Dimens.grid4), Text( reading.value, style: textTheme.titleMedium?.copyWith(color: textColor), + maxLines: 1, + overflow: TextOverflow.visible, + softWrap: false, ), ], ); diff --git a/lib/screens/metering/components/shared/readings_container/widget_container_readings.dart b/lib/screens/metering/components/shared/readings_container/widget_container_readings.dart new file mode 100644 index 0000000..b47817d --- /dev/null +++ b/lib/screens/metering/components/shared/readings_container/widget_container_readings.dart @@ -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 onIsoChanged; + final ValueChanged 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 onChanged; + + const _IsoValueTile({required this.value, required this.onChanged}); + + @override + Widget build(BuildContext context) { + return AnimatedDialogPicker( + 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 onChanged; + + const _NdValueTile({required this.value, required this.onChanged}); + + @override + Widget build(BuildContext context) { + return AnimatedDialogPicker( + 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(), + ), + ), + ); + } +} diff --git a/lib/screens/metering/components/topbar/components/widget_camera_preview.dart b/lib/screens/metering/components/topbar/components/widget_camera_preview.dart deleted file mode 100644 index a47f6cd..0000000 --- a/lib/screens/metering/components/topbar/components/widget_camera_preview.dart +++ /dev/null @@ -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( - buildWhen: (previous, current) => current is CameraInitializedState, - builder: (context, state) { - if (state is CameraInitializedState) { - final value = state.controller.value; - return ValueListenableBuilder( - 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.landscapeLeft, DeviceOrientation.landscapeRight] - .contains(_getApplicableOrientation(value)); - } - - int _getQuarterTurns(CameraValue value) { - final Map turns = { - 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); - } -} diff --git a/lib/screens/metering/components/topbar/components/widget_size_render.dart b/lib/screens/metering/components/topbar/components/widget_size_render.dart deleted file mode 100644 index 46c6158..0000000 --- a/lib/screens/metering/components/topbar/components/widget_size_render.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; - -class SizeRenderWidget extends SingleChildRenderObjectWidget { - final ValueChanged? onLayout; - - const SizeRenderWidget({ - super.key, - super.child, - this.onLayout, - }); - - @override - SizeRenderBox createRenderObject(BuildContext context) => SizeRenderBox(onLayout: onLayout); -} - -class SizeRenderBox extends RenderProxyBox { - final ValueChanged? 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); - } -} diff --git a/lib/screens/metering/components/topbar/widget_topbar.dart b/lib/screens/metering/components/topbar/widget_topbar.dart deleted file mode 100644 index 8b73008..0000000 --- a/lib/screens/metering/components/topbar/widget_topbar.dart +++ /dev/null @@ -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 onIsoChanged; - final ValueChanged onNdChanged; - - final ValueChanged 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 createState() => _MeteringTopBarState(); -} - -class _MeteringTopBarState extends State { - 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 onChanged; - - const _IsoValueTile({required this.value, required this.onChanged}); - - @override - Widget build(BuildContext context) { - return _AnimatedDialogPicker( - 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 onChanged; - - const _NdValueTile({required this.value, required this.onChanged}); - - @override - Widget build(BuildContext context) { - return _AnimatedDialogPicker( - 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 extends StatelessWidget { - final _key = GlobalKey(); - final String title; - final String subtitle; - final T selectedValue; - final List values; - final DialogPickerItemBuilder itemTitleBuilder; - final DialogPickerEvDifferenceBuilder evDifferenceBuilder; - final ValueChanged 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( - title: title, - subtitle: subtitle, - initialValue: selectedValue, - values: values, - itemTitleBuilder: itemTitleBuilder, - evDifferenceBuilder: evDifferenceBuilder, - onCancel: () { - _key.currentState?.close(); - }, - onSelect: (value) { - _key.currentState?.close().then((_) => onChanged(value)); - }, - ), - ); - } -} diff --git a/lib/screens/metering/ev_source/camera/event_camera.dart b/lib/screens/metering/ev_source/camera/event_camera.dart deleted file mode 100644 index 96de582..0000000 --- a/lib/screens/metering/ev_source/camera/event_camera.dart +++ /dev/null @@ -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(); -} diff --git a/lib/screens/metering/ev_source/camera/state_camera.dart b/lib/screens/metering/ev_source/camera/state_camera.dart deleted file mode 100644 index cf2c2a7..0000000 --- a/lib/screens/metering/ev_source/camera/state_camera.dart +++ /dev/null @@ -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(); -} diff --git a/lib/screens/metering/ev_source/random_ev/bloc_random_ev.dart b/lib/screens/metering/ev_source/random_ev/bloc_random_ev.dart deleted file mode 100644 index 1d72bed..0000000 --- a/lib/screens/metering/ev_source/random_ev/bloc_random_ev.dart +++ /dev/null @@ -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 { - 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)); - } - } -} diff --git a/lib/screens/metering/ev_source/random_ev/event_random_ev.dart b/lib/screens/metering/ev_source/random_ev/event_random_ev.dart deleted file mode 100644 index 61e5e54..0000000 --- a/lib/screens/metering/ev_source/random_ev/event_random_ev.dart +++ /dev/null @@ -1,3 +0,0 @@ -abstract class RandomEvEvent { - const RandomEvEvent(); -} diff --git a/lib/screens/metering/ev_source/random_ev/state_random_ev.dart b/lib/screens/metering/ev_source/random_ev/state_random_ev.dart deleted file mode 100644 index 1bbec89..0000000 --- a/lib/screens/metering/ev_source/random_ev/state_random_ev.dart +++ /dev/null @@ -1,5 +0,0 @@ -class RandomEvState { - final double ev; - - const RandomEvState(this.ev); -} diff --git a/lib/screens/metering/flow_metering.dart b/lib/screens/metering/flow_metering.dart index 5204301..e0a6125 100644 --- a/lib/screens/metering/flow_metering.dart +++ b/lib/screens/metering/flow_metering.dart @@ -1,27 +1,31 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.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/shared_prefs_service.dart'; import 'package:lightmeter/interactors/metering_interactor.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 'communication/bloc_communication_metering.dart'; import 'screen_metering.dart'; -class MeteringFlow extends StatelessWidget { +class MeteringFlow extends StatefulWidget { const MeteringFlow({super.key}); + @override + State createState() => _MeteringFlowState(); +} + +class _MeteringFlowState extends State { @override Widget build(BuildContext context) { return Provider( create: (context) => MeteringInteractor( context.read(), context.read(), + context.read(), ), child: MultiBlocProvider( providers: [ @@ -34,17 +38,6 @@ class MeteringFlow extends StatelessWidget { context.read(), ), ), - BlocProvider( - create: (context) => CameraBloc( - context.read(), - context.read(), - ), - ), - if (context.read() == EvSourceType.sensor) - BlocProvider( - lazy: false, - create: (context) => RandomEvBloc(context.read()), - ), ], child: const MeteringScreen(), ), diff --git a/lib/screens/metering/screen_metering.dart b/lib/screens/metering/screen_metering.dart index f9dcee3..2dba320 100644 --- a/lib/screens/metering/screen_metering.dart +++ b/lib/screens/metering/screen_metering.dart @@ -1,13 +1,13 @@ import 'package:flutter/material.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/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/camera/widget_exposure_slider.dart'; -import 'components/camera/widget_zoom_camera.dart'; -import 'components/exposure_pairs_list/widget_list_exposure_pairs.dart'; -import 'components/topbar/widget_topbar.dart'; +import 'components/camera_container/provider_container_camera.dart'; +import 'components/light_sensor_container/provider_container_light_sensor.dart'; import 'bloc_metering.dart'; import 'event_metering.dart'; import 'state_metering.dart'; @@ -20,8 +20,6 @@ class MeteringScreen extends StatefulWidget { } class _MeteringScreenState extends State { - double topBarOverflow = 0.0; - MeteringBloc get _bloc => context.read(); @override @@ -34,84 +32,39 @@ class _MeteringScreenState extends State { Widget build(BuildContext context) { return Scaffold( backgroundColor: Theme.of(context).colorScheme.background, - body: BlocBuilder( - builder: (context, state) => Column( - children: [ - MeteringTopBar( - fastest: state.fastest, - slowest: state.slowest, - ev: state.ev, - iso: state.iso, - nd: state.nd, - onIsoChanged: (value) => _bloc.add(IsoChangedEvent(value)), - onNdChanged: (value) => _bloc.add(NdChangedEvent(value)), - onCutoutLayout: (value) => topBarOverflow = value, - ), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM), - child: _MiddleContentWrapper( - topBarOverflow: topBarOverflow, - leftContent: ExposurePairsList(state.exposurePairs), - rightContent: Padding( - padding: const EdgeInsets.symmetric(vertical: Dimens.paddingM), - child: Column( - children: const [ - Expanded(child: CameraExposureSlider()), - SizedBox(height: Dimens.grid24), - CameraZoomSlider(), - ], + body: Column( + children: [ + Expanded( + child: BlocBuilder( + builder: (context, state) => context.watch() == EvSourceType.camera + ? CameraContainerProvider( + fastest: state.fastest, + slowest: state.slowest, + iso: state.iso, + nd: state.nd, + onIsoChanged: (value) => _bloc.add(IsoChangedEvent(value)), + onNdChanged: (value) => _bloc.add(NdChangedEvent(value)), + exposurePairs: state.exposurePairs, + ) + : LightSensorContainerProvider( + fastest: state.fastest, + slowest: state.slowest, + iso: state.iso, + nd: state.nd, + onIsoChanged: (value) => _bloc.add(IsoChangedEvent(value)), + onNdChanged: (value) => _bloc.add(NdChangedEvent(value)), + exposurePairs: state.exposurePairs, ), - ), - ), - ), ), - MeteringBottomControls( - onMeasure: () => _bloc.add(const MeasureEvent()), - 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, - ), - ), - ], - ), + ), + MeteringBottomControls( + onSwitchEvSourceType: context.read().hasLightSensor + ? EvSourceTypeProvider.of(context).toggleType + : null, + onMeasure: () => _bloc.add(const MeasureEvent()), + onSettings: () => Navigator.pushNamed(context, 'settings'), + ), + ], ), ); } diff --git a/lib/screens/settings/components/calibration/components/calibration_dialog/bloc_dialog_calibration.dart b/lib/screens/settings/components/calibration/components/calibration_dialog/bloc_dialog_calibration.dart index 1466d04..854ac7f 100644 --- a/lib/screens/settings/components/calibration/components/calibration_dialog/bloc_dialog_calibration.dart +++ b/lib/screens/settings/components/calibration/components/calibration_dialog/bloc_dialog_calibration.dart @@ -8,26 +8,47 @@ class CalibrationDialogBloc extends Bloc(_onCameraEvCalibrationChanged); on(_onCameraEvCalibrationReset); + on(_onLightSensorEvCalibrationChanged); + on(_onLightSensorEvCalibrationReset); on(_onSaveCalibration); } void _onCameraEvCalibrationChanged(CameraEvCalibrationChangedEvent event, Emitter emit) { _cameraEvCalibration = event.value; - emit(CalibrationDialogState(event.value)); + emit(CalibrationDialogState(_cameraEvCalibration, _lightSensorEvCalibration)); } void _onCameraEvCalibrationReset(CameraEvCalibrationResetEvent event, Emitter emit) { _settingsInteractor.quickVibration(); _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, __) { _settingsInteractor.setCameraEvCalibration(_cameraEvCalibration); + _settingsInteractor.setLightSensorEvCalibration(_lightSensorEvCalibration); } } diff --git a/lib/screens/settings/components/calibration/components/calibration_dialog/event_dialog_calibration.dart b/lib/screens/settings/components/calibration/components/calibration_dialog/event_dialog_calibration.dart index e949032..08be80e 100644 --- a/lib/screens/settings/components/calibration/components/calibration_dialog/event_dialog_calibration.dart +++ b/lib/screens/settings/components/calibration/components/calibration_dialog/event_dialog_calibration.dart @@ -12,6 +12,16 @@ class CameraEvCalibrationResetEvent extends CalibrationDialogEvent { const CameraEvCalibrationResetEvent(); } +class LightSensorEvCalibrationChangedEvent extends CalibrationDialogEvent { + final double value; + + const LightSensorEvCalibrationChangedEvent(this.value); +} + +class LightSensorEvCalibrationResetEvent extends CalibrationDialogEvent { + const LightSensorEvCalibrationResetEvent(); +} + class SaveCalibrationDialogEvent extends CalibrationDialogEvent { const SaveCalibrationDialogEvent(); } diff --git a/lib/screens/settings/components/calibration/components/calibration_dialog/state_dialog_calibration.dart b/lib/screens/settings/components/calibration/components/calibration_dialog/state_dialog_calibration.dart index 3561bcc..928618f 100644 --- a/lib/screens/settings/components/calibration/components/calibration_dialog/state_dialog_calibration.dart +++ b/lib/screens/settings/components/calibration/components/calibration_dialog/state_dialog_calibration.dart @@ -1,5 +1,9 @@ class CalibrationDialogState { final double cameraEvCalibration; + final double lightSensorEvCalibration; - const CalibrationDialogState(this.cameraEvCalibration); + const CalibrationDialogState( + this.cameraEvCalibration, + this.lightSensorEvCalibration, + ); } diff --git a/lib/screens/settings/components/calibration/components/calibration_dialog/widget_dialog_calibration.dart b/lib/screens/settings/components/calibration/components/calibration_dialog/widget_dialog_calibration.dart index ed49180..2b5c9b2 100644 --- a/lib/screens/settings/components/calibration/components/calibration_dialog/widget_dialog_calibration.dart +++ b/lib/screens/settings/components/calibration/components/calibration_dialog/widget_dialog_calibration.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:lightmeter/environment.dart'; import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/res/dimens.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 'state_dialog_calibration.dart'; -class CalibrationDialog extends StatefulWidget { +class CalibrationDialog extends StatelessWidget { const CalibrationDialog({super.key}); - @override - State createState() => _CalibrationDialogState(); -} - -class _CalibrationDialogState extends State { - CalibrationDialogBloc get bloc => context.read(); - @override Widget build(BuildContext context) { + final bool hasLightSensor = context.read().hasLightSensor; return AlertDialog( titlePadding: const EdgeInsets.fromLTRB( Dimens.paddingL, @@ -30,18 +25,45 @@ class _CalibrationDialogState extends State { ), title: Text(S.of(context).calibration), contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingL), - content: BlocBuilder( - builder: (context, state) => Column( + content: SingleChildScrollView( + child: Column( mainAxisSize: MainAxisSize.min, children: [ - Text(S.of(context).calibrationMessage), - const SizedBox(height: Dimens.grid16), - _CalibrationUnit( - title: S.of(context).camera, - value: state.cameraEvCalibration, - onChanged: (value) => bloc.add(CameraEvCalibrationChangedEvent(value)), - onReset: () => bloc.add(const CameraEvCalibrationResetEvent()), + Text( + hasLightSensor + ? S.of(context).calibrationMessage + : S.of(context).calibrationMessageCameraOnly, ), + const SizedBox(height: Dimens.grid16), + BlocBuilder( + buildWhen: (previous, current) => + previous.cameraEvCalibration != current.cameraEvCalibration, + builder: (context, state) => _CalibrationUnit( + title: S.of(context).camera, + value: state.cameraEvCalibration, + onChanged: (value) => context + .read() + .add(CameraEvCalibrationChangedEvent(value)), + onReset: () => context + .read() + .add(const CameraEvCalibrationResetEvent()), + ), + ), + if (hasLightSensor) + BlocBuilder( + buildWhen: (previous, current) => + previous.lightSensorEvCalibration != current.lightSensorEvCalibration, + builder: (context, state) => _CalibrationUnit( + title: S.of(context).lightSensor, + value: state.lightSensorEvCalibration, + onChanged: (value) => context + .read() + .add(LightSensorEvCalibrationChangedEvent(value)), + onReset: () => context + .read() + .add(const LightSensorEvCalibrationResetEvent()), + ), + ), ], ), ), @@ -58,7 +80,7 @@ class _CalibrationDialogState extends State { ), TextButton( onPressed: () { - bloc.add(const SaveCalibrationDialogEvent()); + context.read().add(const SaveCalibrationDialogEvent()); Navigator.of(context).pop(); }, child: Text(S.of(context).save), diff --git a/pubspec.yaml b/pubspec.yaml index e5f51f3..9de9f32 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,6 +17,7 @@ dependencies: sdk: flutter intl: 0.17.0 intl_utils: 2.8.1 + light_sensor: 2.0.2 material_color_utilities: 0.2.0 package_info_plus: 3.0.2 permission_handler: 10.2.0