Vaani/lib/features/shake_detector/shake_detector_provider.dart

179 lines
5.5 KiB
Dart
Raw Normal View History

import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:just_audio/just_audio.dart';
import 'package:logging/logging.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
2025-12-09 17:26:04 +08:00
import 'package:vaani/features/player/providers/abs_provider.dart'
hide AudioPlayer;
import 'package:vaani/features/settings/app_settings_provider.dart'
show appSettingsProvider;
import 'package:vaani/features/settings/models/app_settings.dart';
2025-12-08 23:46:43 +08:00
import 'package:vaani/features/sleep_timer/providers/sleep_timer_provider.dart'
show sleepTimerProvider;
2025-12-09 17:26:04 +08:00
import 'package:vaani/features/player/core/abs_audio_player.dart';
import 'package:vibration/vibration.dart';
import 'shake_detector.dart' as core;
part 'shake_detector_provider.g.dart';
Logger _logger = Logger('ShakeDetector');
@riverpod
class ShakeDetector extends _$ShakeDetector {
bool wasPlayerLoaded = false;
@override
core.ShakeDetector? build() {
final appSettings = ref.watch(appSettingsProvider);
final shakeDetectionSettings = appSettings.shakeDetectionSettings;
if (!shakeDetectionSettings.isEnabled) {
_logger.config('Shake detection is disabled');
return null;
}
// if no book is loaded, shake detection should not be enabled
2025-12-09 17:26:04 +08:00
final player = ref.watch(audioPlayerProvider);
player.playerStateStream.listen((event) {
2025-12-08 23:46:43 +08:00
if (event.processingState == AbsProcessingState.idle && wasPlayerLoaded) {
_logger.config('Player is now not loaded, invalidating');
wasPlayerLoaded = false;
ref.invalidateSelf();
}
2025-12-08 23:46:43 +08:00
if (event.processingState != AbsProcessingState.idle &&
!wasPlayerLoaded) {
_logger.config('Player is now loaded, invalidating');
wasPlayerLoaded = true;
ref.invalidateSelf();
}
});
2025-12-01 15:34:12 +08:00
if (player.book == null) {
_logger.config('No book is loaded, disabling shake detection');
wasPlayerLoaded = false;
return null;
} else {
_logger.finer('Book is loaded, marking player as loaded');
wasPlayerLoaded = true;
}
// if sleep timer is not enabled, shake detection should not be enabled
final sleepTimer = ref.watch(sleepTimerProvider);
if (!shakeDetectionSettings.shakeAction.isPlaybackManagementEnabled &&
sleepTimer == null) {
_logger
.config('No playback management is enabled and sleep timer is off, '
'so shake detection is disabled');
return null;
}
_logger.config('Creating shake detector');
final detector = core.ShakeDetector(
shakeDetectionSettings,
() {
final wasActionComplete = doShakeAction(
shakeDetectionSettings.shakeAction,
ref: ref,
);
if (wasActionComplete) {
shakeDetectionSettings.feedback.forEach(postShakeFeedback);
}
},
);
ref.onDispose(detector.dispose);
return detector;
}
/// Perform the shake action and return whether the action was successful
bool doShakeAction(
ShakeAction shakeAction, {
required Ref ref,
}) {
2025-12-09 17:26:04 +08:00
final player = ref.read(audioPlayerProvider);
2025-12-01 15:34:12 +08:00
if (player.book == null && shakeAction.isPlaybackManagementEnabled) {
_logger.warning('No book is loaded');
return false;
}
switch (shakeAction) {
case ShakeAction.resetSleepTimer:
_logger.fine('Resetting sleep timer');
var sleepTimer = ref.read(sleepTimerProvider);
if (sleepTimer == null || !sleepTimer.isActive) {
_logger.warning('No sleep timer is running');
return false;
}
sleepTimer.restartTimer();
return true;
case ShakeAction.fastForward:
_logger.fine('Fast forwarding');
2025-12-08 23:46:43 +08:00
if (!player.playing) {
_logger.warning('Player is not playing');
return false;
}
2025-12-08 23:46:43 +08:00
player.seek(player.position + const Duration(seconds: 30));
return true;
case ShakeAction.rewind:
_logger.fine('Rewinding');
2025-12-08 23:46:43 +08:00
if (!player.playing) {
_logger.warning('Player is not playing');
return false;
}
2025-12-08 23:46:43 +08:00
player.seek(player.position - const Duration(seconds: 30));
return true;
case ShakeAction.playPause:
_logger.fine('Toggling play/pause');
2025-12-08 23:46:43 +08:00
player.playOrPause();
return true;
default:
return false;
}
}
Future<void> postShakeFeedback(ShakeDetectedFeedback feedback) async {
switch (feedback) {
case ShakeDetectedFeedback.vibrate:
_logger.fine('Vibrating');
2025-11-20 08:36:55 +08:00
if (await Vibration.hasAmplitudeControl()) {
Vibration.vibrate(amplitude: 128, duration: 200);
break;
}
2025-11-20 08:36:55 +08:00
if (await Vibration.hasVibrator()) {
Vibration.vibrate();
break;
}
_logger.warning('No vibration support');
break;
case ShakeDetectedFeedback.beep:
_logger.fine('Beeping');
final player = AudioPlayer();
await player.setAsset('assets/sounds/beep.mp3');
await player.setVolume(0.5);
await player.play();
await player.dispose();
break;
default:
break;
}
}
}
extension on ShakeAction {
bool get isActiveWhenPaused {
// If the shake action is play/pause, it should be required when not playing
return this == ShakeAction.playPause;
}
bool get isPlaybackManagementEnabled {
return {ShakeAction.playPause, ShakeAction.fastForward, ShakeAction.rewind}
.contains(this);
}
bool get shouldActOnSleepTimer {
return {ShakeAction.resetSleepTimer}.contains(this);
}
}