This commit is contained in:
Vadim 2023-02-23 21:41:35 +03:00
commit 03dd9864bf
50 changed files with 421 additions and 206 deletions

View file

@ -3,13 +3,19 @@
# separate terms of service, privacy policy, and support # separate terms of service, privacy policy, and support
# documentation. # documentation.
name: Build Dev APK name: Build APK
on: on:
push:
tags:
- "v*.*.*"
workflow_dispatch: workflow_dispatch:
inputs:
flavor:
description: 'Flavor'
type: choice
required: true
options:
- dev
- prod
default: 'dev'
jobs: jobs:
build: build:
@ -47,9 +53,12 @@ jobs:
flutter pub run intl_utils:generate flutter pub run intl_utils:generate
- name: Build Apk - name: Build Apk
run: flutter build apk --release --flavor dev --dart-define cameraPreviewAspectRatio=2/3 -t lib/main_dev.dart env:
FLAVOR: ${{ github.event.inputs.flavor }}
run: flutter build apk --release --flavor $FLAVOR --dart-define cameraPreviewAspectRatio=2/3 -t lib/main_$FLAVOR.dart
- uses: actions/upload-artifact@v3 - name: Upload artifact
uses: actions/upload-artifact@v3
with: with:
name: m3_lightmeter.apk name: m3_lightmeter_${{ github.event.inputs.flavor }}
path: build/app/outputs/flutter-apk/app-dev-release.apk path: build/app/outputs/flutter-apk/app-${{ github.event.inputs.flavor }}-release.apk

View file

@ -43,7 +43,7 @@ jobs:
flutter pub get flutter pub get
flutter pub run intl_utils:generate flutter pub run intl_utils:generate
- name: Build Apk - name: Build appbundle
run: flutter build appbundle --release --flavor prod --dart-define cameraPreviewAspectRatio=2/3 -t lib/main_prod.dart run: flutter build appbundle --release --flavor prod --dart-define cameraPreviewAspectRatio=2/3 -t lib/main_prod.dart
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3

View file

