diff --git a/lib/application.dart b/lib/application.dart index 855f3cf..b140b37 100644 --- a/lib/application.dart +++ b/lib/application.dart @@ -1,8 +1,8 @@ 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'; @@ -11,6 +11,7 @@ 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'; @@ -18,63 +19,72 @@ import 'utils/stop_type_provider.dart'; final RouteObserver routeObserver = RouteObserver(); -class Application extends StatelessWidget { +class Application extends StatefulWidget { final Environment env; const Application(this.env, {super.key}); + @override + State createState() => _ApplicationState(); +} + +class _ApplicationState extends State { @override Widget build(BuildContext context) { - return FutureBuilder( - future: SharedPreferences.getInstance(), + return FutureBuilder( + future: Future.wait([ + SharedPreferences.getInstance(), + LightSensor.hasSensor, + ]), 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: widget.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/shared_prefs_service.dart b/lib/data/shared_prefs_service.dart index 686c0d1..bcfbeae 100644 --- a/lib/data/shared_prefs_service.dart +++ b/lib/data/shared_prefs_service.dart @@ -1,13 +1,15 @@ 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 _hapticsKey = "haptics"; @@ -24,6 +26,9 @@ 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); 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/providers/ev_source_type_provider.dart b/lib/providers/ev_source_type_provider.dart new file mode 100644 index 0000000..74b09d0 --- /dev/null +++ b/lib/providers/ev_source_type_provider.dart @@ -0,0 +1,64 @@ +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; + } + } +} 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..9a9a21c 100644 --- a/lib/screens/metering/components/bottom_controls/widget_bottom_controls.dart +++ b/lib/screens/metering/components/bottom_controls/widget_bottom_controls.dart @@ -5,10 +5,12 @@ 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 +32,15 @@ class MeteringBottomControls extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - const Spacer(), + if (onSwitchEvSourceType != null) + Expanded( + child: MeteringSecondaryButton( + onPressed: onSwitchEvSourceType!, + icon: Icons.sync, + ), + ) + else + const Spacer(), MeteringMeasureButton( onTap: onMeasure, ), diff --git a/lib/screens/metering/ev_source/camera/bloc_camera.dart b/lib/screens/metering/components/camera/bloc_container_camera.dart similarity index 94% rename from lib/screens/metering/ev_source/camera/bloc_camera.dart rename to lib/screens/metering/components/camera/bloc_container_camera.dart index d8e016c..f78752d 100644 --- a/lib/screens/metering/ev_source/camera/bloc_camera.dart +++ b/lib/screens/metering/components/camera/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/bloc_base_ev_source.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 EvSourceBlocBase { +class CameraContainerBloc extends EvSourceBlocBase { final MeteringInteractor _meteringInteractor; late final _WidgetsBindingObserver _observer; CameraController? _cameraController; @@ -33,7 +33,7 @@ class CameraBloc extends EvSourceBlocBase { double _exposureStep = 0.0; double _currentExposureOffset = 0.0; - CameraBloc( + CameraContainerBloc( MeteringCommunicationBloc communicationBloc, this._meteringInteractor, ) : super( @@ -139,11 +139,9 @@ class CameraBloc extends EvSourceBlocBase { 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/components/camera_controls/components/exposure_offset_slider/widget_slider_exposure_offset.dart b/lib/screens/metering/components/camera/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/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/components/camera_controls/components/zoom_slider/widget_slider_zoom.dart b/lib/screens/metering/components/camera/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/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/components/camera_controls/widget_camera_controls.dart b/lib/screens/metering/components/camera/components/camera_controls/widget_camera_controls.dart new file mode 100644 index 0000000..57be1d6 --- /dev/null +++ b/lib/screens/metering/components/camera/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/components/camera_view/widget_camera_view.dart b/lib/screens/metering/components/camera/components/camera_view/widget_camera_view.dart new file mode 100644 index 0000000..845d9af --- /dev/null +++ b/lib/screens/metering/components/camera/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/event_container_camera.dart b/lib/screens/metering/components/camera/event_container_camera.dart new file mode 100644 index 0000000..9467a5c --- /dev/null +++ b/lib/screens/metering/components/camera/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/provider_container_camera.dart b/lib/screens/metering/components/camera/provider_container_camera.dart new file mode 100644 index 0000000..d26255d --- /dev/null +++ b/lib/screens/metering/components/camera/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/state_container_camera.dart b/lib/screens/metering/components/camera/state_container_camera.dart new file mode 100644 index 0000000..ccc54bc --- /dev/null +++ b/lib/screens/metering/components/camera/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/widget_container_camera.dart b/lib/screens/metering/components/camera/widget_container_camera.dart new file mode 100644 index 0000000..ddc8c26 --- /dev/null +++ b/lib/screens/metering/components/camera/widget_container_camera.dart @@ -0,0 +1,108 @@ +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/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'; + +// TODO: add stepHeight calculation based on Text +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) { + 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), + ), + ), + ], + ); + } +} + +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 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(), + ), + ); + } +} 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/light_sensor/bloc_light_sensor.dart b/lib/screens/metering/components/light_sensor_container/bloc_container_light_sensor.dart similarity index 80% rename from lib/screens/metering/ev_source/light_sensor/bloc_light_sensor.dart rename to lib/screens/metering/components/light_sensor_container/bloc_container_light_sensor.dart index 2ad4afa..c28df2e 100644 --- a/lib/screens/metering/ev_source/light_sensor/bloc_light_sensor.dart +++ b/lib/screens/metering/components/light_sensor_container/bloc_container_light_sensor.dart @@ -1,6 +1,6 @@ import 'dart:async'; import 'package:lightmeter/interactors/metering_interactor.dart'; -import 'package:lightmeter/screens/metering/ev_source/bloc_base_ev_source.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; @@ -8,15 +8,16 @@ import 'package:lightmeter/screens/metering/communication/state_communication_me as communication_states; import 'package:lightmeter/utils/log_2.dart'; -import 'event_light_sensor.dart'; -import 'state_light_sensor.dart'; +import 'event_container_light_sensor.dart'; +import 'state_container_light_sensor.dart'; -class LightSensorBloc extends EvSourceBlocBase { +class LightSensorContainerBloc + extends EvSourceBlocBase { final MeteringInteractor _meteringInteractor; StreamSubscription? _luxSubscriptions; - LightSensorBloc( + LightSensorContainerBloc( MeteringCommunicationBloc communicationBloc, this._meteringInteractor, ) : super( 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..69179ea --- /dev/null +++ b/lib/screens/metering/components/light_sensor_container/provider_container_light_sensor.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_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( + 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/bloc_base_ev_source.dart b/lib/screens/metering/components/shared/ev_source_base/bloc_base_ev_source.dart similarity index 99% rename from lib/screens/metering/ev_source/bloc_base_ev_source.dart rename to lib/screens/metering/components/shared/ev_source_base/bloc_base_ev_source.dart index 5d8f9c2..50299a1 100644 --- a/lib/screens/metering/ev_source/bloc_base_ev_source.dart +++ b/lib/screens/metering/components/shared/ev_source_base/bloc_base_ev_source.dart @@ -1,3 +1,4 @@ + import 'dart:async'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart'; 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 100% 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 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/light_sensor/event_light_sensor.dart b/lib/screens/metering/ev_source/light_sensor/event_light_sensor.dart deleted file mode 100644 index bfbe89b..0000000 --- a/lib/screens/metering/ev_source/light_sensor/event_light_sensor.dart +++ /dev/null @@ -1,3 +0,0 @@ -abstract class LightSensorEvent { - const LightSensorEvent(); -} diff --git a/lib/screens/metering/ev_source/light_sensor/state_light_sensor.dart b/lib/screens/metering/ev_source/light_sensor/state_light_sensor.dart deleted file mode 100644 index ee76c8f..0000000 --- a/lib/screens/metering/ev_source/light_sensor/state_light_sensor.dart +++ /dev/null @@ -1,7 +0,0 @@ -abstract class LightSensorState { - const LightSensorState(); -} - -class LightSensorInitState extends LightSensorState { - const LightSensorInitState(); -} diff --git a/lib/screens/metering/flow_metering.dart b/lib/screens/metering/flow_metering.dart index fa785d6..e0a6125 100644 --- a/lib/screens/metering/flow_metering.dart +++ b/lib/screens/metering/flow_metering.dart @@ -2,24 +2,25 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:lightmeter/data/haptics_service.dart'; import 'package:lightmeter/data/light_sensor_service.dart'; -import 'package:lightmeter/data/models/ev_source_type.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:lightmeter/screens/metering/ev_source/light_sensor/bloc_light_sensor.dart'; import 'package:provider/provider.dart'; -import 'ev_source/camera/bloc_camera.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) { - final sourceType = context.watch(); return Provider( create: (context) => MeteringInteractor( context.read(), @@ -37,20 +38,6 @@ class MeteringFlow extends StatelessWidget { context.read(), ), ), - if (sourceType == EvSourceType.camera) - BlocProvider( - create: (context) => CameraBloc( - context.read(), - context.read(), - ), - ), - if (sourceType == EvSourceType.sensor) - BlocProvider( - create: (context) => LightSensorBloc( - context.read(), - context.read(), - ), - ), ], child: const MeteringScreen(), ), diff --git a/lib/screens/metering/screen_metering.dart b/lib/screens/metering/screen_metering.dart index f9dcee3..2c31e5f 100644 --- a/lib/screens/metering/screen_metering.dart +++ b/lib/screens/metering/screen_metering.dart @@ -1,13 +1,14 @@ 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/environment.dart'; +import 'package:lightmeter/providers/ev_source_type_provider.dart'; import 'package:lightmeter/res/dimens.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/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 +21,6 @@ class MeteringScreen extends StatefulWidget { } class _MeteringScreenState extends State { - double topBarOverflow = 0.0; - MeteringBloc get _bloc => context.read(); @override @@ -34,84 +33,42 @@ 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) => AnimatedSwitcher( + duration: Dimens.durationS, + child: 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'), + ), + ], ), ); }