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/screens/metering/flow_metering.dart';
|
||||
import 'package:lightmeter/screens/settings/flow_settings.dart';
|
||||
import 'package:lightmeter/screens/timer/flow_timer.dart';
|
||||
|
||||
class Application extends StatelessWidget {
|
||||
const Application({super.key});
|
||||
|
@ -38,10 +39,11 @@ class Application extends StatelessWidget {
|
|||
data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
|
||||
child: child!,
|
||||
),
|
||||
initialRoute: "metering",
|
||||
initialRoute: "timer",
|
||||
routes: {
|
||||
"metering": (context) => const MeteringFlow(),
|
||||
"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