diff --git a/lib/screens/timer/bloc_timer.dart b/lib/screens/timer/bloc_timer.dart index 753a7e7..4c00a25 100644 --- a/lib/screens/timer/bloc_timer.dart +++ b/lib/screens/timer/bloc_timer.dart @@ -1,201 +1,76 @@ 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'; +import 'package:lightmeter/screens/timer/event_timer.dart'; +import 'package:lightmeter/screens/timer/state_timer.dart'; -class MeteringBloc extends Bloc { +class TimerBloc extends Bloc { final MeteringInteractor _meteringInteractor; - final VolumeKeysNotifier _volumeKeysNotifier; - final MeteringCommunicationBloc _communicationBloc; - late final StreamSubscription _communicationSubscription; + late Timer? _timer; + final int timerLength; - MeteringBloc( - this._meteringInteractor, - this._volumeKeysNotifier, - this._communicationBloc, - ) : super( - MeteringDataState( - ev100: null, - iso: _meteringInteractor.iso, - nd: _meteringInteractor.ndFilter, - isMetering: false, + TimerBloc(this._meteringInteractor, this.timerLength) + : super( + TimerStoppedState( + duration: Duration(seconds: timerLength), + timeLeft: Duration(seconds: timerLength), ), ) { - _volumeKeysNotifier.addListener(onVolumeKey); - _communicationSubscription = _communicationBloc.stream - .where((state) => state is communication_states.ScreenState) - .map((state) => state as communication_states.ScreenState) - .listen(onCommunicationState); - - on(_onEquipmentProfileChanged); - on(_onIsoChanged); - on(_onNdChanged); - on(_onMeasure, transformer: droppable()); - on(_onMeasured); - on(_onMeasureError); - on(_onSettingsOpened); - on(_onSettingsClosed); - } - - @override - void onTransition(Transition 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(); - } - } - } + on(_onStartTimer); + on(_onSetTimeLeft); + on(_onStopTimer); + on(_onResetTimer); } @override Future close() async { - _volumeKeysNotifier.removeListener(onVolumeKey); - await _communicationSubscription.cancel(); + _timer?.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()); + Future _onStartTimer(StartTimerEvent _, Emitter emit) async { emit( - LoadingState( - iso: state.iso, - nd: state.nd, + TimerResumedState( + duration: state.duration, + timeLeft: state.timeLeft, + ), + ); + + _timer = Timer.periodic(const Duration(seconds: 1), (_) { + add(SetTimeLeftEvent(state.timeLeft - const Duration(seconds: 1))); + if (state.timeLeft.inMilliseconds == 0) { + add(const StopTimerEvent()); + } + }); + } + + Future _onSetTimeLeft(SetTimeLeftEvent event, Emitter emit) async { + emit( + TimerResumedState( + duration: state.duration, + timeLeft: event.timeLeft, ), ); } - 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) { + Future _onStopTimer(StopTimerEvent _, Emitter emit) async { + _timer?.cancel(); emit( - MeteringDataState( - ev100: event.ev100, - iso: state.iso, - nd: state.nd, - isMetering: event.isMetering, + TimerStoppedState( + duration: state.duration, + timeLeft: state.timeLeft, ), ); } - void _onMeasureError(MeasureErrorEvent event, Emitter emit) { + Future _onResetTimer(ResetTimerEvent _, Emitter emit) async { + _timer?.cancel(); emit( - MeteringDataState( - ev100: null, - iso: state.iso, - nd: state.nd, - isMetering: event.isMetering, + TimerStoppedState( + duration: state.duration, + timeLeft: state.duration, ), ); } - - @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()); - } } diff --git a/lib/screens/timer/event_timer.dart b/lib/screens/timer/event_timer.dart index 0a39844..d69b7c5 100644 --- a/lib/screens/timer/event_timer.dart +++ b/lib/screens/timer/event_timer.dart @@ -1,48 +1,21 @@ -import 'package:m3_lightmeter_resources/m3_lightmeter_resources.dart'; - -sealed class MeteringEvent { - const MeteringEvent(); +sealed class TimerEvent { + const TimerEvent(); } -class EquipmentProfileChangedEvent extends MeteringEvent { - final EquipmentProfile equipmentProfileData; - - const EquipmentProfileChangedEvent(this.equipmentProfileData); +class StartTimerEvent extends TimerEvent { + const StartTimerEvent(); } -class IsoChangedEvent extends MeteringEvent { - final IsoValue isoValue; - - const IsoChangedEvent(this.isoValue); +class StopTimerEvent extends TimerEvent { + const StopTimerEvent(); } -class NdChangedEvent extends MeteringEvent { - final NdValue ndValue; +class SetTimeLeftEvent extends TimerEvent { + final Duration timeLeft; - const NdChangedEvent(this.ndValue); + const SetTimeLeftEvent(this.timeLeft); } -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(); +class ResetTimerEvent extends TimerEvent { + const ResetTimerEvent(); } diff --git a/lib/screens/timer/flow_timer.dart b/lib/screens/timer/flow_timer.dart index ee2136f..7b3367f 100644 --- a/lib/screens/timer/flow_timer.dart +++ b/lib/screens/timer/flow_timer.dart @@ -2,10 +2,7 @@ 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/bloc_timer.dart'; import 'package:lightmeter/screens/timer/screen_timer.dart'; class TimerFlow extends StatelessWidget { @@ -22,17 +19,11 @@ class TimerFlow extends StatelessWidget { 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(), - ), - ), - ], + child: BlocProvider( + create: (context) => TimerBloc( + MeteringInteractorProvider.of(context), + 124, + ), child: const TimerScreen(), ), ); diff --git a/lib/screens/timer/screen_timer.dart b/lib/screens/timer/screen_timer.dart index 5d203e5..39ede74 100644 --- a/lib/screens/timer/screen_timer.dart +++ b/lib/screens/timer/screen_timer.dart @@ -1,81 +1,145 @@ import 'dart:math'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/res/dimens.dart'; +import 'package:lightmeter/screens/timer/bloc_timer.dart'; +import 'package:lightmeter/screens/timer/event_timer.dart'; +import 'package:lightmeter/screens/timer/state_timer.dart'; import 'package:material_color_utilities/material_color_utilities.dart'; -class TimerScreen extends StatelessWidget { +class TimerScreen extends StatefulWidget { const TimerScreen({super.key}); + @override + State createState() => _TimerScreenState(); +} + +class _TimerScreenState extends State with TickerProviderStateMixin { + late AnimationController timelineController; + late Animation timelineAnimation; + late AnimationController startStopIconController; + late Animation startStopIconAnimation; + + @override + void initState() { + super.initState(); + + timelineController = AnimationController(vsync: this, duration: Dimens.durationS); + timelineAnimation = Tween(begin: 0.0, end: 1.0).animate(timelineController); + + startStopIconController = AnimationController(vsync: this, duration: Dimens.durationS); + startStopIconAnimation = Tween(begin: 0.0, end: 1.0).animate(startStopIconController); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + context.read().add(const StartTimerEvent()); + } + + @override + void dispose() { + timelineController.dispose(); + startStopIconController.dispose(); + super.dispose(); + } + @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, + return BlocListener( + listenWhen: (previous, current) => + previous is TimerStoppedState && current is TimerResumedState || + previous is TimerResumedState && current is TimerStoppedState, + listener: (context, state) => _updateAnimations(state), + child: Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + centerTitle: true, + elevation: 0, + title: Text( + 'Test', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + fontSize: Dimens.grid24, + ), ), + actions: [if (Navigator.of(context).canPop()) const CloseButton()], ), - 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, - ), + body: SafeArea( + child: Center( + child: Padding( + padding: const EdgeInsets.all(Dimens.paddingL), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Spacer(), + // SizedBox.fromSize( + // size: Size.square(MediaQuery.sizeOf(context).width - Dimens.paddingL * 4), + // child: BlocBuilder( + // builder: (context, state) { + // return _Timer( + // timeLeft: state.timeLeft, + // duration: state.duration, + // ); + // }, + // ), + // ), + BlocBuilder( + buildWhen: (previous, current) => previous.timeLeft != current.timeLeft, + builder: (_, state) => _Timer( + timeLeft: state.timeLeft, + duration: state.duration, + ), + ), + const Spacer(), + BlocBuilder( + builder: (_, state) => FloatingActionButton( + onPressed: () { + context.read().add( + state is TimerStoppedState ? const StartTimerEvent() : const StopTimerEvent(), + ); + }, + child: AnimatedIcon( + icon: AnimatedIcons.play_pause, + progress: startStopIconAnimation, + ), + ), + ), + ], ), - ], + ), ), ), ), - floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, - floatingActionButton: FloatingActionButton( - onPressed: () {}, - ), ); } + + void _updateAnimations(TimerState state) { + switch (state) { + case TimerResumedState(): + startStopIconController.forward(); + case TimerStoppedState(): + startStopIconController.reverse(); + } + } } class _Timer extends StatelessWidget { - final int remainingSeconds; - final int timerLength; + final Duration timeLeft; + final Duration duration; const _Timer({ - required this.remainingSeconds, - required this.timerLength, + required this.timeLeft, + required this.duration, }); @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, - ), - ), + return Text( + parseSeconds(), + style: Theme.of(context).textTheme.headlineLarge, ); } @@ -91,15 +155,15 @@ class _Timer extends StatelessWidget { } final buffer = StringBuffer(); - int remainingSeconds = this.remainingSeconds; + int remainingSeconds = timeLeft.inSeconds; // longer than 1 hours - if (timerLength >= 3600) { + if (duration.inSeconds >= 3600) { final hours = remainingSeconds ~/ 3600; buffer.writeAll([addZeroIfNeeded(hours), ':']); remainingSeconds -= hours * 3600; } // longer than 1 minute - if (timerLength >= 60 || timerLength == 0) { + if (duration.inSeconds >= 60 || duration.inSeconds == 0) { final minutes = remainingSeconds ~/ 60; buffer.writeAll([addZeroIfNeeded(minutes), ':']); remainingSeconds -= minutes * 60; @@ -110,6 +174,33 @@ class _Timer extends StatelessWidget { } } +class _TimerTimeline extends StatelessWidget { + final double progress; + final Widget child; + + const _TimerTimeline({ + required this.progress, + required this.child, + }) : assert(progress >= 0 && progress <= 1); + + @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: progress, + ), + willChange: true, + child: Center(child: child), + ); + } +} + class _TimelinePainter extends CustomPainter { final Color progressColor; final Color backgroundColor; diff --git a/lib/screens/timer/state_timer.dart b/lib/screens/timer/state_timer.dart index 74dc792..b5916a9 100644 --- a/lib/screens/timer/state_timer.dart +++ b/lib/screens/timer/state_timer.dart @@ -1,36 +1,26 @@ 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; +sealed class TimerState { + final Duration duration; + final Duration timeLeft; - const MeteringState({ - this.ev100, - required this.iso, - required this.nd, - required this.isMetering, + const TimerState({ + required this.duration, + required this.timeLeft, }); } -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, +class TimerStoppedState extends TimerState { + const TimerStoppedState({ + required super.duration, + required super.timeLeft, + }); +} + +class TimerResumedState extends TimerState { + const TimerResumedState({ + required super.duration, + required super.timeLeft, }); - - double? get ev => ev100 != null ? ev100! + log2(iso.value / 100) - nd.stopReduction : null; - bool get hasError => ev == null; }