mirror of
https://github.com/vodemn/m3_lightmeter.git
synced 2024-11-21 15:00:40 +00:00
ML-16 [Android] Implement incident light metering (#17)
* wip * rename * wip * rename * fixed camera screen layout * omit camera measure on startup * added calibration for light sensor * save evsource * Update widget_button_measure.dart * fixed iOS init * hide light sensor calibration on ios * cleanup
This commit is contained in:
parent
179008ff77
commit
9ffb5112c1
54 changed files with 1230 additions and 753 deletions
|
@ -33,7 +33,7 @@ The list of features that the old lightmeter app has and that have to be impleme
|
|||
- [x] ISO selecting
|
||||
- [ ] Reciprocity for different films
|
||||
- [x] Reflected light metering
|
||||
- [ ] Incident light metering
|
||||
- [x] Incident light metering
|
||||
|
||||
### Adjust
|
||||
- [x] Light sources EV calibration
|
||||
|
|
|
@ -1,22 +1,24 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:light_sensor/light_sensor.dart';
|
||||
import 'package:lightmeter/data/haptics_service.dart';
|
||||
import 'package:lightmeter/data/models/ev_source_type.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'data/light_sensor_service.dart';
|
||||
import 'data/permissions_service.dart';
|
||||
import 'data/shared_prefs_service.dart';
|
||||
import 'environment.dart';
|
||||
import 'generated/l10n.dart';
|
||||
import 'providers/ev_source_type_provider.dart';
|
||||
import 'res/theme.dart';
|
||||
import 'screens/metering/flow_metering.dart';
|
||||
import 'screens/settings/flow_settings.dart';
|
||||
import 'utils/stop_type_provider.dart';
|
||||
|
||||
final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();
|
||||
|
||||
class Application extends StatelessWidget {
|
||||
final Environment env;
|
||||
|
||||
|
@ -24,55 +26,60 @@ class Application extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<SharedPreferences>(
|
||||
future: SharedPreferences.getInstance(),
|
||||
return FutureBuilder(
|
||||
future: Future.wait([
|
||||
SharedPreferences.getInstance(),
|
||||
Platform.isAndroid ? LightSensor.hasSensor : Future.value(false),
|
||||
]),
|
||||
builder: (_, snapshot) {
|
||||
if (snapshot.data != null) {
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
Provider.value(value: env),
|
||||
Provider.value(value: EvSourceType.camera),
|
||||
Provider(create: (_) => UserPreferencesService(snapshot.data!)),
|
||||
Provider.value(value: env.copyWith(hasLightSensor: snapshot.data![1] as bool)),
|
||||
Provider(create: (_) => UserPreferencesService(snapshot.data![0] as SharedPreferences)),
|
||||
Provider(create: (_) => const HapticsService()),
|
||||
Provider(create: (_) => PermissionsService()),
|
||||
Provider(create: (_) => const LightSensorService()),
|
||||
],
|
||||
child: StopTypeProvider(
|
||||
child: ThemeProvider(
|
||||
builder: (context, _) {
|
||||
final systemIconsBrightness = ThemeData.estimateBrightnessForColor(
|
||||
context.watch<ThemeData>().colorScheme.onSurface,
|
||||
);
|
||||
return AnnotatedRegion(
|
||||
value: SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarBrightness: systemIconsBrightness == Brightness.light
|
||||
? Brightness.dark
|
||||
: Brightness.light,
|
||||
statusBarIconBrightness: 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!,
|
||||
child: EvSourceTypeProvider(
|
||||
child: ThemeProvider(
|
||||
builder: (context, _) {
|
||||
final systemIconsBrightness = ThemeData.estimateBrightnessForColor(
|
||||
context.watch<ThemeData>().colorScheme.onSurface,
|
||||
);
|
||||
return AnnotatedRegion(
|
||||
value: SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarBrightness: systemIconsBrightness == Brightness.light
|
||||
? Brightness.dark
|
||||
: Brightness.light,
|
||||
statusBarIconBrightness: systemIconsBrightness,
|
||||
systemNavigationBarColor: context.watch<ThemeData>().colorScheme.surface,
|
||||
systemNavigationBarIconBrightness: systemIconsBrightness,
|
||||
),
|
||||
initialRoute: "metering",
|
||||
routes: {
|
||||
"metering": (context) => const MeteringFlow(),
|
||||
"settings": (context) => const SettingsFlow(),
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
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",
|
||||
routes: {
|
||||
"metering": (context) => const MeteringFlow(),
|
||||
"settings": (context) => const SettingsFlow(),
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
9
lib/data/light_sensor_service.dart
Normal file
9
lib/data/light_sensor_service.dart
Normal file
|
@ -0,0 +1,9 @@
|
|||
import 'package:light_sensor/light_sensor.dart';
|
||||
|
||||
class LightSensorService {
|
||||
const LightSensorService();
|
||||
|
||||
Future<bool> hasSensor() async => await LightSensor.hasSensor ?? false;
|
||||
|
||||
Stream<int> luxStream() => LightSensor.lightSensorStream;
|
||||
}
|
|
@ -1,14 +1,17 @@
|
|||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'models/ev_source_type.dart';
|
||||
import 'models/photography_values/iso_value.dart';
|
||||
import 'models/photography_values/nd_value.dart';
|
||||
import 'models/theme_type.dart';
|
||||
|
||||
class UserPreferencesService {
|
||||
static const _isoKey = "iso";
|
||||
static const _ndFilterKey = "nd";
|
||||
static const _ndFilterKey = "ndFilter";
|
||||
|
||||
static const _evSourceTypeKey = "evSourceType";
|
||||
static const _cameraEvCalibrationKey = "cameraEvCalibration";
|
||||
static const _lightSensorEvCalibrationKey = "lightSensorEvCalibration";
|
||||
|
||||
static const _hapticsKey = "haptics";
|
||||
static const _themeTypeKey = "themeType";
|
||||
|
@ -24,12 +27,18 @@ class UserPreferencesService {
|
|||
NdValue get ndFilter => ndValues.firstWhere((v) => v.value == (_sharedPreferences.getInt(_ndFilterKey) ?? 0));
|
||||
set ndFilter(NdValue value) => _sharedPreferences.setInt(_ndFilterKey, value.value);
|
||||
|
||||
EvSourceType get evSourceType => EvSourceType.values[_sharedPreferences.getInt(_evSourceTypeKey) ?? 0];
|
||||
set evSourceType(EvSourceType value) => _sharedPreferences.setInt(_evSourceTypeKey, value.index);
|
||||
|
||||
bool get haptics => _sharedPreferences.getBool(_hapticsKey) ?? false;
|
||||
set haptics(bool value) => _sharedPreferences.setBool(_hapticsKey, value);
|
||||
|
||||
double get cameraEvCalibration => _sharedPreferences.getDouble(_cameraEvCalibrationKey) ?? 0.0;
|
||||
set cameraEvCalibration(double value) => _sharedPreferences.setDouble(_cameraEvCalibrationKey, value);
|
||||
|
||||
double get lightSensorEvCalibration => _sharedPreferences.getDouble(_lightSensorEvCalibrationKey) ?? 0.0;
|
||||
set lightSensorEvCalibration(double value) => _sharedPreferences.setDouble(_lightSensorEvCalibrationKey, value);
|
||||
|
||||
ThemeType get themeType => ThemeType.values[_sharedPreferences.getInt(_themeTypeKey) ?? 0];
|
||||
set themeType(ThemeType value) => _sharedPreferences.setInt(_themeTypeKey, value.index);
|
||||
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,16 +1,22 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:lightmeter/data/haptics_service.dart';
|
||||
import 'package:lightmeter/data/light_sensor_service.dart';
|
||||
import 'package:lightmeter/data/shared_prefs_service.dart';
|
||||
|
||||
class MeteringInteractor {
|
||||
final UserPreferencesService _userPreferencesService;
|
||||
final HapticsService _hapticsService;
|
||||
final LightSensorService _lightSensorService;
|
||||
|
||||
const MeteringInteractor(
|
||||
this._userPreferencesService,
|
||||
this._hapticsService,
|
||||
this._lightSensorService,
|
||||
);
|
||||
|
||||
double get cameraEvCalibration => _userPreferencesService.cameraEvCalibration;
|
||||
double get lightSensorEvCalibration => _userPreferencesService.lightSensorEvCalibration;
|
||||
|
||||
bool get isHapticsEnabled => _userPreferencesService.haptics;
|
||||
|
||||
|
@ -25,4 +31,14 @@ class MeteringInteractor {
|
|||
}
|
||||
|
||||
void enableHaptics(bool enable) => _userPreferencesService.haptics = enable;
|
||||
|
||||
Future<bool> hasAmbientLightSensor() async {
|
||||
if (Platform.isAndroid) {
|
||||
return _lightSensorService.hasSensor();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Stream<int> luxStream() => _lightSensorService.luxStream();
|
||||
}
|
||||
|
|
|
@ -13,6 +13,9 @@ class SettingsInteractor {
|
|||
double get cameraEvCalibration => _userPreferencesService.cameraEvCalibration;
|
||||
void setCameraEvCalibration(double value) => _userPreferencesService.cameraEvCalibration = value;
|
||||
|
||||
double get lightSensorEvCalibration => _userPreferencesService.lightSensorEvCalibration;
|
||||
void setLightSensorEvCalibration(double value) => _userPreferencesService.lightSensorEvCalibration = value;
|
||||
|
||||
bool get isHapticsEnabled => _userPreferencesService.haptics;
|
||||
|
||||
/// Executes vibration if haptics are enabled in settings
|
||||
|
|
|
@ -28,8 +28,10 @@
|
|||
"halfStops": "1/2",
|
||||
"thirdStops": "1/3",
|
||||
"calibration": "Calibration",
|
||||
"calibrationMessage": "The accuracy of the readings measured by this application depends entirely on the hardware of the device. Therefore, consider testing this application and setting up an EV calibration value that will give you the desired measurement results.",
|
||||
"calibrationMessage": "The accuracy of the readings measured by this application depends entirely on the hardware of the device. Therefore, consider testing this application and setting up EV calibration values that will give you the desired measurement results.",
|
||||
"calibrationMessageCameraOnly": "The accuracy of the readings measured by this application depends entirely on the rear camera of the device. Therefore, consider testing this application and setting up an EV calibration value that will give you the desired measurement results.",
|
||||
"camera": "Camera",
|
||||
"lightSensor": "Light sensor",
|
||||
"general": "General",
|
||||
"haptics": "Haptics",
|
||||
"theme": "Theme",
|
||||
|
|
65
lib/providers/ev_source_type_provider.dart
Normal file
65
lib/providers/ev_source_type_provider.dart
Normal file
|
@ -0,0 +1,65 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/data/models/ev_source_type.dart';
|
||||
import 'package:lightmeter/data/shared_prefs_service.dart';
|
||||
import 'package:lightmeter/environment.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class EvSourceTypeProvider extends StatefulWidget {
|
||||
final Widget child;
|
||||
|
||||
const EvSourceTypeProvider({required this.child, super.key});
|
||||
|
||||
static EvSourceTypeProviderState of(BuildContext context) {
|
||||
return context.findAncestorStateOfType<EvSourceTypeProviderState>()!;
|
||||
}
|
||||
|
||||
@override
|
||||
State<EvSourceTypeProvider> createState() => EvSourceTypeProviderState();
|
||||
}
|
||||
|
||||
class EvSourceTypeProviderState extends State<EvSourceTypeProvider> {
|
||||
late final ValueNotifier<EvSourceType> valueListenable;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final evSourceType = context.read<UserPreferencesService>().evSourceType;
|
||||
valueListenable = ValueNotifier(
|
||||
evSourceType == EvSourceType.sensor && !context.read<Environment>().hasLightSensor
|
||||
? EvSourceType.camera
|
||||
: evSourceType,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
valueListenable.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: valueListenable,
|
||||
builder: (_, value, child) => Provider.value(
|
||||
value: value,
|
||||
child: child,
|
||||
),
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
|
||||
void toggleType() {
|
||||
switch (valueListenable.value) {
|
||||
case EvSourceType.camera:
|
||||
if (context.read<Environment>().hasLightSensor) {
|
||||
valueListenable.value = EvSourceType.sensor;
|
||||
}
|
||||
break;
|
||||
case EvSourceType.sensor:
|
||||
valueListenable.value = EvSourceType.camera;
|
||||
break;
|
||||
}
|
||||
context.read<UserPreferencesService>().evSourceType = valueListenable.value;
|
||||
}
|
||||
}
|
|
@ -21,6 +21,10 @@ class Dimens {
|
|||
static const Duration durationML = Duration(milliseconds: 250);
|
||||
static const Duration durationL = Duration(milliseconds: 300);
|
||||
|
||||
// TopBar
|
||||
/// Probably this is a bad practice, but with text size locked, the height is always 212
|
||||
static const double readingContainerHeight = 212;
|
||||
|
||||
// `CenteredSlider`
|
||||
static const double cameraSliderTrackHeight = grid4;
|
||||
static const double cameraSliderTrackRadius = cameraSliderTrackHeight / 2;
|
||||
|
|
|
@ -6,7 +6,9 @@ import 'state_communication_metering.dart';
|
|||
class MeteringCommunicationBloc
|
||||
extends Bloc<MeteringCommunicationEvent, MeteringCommunicationState> {
|
||||
MeteringCommunicationBloc() : super(const InitState()) {
|
||||
on<MeasureEvent>((_, emit) => emit(const MeasureState()));
|
||||
// `MeasureState` is not const, so that `Bloc` treats each state as new and updates state stream
|
||||
// ignore: prefer_const_constructors
|
||||
on<MeasureEvent>((_, emit) => emit(MeasureState()));
|
||||
on<MeasuredEvent>((event, emit) => emit(MeasuredState(event.ev100)));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,6 +36,11 @@ class _MeteringMeasureButtonState extends State<MeteringMeasureButton> {
|
|||
_isPressed = false;
|
||||
});
|
||||
},
|
||||
onTapCancel: () {
|
||||
setState(() {
|
||||
_isPressed = false;
|
||||
});
|
||||
},
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(widget.size / 2),
|
||||
|
|
|
@ -1,14 +1,18 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:lightmeter/data/models/ev_source_type.dart';
|
||||
import 'package:lightmeter/res/dimens.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'components/widget_button_measure.dart';
|
||||
import 'components/widget_button_secondary.dart';
|
||||
|
||||
class MeteringBottomControls extends StatelessWidget {
|
||||
final VoidCallback? onSwitchEvSourceType;
|
||||
final VoidCallback onMeasure;
|
||||
final VoidCallback onSettings;
|
||||
|
||||
const MeteringBottomControls({
|
||||
required this.onSwitchEvSourceType,
|
||||
required this.onMeasure,
|
||||
required this.onSettings,
|
||||
super.key,
|
||||
|
@ -30,7 +34,17 @@ class MeteringBottomControls extends StatelessWidget {
|
|||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
const Spacer(),
|
||||
if (onSwitchEvSourceType != null)
|
||||
Expanded(
|
||||
child: MeteringSecondaryButton(
|
||||
onPressed: onSwitchEvSourceType!,
|
||||
icon: context.watch<EvSourceType>() != EvSourceType.camera
|
||||
? Icons.camera_rear
|
||||
: Icons.wb_incandescent,
|
||||
),
|
||||
)
|
||||
else
|
||||
const Spacer(),
|
||||
MeteringMeasureButton(
|
||||
onTap: onMeasure,
|
||||
),
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -7,7 +7,7 @@ import 'package:exif/exif.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:lightmeter/interactors/metering_interactor.dart';
|
||||
import 'package:lightmeter/screens/metering/ev_source/ev_source_bloc.dart';
|
||||
import 'package:lightmeter/screens/metering/components/shared/ev_source_base/bloc_base_ev_source.dart';
|
||||
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
|
||||
import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart'
|
||||
as communication_event;
|
||||
|
@ -15,10 +15,10 @@ import 'package:lightmeter/screens/metering/communication/state_communication_me
|
|||
as communication_states;
|
||||
import 'package:lightmeter/utils/log_2.dart';
|
||||
|
||||
import 'event_camera.dart';
|
||||
import 'state_camera.dart';
|
||||
import 'event_container_camera.dart';
|
||||
import 'state_container_camera.dart';
|
||||
|
||||
class CameraBloc extends EvSourceBloc<CameraEvent, CameraState> {
|
||||
class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraContainerState> {
|
||||
final MeteringInteractor _meteringInteractor;
|
||||
late final _WidgetsBindingObserver _observer;
|
||||
CameraController? _cameraController;
|
||||
|
@ -33,9 +33,9 @@ class CameraBloc extends EvSourceBloc<CameraEvent, CameraState> {
|
|||
double _exposureStep = 0.0;
|
||||
double _currentExposureOffset = 0.0;
|
||||
|
||||
CameraBloc(
|
||||
MeteringCommunicationBloc communicationBloc,
|
||||
CameraContainerBloc(
|
||||
this._meteringInteractor,
|
||||
MeteringCommunicationBloc communicationBloc,
|
||||
) : super(
|
||||
communicationBloc,
|
||||
const CameraInitState(),
|
||||
|
@ -54,8 +54,8 @@ class CameraBloc extends EvSourceBloc<CameraEvent, CameraState> {
|
|||
@override
|
||||
Future<void> close() async {
|
||||
WidgetsBinding.instance.removeObserver(_observer);
|
||||
_cameraController?.dispose();
|
||||
super.close();
|
||||
unawaited(_cameraController?.dispose());
|
||||
return super.close();
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -110,11 +110,6 @@ class CameraBloc extends EvSourceBloc<CameraEvent, CameraState> {
|
|||
emit(CameraInitializedState(_cameraController!));
|
||||
|
||||
_emitActiveState(emit);
|
||||
_takePhoto().then((ev100) {
|
||||
if (ev100 != null) {
|
||||
communicationBloc.add(communication_event.MeasuredEvent(ev100));
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
emit(const CameraErrorState());
|
||||
}
|
||||
|
@ -139,11 +134,9 @@ class CameraBloc extends EvSourceBloc<CameraEvent, CameraState> {
|
|||
|
||||
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,
|
||||
));
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:lightmeter/data/models/exposure_pair.dart';
|
||||
import 'package:lightmeter/data/models/photography_values/iso_value.dart';
|
||||
import 'package:lightmeter/data/models/photography_values/nd_value.dart';
|
||||
import 'package:lightmeter/interactors/metering_interactor.dart';
|
||||
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
|
||||
|
||||
import 'bloc_container_camera.dart';
|
||||
import 'widget_container_camera.dart';
|
||||
|
||||
class CameraContainerProvider extends StatelessWidget {
|
||||
final ExposurePair? fastest;
|
||||
final ExposurePair? slowest;
|
||||
final IsoValue iso;
|
||||
final NdValue nd;
|
||||
final ValueChanged<IsoValue> onIsoChanged;
|
||||
final ValueChanged<NdValue> onNdChanged;
|
||||
final List<ExposurePair> exposurePairs;
|
||||
|
||||
const CameraContainerProvider({
|
||||
required this.fastest,
|
||||
required this.slowest,
|
||||
required this.iso,
|
||||
required this.nd,
|
||||
required this.onIsoChanged,
|
||||
required this.onNdChanged,
|
||||
required this.exposurePairs,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => CameraContainerBloc(
|
||||
context.read<MeteringInteractor>(),
|
||||
context.read<MeteringCommunicationBloc>(),
|
||||
),
|
||||
child: CameraContainer(
|
||||
fastest: fastest,
|
||||
slowest: slowest,
|
||||
iso: iso,
|
||||
nd: nd,
|
||||
onIsoChanged: onIsoChanged,
|
||||
onNdChanged: onNdChanged,
|
||||
exposurePairs: exposurePairs,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -0,0 +1,159 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:lightmeter/data/models/exposure_pair.dart';
|
||||
import 'package:lightmeter/data/models/photography_values/iso_value.dart';
|
||||
import 'package:lightmeter/data/models/photography_values/nd_value.dart';
|
||||
import 'package:lightmeter/platform_config.dart';
|
||||
import 'package:lightmeter/res/dimens.dart';
|
||||
import 'package:lightmeter/screens/metering/components/camera_container/components/camera_view/widget_camera_view.dart';
|
||||
import 'package:lightmeter/screens/metering/components/shared/exposure_pairs_list/widget_list_exposure_pairs.dart';
|
||||
import 'package:lightmeter/screens/metering/components/shared/metering_top_bar/widget_top_bar_metering.dart';
|
||||
import 'package:lightmeter/screens/metering/components/shared/readings_container/widget_container_readings.dart';
|
||||
|
||||
import 'bloc_container_camera.dart';
|
||||
import 'components/camera_controls/widget_camera_controls.dart';
|
||||
import 'event_container_camera.dart';
|
||||
import 'state_container_camera.dart';
|
||||
|
||||
class CameraContainer extends StatelessWidget {
|
||||
final ExposurePair? fastest;
|
||||
final ExposurePair? slowest;
|
||||
final IsoValue iso;
|
||||
final NdValue nd;
|
||||
final ValueChanged<IsoValue> onIsoChanged;
|
||||
final ValueChanged<NdValue> onNdChanged;
|
||||
final List<ExposurePair> exposurePairs;
|
||||
|
||||
const CameraContainer({
|
||||
required this.fastest,
|
||||
required this.slowest,
|
||||
required this.iso,
|
||||
required this.nd,
|
||||
required this.onIsoChanged,
|
||||
required this.onNdChanged,
|
||||
required this.exposurePairs,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final topBarOverflow = Dimens.readingContainerHeight -
|
||||
((MediaQuery.of(context).size.width - Dimens.grid8 - 2 * Dimens.paddingM) / 2) /
|
||||
PlatformConfig.cameraPreviewAspectRatio;
|
||||
return Column(
|
||||
children: [
|
||||
MeteringTopBar(
|
||||
readingsContainer: ReadingsContainer(
|
||||
fastest: fastest,
|
||||
slowest: slowest,
|
||||
iso: iso,
|
||||
nd: nd,
|
||||
onIsoChanged: onIsoChanged,
|
||||
onNdChanged: onNdChanged,
|
||||
),
|
||||
appendixHeight: topBarOverflow,
|
||||
preview: const _CameraViewBuilder(),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: Dimens.paddingM),
|
||||
child: _MiddleContentWrapper(
|
||||
topBarOverflow: topBarOverflow,
|
||||
leftContent: ExposurePairsList(exposurePairs),
|
||||
rightContent: const _CameraControlsBuilder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CameraViewBuilder extends StatelessWidget {
|
||||
const _CameraViewBuilder({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AspectRatio(
|
||||
aspectRatio: PlatformConfig.cameraPreviewAspectRatio,
|
||||
child: Center(
|
||||
child: BlocBuilder<CameraContainerBloc, CameraContainerState>(
|
||||
buildWhen: (previous, current) => current is CameraInitializedState,
|
||||
builder: (context, state) => state is CameraInitializedState
|
||||
? CameraView(controller: state.controller)
|
||||
: const ColoredBox(color: Colors.black),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CameraControlsBuilder extends StatelessWidget {
|
||||
const _CameraControlsBuilder();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: Dimens.paddingM),
|
||||
child: BlocBuilder<CameraContainerBloc, CameraContainerState>(
|
||||
builder: (context, state) => AnimatedSwitcher(
|
||||
duration: Dimens.durationS,
|
||||
child: state is CameraActiveState
|
||||
? CameraControls(
|
||||
exposureOffsetRange: state.exposureOffsetRange,
|
||||
exposureOffsetValue: state.currentExposureOffset,
|
||||
onExposureOffsetChanged: (value) {
|
||||
context.read<CameraContainerBloc>().add(ExposureOffsetChangedEvent(value));
|
||||
},
|
||||
zoomRange: state.zoomRange,
|
||||
zoomValue: state.currentZoom,
|
||||
onZoomChanged: (value) {
|
||||
context.read<CameraContainerBloc>().add(ZoomChangedEvent(value));
|
||||
},
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MiddleContentWrapper extends StatelessWidget {
|
||||
final double topBarOverflow;
|
||||
final Widget leftContent;
|
||||
final Widget rightContent;
|
||||
|
||||
const _MiddleContentWrapper({
|
||||
required this.topBarOverflow,
|
||||
required this.leftContent,
|
||||
required this.rightContent,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) => OverflowBox(
|
||||
alignment: Alignment.bottomCenter,
|
||||
maxHeight: constraints.maxHeight + topBarOverflow.abs(),
|
||||
maxWidth: constraints.maxWidth,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: topBarOverflow >= 0 ? topBarOverflow : 0),
|
||||
child: leftContent,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: Dimens.grid8),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: topBarOverflow <= 0 ? -topBarOverflow : 0),
|
||||
child: rightContent,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
import 'dart:async';
|
||||
import 'package:lightmeter/interactors/metering_interactor.dart';
|
||||
import 'package:lightmeter/screens/metering/components/shared/ev_source_base/bloc_base_ev_source.dart';
|
||||
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
|
||||
import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart'
|
||||
as communication_event;
|
||||
import 'package:lightmeter/screens/metering/communication/state_communication_metering.dart'
|
||||
as communication_states;
|
||||
import 'package:lightmeter/utils/log_2.dart';
|
||||
|
||||
import 'event_container_light_sensor.dart';
|
||||
import 'state_container_light_sensor.dart';
|
||||
|
||||
class LightSensorContainerBloc
|
||||
extends EvSourceBlocBase<LightSensorContainerEvent, LightSensorContainerState> {
|
||||
final MeteringInteractor _meteringInteractor;
|
||||
|
||||
StreamSubscription<int>? _luxSubscriptions;
|
||||
|
||||
LightSensorContainerBloc(
|
||||
this._meteringInteractor,
|
||||
MeteringCommunicationBloc communicationBloc,
|
||||
) : super(
|
||||
communicationBloc,
|
||||
const LightSensorInitState(),
|
||||
);
|
||||
|
||||
@override
|
||||
void onCommunicationState(communication_states.SourceState communicationState) {
|
||||
if (communicationState is communication_states.MeasureState) {
|
||||
if (_luxSubscriptions == null) {
|
||||
_luxSubscriptions = _meteringInteractor.luxStream().listen((event) {
|
||||
communicationBloc.add(communication_event.MeasuredEvent(
|
||||
log2(event.toDouble() / 2.5) + _meteringInteractor.lightSensorEvCalibration,
|
||||
));
|
||||
});
|
||||
} else {
|
||||
_luxSubscriptions?.cancel().then((_) => _luxSubscriptions = null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
_luxSubscriptions?.cancel().then((_) => _luxSubscriptions = null);
|
||||
return super.close();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
abstract class LightSensorContainerEvent {
|
||||
const LightSensorContainerEvent();
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:lightmeter/data/models/exposure_pair.dart';
|
||||
import 'package:lightmeter/data/models/photography_values/iso_value.dart';
|
||||
import 'package:lightmeter/data/models/photography_values/nd_value.dart';
|
||||
import 'package:lightmeter/interactors/metering_interactor.dart';
|
||||
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
|
||||
|
||||
import 'bloc_container_light_sensor.dart';
|
||||
import 'widget_container_light_sensor.dart';
|
||||
|
||||
class LightSensorContainerProvider extends StatelessWidget {
|
||||
final ExposurePair? fastest;
|
||||
final ExposurePair? slowest;
|
||||
final IsoValue iso;
|
||||
final NdValue nd;
|
||||
final ValueChanged<IsoValue> onIsoChanged;
|
||||
final ValueChanged<NdValue> onNdChanged;
|
||||
final List<ExposurePair> exposurePairs;
|
||||
|
||||
const LightSensorContainerProvider({
|
||||
required this.fastest,
|
||||
required this.slowest,
|
||||
required this.iso,
|
||||
required this.nd,
|
||||
required this.onIsoChanged,
|
||||
required this.onNdChanged,
|
||||
required this.exposurePairs,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
lazy: false,
|
||||
create: (context) => LightSensorContainerBloc(
|
||||
context.read<MeteringInteractor>(),
|
||||
context.read<MeteringCommunicationBloc>(),
|
||||
),
|
||||
child: LightSensorContainer(
|
||||
fastest: fastest,
|
||||
slowest: slowest,
|
||||
iso: iso,
|
||||
nd: nd,
|
||||
onIsoChanged: onIsoChanged,
|
||||
onNdChanged: onNdChanged,
|
||||
exposurePairs: exposurePairs,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
abstract class LightSensorContainerState {
|
||||
const LightSensorContainerState();
|
||||
}
|
||||
|
||||
class LightSensorInitState extends LightSensorContainerState {
|
||||
const LightSensorInitState();
|
||||
}
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -4,11 +4,11 @@ import 'package:lightmeter/screens/metering/communication/bloc_communication_met
|
|||
import 'package:lightmeter/screens/metering/communication/state_communication_metering.dart'
|
||||
as communication_states;
|
||||
|
||||
abstract class EvSourceBloc<E, S> extends Bloc<E, S> {
|
||||
abstract class EvSourceBlocBase<E, S> extends Bloc<E, S> {
|
||||
final MeteringCommunicationBloc communicationBloc;
|
||||
late final StreamSubscription<communication_states.SourceState> _communicationSubscription;
|
||||
|
||||
EvSourceBloc(this.communicationBloc, super.initialState) {
|
||||
EvSourceBlocBase(this.communicationBloc, super.initialState) {
|
||||
_communicationSubscription = communicationBloc.stream
|
||||
.where((event) => event is communication_states.SourceState)
|
||||
.map((event) => event as communication_states.SourceState)
|
||||
|
@ -18,7 +18,7 @@ abstract class EvSourceBloc<E, S> extends Bloc<E, S> {
|
|||
@override
|
||||
Future<void> close() async {
|
||||
await _communicationSubscription.cancel();
|
||||
super.close();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void onCommunicationState(communication_states.SourceState communicationState);
|
|
@ -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<ExposurePair> exposurePairs;
|
|
@ -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,
|
||||
),
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
]
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -7,7 +7,7 @@ typedef DialogPickerItemBuilder<T extends PhotographyValue> = Widget Function(Bu
|
|||
typedef DialogPickerEvDifferenceBuilder<T extends PhotographyValue> = String Function(
|
||||
T selected, T other);
|
||||
|
||||
class MeteringScreenDialogPicker<T extends PhotographyValue> extends StatefulWidget {
|
||||
class PhotographyValuePickerDialog<T extends PhotographyValue> extends StatefulWidget {
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final T initialValue;
|
||||
|
@ -17,7 +17,7 @@ class MeteringScreenDialogPicker<T extends PhotographyValue> 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<T extends PhotographyValue> extends StatefulWid
|
|||
});
|
||||
|
||||
@override
|
||||
State<MeteringScreenDialogPicker<T>> createState() => _MeteringScreenDialogPickerState<T>();
|
||||
State<PhotographyValuePickerDialog<T>> createState() => _PhotographyValuePickerDialogState<T>();
|
||||
}
|
||||
|
||||
class _MeteringScreenDialogPickerState<T extends PhotographyValue>
|
||||
extends State<MeteringScreenDialogPicker<T>> {
|
||||
class _PhotographyValuePickerDialogState<T extends PhotographyValue>
|
||||
extends State<PhotographyValuePickerDialog<T>> {
|
||||
late T _selectedValue = widget.initialValue;
|
||||
final _scrollController = ScrollController();
|
||||
|
|
@ -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));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -68,11 +68,17 @@ class _ReadingValueBuilder extends StatelessWidget {
|
|||
Text(
|
||||
reading.label,
|
||||
style: textTheme.labelMedium?.copyWith(color: textColor),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.visible,
|
||||
softWrap: false,
|
||||
),
|
||||
const SizedBox(height: Dimens.grid4),
|
||||
Text(
|
||||
reading.value,
|
||||
style: textTheme.titleMedium?.copyWith(color: textColor),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.visible,
|
||||
softWrap: false,
|
||||
),
|
||||
],
|
||||
);
|
|
@ -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(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
import 'dart:math';
|
||||
import 'package:lightmeter/screens/metering/ev_source/ev_source_bloc.dart';
|
||||
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
|
||||
import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart'
|
||||
as communication_event;
|
||||
import 'package:lightmeter/screens/metering/communication/state_communication_metering.dart'
|
||||
as communication_states;
|
||||
|
||||
import 'event_random_ev.dart';
|
||||
import 'state_random_ev.dart';
|
||||
|
||||
class RandomEvBloc extends EvSourceBloc<RandomEvEvent, RandomEvState> {
|
||||
final random = Random();
|
||||
|
||||
RandomEvBloc(MeteringCommunicationBloc communicationBloc)
|
||||
: super(
|
||||
communicationBloc,
|
||||
RandomEvState(Random().nextDouble() * 15),
|
||||
);
|
||||
|
||||
@override
|
||||
void onCommunicationState(communication_states.SourceState communicationState) {
|
||||
if (communicationState is communication_states.MeasureState) {
|
||||
communicationBloc.add(communication_event.MeasuredEvent(random.nextDouble() * 15));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
abstract class RandomEvEvent {
|
||||
const RandomEvEvent();
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
class RandomEvState {
|
||||
final double ev;
|
||||
|
||||
const RandomEvState(this.ev);
|
||||
}
|
|
@ -1,27 +1,31 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:lightmeter/data/haptics_service.dart';
|
||||
import 'package:lightmeter/data/models/ev_source_type.dart';
|
||||
import 'package:lightmeter/data/light_sensor_service.dart';
|
||||
import 'package:lightmeter/data/models/photography_values/photography_value.dart';
|
||||
import 'package:lightmeter/data/shared_prefs_service.dart';
|
||||
import 'package:lightmeter/interactors/metering_interactor.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'ev_source/camera/bloc_camera.dart';
|
||||
import 'ev_source/random_ev/bloc_random_ev.dart';
|
||||
import 'bloc_metering.dart';
|
||||
import 'communication/bloc_communication_metering.dart';
|
||||
import 'screen_metering.dart';
|
||||
|
||||
class MeteringFlow extends StatelessWidget {
|
||||
class MeteringFlow extends StatefulWidget {
|
||||
const MeteringFlow({super.key});
|
||||
|
||||
@override
|
||||
State<MeteringFlow> createState() => _MeteringFlowState();
|
||||
}
|
||||
|
||||
class _MeteringFlowState extends State<MeteringFlow> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Provider(
|
||||
create: (context) => MeteringInteractor(
|
||||
context.read<UserPreferencesService>(),
|
||||
context.read<HapticsService>(),
|
||||
context.read<LightSensorService>(),
|
||||
),
|
||||
child: MultiBlocProvider(
|
||||
providers: [
|
||||
|
@ -34,17 +38,6 @@ class MeteringFlow extends StatelessWidget {
|
|||
context.read<StopType>(),
|
||||
),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => CameraBloc(
|
||||
context.read<MeteringCommunicationBloc>(),
|
||||
context.read<MeteringInteractor>(),
|
||||
),
|
||||
),
|
||||
if (context.read<EvSourceType>() == EvSourceType.sensor)
|
||||
BlocProvider(
|
||||
lazy: false,
|
||||
create: (context) => RandomEvBloc(context.read<MeteringCommunicationBloc>()),
|
||||
),
|
||||
],
|
||||
child: const MeteringScreen(),
|
||||
),
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:lightmeter/data/models/ev_source_type.dart';
|
||||
import 'package:lightmeter/data/models/photography_values/photography_value.dart';
|
||||
import 'package:lightmeter/res/dimens.dart';
|
||||
import 'package:lightmeter/environment.dart';
|
||||
import 'package:lightmeter/providers/ev_source_type_provider.dart';
|
||||
|
||||
import 'components/bottom_controls/widget_bottom_controls.dart';
|
||||
import 'components/camera/widget_exposure_slider.dart';
|
||||
import 'components/camera/widget_zoom_camera.dart';
|
||||
import 'components/exposure_pairs_list/widget_list_exposure_pairs.dart';
|
||||
import 'components/topbar/widget_topbar.dart';
|
||||
import 'components/camera_container/provider_container_camera.dart';
|
||||
import 'components/light_sensor_container/provider_container_light_sensor.dart';
|
||||
import 'bloc_metering.dart';
|
||||
import 'event_metering.dart';
|
||||
import 'state_metering.dart';
|
||||
|
@ -20,8 +20,6 @@ class MeteringScreen extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _MeteringScreenState extends State<MeteringScreen> {
|
||||
double topBarOverflow = 0.0;
|
||||
|
||||
MeteringBloc get _bloc => context.read<MeteringBloc>();
|
||||
|
||||
@override
|
||||
|
@ -34,84 +32,39 @@ class _MeteringScreenState extends State<MeteringScreen> {
|
|||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Theme.of(context).colorScheme.background,
|
||||
body: BlocBuilder<MeteringBloc, MeteringState>(
|
||||
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<MeteringBloc, MeteringState>(
|
||||
builder: (context, state) => context.watch<EvSourceType>() == 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<Environment>().hasLightSensor
|
||||
? EvSourceTypeProvider.of(context).toggleType
|
||||
: null,
|
||||
onMeasure: () => _bloc.add(const MeasureEvent()),
|
||||
onSettings: () => Navigator.pushNamed(context, 'settings'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -8,26 +8,47 @@ class CalibrationDialogBloc extends Bloc<CalibrationDialogEvent, CalibrationDial
|
|||
final SettingsInteractor _settingsInteractor;
|
||||
|
||||
late double _cameraEvCalibration = _settingsInteractor.cameraEvCalibration;
|
||||
late double _lightSensorEvCalibration = _settingsInteractor.lightSensorEvCalibration;
|
||||
|
||||
CalibrationDialogBloc(this._settingsInteractor)
|
||||
: super(CalibrationDialogState(_settingsInteractor.cameraEvCalibration)) {
|
||||
: super(
|
||||
CalibrationDialogState(
|
||||
_settingsInteractor.cameraEvCalibration,
|
||||
_settingsInteractor.lightSensorEvCalibration,
|
||||
),
|
||||
) {
|
||||
on<CameraEvCalibrationChangedEvent>(_onCameraEvCalibrationChanged);
|
||||
on<CameraEvCalibrationResetEvent>(_onCameraEvCalibrationReset);
|
||||
on<LightSensorEvCalibrationChangedEvent>(_onLightSensorEvCalibrationChanged);
|
||||
on<LightSensorEvCalibrationResetEvent>(_onLightSensorEvCalibrationReset);
|
||||
on<SaveCalibrationDialogEvent>(_onSaveCalibration);
|
||||
}
|
||||
|
||||
void _onCameraEvCalibrationChanged(CameraEvCalibrationChangedEvent event, Emitter emit) {
|
||||
_cameraEvCalibration = event.value;
|
||||
emit(CalibrationDialogState(event.value));
|
||||
emit(CalibrationDialogState(_cameraEvCalibration, _lightSensorEvCalibration));
|
||||
}
|
||||
|
||||
void _onCameraEvCalibrationReset(CameraEvCalibrationResetEvent event, Emitter emit) {
|
||||
_settingsInteractor.quickVibration();
|
||||
_cameraEvCalibration = 0;
|
||||
emit(CalibrationDialogState(_cameraEvCalibration));
|
||||
emit(CalibrationDialogState(_cameraEvCalibration, _lightSensorEvCalibration));
|
||||
}
|
||||
|
||||
void _onLightSensorEvCalibrationChanged(
|
||||
LightSensorEvCalibrationChangedEvent event, Emitter emit) {
|
||||
_lightSensorEvCalibration = event.value;
|
||||
emit(CalibrationDialogState(_cameraEvCalibration, _lightSensorEvCalibration));
|
||||
}
|
||||
|
||||
void _onLightSensorEvCalibrationReset(LightSensorEvCalibrationResetEvent event, Emitter emit) {
|
||||
_settingsInteractor.quickVibration();
|
||||
_lightSensorEvCalibration = 0;
|
||||
emit(CalibrationDialogState(_cameraEvCalibration, _lightSensorEvCalibration));
|
||||
}
|
||||
|
||||
void _onSaveCalibration(SaveCalibrationDialogEvent event, __) {
|
||||
_settingsInteractor.setCameraEvCalibration(_cameraEvCalibration);
|
||||
_settingsInteractor.setLightSensorEvCalibration(_lightSensorEvCalibration);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,16 @@ class CameraEvCalibrationResetEvent extends CalibrationDialogEvent {
|
|||
const CameraEvCalibrationResetEvent();
|
||||
}
|
||||
|
||||
class LightSensorEvCalibrationChangedEvent extends CalibrationDialogEvent {
|
||||
final double value;
|
||||
|
||||
const LightSensorEvCalibrationChangedEvent(this.value);
|
||||
}
|
||||
|
||||
class LightSensorEvCalibrationResetEvent extends CalibrationDialogEvent {
|
||||
const LightSensorEvCalibrationResetEvent();
|
||||
}
|
||||
|
||||
class SaveCalibrationDialogEvent extends CalibrationDialogEvent {
|
||||
const SaveCalibrationDialogEvent();
|
||||
}
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
class CalibrationDialogState {
|
||||
final double cameraEvCalibration;
|
||||
final double lightSensorEvCalibration;
|
||||
|
||||
const CalibrationDialogState(this.cameraEvCalibration);
|
||||
const CalibrationDialogState(
|
||||
this.cameraEvCalibration,
|
||||
this.lightSensorEvCalibration,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:lightmeter/environment.dart';
|
||||
import 'package:lightmeter/generated/l10n.dart';
|
||||
import 'package:lightmeter/res/dimens.dart';
|
||||
import 'package:lightmeter/screens/settings/components/calibration/components/calibration_dialog/event_dialog_calibration.dart';
|
||||
|
@ -9,18 +10,12 @@ import 'package:lightmeter/utils/to_string_signed.dart';
|
|||
import 'bloc_dialog_calibration.dart';
|
||||
import 'state_dialog_calibration.dart';
|
||||
|
||||
class CalibrationDialog extends StatefulWidget {
|
||||
class CalibrationDialog extends StatelessWidget {
|
||||
const CalibrationDialog({super.key});
|
||||
|
||||
@override
|
||||
State<CalibrationDialog> createState() => _CalibrationDialogState();
|
||||
}
|
||||
|
||||
class _CalibrationDialogState extends State<CalibrationDialog> {
|
||||
CalibrationDialogBloc get bloc => context.read<CalibrationDialogBloc>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bool hasLightSensor = context.read<Environment>().hasLightSensor;
|
||||
return AlertDialog(
|
||||
titlePadding: const EdgeInsets.fromLTRB(
|
||||
Dimens.paddingL,
|
||||
|
@ -30,18 +25,45 @@ class _CalibrationDialogState extends State<CalibrationDialog> {
|
|||
),
|
||||
title: Text(S.of(context).calibration),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingL),
|
||||
content: BlocBuilder<CalibrationDialogBloc, CalibrationDialogState>(
|
||||
builder: (context, state) => Column(
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(S.of(context).calibrationMessage),
|
||||
const SizedBox(height: Dimens.grid16),
|
||||
_CalibrationUnit(
|
||||
title: S.of(context).camera,
|
||||
value: state.cameraEvCalibration,
|
||||
onChanged: (value) => bloc.add(CameraEvCalibrationChangedEvent(value)),
|
||||
onReset: () => bloc.add(const CameraEvCalibrationResetEvent()),
|
||||
Text(
|
||||
hasLightSensor
|
||||
? S.of(context).calibrationMessage
|
||||
: S.of(context).calibrationMessageCameraOnly,
|
||||
),
|
||||
const SizedBox(height: Dimens.grid16),
|
||||
BlocBuilder<CalibrationDialogBloc, CalibrationDialogState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.cameraEvCalibration != current.cameraEvCalibration,
|
||||
builder: (context, state) => _CalibrationUnit(
|
||||
title: S.of(context).camera,
|
||||
value: state.cameraEvCalibration,
|
||||
onChanged: (value) => context
|
||||
.read<CalibrationDialogBloc>()
|
||||
.add(CameraEvCalibrationChangedEvent(value)),
|
||||
onReset: () => context
|
||||
.read<CalibrationDialogBloc>()
|
||||
.add(const CameraEvCalibrationResetEvent()),
|
||||
),
|
||||
),
|
||||
if (hasLightSensor)
|
||||
BlocBuilder<CalibrationDialogBloc, CalibrationDialogState>(
|
||||
buildWhen: (previous, current) =>
|
||||
previous.lightSensorEvCalibration != current.lightSensorEvCalibration,
|
||||
builder: (context, state) => _CalibrationUnit(
|
||||
title: S.of(context).lightSensor,
|
||||
value: state.lightSensorEvCalibration,
|
||||
onChanged: (value) => context
|
||||
.read<CalibrationDialogBloc>()
|
||||
.add(LightSensorEvCalibrationChangedEvent(value)),
|
||||
onReset: () => context
|
||||
.read<CalibrationDialogBloc>()
|
||||
.add(const LightSensorEvCalibrationResetEvent()),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -58,7 +80,7 @@ class _CalibrationDialogState extends State<CalibrationDialog> {
|
|||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
bloc.add(const SaveCalibrationDialogEvent());
|
||||
context.read<CalibrationDialogBloc>().add(const SaveCalibrationDialogEvent());
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(S.of(context).save),
|
||||
|
|
|
@ -17,6 +17,7 @@ dependencies:
|
|||
sdk: flutter
|
||||
intl: 0.17.0
|
||||
intl_utils: 2.8.1
|
||||
light_sensor: 2.0.2
|
||||
material_color_utilities: 0.2.0
|
||||
package_info_plus: 3.0.2
|
||||
permission_handler: 10.2.0
|
||||
|
|
Loading…
Reference in a new issue