mirror of
https://github.com/vodemn/m3_lightmeter.git
synced 2024-11-22 07:20:39 +00:00
wip
This commit is contained in:
parent
bc7e6e14d0
commit
378ab45f45
7 changed files with 571 additions and 1 deletions
|
@ -7,6 +7,7 @@ import 'package:lightmeter/platform_config.dart';
|
||||||
import 'package:lightmeter/providers/user_preferences_provider.dart';
|
import 'package:lightmeter/providers/user_preferences_provider.dart';
|
||||||
import 'package:lightmeter/screens/metering/flow_metering.dart';
|
import 'package:lightmeter/screens/metering/flow_metering.dart';
|
||||||
import 'package:lightmeter/screens/settings/flow_settings.dart';
|
import 'package:lightmeter/screens/settings/flow_settings.dart';
|
||||||
|
import 'package:lightmeter/screens/timer/flow_timer.dart';
|
||||||
|
|
||||||
class Application extends StatelessWidget {
|
class Application extends StatelessWidget {
|
||||||
const Application({super.key});
|
const Application({super.key});
|
||||||
|
@ -38,10 +39,11 @@ class Application extends StatelessWidget {
|
||||||
data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
|
data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
|
||||||
child: child!,
|
child: child!,
|
||||||
),
|
),
|
||||||
initialRoute: "metering",
|
initialRoute: "timer",
|
||||||
routes: {
|
routes: {
|
||||||
"metering": (context) => const MeteringFlow(),
|
"metering": (context) => const MeteringFlow(),
|
||||||
"settings": (context) => const SettingsFlow(),
|
"settings": (context) => const SettingsFlow(),
|
||||||
|
"timer": (context) => const TimerFlow(),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
15
lib/screens/shared/close_button/widget_button_close.dart
Normal file
15
lib/screens/shared/close_button/widget_button_close.dart
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:lightmeter/generated/l10n.dart';
|
||||||
|
|
||||||
|
class CloseButton extends StatelessWidget {
|
||||||
|
const CloseButton({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return IconButton(
|
||||||
|
onPressed: Navigator.of(context).pop,
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
tooltip: S.of(context).tooltipClose,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
201
lib/screens/timer/bloc_timer.dart
Normal file
201
lib/screens/timer/bloc_timer.dart
Normal file
|
@ -0,0 +1,201 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:bloc_concurrency/bloc_concurrency.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:lightmeter/data/models/volume_action.dart';
|
||||||
|
import 'package:lightmeter/interactors/metering_interactor.dart';
|
||||||
|
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.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/screens/metering/event_metering.dart';
|
||||||
|
import 'package:lightmeter/screens/metering/state_metering.dart';
|
||||||
|
import 'package:lightmeter/screens/metering/utils/notifier_volume_keys.dart';
|
||||||
|
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||||
|
|
||||||
|
class MeteringBloc extends Bloc<MeteringEvent, MeteringState> {
|
||||||
|
final MeteringInteractor _meteringInteractor;
|
||||||
|
final VolumeKeysNotifier _volumeKeysNotifier;
|
||||||
|
final MeteringCommunicationBloc _communicationBloc;
|
||||||
|
late final StreamSubscription<communication_states.ScreenState> _communicationSubscription;
|
||||||
|
|
||||||
|
MeteringBloc(
|
||||||
|
this._meteringInteractor,
|
||||||
|
this._volumeKeysNotifier,
|
||||||
|
this._communicationBloc,
|
||||||
|
) : super(
|
||||||
|
MeteringDataState(
|
||||||
|
ev100: null,
|
||||||
|
iso: _meteringInteractor.iso,
|
||||||
|
nd: _meteringInteractor.ndFilter,
|
||||||
|
isMetering: false,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
_volumeKeysNotifier.addListener(onVolumeKey);
|
||||||
|
_communicationSubscription = _communicationBloc.stream
|
||||||
|
.where((state) => state is communication_states.ScreenState)
|
||||||
|
.map((state) => state as communication_states.ScreenState)
|
||||||
|
.listen(onCommunicationState);
|
||||||
|
|
||||||
|
on<EquipmentProfileChangedEvent>(_onEquipmentProfileChanged);
|
||||||
|
on<IsoChangedEvent>(_onIsoChanged);
|
||||||
|
on<NdChangedEvent>(_onNdChanged);
|
||||||
|
on<MeasureEvent>(_onMeasure, transformer: droppable());
|
||||||
|
on<MeasuredEvent>(_onMeasured);
|
||||||
|
on<MeasureErrorEvent>(_onMeasureError);
|
||||||
|
on<SettingsOpenedEvent>(_onSettingsOpened);
|
||||||
|
on<SettingsClosedEvent>(_onSettingsClosed);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onTransition(Transition<MeteringEvent, MeteringState> transition) {
|
||||||
|
super.onTransition(transition);
|
||||||
|
if (transition.nextState is MeteringDataState) {
|
||||||
|
final nextState = transition.nextState as MeteringDataState;
|
||||||
|
if (transition.currentState is LoadingState ||
|
||||||
|
transition.currentState is MeteringDataState &&
|
||||||
|
(transition.currentState as MeteringDataState).ev != nextState.ev) {
|
||||||
|
if (nextState.hasError) {
|
||||||
|
_meteringInteractor.errorVibration();
|
||||||
|
} else {
|
||||||
|
_meteringInteractor.responseVibration();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() async {
|
||||||
|
_volumeKeysNotifier.removeListener(onVolumeKey);
|
||||||
|
await _communicationSubscription.cancel();
|
||||||
|
return super.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
void onCommunicationState(communication_states.ScreenState communicationState) {
|
||||||
|
if (communicationState is communication_states.MeasuredState) {
|
||||||
|
_handleEv100(
|
||||||
|
communicationState.ev100,
|
||||||
|
isMetering: communicationState is communication_states.MeteringInProgressState,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onEquipmentProfileChanged(EquipmentProfileChangedEvent event, Emitter emit) {
|
||||||
|
bool willUpdateMeasurements = false;
|
||||||
|
|
||||||
|
/// Update selected ISO value and discard selected film, if selected equipment profile
|
||||||
|
/// doesn't contain currently selected value
|
||||||
|
IsoValue iso = state.iso;
|
||||||
|
if (!event.equipmentProfileData.isoValues.any((v) => state.iso.value == v.value)) {
|
||||||
|
_meteringInteractor.iso = event.equipmentProfileData.isoValues.first;
|
||||||
|
iso = event.equipmentProfileData.isoValues.first;
|
||||||
|
willUpdateMeasurements = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The same for ND filter
|
||||||
|
NdValue nd = state.nd;
|
||||||
|
if (!event.equipmentProfileData.ndValues.any((v) => state.nd.value == v.value)) {
|
||||||
|
_meteringInteractor.ndFilter = event.equipmentProfileData.ndValues.first;
|
||||||
|
nd = event.equipmentProfileData.ndValues.first;
|
||||||
|
willUpdateMeasurements = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (willUpdateMeasurements) {
|
||||||
|
emit(
|
||||||
|
MeteringDataState(
|
||||||
|
ev100: state.ev100,
|
||||||
|
iso: iso,
|
||||||
|
nd: nd,
|
||||||
|
isMetering: state.isMetering,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onIsoChanged(IsoChangedEvent event, Emitter emit) {
|
||||||
|
if (state.iso != event.isoValue) {
|
||||||
|
_meteringInteractor.iso = event.isoValue;
|
||||||
|
emit(
|
||||||
|
MeteringDataState(
|
||||||
|
ev100: state.ev100,
|
||||||
|
iso: event.isoValue,
|
||||||
|
nd: state.nd,
|
||||||
|
isMetering: state.isMetering,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onNdChanged(NdChangedEvent event, Emitter emit) {
|
||||||
|
if (state.nd != event.ndValue) {
|
||||||
|
_meteringInteractor.ndFilter = event.ndValue;
|
||||||
|
emit(
|
||||||
|
MeteringDataState(
|
||||||
|
ev100: state.ev100,
|
||||||
|
iso: state.iso,
|
||||||
|
nd: event.ndValue,
|
||||||
|
isMetering: state.isMetering,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onMeasure(MeasureEvent _, Emitter emit) {
|
||||||
|
_meteringInteractor.quickVibration();
|
||||||
|
_communicationBloc.add(const communication_events.MeasureEvent());
|
||||||
|
emit(
|
||||||
|
LoadingState(
|
||||||
|
iso: state.iso,
|
||||||
|
nd: state.nd,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleEv100(double? ev100, {required bool isMetering}) {
|
||||||
|
if (ev100 == null || ev100.isNaN || ev100.isInfinite) {
|
||||||
|
add(MeasureErrorEvent(isMetering: isMetering));
|
||||||
|
} else {
|
||||||
|
add(MeasuredEvent(ev100, isMetering: isMetering));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onMeasured(MeasuredEvent event, Emitter emit) {
|
||||||
|
emit(
|
||||||
|
MeteringDataState(
|
||||||
|
ev100: event.ev100,
|
||||||
|
iso: state.iso,
|
||||||
|
nd: state.nd,
|
||||||
|
isMetering: event.isMetering,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onMeasureError(MeasureErrorEvent event, Emitter emit) {
|
||||||
|
emit(
|
||||||
|
MeteringDataState(
|
||||||
|
ev100: null,
|
||||||
|
iso: state.iso,
|
||||||
|
nd: state.nd,
|
||||||
|
isMetering: event.isMetering,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
void onVolumeKey() {
|
||||||
|
if (_meteringInteractor.volumeAction == VolumeAction.shutter) {
|
||||||
|
add(const MeasureEvent());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onSettingsOpened(SettingsOpenedEvent _, Emitter __) {
|
||||||
|
_communicationBloc.add(const communication_events.SettingsOpenedEvent());
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onSettingsClosed(SettingsClosedEvent _, Emitter __) {
|
||||||
|
_communicationBloc.add(const communication_events.SettingsClosedEvent());
|
||||||
|
}
|
||||||
|
}
|
48
lib/screens/timer/event_timer.dart
Normal file
48
lib/screens/timer/event_timer.dart
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||||
|
|
||||||
|
sealed class MeteringEvent {
|
||||||
|
const MeteringEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
class EquipmentProfileChangedEvent extends MeteringEvent {
|
||||||
|
final EquipmentProfile equipmentProfileData;
|
||||||
|
|
||||||
|
const EquipmentProfileChangedEvent(this.equipmentProfileData);
|
||||||
|
}
|
||||||
|
|
||||||
|
class IsoChangedEvent extends MeteringEvent {
|
||||||
|
final IsoValue isoValue;
|
||||||
|
|
||||||
|
const IsoChangedEvent(this.isoValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
class NdChangedEvent extends MeteringEvent {
|
||||||
|
final NdValue ndValue;
|
||||||
|
|
||||||
|
const NdChangedEvent(this.ndValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
class MeasureEvent extends MeteringEvent {
|
||||||
|
const MeasureEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
class MeasuredEvent extends MeteringEvent {
|
||||||
|
final double ev100;
|
||||||
|
final bool isMetering;
|
||||||
|
|
||||||
|
const MeasuredEvent(this.ev100, {required this.isMetering});
|
||||||
|
}
|
||||||
|
|
||||||
|
class MeasureErrorEvent extends MeteringEvent {
|
||||||
|
final bool isMetering;
|
||||||
|
|
||||||
|
const MeasureErrorEvent({required this.isMetering});
|
||||||
|
}
|
||||||
|
|
||||||
|
class SettingsOpenedEvent extends MeteringEvent {
|
||||||
|
const SettingsOpenedEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
class SettingsClosedEvent extends MeteringEvent {
|
||||||
|
const SettingsClosedEvent();
|
||||||
|
}
|
57
lib/screens/timer/flow_timer.dart
Normal file
57
lib/screens/timer/flow_timer.dart
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:lightmeter/interactors/metering_interactor.dart';
|
||||||
|
import 'package:lightmeter/providers/services_provider.dart';
|
||||||
|
import 'package:lightmeter/screens/metering/bloc_metering.dart';
|
||||||
|
import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart';
|
||||||
|
import 'package:lightmeter/screens/metering/screen_metering.dart';
|
||||||
|
import 'package:lightmeter/screens/metering/utils/notifier_volume_keys.dart';
|
||||||
|
import 'package:lightmeter/screens/timer/screen_timer.dart';
|
||||||
|
|
||||||
|
class TimerFlow extends StatelessWidget {
|
||||||
|
const TimerFlow({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MeteringInteractorProvider(
|
||||||
|
data: MeteringInteractor(
|
||||||
|
ServicesProvider.of(context).userPreferencesService,
|
||||||
|
ServicesProvider.of(context).caffeineService,
|
||||||
|
ServicesProvider.of(context).hapticsService,
|
||||||
|
ServicesProvider.of(context).permissionsService,
|
||||||
|
ServicesProvider.of(context).lightSensorService,
|
||||||
|
ServicesProvider.of(context).volumeEventsService,
|
||||||
|
)..initialize(),
|
||||||
|
child: MultiBlocProvider(
|
||||||
|
providers: [
|
||||||
|
BlocProvider(create: (_) => MeteringCommunicationBloc()),
|
||||||
|
BlocProvider(
|
||||||
|
create: (context) => MeteringBloc(
|
||||||
|
MeteringInteractorProvider.of(context),
|
||||||
|
VolumeKeysNotifier(ServicesProvider.of(context).volumeEventsService),
|
||||||
|
context.read<MeteringCommunicationBloc>(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: const TimerScreen(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MeteringInteractorProvider extends InheritedWidget {
|
||||||
|
final MeteringInteractor data;
|
||||||
|
|
||||||
|
const MeteringInteractorProvider({
|
||||||
|
required this.data,
|
||||||
|
required super.child,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
static MeteringInteractor of(BuildContext context) {
|
||||||
|
return context.findAncestorWidgetOfExactType<MeteringInteractorProvider>()!.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool updateShouldNotify(MeteringInteractorProvider oldWidget) => false;
|
||||||
|
}
|
211
lib/screens/timer/screen_timer.dart
Normal file
211
lib/screens/timer/screen_timer.dart
Normal file
|
@ -0,0 +1,211 @@
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:lightmeter/generated/l10n.dart';
|
||||||
|
import 'package:lightmeter/res/dimens.dart';
|
||||||
|
import 'package:material_color_utilities/material_color_utilities.dart';
|
||||||
|
|
||||||
|
class TimerScreen extends StatelessWidget {
|
||||||
|
const TimerScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
automaticallyImplyLeading: false,
|
||||||
|
centerTitle: true,
|
||||||
|
elevation: 0,
|
||||||
|
title: Text(
|
||||||
|
'Test',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
fontSize: Dimens.grid24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [const CloseButton()],
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
SizedBox.fromSize(
|
||||||
|
size: Size.square(MediaQuery.sizeOf(context).width - Dimens.paddingL * 4),
|
||||||
|
child: _Timer(
|
||||||
|
remainingSeconds: 5,
|
||||||
|
timerLength: 124,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
|
||||||
|
floatingActionButton: FloatingActionButton(
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Timer extends StatelessWidget {
|
||||||
|
final int remainingSeconds;
|
||||||
|
final int timerLength;
|
||||||
|
|
||||||
|
const _Timer({
|
||||||
|
required this.remainingSeconds,
|
||||||
|
required this.timerLength,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return CustomPaint(
|
||||||
|
painter: _TimelinePainter(
|
||||||
|
backgroundColor: ElevationOverlay.applySurfaceTint(
|
||||||
|
Theme.of(context).cardTheme.color!,
|
||||||
|
Theme.of(context).cardTheme.surfaceTintColor,
|
||||||
|
Theme.of(context).cardTheme.elevation!,
|
||||||
|
),
|
||||||
|
progressColor: Theme.of(context).colorScheme.primary,
|
||||||
|
progress: remainingSeconds / timerLength,
|
||||||
|
),
|
||||||
|
willChange: true,
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
parseSeconds(),
|
||||||
|
style: Theme.of(context).textTheme.headlineLarge,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String parseSeconds() {
|
||||||
|
String addZeroIfNeeded(int value) {
|
||||||
|
if (value == 0) {
|
||||||
|
return '00';
|
||||||
|
} else if (value < 10) {
|
||||||
|
return '0$value';
|
||||||
|
} else {
|
||||||
|
return '$value';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final buffer = StringBuffer();
|
||||||
|
int remainingSeconds = this.remainingSeconds;
|
||||||
|
// longer than 1 hours
|
||||||
|
if (timerLength >= 3600) {
|
||||||
|
final hours = remainingSeconds ~/ 3600;
|
||||||
|
buffer.writeAll([addZeroIfNeeded(hours), ':']);
|
||||||
|
remainingSeconds -= hours * 3600;
|
||||||
|
}
|
||||||
|
// longer than 1 minute
|
||||||
|
if (timerLength >= 60 || timerLength == 0) {
|
||||||
|
final minutes = remainingSeconds ~/ 60;
|
||||||
|
buffer.writeAll([addZeroIfNeeded(minutes), ':']);
|
||||||
|
remainingSeconds -= minutes * 60;
|
||||||
|
}
|
||||||
|
// longer than 1 second
|
||||||
|
buffer.write(addZeroIfNeeded(remainingSeconds));
|
||||||
|
return buffer.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TimelinePainter extends CustomPainter {
|
||||||
|
final Color progressColor;
|
||||||
|
final Color backgroundColor;
|
||||||
|
final double progress;
|
||||||
|
|
||||||
|
late final double timelineEdgeRadius = strokeWidth / 2;
|
||||||
|
late final double radiansProgress = 2 * pi * progress;
|
||||||
|
static const double radiansQuarterTurn = -pi / 2;
|
||||||
|
static const double strokeWidth = Dimens.grid8;
|
||||||
|
|
||||||
|
_TimelinePainter({
|
||||||
|
required this.progressColor,
|
||||||
|
required this.backgroundColor,
|
||||||
|
required this.progress,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final radius = size.height / 2;
|
||||||
|
final timerCenter = Offset(radius, radius);
|
||||||
|
final timelineSegmentPath = Path.combine(
|
||||||
|
PathOperation.difference,
|
||||||
|
Path()
|
||||||
|
..arcTo(
|
||||||
|
Rect.fromCenter(
|
||||||
|
center: timerCenter,
|
||||||
|
height: size.height,
|
||||||
|
width: size.width,
|
||||||
|
),
|
||||||
|
radiansQuarterTurn,
|
||||||
|
radiansProgress,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
..lineTo(radius, radius)
|
||||||
|
..lineTo(radius, 0),
|
||||||
|
Path()
|
||||||
|
..addOval(
|
||||||
|
Rect.fromCircle(
|
||||||
|
center: timerCenter,
|
||||||
|
radius: radius - strokeWidth,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final smoothEdgesPath = Path.combine(
|
||||||
|
PathOperation.union,
|
||||||
|
Path()
|
||||||
|
..addOval(
|
||||||
|
Rect.fromCircle(
|
||||||
|
center: Offset(radius, timelineEdgeRadius),
|
||||||
|
radius: timelineEdgeRadius,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Path()
|
||||||
|
..addOval(
|
||||||
|
Rect.fromCircle(
|
||||||
|
center: Offset(
|
||||||
|
(radius - timelineEdgeRadius) * cos(radiansProgress + radiansQuarterTurn) + radius,
|
||||||
|
(radius - timelineEdgeRadius) * sin(radiansProgress + radiansQuarterTurn) + radius,
|
||||||
|
),
|
||||||
|
radius: timelineEdgeRadius,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
canvas.drawPath(
|
||||||
|
Path.combine(
|
||||||
|
PathOperation.difference,
|
||||||
|
Path()
|
||||||
|
..addOval(
|
||||||
|
Rect.fromCircle(
|
||||||
|
center: timerCenter,
|
||||||
|
radius: radius,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Path()
|
||||||
|
..addOval(
|
||||||
|
Rect.fromCircle(
|
||||||
|
center: timerCenter,
|
||||||
|
radius: radius - strokeWidth,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Paint()..color = backgroundColor,
|
||||||
|
);
|
||||||
|
|
||||||
|
canvas.drawPath(
|
||||||
|
Path.combine(
|
||||||
|
PathOperation.union,
|
||||||
|
timelineSegmentPath,
|
||||||
|
smoothEdgesPath,
|
||||||
|
),
|
||||||
|
Paint()..color = progressColor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(_TimelinePainter oldDelegate) => oldDelegate.progress != progress;
|
||||||
|
}
|
36
lib/screens/timer/state_timer.dart
Normal file
36
lib/screens/timer/state_timer.dart
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart';
|
||||||
|
|
||||||
|
@immutable
|
||||||
|
abstract class MeteringState {
|
||||||
|
final double? ev100;
|
||||||
|
final IsoValue iso;
|
||||||
|
final NdValue nd;
|
||||||
|
final bool isMetering;
|
||||||
|
|
||||||
|
const MeteringState({
|
||||||
|
this.ev100,
|
||||||
|
required this.iso,
|
||||||
|
required this.nd,
|
||||||
|
required this.isMetering,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoadingState extends MeteringState {
|
||||||
|
const LoadingState({
|
||||||
|
required super.iso,
|
||||||
|
required super.nd,
|
||||||
|
}) : super(isMetering: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
class MeteringDataState extends MeteringState {
|
||||||
|
const MeteringDataState({
|
||||||
|
required super.ev100,
|
||||||
|
required super.iso,
|
||||||
|
required super.nd,
|
||||||
|
required super.isMetering,
|
||||||
|
});
|
||||||
|
|
||||||
|
double? get ev => ev100 != null ? ev100! + log2(iso.value / 100) - nd.stopReduction : null;
|
||||||
|
bool get hasError => ev == null;
|
||||||
|
}
|
Loading…
Reference in a new issue