2026-06-10 17:43:21 +05:30
|
|
|
import 'package:flutter/services.dart';
|
|
|
|
|
import 'package:audioplayers/audioplayers.dart';
|
|
|
|
|
import 'package:vibration/vibration.dart';
|
|
|
|
|
import 'package:flutter/foundation.dart' show kIsWeb, debugPrint;
|
2026-06-10 18:19:47 +05:30
|
|
|
import 'dart:io' show Platform;
|
2026-06-10 17:43:21 +05:30
|
|
|
|
|
|
|
|
class AppFeedback {
|
2026-06-10 18:40:58 +05:30
|
|
|
// Global settings for sound and haptic vibration feedback
|
|
|
|
|
static bool isSoundEnabled = true;
|
|
|
|
|
static bool isHapticEnabled = true;
|
|
|
|
|
|
2026-06-10 18:19:47 +05:30
|
|
|
static final Map<String, AudioPlayer> _players = {
|
|
|
|
|
'click.mp3': AudioPlayer(),
|
|
|
|
|
'select.mp3': AudioPlayer(),
|
|
|
|
|
'success.mp3': AudioPlayer(),
|
|
|
|
|
'error.mp3': AudioPlayer(),
|
|
|
|
|
};
|
2026-06-10 17:43:21 +05:30
|
|
|
|
2026-06-10 18:19:47 +05:30
|
|
|
static bool _initialized = false;
|
|
|
|
|
|
|
|
|
|
/// Preload sounds to achieve zero latency
|
|
|
|
|
static void init() {
|
|
|
|
|
if (_initialized) return;
|
|
|
|
|
try {
|
|
|
|
|
_players.forEach((key, player) {
|
|
|
|
|
player.setReleaseMode(ReleaseMode.stop);
|
|
|
|
|
player.setSource(AssetSource('sounds/$key'));
|
|
|
|
|
});
|
|
|
|
|
_initialized = true;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
debugPrint('Error initializing AppFeedback: $e');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Play a sound from assets/sounds/ with zero delay
|
2026-06-10 17:43:21 +05:30
|
|
|
static Future<void> playSound(String fileName) async {
|
2026-06-10 18:40:58 +05:30
|
|
|
if (!isSoundEnabled) return;
|
2026-06-10 17:43:21 +05:30
|
|
|
try {
|
2026-06-10 18:19:47 +05:30
|
|
|
if (!_initialized) {
|
|
|
|
|
init();
|
|
|
|
|
}
|
|
|
|
|
final player = _players[fileName];
|
|
|
|
|
if (player != null) {
|
|
|
|
|
// Stop/seek and resume for instant audio
|
|
|
|
|
await player.seek(Duration.zero);
|
|
|
|
|
await player.resume();
|
|
|
|
|
} else {
|
|
|
|
|
// Fallback for dynamically added files
|
|
|
|
|
final tempPlayer = AudioPlayer();
|
|
|
|
|
await tempPlayer.play(AssetSource('sounds/$fileName'));
|
|
|
|
|
}
|
2026-06-10 17:43:21 +05:30
|
|
|
} catch (e) {
|
|
|
|
|
debugPrint('Audio playback error for $fileName: $e');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-10 18:19:47 +05:30
|
|
|
/// Helper to check Android platform safely
|
|
|
|
|
static bool get _isAndroid => !kIsWeb && Platform.isAndroid;
|
|
|
|
|
|
|
|
|
|
/// Trigger native vibration for Android with customized duration (in milliseconds)
|
|
|
|
|
static void _triggerVibration(int duration) async {
|
2026-06-10 18:40:58 +05:30
|
|
|
if (!isHapticEnabled || kIsWeb) return;
|
2026-06-10 18:19:47 +05:30
|
|
|
try {
|
|
|
|
|
if (_isAndroid) {
|
|
|
|
|
if (await Vibration.hasVibrator() == true) {
|
|
|
|
|
Vibration.vibrate(duration: duration);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
debugPrint('Vibration error: $e');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-10 17:43:21 +05:30
|
|
|
/// Light haptic feedback + subtle click sound
|
|
|
|
|
static void click() {
|
2026-06-10 18:40:58 +05:30
|
|
|
if (isHapticEnabled) {
|
|
|
|
|
HapticFeedback.lightImpact();
|
|
|
|
|
_triggerVibration(30);
|
|
|
|
|
}
|
2026-06-10 17:43:21 +05:30
|
|
|
playSound('click.mp3');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Medium haptic + selection sound
|
|
|
|
|
static void select() {
|
2026-06-10 18:40:58 +05:30
|
|
|
if (isHapticEnabled) {
|
|
|
|
|
HapticFeedback.mediumImpact();
|
|
|
|
|
_triggerVibration(60);
|
|
|
|
|
}
|
2026-06-10 17:43:21 +05:30
|
|
|
playSound('select.mp3');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Celebration sound + strong haptic vibration
|
|
|
|
|
static void success() {
|
2026-06-10 18:40:58 +05:30
|
|
|
if (isHapticEnabled) {
|
|
|
|
|
HapticFeedback.heavyImpact();
|
|
|
|
|
_doubleVibrate();
|
|
|
|
|
}
|
2026-06-10 17:43:21 +05:30
|
|
|
playSound('success.mp3');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Wrong input -> error haptic (double vibrate) + error sound
|
|
|
|
|
static void error() {
|
2026-06-10 18:40:58 +05:30
|
|
|
if (isHapticEnabled) {
|
|
|
|
|
_doubleVibrate();
|
|
|
|
|
}
|
2026-06-10 17:43:21 +05:30
|
|
|
playSound('error.mp3');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Light haptic feedback (e.g. chip selections)
|
|
|
|
|
static void lightHaptic() {
|
2026-06-10 18:40:58 +05:30
|
|
|
if (!isHapticEnabled) return;
|
2026-06-10 17:43:21 +05:30
|
|
|
HapticFeedback.lightImpact();
|
2026-06-10 18:19:47 +05:30
|
|
|
_triggerVibration(30);
|
2026-06-10 17:43:21 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Medium haptic feedback
|
|
|
|
|
static void mediumHaptic() {
|
2026-06-10 18:40:58 +05:30
|
|
|
if (!isHapticEnabled) return;
|
2026-06-10 17:43:21 +05:30
|
|
|
HapticFeedback.mediumImpact();
|
2026-06-10 18:19:47 +05:30
|
|
|
_triggerVibration(60);
|
2026-06-10 17:43:21 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Heavy haptic feedback
|
|
|
|
|
static void heavyHaptic() {
|
2026-06-10 18:40:58 +05:30
|
|
|
if (!isHapticEnabled) return;
|
2026-06-10 17:43:21 +05:30
|
|
|
HapticFeedback.heavyImpact();
|
2026-06-10 18:19:47 +05:30
|
|
|
_triggerVibration(100);
|
2026-06-10 17:43:21 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Subtle haptic feedback (e.g. screen navigation)
|
|
|
|
|
static void subtleHaptic() {
|
2026-06-10 18:40:58 +05:30
|
|
|
if (!isHapticEnabled) return;
|
2026-06-10 17:43:21 +05:30
|
|
|
HapticFeedback.selectionClick();
|
2026-06-10 18:19:47 +05:30
|
|
|
_triggerVibration(20);
|
2026-06-10 17:43:21 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Double vibrate pattern using vibration package
|
|
|
|
|
static Future<void> _doubleVibrate() async {
|
2026-06-10 18:40:58 +05:30
|
|
|
if (!isHapticEnabled) return;
|
2026-06-10 17:43:21 +05:30
|
|
|
try {
|
|
|
|
|
if (!kIsWeb && await Vibration.hasVibrator() == true) {
|
|
|
|
|
// Double vibrate pattern: wait 0, vibrate 100ms, wait 100ms, vibrate 100ms
|
|
|
|
|
Vibration.vibrate(pattern: [0, 100, 100, 100]);
|
|
|
|
|
} else {
|
|
|
|
|
HapticFeedback.mediumImpact();
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
HapticFeedback.mediumImpact();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|