@ -1,6 +1,8 @@
name: Crowdin push name: Crowdin push
on: on:
push: push:
paths:
- lib/l10n/**
branches: branches:
- main - main
- crowdin - crowdin

View file

@ -20,12 +20,15 @@ const List<NdValue> ndValues = [
NdValue(16), NdValue(16),
NdValue(32), NdValue(32),
NdValue(64), NdValue(64),
NdValue(100),
NdValue(128), NdValue(128),
NdValue(256), NdValue(256),
NdValue(400),
NdValue(512), NdValue(512),
NdValue(1024), NdValue(1024),
NdValue(2048), NdValue(2048),
NdValue(4096), NdValue(4096),
NdValue(6310),
NdValue(8192), NdValue(8192),
NdValue(10000), NdValue(10000),
]; ];

View file

@ -25,7 +25,44 @@ class UserPreferencesService {
final SharedPreferences _sharedPreferences; final SharedPreferences _sharedPreferences;
UserPreferencesService(this._sharedPreferences); UserPreferencesService(this._sharedPreferences) {
_migrateOldKeys();
}
Future<void> _migrateOldKeys() async {
final legacyIsoIndex = _sharedPreferences.getInt("curIsoIndex");
if (legacyIsoIndex != null) {
iso = isoValues[legacyIsoIndex];
await _sharedPreferences.remove("curIsoIndex");
}
final legacyNdIndex = _sharedPreferences.getInt("curndIndex");
if (legacyNdIndex != null) {
/// Legacy ND list has 1 extra value at the end, so this check is needed
if (legacyNdIndex < ndValues.length) {
ndFilter = ndValues[legacyNdIndex];
}
await _sharedPreferences.remove("curndIndex");
}
final legacyCameraCalibration = _sharedPreferences.getDouble("cameraCalibr");
if (legacyCameraCalibration != null) {
cameraEvCalibration = legacyCameraCalibration;
await _sharedPreferences.remove("cameraCalibr");
}
final legacyLightSensorCalibration = _sharedPreferences.getDouble("sensorCalibr");
if (legacyLightSensorCalibration != null) {
lightSensorEvCalibration = legacyLightSensorCalibration;
await _sharedPreferences.remove("sensorCalibr");
}
final legacyHaptics = _sharedPreferences.getBool("vibrate");
if (legacyHaptics != null) {
haptics = legacyHaptics;
await _sharedPreferences.remove("vibrate");
}
}
IsoValue get iso => isoValues.firstWhere((v) => v.value == (_sharedPreferences.getInt(_isoKey) ?? 100)); IsoValue get iso => isoValues.firstWhere((v) => v.value == (_sharedPreferences.getInt(_isoKey) ?? 100));
set iso(IsoValue value) => _sharedPreferences.setInt(_isoKey, value.value); set iso(IsoValue value) => _sharedPreferences.setInt(_isoKey, value.value);

View file

@ -1,4 +1,7 @@
enum BuildType { dev, prod }
class Environment { class Environment {
final BuildType buildType;
final String sourceCodeUrl; final String sourceCodeUrl;
final String issuesReportUrl; final String issuesReportUrl;
final String contactEmail; final String contactEmail;
@ -6,6 +9,7 @@ class Environment {
final bool hasLightSensor; final bool hasLightSensor;
const Environment({ const Environment({
required this.buildType,
required this.sourceCodeUrl, required this.sourceCodeUrl,
required this.issuesReportUrl, required this.issuesReportUrl,
required this.contactEmail, required this.contactEmail,
@ -13,18 +17,21 @@ class Environment {
}); });
const Environment.dev() const Environment.dev()
: sourceCodeUrl = 'https://github.com/vodemn/m3_lightmeter', : buildType = BuildType.dev,
sourceCodeUrl = 'https://github.com/vodemn/m3_lightmeter',
issuesReportUrl = 'https://github.com/vodemn/m3_lightmeter/issues', issuesReportUrl = 'https://github.com/vodemn/m3_lightmeter/issues',
contactEmail = 'contact.vodemn@gmail.com', contactEmail = 'contact.vodemn@gmail.com',
hasLightSensor = false; hasLightSensor = false;
const Environment.prod() const Environment.prod()
: sourceCodeUrl = 'https://github.com/vodemn/m3_lightmeter', : buildType = BuildType.prod,
sourceCodeUrl = 'https://github.com/vodemn/m3_lightmeter',
issuesReportUrl = 'https://github.com/vodemn/m3_lightmeter/issues', issuesReportUrl = 'https://github.com/vodemn/m3_lightmeter/issues',
contactEmail = 'contact.vodemn@gmail.com', contactEmail = 'contact.vodemn@gmail.com',
hasLightSensor = false; hasLightSensor = false;
Environment copyWith({bool? hasLightSensor}) => Environment( Environment copyWith({bool? hasLightSensor}) => Environment(
buildType: buildType,
sourceCodeUrl: sourceCodeUrl, sourceCodeUrl: sourceCodeUrl,
issuesReportUrl: issuesReportUrl, issuesReportUrl: issuesReportUrl,
contactEmail: contactEmail, contactEmail: contactEmail,

View file

@ -2,8 +2,9 @@
"@@locale": "en", "@@locale": "en",
"fastestExposurePair": "Fastest", "fastestExposurePair": "Fastest",
"slowestExposurePair": "Slowest", "slowestExposurePair": "Slowest",
"ev": "{value} EV", "ev": "EV",
"@ev": { "evValue": "{value} EV",
"@evValue": {
"placeholders": { "placeholders": {
"value": { "value": {
"type": "String" "type": "String"

View file

@ -2,8 +2,9 @@
"@@locale": "fr", "@@locale": "fr",
"fastestExposurePair": "Le plus rapide", "fastestExposurePair": "Le plus rapide",
"slowestExposurePair": "Le plus lent", "slowestExposurePair": "Le plus lent",
"ev": "{value} EV", "ev": "EV",
"@ev": { "evValue": "{value} EV",
"@evValue": {
"placeholders": { "placeholders": {
"value": { "value": {
"type": "String" "type": "String"

View file

@ -2,8 +2,9 @@
"@@locale": "ru", "@@locale": "ru",
"fastestExposurePair": "Короткая выдержка", "fastestExposurePair": "Короткая выдержка",
"slowestExposurePair": "Длинная выдержка", "slowestExposurePair": "Длинная выдержка",
"ev": "{value} EV", "ev": "EV",
"@ev": { "evValue": "{value} EV",
"@evValue": {
"placeholders": { "placeholders": {
"value": { "value": {
"type": "String" "type": "String"

View file

@ -13,6 +13,7 @@ class Dimens {
static const double grid24 = 24; static const double grid24 = 24;
static const double grid48 = 48; static const double grid48 = 48;
static const double grid56 = 56; static const double grid56 = 56;
static const double grid72 = 72;
static const double grid168 = 168; static const double grid168 = 168;
static const double paddingS = 8; static const double paddingS = 8;

View file

@ -4,6 +4,8 @@ import 'dart:math';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/data/models/photography_values/aperture_value.dart'; import 'package:lightmeter/data/models/photography_values/aperture_value.dart';
import 'package:lightmeter/data/models/exposure_pair.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/data/models/photography_values/photography_value.dart';
import 'package:lightmeter/data/models/photography_values/shutter_speed_value.dart'; import 'package:lightmeter/data/models/photography_values/shutter_speed_value.dart';
import 'package:lightmeter/data/shared_prefs_service.dart'; import 'package:lightmeter/data/shared_prefs_service.dart';
@ -29,16 +31,20 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
StopType stopType; StopType stopType;
late IsoValue _iso = _userPreferencesService.iso;
late NdValue _nd = _userPreferencesService.ndFilter;
double _ev = 0.0;
bool _isMeteringInProgress = false;
MeteringBloc( MeteringBloc(
this._communicationBloc, this._communicationBloc,
this._userPreferencesService, this._userPreferencesService,
this._meteringInteractor, this._meteringInteractor,
this.stopType, this.stopType,
) : super( ) : super(
MeteringState( MeteringEndedState(
iso: _userPreferencesService.iso, iso: _userPreferencesService.iso,
ev: 0.0, ev: 0.0,
evCompensation: 0.0,
nd: _userPreferencesService.ndFilter, nd: _userPreferencesService.ndFilter,
exposurePairs: [], exposurePairs: [],
), ),
@ -53,8 +59,6 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
on<NdChangedEvent>(_onNdChanged); on<NdChangedEvent>(_onNdChanged);
on<MeasureEvent>(_onMeasure); on<MeasureEvent>(_onMeasure);
on<MeasuredEvent>(_onMeasured); on<MeasuredEvent>(_onMeasured);
add(const MeasureEvent());
} }
@override @override
@ -65,76 +69,63 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
void _onCommunicationState(communication_states.ScreenState communicationState) { void _onCommunicationState(communication_states.ScreenState communicationState) {
if (communicationState is communication_states.MeasuredState) { if (communicationState is communication_states.MeasuredState) {
_isMeteringInProgress = communicationState is communication_states.MeteringInProgressState;
add(MeasuredEvent(communicationState.ev100)); add(MeasuredEvent(communicationState.ev100));
} }
} }
void _onStopTypeChanged(StopTypeChangedEvent event, Emitter emit) { void _onStopTypeChanged(StopTypeChangedEvent event, Emitter emit) {
stopType = event.stopType; stopType = event.stopType;
emit(MeteringState( _emitMeasuredState(emit);
iso: state.iso,
ev: state.ev,
evCompensation: state.evCompensation,
nd: state.nd,
exposurePairs: _buildExposureValues(state.ev),
));
} }
void _onIsoChanged(IsoChangedEvent event, Emitter emit) { void _onIsoChanged(IsoChangedEvent event, Emitter emit) {
_userPreferencesService.iso = event.isoValue; _userPreferencesService.iso = event.isoValue;
final ev = state.ev + log2(event.isoValue.value / state.iso.value); _ev = _ev + log2(event.isoValue.value / _iso.value);
emit(MeteringState( _iso = event.isoValue;
iso: event.isoValue, _emitMeasuredState(emit);
ev: ev,
evCompensation: state.evCompensation,
nd: state.nd,
exposurePairs: _buildExposureValues(ev),
));
} }
void _onNdChanged(NdChangedEvent event, Emitter emit) { void _onNdChanged(NdChangedEvent event, Emitter emit) {
_userPreferencesService.ndFilter = event.ndValue; _userPreferencesService.ndFilter = event.ndValue;
final ev = state.ev - event.ndValue.stopReduction + state.nd.stopReduction; _ev = _ev - event.ndValue.stopReduction + _nd.stopReduction;
emit(MeteringState( _nd = event.ndValue;
iso: state.iso, _emitMeasuredState(emit);
ev: ev,
evCompensation: state.evCompensation,
nd: event.ndValue,
exposurePairs: _buildExposureValues(ev),
));
} }
void _onMeasure(_, __) { void _onMeasure(_, Emitter emit) {
_meteringInteractor.quickVibration(); _meteringInteractor.quickVibration();
_communicationBloc.add(const communication_events.MeasureEvent()); _communicationBloc.add(const communication_events.MeasureEvent());
_isMeteringInProgress = true;
emit(const LoadingState());
} }
void _onMeasured(MeasuredEvent event, Emitter emit) { void _onMeasured(MeasuredEvent event, Emitter emit) {
_meteringInteractor.responseVibration(); _meteringInteractor.responseVibration();
final ev = event.ev100 + log2(state.iso.value / 100); _ev = event.ev100 + log2(_iso.value / 100);
emit(MeteringState( _emitMeasuredState(emit);
iso: state.iso, }
ev: ev,
evCompensation: state.evCompensation, void _emitMeasuredState(Emitter emit) {
nd: state.nd, emit(_isMeteringInProgress
exposurePairs: _buildExposureValues(ev), ? MeteringInProgressState(
)); iso: _iso,
ev: _ev,
nd: _nd,
exposurePairs: _buildExposureValues(_ev),
)
: MeteringEndedState(
iso: _iso,
ev: _ev,
nd: _nd,
exposurePairs: _buildExposureValues(_ev),
));
} }
List<ExposurePair> _buildExposureValues(double ev) { List<ExposurePair> _buildExposureValues(double ev) {
late final int evSteps; /// Depending on the `stopType` the exposure pairs list length is multiplied by 1,2 or 3
switch (stopType) { final int evSteps = (ev * (stopType.index + 1)).round();
case StopType.full: final int evOffset =
evSteps = ev.floor();
break;
case StopType.half:
evSteps = (ev / 0.5).floor();
break;
case StopType.third:
evSteps = (ev / 0.3).floor();
break;
}
final evOffset =
_shutterSpeedValues.indexOf(const ShutterSpeedValue(1, false, StopType.full)) - evSteps; _shutterSpeedValues.indexOf(const ShutterSpeedValue(1, false, StopType.full)) - evSteps;
late final int apertureOffset; late final int apertureOffset;
@ -147,7 +138,7 @@ class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
shutterSpeedOffset = 0; shutterSpeedOffset = 0;
} }
int itemsCount = min(_apertureValues.length + shutterSpeedOffset, final int itemsCount = min(_apertureValues.length + shutterSpeedOffset,
_shutterSpeedValues.length + apertureOffset) - _shutterSpeedValues.length + apertureOffset) -
max(apertureOffset, shutterSpeedOffset); max(apertureOffset, shutterSpeedOffset);

View file

@ -9,6 +9,7 @@ class MeteringCommunicationBloc
// `MeasureState` is not const, so that `Bloc` treats each state as new and updates state stream // `MeasureState` is not const, so that `Bloc` treats each state as new and updates state stream
// ignore: prefer_const_constructors // ignore: prefer_const_constructors
on<MeasureEvent>((_, emit) => emit(MeasureState())); on<MeasureEvent>((_, emit) => emit(MeasureState()));
on<MeasuredEvent>((event, emit) => emit(MeasuredState(event.ev100))); on<MeteringInProgressEvent>((event, emit) => emit(MeteringInProgressState(event.ev100)));
on<MeteringEndedEvent>((event, emit) => emit(MeteringEndedState(event.ev100)));
} }
} }

View file

@ -14,8 +14,16 @@ class MeasureEvent extends ScreenEvent {
const MeasureEvent(); const MeasureEvent();
} }
class MeasuredEvent extends SourceEvent { abstract class MeasuredEvent extends SourceEvent {
final double ev100; final double ev100;
const MeasuredEvent(this.ev100); const MeasuredEvent(this.ev100);
} }
class MeteringInProgressEvent extends MeasuredEvent {
const MeteringInProgressEvent(super.ev100);
}
class MeteringEndedEvent extends MeasuredEvent {
const MeteringEndedEvent(super.ev100);
}

View file

@ -18,8 +18,16 @@ class MeasureState extends SourceState {
const MeasureState(); const MeasureState();
} }
class MeasuredState extends ScreenState { abstract class MeasuredState extends ScreenState {
final double ev100; final double ev100;
const MeasuredState(this.ev100); const MeasuredState(this.ev100);
} }
class MeteringInProgressState extends MeasuredState {
const MeteringInProgressState(super.ev100);
}
class MeteringEndedState extends MeasuredState {
const MeteringEndedState(super.ev100);
}

View file

@ -1,14 +1,17 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/shared/filled_circle/widget_circle_filled.dart'; import 'package:lightmeter/screens/shared/filled_circle/widget_circle_filled.dart';
class MeteringMeasureButton extends StatefulWidget { class MeteringMeasureButton extends StatefulWidget {
final double size; final double? ev;
final bool isMetering;
final VoidCallback onTap; final VoidCallback onTap;
const MeteringMeasureButton({ const MeteringMeasureButton({
required this.ev,
required this.isMetering,
required this.onTap, required this.onTap,
this.size = 72,
super.key, super.key,
}); });
@ -19,10 +22,18 @@ class MeteringMeasureButton extends StatefulWidget {
class _MeteringMeasureButtonState extends State<MeteringMeasureButton> { class _MeteringMeasureButtonState extends State<MeteringMeasureButton> {
bool _isPressed = false; bool _isPressed = false;
@override
void didUpdateWidget(covariant MeteringMeasureButton oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.isMetering != widget.isMetering) {
_isPressed = widget.isMetering;
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SizedBox.fromSize( return IgnorePointer(
size: Size.square(widget.size), ignoring: widget.isMetering && widget.ev == null,
child: GestureDetector( child: GestureDetector(
onTap: widget.onTap, onTap: widget.onTap,
onTapDown: (_) { onTapDown: (_) {
@ -40,26 +51,52 @@ class _MeteringMeasureButtonState extends State<MeteringMeasureButton> {
_isPressed = false; _isPressed = false;
}); });
}, },
child: DecoratedBox( child: SizedBox.fromSize(
decoration: BoxDecoration( size: const Size.square(Dimens.grid72),
borderRadius: BorderRadius.circular(widget.size / 2), child: Stack(
border: Border.all( children: [
width: Dimens.grid4, Center(
color: Theme.of(context).colorScheme.onSurface, child: AnimatedScale(
), duration: Dimens.durationS,
), scale: _isPressed ? 0.9 : 1.0,
child: Center( child: FilledCircle(
child: AnimatedScale( color: Theme.of(context).colorScheme.onSurface,
duration: Dimens.durationS, size: Dimens.grid72 - Dimens.grid8,
scale: _isPressed ? 0.9 : 1.0, child: Center(
child: FilledCircle( child: widget.ev != null ? _EvValueText(ev: widget.ev!) : null,
color: Theme.of(context).colorScheme.onSurface, ),
size: widget.size - Dimens.grid16, ),
),
), ),
), Positioned.fill(
child: CircularProgressIndicator(
/// This key is needed to make indicator start from the same point every time
key: ValueKey(widget.isMetering),
color: Theme.of(context).colorScheme.onSurface,
strokeWidth: Dimens.grid4,
value: widget.isMetering ? null : 1,
),
),
],
), ),
), ),
), ),
); );
} }
} }
class _EvValueText extends StatelessWidget {
final double ev;
const _EvValueText({required this.ev});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Text(
'${ev.toStringAsFixed(1)}\n${S.of(context).ev}',
style: theme.textTheme.bodyMedium?.copyWith(color: theme.colorScheme.surface),
textAlign: TextAlign.center,
);
}
}

View file

@ -4,11 +4,15 @@ import 'package:lightmeter/res/dimens.dart';
import 'widget_bottom_controls.dart'; import 'widget_bottom_controls.dart';
class MeteringBottomControlsProvider extends StatelessWidget { class MeteringBottomControlsProvider extends StatelessWidget {
final double? ev;
final bool isMetering;
final VoidCallback? onSwitchEvSourceType; final VoidCallback? onSwitchEvSourceType;
final VoidCallback onMeasure; final VoidCallback onMeasure;
final VoidCallback onSettings; final VoidCallback onSettings;
const MeteringBottomControlsProvider({ const MeteringBottomControlsProvider({
required this.ev,
required this.isMetering,
required this.onSwitchEvSourceType, required this.onSwitchEvSourceType,
required this.onMeasure, required this.onMeasure,
required this.onSettings, required this.onSettings,
@ -30,6 +34,8 @@ class MeteringBottomControlsProvider extends StatelessWidget {
), ),
), ),
child: MeteringBottomControls( child: MeteringBottomControls(
ev: ev,
isMetering: isMetering,
onSwitchEvSourceType: onSwitchEvSourceType, onSwitchEvSourceType: onSwitchEvSourceType,
onMeasure: onMeasure, onMeasure: onMeasure,
onSettings: onSettings, onSettings: onSettings,

View file

@ -6,11 +6,15 @@ import 'package:provider/provider.dart';
import 'components/measure_button/widget_button_measure.dart'; import 'components/measure_button/widget_button_measure.dart';
class MeteringBottomControls extends StatelessWidget { class MeteringBottomControls extends StatelessWidget {
final double? ev;
final bool isMetering;
final VoidCallback? onSwitchEvSourceType; final VoidCallback? onSwitchEvSourceType;
final VoidCallback onMeasure; final VoidCallback onMeasure;
final VoidCallback onSettings; final VoidCallback onSettings;
const MeteringBottomControls({ const MeteringBottomControls({
required this.ev,
required this.isMetering,
required this.onSwitchEvSourceType, required this.onSwitchEvSourceType,
required this.onMeasure, required this.onMeasure,
required this.onSettings, required this.onSettings,
@ -46,7 +50,11 @@ class MeteringBottomControls extends StatelessWidget {
) )
else else
const Spacer(), const Spacer(),
MeteringMeasureButton(onTap: onMeasure), MeteringMeasureButton(
ev: ev,
isMetering: isMetering,
onTap: onMeasure,
),
Expanded( Expanded(
child: Center( child: Center(
child: IconButton( child: IconButton(

View file

@ -34,6 +34,8 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
double _exposureStep = 0.0; double _exposureStep = 0.0;
double _currentExposureOffset = 0.0; double _currentExposureOffset = 0.0;
double _ev100 = 0.0;
CameraContainerBloc( CameraContainerBloc(
this._meteringInteractor, this._meteringInteractor,
MeteringCommunicationBloc communicationBloc, MeteringCommunicationBloc communicationBloc,
@ -58,17 +60,17 @@ class CameraContainerBloc extends EvSourceBlocBase<CameraContainerEvent, CameraC
Future<void> close() async { Future<void> close() async {
WidgetsBinding.instance.removeObserver(_observer); WidgetsBinding.instance.removeObserver(_observer);
unawaited(_cameraController?.dispose()); unawaited(_cameraController?.dispose());
communicationBloc.add(communication_event.MeteringEndedEvent(_ev100));
return super.close(); return super.close();
} }
@override @override
void onCommunicationState(communication_states.SourceState communicationState) { void onCommunicationState(communication_states.SourceState communicationState) {
if (communicationState is communication_states.MeasureState) { if (communicationState is communication_states.MeasureState) {
_takePhoto().then((ev100) { _takePhoto().then((ev100Raw) {
if (ev100 != null) { if (ev100Raw != null) {
communicationBloc.add( _ev100 = ev100Raw + _meteringInteractor.cameraEvCalibration;
communication_event.MeasuredEvent(ev100 + _meteringInteractor.cameraEvCalibration), communicationBloc.add(communication_event.MeteringEndedEvent(_ev100));
);
} }
}); });
} }

View file

@ -16,6 +16,7 @@ class LightSensorContainerBloc
final MeteringInteractor _meteringInteractor; final MeteringInteractor _meteringInteractor;
StreamSubscription<int>? _luxSubscriptions; StreamSubscription<int>? _luxSubscriptions;
double _ev100 = 0.0;
LightSensorContainerBloc( LightSensorContainerBloc(
this._meteringInteractor, this._meteringInteractor,
@ -30,11 +31,11 @@ class LightSensorContainerBloc
if (communicationState is communication_states.MeasureState) { if (communicationState is communication_states.MeasureState) {
if (_luxSubscriptions == null) { if (_luxSubscriptions == null) {
_luxSubscriptions = _meteringInteractor.luxStream().listen((event) { _luxSubscriptions = _meteringInteractor.luxStream().listen((event) {
communicationBloc.add(communication_event.MeasuredEvent( _ev100 = log2(event.toDouble() / 2.5) + _meteringInteractor.lightSensorEvCalibration;
log2(event.toDouble() / 2.5) + _meteringInteractor.lightSensorEvCalibration, communicationBloc.add(communication_event.MeteringInProgressEvent(_ev100));
));
}); });
} else { } else {
communicationBloc.add(communication_event.MeteringEndedEvent(_ev100));
_luxSubscriptions?.cancel().then((_) => _luxSubscriptions = null); _luxSubscriptions?.cancel().then((_) => _luxSubscriptions = null);
} }
} }
@ -42,6 +43,7 @@ class LightSensorContainerBloc
@override @override
Future<void> close() async { Future<void> close() async {
communicationBloc.add(communication_event.MeteringEndedEvent(_ev100));
_luxSubscriptions?.cancel().then((_) => _luxSubscriptions = null); _luxSubscriptions?.cancel().then((_) => _luxSubscriptions = null);
return super.close(); return super.close();
} }

View file

@ -83,9 +83,8 @@ class _PhotographyValuePickerDialogState<T extends PhotographyValue>
child: widget.itemTitleBuilder(context, widget.values[index]), child: widget.itemTitleBuilder(context, widget.values[index]),
), ),
secondary: widget.values[index].value != _selectedValue.value secondary: widget.values[index].value != _selectedValue.value
? Text(S ? Text(S.of(context).evValue(
.of(context) widget.evDifferenceBuilder.call(_selectedValue, widget.values[index])))
.ev(widget.evDifferenceBuilder.call(_selectedValue, widget.values[index])))
: null, : null,
onChanged: (value) { onChanged: (value) {
if (value != null) { if (value != null) {

View file

@ -1,6 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/data/models/ev_source_type.dart'; import 'package:lightmeter/data/models/ev_source_type.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/data/models/photography_values/photography_value.dart';
import 'package:lightmeter/environment.dart'; import 'package:lightmeter/environment.dart';
import 'package:lightmeter/providers/ev_source_type_provider.dart'; import 'package:lightmeter/providers/ev_source_type_provider.dart';
@ -36,8 +39,9 @@ class _MeteringScreenState extends State<MeteringScreen> {
children: [ children: [
Expanded( Expanded(
child: BlocBuilder<MeteringBloc, MeteringState>( child: BlocBuilder<MeteringBloc, MeteringState>(
builder: (context, state) => context.watch<EvSourceType>() == EvSourceType.camera buildWhen: (_, current) => current is MeteringDataState,
? CameraContainerProvider( builder: (_, state) => state is MeteringDataState
? _MeteringContainerBuidler(
fastest: state.fastest, fastest: state.fastest,
slowest: state.slowest, slowest: state.slowest,
iso: state.iso, iso: state.iso,
@ -46,26 +50,65 @@ class _MeteringScreenState extends State<MeteringScreen> {
onNdChanged: (value) => _bloc.add(NdChangedEvent(value)), onNdChanged: (value) => _bloc.add(NdChangedEvent(value)),
exposurePairs: state.exposurePairs, exposurePairs: state.exposurePairs,
) )
: LightSensorContainerProvider( : const SizedBox.shrink(),
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,
),
), ),
), ),
MeteringBottomControlsProvider( BlocBuilder<MeteringBloc, MeteringState>(
onSwitchEvSourceType: context.read<Environment>().hasLightSensor builder: (context, state) => MeteringBottomControlsProvider(
? EvSourceTypeProvider.of(context).toggleType ev: state is MeteringDataState ? state.ev : null,
: null, isMetering: state is LoadingState || state is MeteringInProgressState,
onMeasure: () => _bloc.add(const MeasureEvent()), onSwitchEvSourceType: context.read<Environment>().hasLightSensor
onSettings: () => Navigator.pushNamed(context, 'settings'), ? EvSourceTypeProvider.of(context).toggleType
: null,
onMeasure: () => _bloc.add(const MeasureEvent()),
onSettings: () => Navigator.pushNamed(context, 'settings'),
),
), ),
], ],
), ),
); );
} }
} }
class _MeteringContainerBuidler 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 _MeteringContainerBuidler({
required this.fastest,
required this.slowest,
required this.iso,
required this.nd,
required this.onIsoChanged,
required this.onNdChanged,
required this.exposurePairs,
});
@override
Widget build(BuildContext context) {
return context.watch<EvSourceType>() == EvSourceType.camera
? CameraContainerProvider(
fastest: fastest,
slowest: slowest,
iso: iso,
nd: nd,
onIsoChanged: onIsoChanged,
onNdChanged: onNdChanged,
exposurePairs: exposurePairs,
)
: LightSensorContainerProvider(
fastest: fastest,
slowest: slowest,
iso: iso,
nd: nd,
onIsoChanged: onIsoChanged,
onNdChanged: onNdChanged,
exposurePairs: exposurePairs,
);
}
}

View file

@ -2,16 +2,22 @@ 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/iso_value.dart';
import 'package:lightmeter/data/models/photography_values/nd_value.dart'; import 'package:lightmeter/data/models/photography_values/nd_value.dart';
class MeteringState { abstract class MeteringState {
const MeteringState();
}
class LoadingState extends MeteringState {
const LoadingState();
}
abstract class MeteringDataState extends MeteringState {
final double ev; final double ev;
final double evCompensation;
final IsoValue iso; final IsoValue iso;
final NdValue nd; final NdValue nd;
final List<ExposurePair> exposurePairs; final List<ExposurePair> exposurePairs;
const MeteringState({ const MeteringDataState({
required this.ev, required this.ev,
required this.evCompensation,
required this.iso, required this.iso,
required this.nd, required this.nd,
required this.exposurePairs, required this.exposurePairs,
@ -20,3 +26,21 @@ class MeteringState {
ExposurePair? get fastest => exposurePairs.isEmpty ? null : exposurePairs.first; ExposurePair? get fastest => exposurePairs.isEmpty ? null : exposurePairs.first;
ExposurePair? get slowest => exposurePairs.isEmpty ? null : exposurePairs.last; ExposurePair? get slowest => exposurePairs.isEmpty ? null : exposurePairs.last;
} }
class MeteringInProgressState extends MeteringDataState {
MeteringInProgressState({
required super.ev,
required super.iso,
required super.nd,
required super.exposurePairs,
});
}
class MeteringEndedState extends MeteringDataState {
MeteringEndedState({
required super.ev,
required super.iso,
required super.nd,
required super.exposurePairs,
});
}

View file

@ -0,0 +1,25 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/screens/settings/components/shared/settings_section/widget_settings_section.dart';
import 'components/report_issue/widget_list_tile_report_issue.dart';
import 'components/source_code/widget_list_tile_source_code.dart';
import 'components/version/widget_list_tile_version.dart';
import 'components/write_email/widget_list_tile_write_email.dart';
class AboutSettingsSection extends StatelessWidget {
const AboutSettingsSection({super.key});
@override
Widget build(BuildContext context) {
return SettingsSection(
title: S.of(context).about,
children: const [
SourceCodeListTile(),
ReportIssueListTile(),
WriteEmailListTile(),
VersionListTile(),
],
);
}
}

View file

@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/screens/settings/components/shared/settings_section/widget_settings_section.dart';
import 'components/caffeine/provider_list_tile_caffeine.dart';
import 'components/haptics/provider_list_tile_haptics.dart';
import 'components/language/widget_list_tile_language.dart';
class GeneralSettingsSection extends StatelessWidget {
const GeneralSettingsSection({super.key});
@override
Widget build(BuildContext context) {
return SettingsSection(
title: S.of(context).general,
children: const [
CaffeineListTileProvider(),
HapticsListTileProvider(),
LanguageListTile(),
],
);
}
}

View file

@ -101,7 +101,7 @@ class _CalibrationUnit extends StatelessWidget {
ListTile( ListTile(
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
title: Text(title), title: Text(title),
trailing: Text(S.of(context).ev(value.toStringSignedAsFixed(1))), trailing: Text(S.of(context).evValue(value.toStringSignedAsFixed(1))),
), ),
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,

View file

@ -0,0 +1,21 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/screens/settings/components/shared/settings_section/widget_settings_section.dart';
import 'components/calibration/widget_list_tile_calibration.dart';
import 'components/fractional_stops/widget_list_tile_fractional_stops.dart';
class MeteringSettingsSection extends StatelessWidget {
const MeteringSettingsSection({super.key});
@override
Widget build(BuildContext context) {
return SettingsSection(
title: S.of(context).metering,
children: const [
StopTypeListTile(),
CalibrationListTile(),
],
);
}
}

View file

@ -0,0 +1,26 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:lightmeter/data/models/dynamic_colors_state.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/screens/settings/components/shared/settings_section/widget_settings_section.dart';
import 'components/dynamic_color/widget_list_tile_dynamic_color.dart';
import 'components/primary_color/widget_list_tile_primary_color.dart';
import 'components/theme_type/widget_list_tile_theme_type.dart';
class ThemeSettingsSection extends StatelessWidget {
const ThemeSettingsSection({super.key});
@override
Widget build(BuildContext context) {
return SettingsSection(
title: S.of(context).theme,
children: [
const ThemeTypeListTile(),
const PrimaryColorListTile(),
if (context.read<DynamicColorState>() != DynamicColorState.unavailable)
const DynamicColorListTile(),
],
);
}
}

View file

@ -1,22 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/dynamic_colors_state.dart';
import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/res/dimens.dart';
import 'package:provider/provider.dart';
import 'components/caffeine/provider_list_tile_caffeine.dart'; import 'components/about/widget_settings_section_about.dart';
import 'components/calibration/widget_list_tile_calibration.dart'; import 'components/general/widget_settings_section_general.dart';
import 'components/haptics/provider_list_tile_haptics.dart'; import 'components/metering/widget_settings_section_metering.dart';
import 'components/language/widget_list_tile_language.dart'; import 'components/theme/widget_settings_section_theme.dart';
import 'components/primary_color/widget_list_tile_primary_color.dart';
import 'components/report_issue/widget_list_tile_report_issue.dart';
import 'components/shared/settings_section/widget_settings_section.dart';
import 'components/source_code/widget_list_tile_source_code.dart';
import 'components/dynamic_color/widget_list_tile_dynamic_color.dart';
import 'components/theme_type/widget_list_tile_theme_type.dart';
import 'components/version/widget_list_tile_version.dart';
import 'components/fractional_stops/widget_list_tile_fractional_stops.dart';
import 'components/write_email/widget_list_tile_write_email.dart';
class SettingsScreen extends StatelessWidget { class SettingsScreen extends StatelessWidget {
const SettingsScreen({super.key}); const SettingsScreen({super.key});
@ -54,39 +43,10 @@ class SettingsScreen extends StatelessWidget {
SliverList( SliverList(
delegate: SliverChildListDelegate( delegate: SliverChildListDelegate(
<Widget>[ <Widget>[
SettingsSection( const MeteringSettingsSection(),
title: S.of(context).metering, const GeneralSettingsSection(),
children: const [ const ThemeSettingsSection(),
StopTypeListTile(), const AboutSettingsSection(),
CalibrationListTile(),
],
),
SettingsSection(
title: S.of(context).general,
children: const [
CaffeineListTileProvider(),
HapticsListTileProvider(),
LanguageListTile(),
],
),
SettingsSection(
title: S.of(context).theme,
children: [
const ThemeTypeListTile(),
const PrimaryColorListTile(),
if (context.read<DynamicColorState>() != DynamicColorState.unavailable)
const DynamicColorListTile(),
],
),
SettingsSection(
title: S.of(context).about,
children: const [
SourceCodeListTile(),
ReportIssueListTile(),
WriteEmailListTile(),
VersionListTile(),
],
),
SizedBox(height: MediaQuery.of(context).padding.bottom), SizedBox(height: MediaQuery.of(context).padding.bottom),
], ],
), ),

View file

@ -1,7 +1,7 @@
name: lightmeter name: lightmeter
description: A new Flutter project. description: A new Flutter project.
publish_to: "none" publish_to: "none"
version: 0.6.1+10 version: 0.8.1+13
environment: environment:
sdk: ">=2.18.0 <3.0.0" sdk: ">=2.18.0 <3.0.0"
@ -40,37 +40,6 @@ dependency_overrides:
flutter: flutter:
uses-material-design: true uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/assets-and-images/#from-packages
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/custom-fonts/#from-packages
flutter_icons: flutter_icons:
image_path_android: "assets/launcher_icon_circle.png" image_path_android: "assets/launcher_icon_circle.png"
android: "launcher_icon" android: "launcher_icon"
@ -78,7 +47,7 @@ flutter_icons:
image_path_ios: "assets/launcher_icon_square.png" image_path_ios: "assets/launcher_icon_square.png"
ios: true ios: true
remove_alpha_ios: true remove_alpha_ios: true
min_sdk_android: 21 # android min sdk min:16, default 21 min_sdk_android: 21
flutter_intl: flutter_intl:
enabled: true enabled: true