diff --git a/lib/application.dart b/lib/application.dart index 0cf1df2..cb298f2 100644 --- a/lib/application.dart +++ b/lib/application.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:lightmeter/data/haptics_service.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -29,6 +30,7 @@ class Application extends StatelessWidget { return MultiProvider( providers: [ Provider(create: (_) => UserPreferencesService(snapshot.data!)), + Provider(create: (_) => HapticsService()), Provider.value(value: evSource), ], child: Provider( diff --git a/lib/data/haptics_service.dart b/lib/data/haptics_service.dart new file mode 100644 index 0000000..0b6e7c3 --- /dev/null +++ b/lib/data/haptics_service.dart @@ -0,0 +1,18 @@ +import 'package:vibration/vibration.dart'; + +class HapticsService { + Future quickVibration() async => _tryVibrate(duration: 25, amplitude: 96); + + Future responseVibration() async => _tryVibrate(duration: 50, amplitude: 128); + + Future _tryVibrate({required int duration, required int amplitude}) async { + if (await _canVibrate()) { + Vibration.vibrate( + duration: duration, + amplitude: amplitude, + ); + } + } + + Future _canVibrate() async => await Vibration.hasVibrator() ?? false; +} diff --git a/lib/data/shared_prefs_service.dart b/lib/data/shared_prefs_service.dart index b456b0b..71d7c0c 100644 --- a/lib/data/shared_prefs_service.dart +++ b/lib/data/shared_prefs_service.dart @@ -5,10 +5,11 @@ import 'models/photography_values/nd_value.dart'; import 'models/theme_type.dart'; class UserPreferencesService { - static const _isoKey = "ISO"; - static const _ndFilterKey = "ND"; + static const _isoKey = "iso"; + static const _ndFilterKey = "nd"; - static const _themeTypeKey = "ThemeType"; + static const _hapticsKey = "haptics"; + static const _themeTypeKey = "themeType"; final SharedPreferences _sharedPreferences; @@ -20,6 +21,9 @@ class UserPreferencesService { NdValue get ndFilter => ndValues.firstWhere((v) => v.value == (_sharedPreferences.getInt(_ndFilterKey) ?? 0)); set ndFilter(NdValue value) => _sharedPreferences.setInt(_ndFilterKey, value.value); + bool get haptics => _sharedPreferences.getBool(_hapticsKey) ?? false; + set haptics(bool value) => _sharedPreferences.setBool(_hapticsKey, value); + ThemeType get themeType => ThemeType.values[_sharedPreferences.getInt(_themeTypeKey) ?? 0]; set themeType(ThemeType value) => _sharedPreferences.setInt(_themeTypeKey, value.index); } diff --git a/lib/interactors/haptics_interactor.dart b/lib/interactors/haptics_interactor.dart new file mode 100644 index 0000000..abb537a --- /dev/null +++ b/lib/interactors/haptics_interactor.dart @@ -0,0 +1,26 @@ +import 'package:lightmeter/data/haptics_service.dart'; +import 'package:lightmeter/data/shared_prefs_service.dart'; + +class HapticsInteractor { + final UserPreferencesService _userPreferencesService; + final HapticsService _hapticsService; + + const HapticsInteractor( + this._userPreferencesService, + this._hapticsService, + ); + + bool get isEnabled => _userPreferencesService.haptics; + + /// Executes vibration if haptics are enabled in settings + void quickVibration() { + if (_userPreferencesService.haptics) _hapticsService.quickVibration(); + } + + /// Executes vibration if haptics are enabled in settings + void responseVibration() { + if (_userPreferencesService.haptics) _hapticsService.responseVibration(); + } + + void enableHaptics(bool enable) => _userPreferencesService.haptics = enable; +} diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 3ac7b5a..f605eb2 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -17,6 +17,7 @@ "showFractionalStops": "Show fractional stops", "halfStops": "1/2", "thirdStops": "1/3", + "haptics": "Haptics", "theme": "Theme", "chooseTheme": "Choose theme", "themeLight": "Light", diff --git a/lib/screens/metering/bloc_metering.dart b/lib/screens/metering/bloc_metering.dart index a15fc1a..7e4d357 100644 --- a/lib/screens/metering/bloc_metering.dart +++ b/lib/screens/metering/bloc_metering.dart @@ -7,6 +7,7 @@ import 'package:lightmeter/data/models/exposure_pair.dart'; import 'package:lightmeter/data/models/photography_values/photography_value.dart'; import 'package:lightmeter/data/models/photography_values/shutter_speed_value.dart'; import 'package:lightmeter/data/shared_prefs_service.dart'; +import 'package:lightmeter/interactors/haptics_interactor.dart'; import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart' as communication_events; import 'package:lightmeter/screens/metering/communication/state_communication_metering.dart' as communication_states; import 'package:lightmeter/utils/log_2.dart'; @@ -18,6 +19,7 @@ import 'state_metering.dart'; class MeteringBloc extends Bloc { final MeteringCommunicationBloc _communicationBloc; final UserPreferencesService _userPreferencesService; + final HapticsInteractor _hapticsInteractor; late final StreamSubscription _communicationSubscription; List get _apertureValues => apertureValues.whereStopType(stopType); @@ -28,6 +30,7 @@ class MeteringBloc extends Bloc { MeteringBloc( this._communicationBloc, this._userPreferencesService, + this._hapticsInteractor, this.stopType, ) : super( MeteringState( @@ -46,7 +49,7 @@ class MeteringBloc extends Bloc { on(_onStopTypeChanged); on(_onIsoChanged); on(_onNdChanged); - on((_, __) => _communicationBloc.add(const communication_events.MeasureEvent())); + on(_onMeasure); on(_onMeasured); add(const MeasureEvent()); @@ -99,7 +102,13 @@ class MeteringBloc extends Bloc { )); } + void _onMeasure(_, __) { + _hapticsInteractor.quickVibration(); + _communicationBloc.add(const communication_events.MeasureEvent()); + } + void _onMeasured(MeasuredEvent event, Emitter emit) { + _hapticsInteractor.responseVibration(); final ev = event.ev100 + log2(state.iso.value / 100); emit(MeteringState( iso: state.iso, diff --git a/lib/screens/metering/components/camera/widget_exposure_slider.dart b/lib/screens/metering/components/camera/widget_exposure_slider.dart index 72b4976..e0dc152 100644 --- a/lib/screens/metering/components/camera/widget_exposure_slider.dart +++ b/lib/screens/metering/components/camera/widget_exposure_slider.dart @@ -21,7 +21,7 @@ class CameraExposureSlider extends StatelessWidget { IconButton( icon: const Icon(Icons.sync), onPressed: state.currentExposureOffset != 0.0 - ? () => context.read().add(const ExposureOffsetChangedEvent(0)) + ? () => context.read().add(const ExposureOffsetResetEvent()) : null, ), Expanded( diff --git a/lib/screens/metering/ev_source/camera/bloc_camera.dart b/lib/screens/metering/ev_source/camera/bloc_camera.dart index a98b8a6..fc85e93 100644 --- a/lib/screens/metering/ev_source/camera/bloc_camera.dart +++ b/lib/screens/metering/ev_source/camera/bloc_camera.dart @@ -6,6 +6,7 @@ import 'package:camera/camera.dart'; import 'package:exif/exif.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:lightmeter/interactors/haptics_interactor.dart'; import 'package:lightmeter/screens/metering/ev_source/ev_source_bloc.dart'; import 'package:lightmeter/screens/metering/communication/bloc_communication_metering.dart'; import 'package:lightmeter/screens/metering/communication/event_communication_metering.dart' as communication_event; @@ -16,6 +17,7 @@ import 'event_camera.dart'; import 'state_camera.dart'; class CameraBloc extends EvSourceBloc { + final HapticsInteractor _hapticsInteractor; late final _WidgetsBindingObserver _observer; CameraController? _cameraController; CameraController? get cameraController => _cameraController; @@ -29,8 +31,10 @@ class CameraBloc extends EvSourceBloc { double _exposureStep = 0.0; double _currentExposureOffset = 0.0; - CameraBloc(MeteringCommunicationBloc communicationBloc) - : super( + CameraBloc( + MeteringCommunicationBloc communicationBloc, + this._hapticsInteractor, + ) : super( communicationBloc, const CameraInitState(), ) { @@ -40,6 +44,7 @@ class CameraBloc extends EvSourceBloc { on(_onInitialize); on(_onZoomChanged); on(_onExposureOffsetChanged); + on(_onExposureOffsetResetEvent); add(const InitializeEvent()); } @@ -117,6 +122,11 @@ class CameraBloc extends EvSourceBloc { _emitActiveState(emit); } + Future _onExposureOffsetResetEvent(ExposureOffsetResetEvent event, Emitter emit) async { + _hapticsInteractor.quickVibration(); + add(const ExposureOffsetChangedEvent(0)); + } + void _emitActiveState(Emitter emit) { emit(CameraActiveState( minZoom: _zoomRange!.start, diff --git a/lib/screens/metering/ev_source/camera/event_camera.dart b/lib/screens/metering/ev_source/camera/event_camera.dart index 0a30d07..96de582 100644 --- a/lib/screens/metering/ev_source/camera/event_camera.dart +++ b/lib/screens/metering/ev_source/camera/event_camera.dart @@ -17,3 +17,7 @@ class ExposureOffsetChangedEvent extends CameraEvent { const ExposureOffsetChangedEvent(this.value); } + +class ExposureOffsetResetEvent extends CameraEvent { + const ExposureOffsetResetEvent(); +} diff --git a/lib/screens/metering/flow_metering.dart b/lib/screens/metering/flow_metering.dart index 83d53e5..e81f7c5 100644 --- a/lib/screens/metering/flow_metering.dart +++ b/lib/screens/metering/flow_metering.dart @@ -1,14 +1,16 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:lightmeter/data/haptics_service.dart'; import 'package:lightmeter/data/models/ev_source_type.dart'; import 'package:lightmeter/data/models/photography_values/photography_value.dart'; import 'package:lightmeter/data/shared_prefs_service.dart'; +import 'package:lightmeter/interactors/haptics_interactor.dart'; +import 'package:provider/provider.dart'; import 'ev_source/camera/bloc_camera.dart'; import 'ev_source/random_ev/bloc_random_ev.dart'; import 'bloc_metering.dart'; import 'communication/bloc_communication_metering.dart'; - import 'screen_metering.dart'; class MeteringFlow extends StatelessWidget { @@ -16,24 +18,36 @@ class MeteringFlow extends StatelessWidget { @override Widget build(BuildContext context) { - return MultiBlocProvider( - providers: [ - BlocProvider(create: (_) => MeteringCommunicationBloc()), - BlocProvider( - create: (context) => MeteringBloc( - context.read(), - context.read(), - context.read(), - ), - ), - BlocProvider(create: (context) => CameraBloc(context.read())), - if (context.read() == EvSourceType.mock) + return Provider( + create: (context) => HapticsInteractor( + context.read(), + context.read(), + ), + child: MultiBlocProvider( + providers: [ + BlocProvider(create: (_) => MeteringCommunicationBloc()), BlocProvider( - lazy: false, - create: (context) => RandomEvBloc(context.read()), + create: (context) => MeteringBloc( + context.read(), + context.read(), + context.read(), + context.read(), + ), ), - ], - child: const MeteringScreen(), + BlocProvider( + create: (context) => CameraBloc( + context.read(), + context.read(), + ), + ), + if (context.read() == EvSourceType.mock) + BlocProvider( + lazy: false, + create: (context) => RandomEvBloc(context.read()), + ), + ], + child: const MeteringScreen(), + ), ); } } diff --git a/lib/screens/settings/components/haptics/bloc_list_tile_haptics.dart b/lib/screens/settings/components/haptics/bloc_list_tile_haptics.dart new file mode 100644 index 0000000..134952a --- /dev/null +++ b/lib/screens/settings/components/haptics/bloc_list_tile_haptics.dart @@ -0,0 +1,18 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:lightmeter/interactors/haptics_interactor.dart'; + +class HapticsListTileBloc extends Cubit { + final HapticsInteractor _hapticsInteractor; + + HapticsListTileBloc( + this._hapticsInteractor, + ) : super(_hapticsInteractor.isEnabled); + + void onHapticsChange(bool value) { + _hapticsInteractor.enableHaptics(value); + if (value) { + _hapticsInteractor.quickVibration(); + } + emit(value); + } +} diff --git a/lib/screens/settings/components/haptics/provider_list_tile_haptics.dart b/lib/screens/settings/components/haptics/provider_list_tile_haptics.dart new file mode 100644 index 0000000..2c431c1 --- /dev/null +++ b/lib/screens/settings/components/haptics/provider_list_tile_haptics.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:lightmeter/data/haptics_service.dart'; +import 'package:lightmeter/data/shared_prefs_service.dart'; +import 'package:lightmeter/interactors/haptics_interactor.dart'; +import 'package:provider/provider.dart'; + +import 'bloc_list_tile_haptics.dart'; +import 'widget_list_tile_haptics.dart'; + +class HapticsListTileProvider extends StatelessWidget { + const HapticsListTileProvider({super.key}); + + @override + Widget build(BuildContext context) { + return Provider( + create: (context) => HapticsInteractor( + context.read(), + context.read(), + ), + child: BlocProvider( + create: (context) => HapticsListTileBloc( + context.read() + ), + child: const HapticsListTile(), + ), + ); + } +} diff --git a/lib/screens/settings/components/haptics/widget_list_tile_haptics.dart b/lib/screens/settings/components/haptics/widget_list_tile_haptics.dart new file mode 100644 index 0000000..d640106 --- /dev/null +++ b/lib/screens/settings/components/haptics/widget_list_tile_haptics.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:lightmeter/generated/l10n.dart'; + +import 'bloc_list_tile_haptics.dart'; + +class HapticsListTile extends StatelessWidget { + const HapticsListTile({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) => SwitchListTile( + secondary: const Icon(Icons.vibration), + title: Text(S.of(context).haptics), + value: state, + onChanged: context.read().onHapticsChange, + ), + ); + } +} diff --git a/lib/screens/settings/screen_settings.dart b/lib/screens/settings/screen_settings.dart index 59e0a77..956a47c 100644 --- a/lib/screens/settings/screen_settings.dart +++ b/lib/screens/settings/screen_settings.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/res/dimens.dart'; +import 'components/haptics/provider_list_tile_haptics.dart'; import 'components/widget_list_tile_fractional_stops.dart'; import 'components/widget_list_tile_theme_type.dart'; import 'components/widget_label_version.dart'; @@ -40,8 +41,7 @@ class SettingsScreen extends StatelessWidget { delegate: SliverChildListDelegate( [ const StopTypeListTile(), - // const CaffeineListTile(), - // const HapticsListTile(), + const HapticsListTileProvider(), const ThemeTypeListTile(), ], ), diff --git a/pubspec.yaml b/pubspec.yaml index 97e65db..d916082 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,26 +11,27 @@ dependencies: exif: 3.1.2 flutter: sdk: flutter - flutter_bloc: ^8.1.1 + flutter_bloc: 8.1.1 flutter_localizations: sdk: flutter intl: 0.17.0 intl_utils: 2.8.1 - material_color_utilities: ^0.2.0 + material_color_utilities: 0.2.0 package_info_plus: 3.0.2 permission_handler: 10.2.0 - provider: ^6.0.4 + provider: 6.0.4 shared_preferences: 2.0.15 + vibration: 1.7.6 dev_dependencies: - google_fonts: ^3.0.1 + google_fonts: 3.0.1 flutter_launcher_icons: 0.11.0 - flutter_lints: ^2.0.0 + flutter_lints: 2.0.0 flutter_native_splash: 2.2.16 - test: ^1.21.6 + test: 1.21.6 dependency_overrides: - material_color_utilities: ^0.2.0 + material_color_utilities: 0.2.0 flutter: uses-material-design: true