This commit is contained in:
Vadim 2023-01-29 13:48:55 +03:00
parent 827c5e1981
commit ae07f882ad
40 changed files with 995 additions and 699 deletions

View file

@ -1,8 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:light_sensor/light_sensor.dart';
import 'package:lightmeter/data/haptics_service.dart'; import 'package:lightmeter/data/haptics_service.dart';
import 'package:lightmeter/data/models/ev_source_type.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
@ -11,6 +11,7 @@ import 'data/permissions_service.dart';
import 'data/shared_prefs_service.dart'; import 'data/shared_prefs_service.dart';
import 'environment.dart'; import 'environment.dart';
import 'generated/l10n.dart'; import 'generated/l10n.dart';
import 'providers/ev_source_type_provider.dart';
import 'res/theme.dart'; import 'res/theme.dart';
import 'screens/metering/flow_metering.dart'; import 'screens/metering/flow_metering.dart';
import 'screens/settings/flow_settings.dart'; import 'screens/settings/flow_settings.dart';
@ -18,63 +19,72 @@ import 'utils/stop_type_provider.dart';
final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>(); final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();
class Application extends StatelessWidget { class Application extends StatefulWidget {
final Environment env; final Environment env;
const Application(this.env, {super.key}); const Application(this.env, {super.key});
@override
State<Application> createState() => _ApplicationState();
}
class _ApplicationState extends State<Application> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FutureBuilder<SharedPreferences>( return FutureBuilder(
future: SharedPreferences.getInstance(), future: Future.wait([
SharedPreferences.getInstance(),
LightSensor.hasSensor,
]),
builder: (_, snapshot) { builder: (_, snapshot) {
if (snapshot.data != null) { if (snapshot.data != null) {
return MultiProvider( return MultiProvider(
providers: [ providers: [
Provider.value(value: env), Provider.value(value: widget.env.copyWith(hasLightSensor: snapshot.data![1] as bool)),
Provider.value(value: EvSourceType.camera), Provider(create: (_) => UserPreferencesService(snapshot.data![0] as SharedPreferences)),
Provider(create: (_) => UserPreferencesService(snapshot.data!)),
Provider(create: (_) => const HapticsService()), Provider(create: (_) => const HapticsService()),
Provider(create: (_) => PermissionsService()), Provider(create: (_) => PermissionsService()),
Provider(create: (_) => const LightSensorService()), Provider(create: (_) => const LightSensorService()),
], ],
child: StopTypeProvider( child: StopTypeProvider(
child: ThemeProvider( child: EvSourceTypeProvider(
builder: (context, _) { child: ThemeProvider(
final systemIconsBrightness = ThemeData.estimateBrightnessForColor( builder: (context, _) {
context.watch<ThemeData>().colorScheme.onSurface, final systemIconsBrightness = ThemeData.estimateBrightnessForColor(
); context.watch<ThemeData>().colorScheme.onSurface,
return AnnotatedRegion( );
value: SystemUiOverlayStyle( return AnnotatedRegion(
statusBarColor: Colors.transparent, value: SystemUiOverlayStyle(
statusBarBrightness: systemIconsBrightness == Brightness.light statusBarColor: Colors.transparent,
? Brightness.dark statusBarBrightness: systemIconsBrightness == Brightness.light
: Brightness.light, ? Brightness.dark
statusBarIconBrightness: systemIconsBrightness, : Brightness.light,
systemNavigationBarColor: context.watch<ThemeData>().colorScheme.surface, statusBarIconBrightness: systemIconsBrightness,
systemNavigationBarIconBrightness: systemIconsBrightness, systemNavigationBarColor: context.watch<ThemeData>().colorScheme.surface,
), systemNavigationBarIconBrightness: systemIconsBrightness,
child: MaterialApp(
theme: context.watch<ThemeData>(),
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", child: MaterialApp(
routes: { theme: context.watch<ThemeData>(),
"metering": (context) => const MeteringFlow(), localizationsDelegates: const [
"settings": (context) => const SettingsFlow(), 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(),
},
),
);
},
),
), ),
), ),
); );

View file

@ -1,13 +1,15 @@
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'models/ev_source_type.dart';
import 'models/photography_values/iso_value.dart'; import 'models/photography_values/iso_value.dart';
import 'models/photography_values/nd_value.dart'; import 'models/photography_values/nd_value.dart';
import 'models/theme_type.dart'; import 'models/theme_type.dart';
class UserPreferencesService { class UserPreferencesService {
static const _isoKey = "iso"; static const _isoKey = "iso";
static const _ndFilterKey = "nd"; static const _ndFilterKey = "ndFilter";
static const _evSourceTypeKey = "evSourceType";
static const _cameraEvCalibrationKey = "cameraEvCalibration"; static const _cameraEvCalibrationKey = "cameraEvCalibration";
static const _hapticsKey = "haptics"; static const _hapticsKey = "haptics";
@ -24,6 +26,9 @@ class UserPreferencesService {
NdValue get ndFilter => ndValues.firstWhere((v) => v.value == (_sharedPreferences.getInt(_ndFilterKey) ?? 0)); NdValue get ndFilter => ndValues.firstWhere((v) => v.value == (_sharedPreferences.getInt(_ndFilterKey) ?? 0));
set ndFilter(NdValue value) => _sharedPreferences.setInt(_ndFilterKey, value.value); set ndFilter(NdValue value) => _sharedPreferences.setInt(_ndFilterKey, value.value);
EvSourceType get evSourceType => EvSourceType.values[_sharedPreferences.getInt(_evSourceTypeKey) ?? 0];
set evSourceType(EvSourceType value) => _sharedPreferences.setInt(_evSourceTypeKey, value.index);
bool get haptics => _sharedPreferences.getBool(_hapticsKey) ?? false; bool get haptics => _sharedPreferences.getBool(_hapticsKey) ?? false;
set haptics(bool value) => _sharedPreferences.setBool(_hapticsKey, value); set haptics(bool value) => _sharedPreferences.setBool(_hapticsKey, value);

View file

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

View file

@ -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<EvSourceTypeProviderState>()!;
}
@override
State<EvSourceTypeProvider> createState() => EvSourceTypeProviderState();
}
class EvSourceTypeProviderState extends State<EvSourceTypeProvider> {
late final ValueNotifier<EvSourceType> valueListenable;
@override
void initState() {
super.initState();
final evSourceType = context.read<UserPreferencesService>().evSourceType;
valueListenable = ValueNotifier(
evSourceType == EvSourceType.sensor && !context.read<Environment>().hasLightSensor
? EvSourceType.camera
: evSourceType,
);
}
@override
void dispose() {
valueListenable.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: valueListenable,
builder: (_, value, child) => Provider.value(
value: value,
child: child,
),
child: widget.child,
);
}
void toggleType() {
switch (valueListenable.value) {
case EvSourceType.camera:
if (context.read<Environment>().hasLightSensor) {
valueListenable.value = EvSourceType.sensor;
}
break;
case EvSourceType.sensor:
valueListenable.value = EvSourceType.camera;
break;
}
}
}

View file

@ -5,10 +5,12 @@ import 'components/widget_button_measure.dart';
import 'components/widget_button_secondary.dart'; import 'components/widget_button_secondary.dart';
class MeteringBottomControls extends StatelessWidget { class MeteringBottomControls extends StatelessWidget {
final VoidCallback? onSwitchEvSourceType;
final VoidCallback onMeasure; final VoidCallback onMeasure;
final VoidCallback onSettings; final VoidCallback onSettings;
const MeteringBottomControls({ const MeteringBottomControls({
required this.onSwitchEvSourceType,
required this.onMeasure, required this.onMeasure,
required this.onSettings, required this.onSettings,
super.key, super.key,
@ -30,7 +32,15 @@ class MeteringBottomControls extends StatelessWidget {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
const Spacer(), if (onSwitchEvSourceType != null)
Expanded(
child: MeteringSecondaryButton(
onPressed: onSwitchEvSourceType!,
icon: Icons.sync,
),
)
else
const Spacer(),
MeteringMeasureButton( MeteringMeasureButton(
onTap: onMeasure, onTap: onMeasure,
), ),

View file

@ -7,7 +7,7 @@ import 'package:exif/exif.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/interactors/metering_interactor.dart'; import 'package:lightmeter/interactors/metering_interactor.dart';
import 'package:lightmeter/screens/metering/ev_source/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/bloc_communication_metering.dart';
import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart' import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart'
as communication_event; as communication_event;
@ -15,10 +15,10 @@ import 'package:lightmeter/screens/metering/communication/state_communication_me
as communication_states; as communication_states;
import 'package:lightmeter/utils/log_2.dart'; import 'package:lightmeter/utils/log_2.dart';
import 'event_camera.dart'; import 'event_container_camera.dart';
import 'state_camera.dart'; import 'state_container_camera.dart';
class CameraBloc extends EvSourceBlocBase<CameraEvent, CameraState> { class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraContainerState> {
final MeteringInteractor _meteringInteractor; final MeteringInteractor _meteringInteractor;
late final _WidgetsBindingObserver _observer; late final _WidgetsBindingObserver _observer;
CameraController? _cameraController; CameraController? _cameraController;
@ -33,7 +33,7 @@ class CameraBloc extends EvSourceBlocBase<CameraEvent, CameraState> {
double _exposureStep = 0.0; double _exposureStep = 0.0;
double _currentExposureOffset = 0.0; double _currentExposureOffset = 0.0;
CameraBloc( CameraContainerBloc(
MeteringCommunicationBloc communicationBloc, MeteringCommunicationBloc communicationBloc,
this._meteringInteractor, this._meteringInteractor,
) : super( ) : super(
@ -139,11 +139,9 @@ class CameraBloc extends EvSourceBlocBase<CameraEvent, CameraState> {
void _emitActiveState(Emitter emit) { void _emitActiveState(Emitter emit) {
emit(CameraActiveState( emit(CameraActiveState(
minZoom: _zoomRange!.start, zoomRange: _zoomRange!,
maxZoom: _zoomRange!.end,
currentZoom: _currentZoom, currentZoom: _currentZoom,
minExposureOffset: _exposureOffsetRange!.start, exposureOffsetRange: _exposureOffsetRange!,
maxExposureOffset: _exposureOffsetRange!.end,
exposureOffsetStep: _exposureStep, exposureOffsetStep: _exposureStep,
currentExposureOffset: _currentExposureOffset, currentExposureOffset: _currentExposureOffset,
)); ));

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,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<IsoValue> onIsoChanged;
final ValueChanged<NdValue> onNdChanged;
final List<ExposurePair> exposurePairs;
const CameraContainer({
required this.fastest,
required this.slowest,
required this.iso,
required this.nd,
required this.onIsoChanged,
required this.onNdChanged,
required this.exposurePairs,
super.key,
});
@override
Widget build(BuildContext context) {
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<CameraContainerBloc, CameraContainerState>(
buildWhen: (previous, current) => current is CameraInitializedState,
builder: (context, state) => state is CameraInitializedState
? CameraView(controller: state.controller)
: const ColoredBox(color: Colors.black),
),
),
);
}
}
class _CameraControlsBuilder extends StatelessWidget {
const _CameraControlsBuilder();
@override
Widget build(BuildContext context) {
return BlocBuilder<CameraContainerBloc, CameraContainerState>(
builder: (context, state) => AnimatedSwitcher(
duration: Dimens.durationS,
child: state is CameraActiveState
? CameraControls(
exposureOffsetRange: state.exposureOffsetRange,
exposureOffsetValue: state.currentExposureOffset,
onExposureOffsetChanged: (value) {
context.read<CameraContainerBloc>().add(ExposureOffsetChangedEvent(value));
},
zoomRange: state.zoomRange,
zoomValue: state.currentZoom,
onZoomChanged: (value) {
context.read<CameraContainerBloc>().add(ZoomChangedEvent(value));
},
)
: const SizedBox.shrink(),
),
);
}
}

View file

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

View file

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

View file

@ -1,6 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:lightmeter/interactors/metering_interactor.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/bloc_communication_metering.dart';
import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart' import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart'
as communication_event; as communication_event;
@ -8,15 +8,16 @@ import 'package:lightmeter/screens/metering/communication/state_communication_me
as communication_states; as communication_states;
import 'package:lightmeter/utils/log_2.dart'; import 'package:lightmeter/utils/log_2.dart';
import 'event_light_sensor.dart'; import 'event_container_light_sensor.dart';
import 'state_light_sensor.dart'; import 'state_container_light_sensor.dart';
class LightSensorBloc extends EvSourceBlocBase<LightSensorEvent, LightSensorState> { class LightSensorContainerBloc
extends EvSourceBlocBase<LightSensorContainerEvent, LightSensorContainerState> {
final MeteringInteractor _meteringInteractor; final MeteringInteractor _meteringInteractor;
StreamSubscription<int>? _luxSubscriptions; StreamSubscription<int>? _luxSubscriptions;
LightSensorBloc( LightSensorContainerBloc(
MeteringCommunicationBloc communicationBloc, MeteringCommunicationBloc communicationBloc,
this._meteringInteractor, this._meteringInteractor,
) : super( ) : super(

View file

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

View file

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

View file

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

View file

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

View file

@ -1,3 +1,4 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart'; import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,24 +2,25 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/data/haptics_service.dart'; import 'package:lightmeter/data/haptics_service.dart';
import 'package:lightmeter/data/light_sensor_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/models/photography_values/photography_value.dart';
import 'package:lightmeter/data/shared_prefs_service.dart'; import 'package:lightmeter/data/shared_prefs_service.dart';
import 'package:lightmeter/interactors/metering_interactor.dart'; import 'package:lightmeter/interactors/metering_interactor.dart';
import 'package:lightmeter/screens/metering/ev_source/light_sensor/bloc_light_sensor.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'ev_source/camera/bloc_camera.dart';
import 'bloc_metering.dart'; import 'bloc_metering.dart';
import 'communication/bloc_communication_metering.dart'; import 'communication/bloc_communication_metering.dart';
import 'screen_metering.dart'; import 'screen_metering.dart';
class MeteringFlow extends StatelessWidget { class MeteringFlow extends StatefulWidget {
const MeteringFlow({super.key}); const MeteringFlow({super.key});
@override
State<MeteringFlow> createState() => _MeteringFlowState();
}
class _MeteringFlowState extends State<MeteringFlow> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final sourceType = context.watch<EvSourceType>();
return Provider( return Provider(
create: (context) => MeteringInteractor( create: (context) => MeteringInteractor(
context.read<UserPreferencesService>(), context.read<UserPreferencesService>(),
@ -37,20 +38,6 @@ class MeteringFlow extends StatelessWidget {
context.read<StopType>(), context.read<StopType>(),
), ),
), ),
if (sourceType == EvSourceType.camera)
BlocProvider(
create: (context) => CameraBloc(
context.read<MeteringCommunicationBloc>(),
context.read<MeteringInteractor>(),
),
),
if (sourceType == EvSourceType.sensor)
BlocProvider(
create: (context) => LightSensorBloc(
context.read<MeteringCommunicationBloc>(),
context.read<MeteringInteractor>(),
),
),
], ],
child: const MeteringScreen(), child: const MeteringScreen(),
), ),

View file

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