This commit is contained in:
Vadim 2023-01-25 21:58:29 +03:00
parent 31ef42c4c0
commit ab5b747831
24 changed files with 298 additions and 88 deletions

View file

@ -12,7 +12,7 @@ import 'environment.dart';
import 'generated/l10n.dart';
import 'res/theme.dart';
import 'screens/metering/flow_metering.dart';
import 'screens/settings/screen_settings.dart';
import 'screens/settings/flow_settings.dart';
import 'utils/stop_type_provider.dart';
final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();
@ -66,7 +66,7 @@ class Application extends StatelessWidget {
initialRoute: "metering",
routes: {
"metering": (context) => const MeteringFlow(),
"settings": (context) => const SettingsScreen(),
"settings": (context) => const SettingsFlow(),
},
),
);

View file

@ -8,6 +8,8 @@ class UserPreferencesService {
static const _isoKey = "iso";
static const _ndFilterKey = "nd";
static const _cameraEvCalibrationKey = "cameraEvCalibration";
static const _hapticsKey = "haptics";
static const _themeTypeKey = "themeType";
static const _dynamicColorKey = "dynamicColor";
@ -25,6 +27,9 @@ class UserPreferencesService {
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);
ThemeType get themeType => ThemeType.values[_sharedPreferences.getInt(_themeTypeKey) ?? 0];
set themeType(ThemeType value) => _sharedPreferences.setInt(_themeTypeKey, value.index);

View file

@ -1,16 +1,16 @@
import 'package:lightmeter/data/haptics_service.dart';
import 'package:lightmeter/data/shared_prefs_service.dart';
class HapticsInteractor {
class MeteringInteractor {
final UserPreferencesService _userPreferencesService;
final HapticsService _hapticsService;
const HapticsInteractor(
const MeteringInteractor(
this._userPreferencesService,
this._hapticsService,
);
bool get isEnabled => _userPreferencesService.haptics;
bool get isHapticsEnabled => _userPreferencesService.haptics;
/// Executes vibration if haptics are enabled in settings
void quickVibration() {

View file

@ -0,0 +1,29 @@
import 'package:lightmeter/data/haptics_service.dart';
import 'package:lightmeter/data/shared_prefs_service.dart';
class SettingsInteractor {
final UserPreferencesService _userPreferencesService;
final HapticsService _hapticsService;
const SettingsInteractor(
this._userPreferencesService,
this._hapticsService,
);
double get cameraEvCalibration => _userPreferencesService.cameraEvCalibration;
void setCameraEvCalibration(double value) => _userPreferencesService.cameraEvCalibration = value;
bool get isHapticsEnabled => _userPreferencesService.haptics;
/// Executes vibration if haptics are enabled in settings
void quickVibration() {
if (_userPreferencesService.haptics) _hapticsService.quickVibration();
}
/// Executes vibration if haptics are enabled in settings
void responseVibration() {
if (_userPreferencesService.haptics) _hapticsService.responseVibration();
}
void enableHaptics(bool enable) => _userPreferencesService.haptics = enable;
}

View file

@ -5,6 +5,14 @@
"openSettings": "Open settings",
"fastestExposurePair": "Fastest",
"slowestExposurePair": "Slowest",
"ev": "{value} EV",
"@ev": {
"placeholders": {
"value": {
"type": "String"
}
}
},
"iso": "ISO",
"filmSpeed": "Film speed",
"nd": "ND",
@ -12,12 +20,16 @@
"none": "None",
"cancel": "Cancel",
"select": "Select",
"save": "Save",
"settings": "Settings",
"metering": "Metering",
"fractionalStops": "Fractional stops",
"showFractionalStops": "Show fractional stops",
"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.",
"camera": "Camera",
"general": "General",
"haptics": "Haptics",
"theme": "Theme",

View file

@ -21,7 +21,7 @@ class Dimens {
static const Duration durationML = Duration(milliseconds: 250);
static const Duration durationL = Duration(milliseconds: 300);
// `CameraSlider`
// `CenteredSlider`
static const double cameraSliderTrackHeight = grid4;
static const double cameraSliderTrackRadius = cameraSliderTrackHeight / 2;
static const double cameraSliderHandleSize = 32;

View file

@ -7,7 +7,7 @@ import 'package:lightmeter/data/models/exposure_pair.dart';
import 'package:lightmeter/data/models/photography_values/photography_value.dart';
import 'package:lightmeter/data/models/photography_values/shutter_speed_value.dart';
import 'package:lightmeter/data/shared_prefs_service.dart';
import 'package:lightmeter/interactors/haptics_interactor.dart';
import 'package:lightmeter/interactors/metering_interactor.dart';
import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart' as communication_events;
import 'package:lightmeter/screens/metering/communication/state_communication_metering.dart' as communication_states;
import 'package:lightmeter/utils/log_2.dart';
@ -19,7 +19,7 @@ import 'state_metering.dart';
class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
final MeteringCommunicationBloc _communicationBloc;
final UserPreferencesService _userPreferencesService;
final HapticsInteractor _hapticsInteractor;
final MeteringInteractor _meteringInteractor;
late final StreamSubscription<communication_states.ScreenState> _communicationSubscription;
List<ApertureValue> get _apertureValues => apertureValues.whereStopType(stopType);
@ -30,7 +30,7 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
MeteringBloc(
this._communicationBloc,
this._userPreferencesService,
this._hapticsInteractor,
this._meteringInteractor,
this.stopType,
) : super(
MeteringState(
@ -103,12 +103,12 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
}
void _onMeasure(_, __) {
_hapticsInteractor.quickVibration();
_meteringInteractor.quickVibration();
_communicationBloc.add(const communication_events.MeasureEvent());
}
void _onMeasured(MeasuredEvent event, Emitter emit) {
_hapticsInteractor.responseVibration();
_meteringInteractor.responseVibration();
final ev = event.ev100 + log2(state.iso.value / 100);
emit(MeteringState(
iso: state.iso,

View file

@ -4,10 +4,9 @@ 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';
import 'shared/widget_slider_camera.dart';
class CameraExposureSlider extends StatelessWidget {
const CameraExposureSlider({super.key});
@ -21,7 +20,7 @@ class CameraExposureSlider extends StatelessWidget {
IconButton(
icon: const Icon(Icons.sync),
onPressed: state.currentExposureOffset != 0.0
? () => context.read<CameraBloc>().add(const ExposureOffsetResetEvent())
? () => context.read<CameraBloc>().add(const ExposureOffsetChangedEvent(0.0))
: null,
),
Expanded(
@ -32,7 +31,7 @@ class CameraExposureSlider extends StatelessWidget {
padding: const EdgeInsets.symmetric(vertical: Dimens.grid8),
child: _Ruler(state.minExposureOffset, state.maxExposureOffset),
),
CameraSlider(
CenteredSlider(
isVertical: true,
icon: const Icon(Icons.light_mode),
value: state.currentExposureOffset,

View file

@ -4,7 +4,7 @@ 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/widget_slider_camera.dart';
import '../../../shared/centered_slider/widget_slider_centered.dart';
class CameraZoomSlider extends StatelessWidget {
const CameraZoomSlider({super.key});
@ -14,7 +14,7 @@ class CameraZoomSlider extends StatelessWidget {
return BlocBuilder<CameraBloc, CameraState>(
builder: (context, state) {
if (state is CameraActiveState) {
return CameraSlider(
return CenteredSlider(
icon: const Icon(Icons.search),
value: state.currentZoom,
min: state.minZoom,

View file

@ -101,7 +101,7 @@ class _MeteringScreenDialogPickerState<T extends PhotographyValue> extends State
child: widget.itemTitleBuilder(context, widget.values[index]),
),
secondary: widget.values[index].value != _selectedValue.value
? Text('${widget.evDifferenceBuilder.call(_selectedValue, widget.values[index])} EV')
? Text(S.of(context).ev(widget.evDifferenceBuilder.call(_selectedValue, widget.values[index])))
: null,
onChanged: (value) {
if (value != null) {

View file

@ -6,7 +6,7 @@ import 'package:camera/camera.dart';
import 'package:exif/exif.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/interactors/haptics_interactor.dart';
import 'package:lightmeter/interactors/metering_interactor.dart';
import 'package:lightmeter/screens/metering/ev_source/ev_source_bloc.dart';
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart' as communication_event;
@ -17,7 +17,7 @@ import 'event_camera.dart';
import 'state_camera.dart';
class CameraBloc extends EvSourceBloc<CameraEvent, CameraState> {
final HapticsInteractor _hapticsInteractor;
final MeteringInteractor _meteringInteractor;
late final _WidgetsBindingObserver _observer;
CameraController? _cameraController;
CameraController? get cameraController => _cameraController;
@ -33,7 +33,7 @@ class CameraBloc extends EvSourceBloc<CameraEvent, CameraState> {
CameraBloc(
MeteringCommunicationBloc communicationBloc,
this._hapticsInteractor,
this._meteringInteractor,
) : super(
communicationBloc,
const CameraInitState(),
@ -44,7 +44,6 @@ class CameraBloc extends EvSourceBloc<CameraEvent, CameraState> {
on<InitializeEvent>(_onInitialize);
on<ZoomChangedEvent>(_onZoomChanged);
on<ExposureOffsetChangedEvent>(_onExposureOffsetChanged);
on<ExposureOffsetResetEvent>(_onExposureOffsetResetEvent);
add(const InitializeEvent());
}
@ -120,14 +119,10 @@ class CameraBloc extends EvSourceBloc<CameraEvent, CameraState> {
Future<void> _onExposureOffsetChanged(ExposureOffsetChangedEvent event, Emitter emit) async {
_cameraController!.setExposureOffset(event.value);
_currentExposureOffset = event.value;
if (event.value == 0.0) _meteringInteractor.quickVibration();
_emitActiveState(emit);
}
Future<void> _onExposureOffsetResetEvent(ExposureOffsetResetEvent event, Emitter emit) async {
_hapticsInteractor.quickVibration();
add(const ExposureOffsetChangedEvent(0));
}
void _emitActiveState(Emitter emit) {
emit(CameraActiveState(
minZoom: _zoomRange!.start,

View file

@ -17,7 +17,3 @@ class ExposureOffsetChangedEvent extends CameraEvent {
const ExposureOffsetChangedEvent(this.value);
}
class ExposureOffsetResetEvent extends CameraEvent {
const ExposureOffsetResetEvent();
}

View file

@ -4,7 +4,7 @@ import 'package:lightmeter/data/haptics_service.dart';
import 'package:lightmeter/data/models/ev_source_type.dart';
import 'package:lightmeter/data/models/photography_values/photography_value.dart';
import 'package:lightmeter/data/shared_prefs_service.dart';
import 'package:lightmeter/interactors/haptics_interactor.dart';
import 'package:lightmeter/interactors/metering_interactor.dart';
import 'package:provider/provider.dart';
import 'ev_source/camera/bloc_camera.dart';
@ -19,7 +19,7 @@ class MeteringFlow extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Provider(
create: (context) => HapticsInteractor(
create: (context) => MeteringInteractor(
context.read<UserPreferencesService>(),
context.read<HapticsService>(),
),
@ -30,14 +30,14 @@ class MeteringFlow extends StatelessWidget {
create: (context) => MeteringBloc(
context.read<MeteringCommunicationBloc>(),
context.read<UserPreferencesService>(),
context.read<HapticsInteractor>(),
context.read<MeteringInteractor>(),
context.read<StopType>(),
),
),
BlocProvider(
create: (context) => CameraBloc(
context.read<MeteringCommunicationBloc>(),
context.read<HapticsInteractor>(),
context.read<MeteringInteractor>(),
),
),
if (context.read<EvSourceType>() == EvSourceType.sensor)

View file

@ -88,7 +88,7 @@ class _MeteringScreenState extends State<MeteringScreen> {
MeteringBottomControls(
onMeasure: () => context.read<MeteringBloc>().add(const MeasureEvent()),
onSettings: () {
Navigator.push(context, MaterialPageRoute(builder: (context) => const SettingsScreen()));
Navigator.pushNamed(context, 'settings');
},
),
],

View file

@ -0,0 +1,112 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.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 CalibrationDialog extends StatefulWidget {
final double cameraEvCalibration;
const CalibrationDialog({
required this.cameraEvCalibration,
super.key,
});
@override
State<CalibrationDialog> createState() => _CalibrationDialogState();
}
class _CalibrationDialogState extends State<CalibrationDialog> {
late double _cameraEvCalibration = widget.cameraEvCalibration;
@override
Widget build(BuildContext context) {
return AlertDialog(
titlePadding: const EdgeInsets.fromLTRB(
Dimens.paddingL,
Dimens.paddingL,
Dimens.paddingL,
Dimens.paddingM,
),
title: Text(S.of(context).calibration),
contentPadding: const EdgeInsets.symmetric(horizontal: Dimens.paddingL),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(S.of(context).calibrationMessage),
const SizedBox(height: Dimens.grid16),
_CalibrationUnit(
title: S.of(context).camera,
value: _cameraEvCalibration,
onChanged: (value) {
setState(() {
_cameraEvCalibration = value;
});
},
),
],
),
actionsPadding: const EdgeInsets.fromLTRB(
Dimens.paddingL,
Dimens.paddingM,
Dimens.paddingL,
Dimens.paddingL,
),
actions: [
TextButton(
onPressed: Navigator.of(context).pop,
child: Text(S.of(context).cancel),
),
TextButton(
onPressed: () => Navigator.of(context).pop(_cameraEvCalibration),
child: Text(S.of(context).save),
),
],
);
}
}
class _CalibrationUnit extends StatelessWidget {
final String title;
final double value;
final ValueChanged<double> onChanged;
const _CalibrationUnit({
required this.title,
required this.value,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
contentPadding: EdgeInsets.zero,
title: Text(title),
trailing: Text(S.of(context).ev(value.toStringSignedAsFixed(1))),
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: CenteredSlider(
value: value,
min: -4,
max: 4,
onChanged: onChanged,
),
),
IconButton(
onPressed: () {
onChanged(0.0);
},
icon: const Icon(Icons.sync),
),
],
)
],
);
}
}

View file

@ -0,0 +1,9 @@
abstract class CalibrationEvent {
const CalibrationEvent();
}
class CameraEvCalibrationChangedEvent extends CalibrationEvent {
final double value;
const CameraEvCalibrationChangedEvent(this.value);
}

View file

@ -0,0 +1,26 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'components/calibration_dialog/widget_dialog_calibration.dart';
class CalibrationListTile extends StatelessWidget {
const CalibrationListTile({super.key});
@override
Widget build(BuildContext context) {
return ListTile(
leading: const Icon(Icons.settings_brightness),
title: Text(S.of(context).calibration),
onTap: () {
showDialog<double>(
context: context,
builder: (_) => CalibrationDialog(
cameraEvCalibration: 0.0,
),
).then((value) {
if (value != null) {}
});
},
);
}
}

View file

@ -1,17 +1,17 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/interactors/haptics_interactor.dart';
import 'package:lightmeter/interactors/settings_interactor.dart';
class HapticsListTileBloc extends Cubit<bool> {
final HapticsInteractor _hapticsInteractor;
final SettingsInteractor _settingsInteractor;
HapticsListTileBloc(
this._hapticsInteractor,
) : super(_hapticsInteractor.isEnabled);
this._settingsInteractor,
) : super(_settingsInteractor.isHapticsEnabled);
void onHapticsChange(bool value) {
_hapticsInteractor.enableHaptics(value);
_settingsInteractor.enableHaptics(value);
if (value) {
_hapticsInteractor.quickVibration();
_settingsInteractor.quickVibration();
}
emit(value);
}

View file

@ -1,9 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/data/haptics_service.dart';
import 'package:lightmeter/data/shared_prefs_service.dart';
import 'package:lightmeter/interactors/haptics_interactor.dart';
import 'package:provider/provider.dart';
import 'package:lightmeter/interactors/settings_interactor.dart';
import 'bloc_list_tile_haptics.dart';
import 'widget_list_tile_haptics.dart';
@ -13,17 +10,9 @@ class HapticsListTileProvider extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Provider(
create: (context) => HapticsInteractor(
context.read<UserPreferencesService>(),
context.read<HapticsService>(),
),
child: BlocProvider(
create: (context) => HapticsListTileBloc(
context.read<HapticsInteractor>()
),
child: const HapticsListTile(),
),
return BlocProvider(
create: (context) => HapticsListTileBloc(context.read<SettingsInteractor>()),
child: const HapticsListTile(),
);
}
}

View file

@ -0,0 +1,22 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/data/haptics_service.dart';
import 'package:lightmeter/data/shared_prefs_service.dart';
import 'package:lightmeter/interactors/settings_interactor.dart';
import 'package:lightmeter/screens/settings/screen_settings.dart';
import 'package:provider/provider.dart';
class SettingsFlow extends StatelessWidget {
const SettingsFlow({super.key});
@override
Widget build(BuildContext context) {
return Provider(
create: (context) => SettingsInteractor(
context.read<UserPreferencesService>(),
context.read<HapticsService>(),
),
child: const SettingsScreen(),
);
}
}

View file

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/res/dimens.dart';
import 'components/calibration/widget_list_tile_calibration.dart';
import 'components/haptics/provider_list_tile_haptics.dart';
import 'components/report_issue/widget_list_tile_report_issue.dart';
import 'components/shared/settings_section/widget_settings_section.dart';
@ -48,6 +49,7 @@ class SettingsScreen extends StatelessWidget {
title: S.of(context).metering,
children: const [
StopTypeListTile(),
CalibrationListTile(),
],
),
SettingsSection(

View file

@ -1,16 +1,16 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/res/dimens.dart';
class CameraSlider extends StatefulWidget {
final Icon icon;
class CenteredSlider extends StatefulWidget {
final Icon? icon;
final double value;
final double min;
final double max;
final ValueChanged<double> onChanged;
final bool isVertical;
const CameraSlider({
required this.icon,
const CenteredSlider({
this.icon,
required this.value,
required this.min,
required this.max,
@ -20,10 +20,10 @@ class CameraSlider extends StatefulWidget {
});
@override
State<CameraSlider> createState() => _CameraSliderState();
State<CenteredSlider> createState() => _CenteredSliderState();
}
class _CameraSliderState extends State<CameraSlider> {
class _CenteredSliderState extends State<CenteredSlider> {
double relativeValue = 0.0;
@override
@ -33,40 +33,44 @@ class _CameraSliderState extends State<CameraSlider> {
}
@override
void didUpdateWidget(CameraSlider oldWidget) {
void didUpdateWidget(CenteredSlider oldWidget) {
super.didUpdateWidget(oldWidget);
relativeValue = (widget.value - widget.min) / (widget.max - widget.min);
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final biggestSize = widget.isVertical ? constraints.maxHeight : constraints.maxWidth;
final handleDistance = biggestSize - Dimens.cameraSliderHandleSize;
return RotatedBox(
quarterTurns: widget.isVertical ? -1 : 0,
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTapUp: (details) => _updateHandlePosition(details.localPosition.dx, handleDistance),
onHorizontalDragUpdate: (details) => _updateHandlePosition(details.localPosition.dx, handleDistance),
child: SizedBox(
height: Dimens.cameraSliderHandleSize,
width: biggestSize,
child: _Slider(
handleDistance: handleDistance,
handleSize: Dimens.cameraSliderHandleSize,
trackThickness: Dimens.cameraSliderTrackHeight,
value: relativeValue,
icon: RotatedBox(
quarterTurns: widget.isVertical ? 1 : 0,
child: widget.icon,
return SizedBox(
height: widget.isVertical ? double.maxFinite : Dimens.cameraSliderHandleSize,
width: !widget.isVertical ? double.maxFinite : Dimens.cameraSliderHandleSize,
child: LayoutBuilder(
builder: (context, constraints) {
final biggestSize = widget.isVertical ? constraints.maxHeight : constraints.maxWidth;
final handleDistance = biggestSize - Dimens.cameraSliderHandleSize;
return RotatedBox(
quarterTurns: widget.isVertical ? -1 : 0,
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTapUp: (details) => _updateHandlePosition(details.localPosition.dx, handleDistance),
onHorizontalDragUpdate: (details) => _updateHandlePosition(details.localPosition.dx, handleDistance),
child: SizedBox(
height: Dimens.cameraSliderHandleSize,
width: biggestSize,
child: _Slider(
handleDistance: handleDistance,
handleSize: Dimens.cameraSliderHandleSize,
trackThickness: Dimens.cameraSliderTrackHeight,
value: relativeValue,
icon: RotatedBox(
quarterTurns: widget.isVertical ? 1 : 0,
child: widget.icon,
),
),
),
),
),
);
},
);
},
),
);
}

View file

@ -7,3 +7,13 @@ extension SignedString on num {
}
}
}
extension SignedStringDouble on double {
String toStringSignedAsFixed(fractionDigits) {
if (this > 0) {
return "+${toStringAsFixed(fractionDigits)}";
} else {
return toStringAsFixed(fractionDigits);
}
}
}