refactor: unify error feedback system with FeedbackService and StatusBar widget
- Create FeedbackService as centralized sound/vibration dispatcher - Create reusable StatusBar widget with StatusDotColor enum (blue/orange/green/red/yellow/amber) - Extend SoundService with beep/error/alert sound types and loop playback - Remove top feedback banners from both registration and boxing pages - Route all feedback through bottom status bar per PRD requirements - Add sound and vibration feedback to boxing module (was completely missing) - Handle network errors with yellow status, general errors with red - Enhance duplicate dialog with alert loop sound that stops on close - Add beep/error/alert sound file selectors in settings page Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,38 +1,64 @@
|
||||
import 'dart:async';
|
||||
import 'package:audioplayers/audioplayers.dart';
|
||||
import 'package:pad_scanner/services/app_config_service.dart';
|
||||
|
||||
class SoundService {
|
||||
static const _keySuccess = 'sound_success';
|
||||
static const _keyFailure = 'sound_failure';
|
||||
static const _keyBeep = 'sound_beep';
|
||||
static const _keyError = 'sound_error';
|
||||
static const _keyAlert = 'sound_alert';
|
||||
|
||||
final _player = AudioPlayer();
|
||||
final _alertPlayer = AudioPlayer();
|
||||
final _configService = AppConfigService();
|
||||
|
||||
Future<String?> getSuccessPath() async {
|
||||
return _configService.getString(_keySuccess);
|
||||
}
|
||||
Timer? _alertTimer;
|
||||
|
||||
Future<String?> getFailurePath() async {
|
||||
return _configService.getString(_keyFailure);
|
||||
}
|
||||
// --- Path getters ---
|
||||
|
||||
Future<void> setSuccessPath(String? path) async {
|
||||
Future<String?> getSuccessPath() =>
|
||||
_configService.getString(_keySuccess);
|
||||
|
||||
Future<String?> getFailurePath() =>
|
||||
_configService.getString(_keyFailure);
|
||||
|
||||
Future<String?> getBeepPath() =>
|
||||
_configService.getString(_keyBeep);
|
||||
|
||||
Future<String?> getErrorPath() =>
|
||||
_configService.getString(_keyError);
|
||||
|
||||
Future<String?> getAlertPath() =>
|
||||
_configService.getString(_keyAlert);
|
||||
|
||||
// --- Path setters ---
|
||||
|
||||
Future<void> setSuccessPath(String? path) =>
|
||||
_setConfigPath(_keySuccess, path);
|
||||
|
||||
Future<void> setFailurePath(String? path) =>
|
||||
_setConfigPath(_keyFailure, path);
|
||||
|
||||
Future<void> setBeepPath(String? path) =>
|
||||
_setConfigPath(_keyBeep, path);
|
||||
|
||||
Future<void> setErrorPath(String? path) =>
|
||||
_setConfigPath(_keyError, path);
|
||||
|
||||
Future<void> setAlertPath(String? path) =>
|
||||
_setConfigPath(_keyAlert, path);
|
||||
|
||||
Future<void> _setConfigPath(String key, String? path) async {
|
||||
if (path != null) {
|
||||
await _configService.setString(_keySuccess, path);
|
||||
await _configService.setString(key, path);
|
||||
} else {
|
||||
final config = await _configService.loadConfig();
|
||||
config.remove(_keySuccess);
|
||||
config.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setFailurePath(String? path) async {
|
||||
if (path != null) {
|
||||
await _configService.setString(_keyFailure, path);
|
||||
} else {
|
||||
final config = await _configService.loadConfig();
|
||||
config.remove(_keyFailure);
|
||||
}
|
||||
}
|
||||
// --- Playback methods ---
|
||||
|
||||
Future<void> playSuccess() async {
|
||||
final path = await getSuccessPath();
|
||||
@@ -48,11 +74,55 @@ class SoundService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> playBeep() async {
|
||||
final path = await getBeepPath();
|
||||
if (path != null) {
|
||||
await _player.play(DeviceFileSource(path));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> playError() async {
|
||||
final path = await getErrorPath();
|
||||
if (path != null) {
|
||||
await _player.play(DeviceFileSource(path));
|
||||
}
|
||||
}
|
||||
|
||||
/// Play alert sound in a loop until [stopAlert] is called.
|
||||
/// Loops up to 10 times as a safety guard (~15 seconds max).
|
||||
Future<void> playAlertLoop() async {
|
||||
final path = await getAlertPath();
|
||||
if (path == null) return;
|
||||
|
||||
_alertTimer?.cancel();
|
||||
var count = 0;
|
||||
_alertTimer = Timer.periodic(const Duration(milliseconds: 1500), (_) async {
|
||||
if (count >= 10) {
|
||||
_alertTimer?.cancel();
|
||||
return;
|
||||
}
|
||||
await _alertPlayer.play(DeviceFileSource(path));
|
||||
count++;
|
||||
});
|
||||
// Play immediately first time
|
||||
await _alertPlayer.play(DeviceFileSource(path));
|
||||
count++;
|
||||
}
|
||||
|
||||
/// Stop the alert loop.
|
||||
void stopAlert() {
|
||||
_alertTimer?.cancel();
|
||||
_alertTimer = null;
|
||||
_alertPlayer.stop();
|
||||
}
|
||||
|
||||
Future<void> play(String path) async {
|
||||
await _player.play(DeviceFileSource(path));
|
||||
}
|
||||
|
||||
Future<void> dispose() async {
|
||||
_alertTimer?.cancel();
|
||||
await _player.dispose();
|
||||
await _alertPlayer.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user