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:
@@ -20,6 +20,9 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
|
||||
String? _successPath;
|
||||
String? _failurePath;
|
||||
String? _beepPath;
|
||||
String? _errorPath;
|
||||
String? _alertPath;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -35,12 +38,20 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
}
|
||||
|
||||
Future<void> _loadSoundPaths() async {
|
||||
final s = await _soundService.getSuccessPath();
|
||||
final f = await _soundService.getFailurePath();
|
||||
final results = await Future.wait([
|
||||
_soundService.getSuccessPath(),
|
||||
_soundService.getFailurePath(),
|
||||
_soundService.getBeepPath(),
|
||||
_soundService.getErrorPath(),
|
||||
_soundService.getAlertPath(),
|
||||
]);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_successPath = s;
|
||||
_failurePath = f;
|
||||
_successPath = results[0];
|
||||
_failurePath = results[1];
|
||||
_beepPath = results[2];
|
||||
_errorPath = results[3];
|
||||
_alertPath = results[4];
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -74,38 +85,66 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _pickSound(bool isSuccess) async {
|
||||
Future<void> _pickSound(String key) async {
|
||||
final result = await FilePicker.pickFiles(
|
||||
type: FileType.audio,
|
||||
);
|
||||
if (result != null && result.files.single.path != null) {
|
||||
final path = result.files.single.path!;
|
||||
if (isSuccess) {
|
||||
await _soundService.setSuccessPath(path);
|
||||
} else {
|
||||
await _soundService.setFailurePath(path);
|
||||
switch (key) {
|
||||
case 'success':
|
||||
await _soundService.setSuccessPath(path);
|
||||
case 'failure':
|
||||
await _soundService.setFailurePath(path);
|
||||
case 'beep':
|
||||
await _soundService.setBeepPath(path);
|
||||
case 'error':
|
||||
await _soundService.setErrorPath(path);
|
||||
case 'alert':
|
||||
await _soundService.setAlertPath(path);
|
||||
}
|
||||
setState(() {
|
||||
if (isSuccess) {
|
||||
_successPath = path;
|
||||
} else {
|
||||
_failurePath = path;
|
||||
switch (key) {
|
||||
case 'success':
|
||||
_successPath = path;
|
||||
case 'failure':
|
||||
_failurePath = path;
|
||||
case 'beep':
|
||||
_beepPath = path;
|
||||
case 'error':
|
||||
_errorPath = path;
|
||||
case 'alert':
|
||||
_alertPath = path;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _clearSound(bool isSuccess) async {
|
||||
if (isSuccess) {
|
||||
await _soundService.setSuccessPath(null);
|
||||
} else {
|
||||
await _soundService.setFailurePath(null);
|
||||
Future<void> _clearSound(String key) async {
|
||||
switch (key) {
|
||||
case 'success':
|
||||
await _soundService.setSuccessPath(null);
|
||||
case 'failure':
|
||||
await _soundService.setFailurePath(null);
|
||||
case 'beep':
|
||||
await _soundService.setBeepPath(null);
|
||||
case 'error':
|
||||
await _soundService.setErrorPath(null);
|
||||
case 'alert':
|
||||
await _soundService.setAlertPath(null);
|
||||
}
|
||||
setState(() {
|
||||
if (isSuccess) {
|
||||
_successPath = null;
|
||||
} else {
|
||||
_failurePath = null;
|
||||
switch (key) {
|
||||
case 'success':
|
||||
_successPath = null;
|
||||
case 'failure':
|
||||
_failurePath = null;
|
||||
case 'beep':
|
||||
_beepPath = null;
|
||||
case 'error':
|
||||
_errorPath = null;
|
||||
case 'alert':
|
||||
_alertPath = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -191,29 +230,67 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 上架成功铃声
|
||||
// 扫码提示音
|
||||
_buildSoundRow(
|
||||
label: '上架成功铃声',
|
||||
path: _successPath,
|
||||
onPick: () => _pickSound(true),
|
||||
onPreview: _successPath != null
|
||||
? () => _previewSound(_successPath!)
|
||||
label: '扫码提示音',
|
||||
path: _beepPath,
|
||||
onPick: () => _pickSound('beep'),
|
||||
onPreview: _beepPath != null
|
||||
? () => _previewSound(_beepPath!)
|
||||
: null,
|
||||
onClear: _successPath != null ? () => _clearSound(true) : null,
|
||||
onClear: _beepPath != null ? () => _clearSound('beep') : null,
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 上架失败铃声
|
||||
// 成功铃声
|
||||
_buildSoundRow(
|
||||
label: '上架失败铃声',
|
||||
label: '成功铃声',
|
||||
path: _successPath,
|
||||
onPick: () => _pickSound('success'),
|
||||
onPreview: _successPath != null
|
||||
? () => _previewSound(_successPath!)
|
||||
: null,
|
||||
onClear: _successPath != null ? () => _clearSound('success') : null,
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 失败铃声
|
||||
_buildSoundRow(
|
||||
label: '失败铃声',
|
||||
path: _failurePath,
|
||||
onPick: () => _pickSound(false),
|
||||
onPick: () => _pickSound('failure'),
|
||||
onPreview: _failurePath != null
|
||||
? () => _previewSound(_failurePath!)
|
||||
: null,
|
||||
onClear:
|
||||
_failurePath != null ? () => _clearSound(false) : null,
|
||||
onClear: _failurePath != null ? () => _clearSound('failure') : null,
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 错误铃声(无效码)
|
||||
_buildSoundRow(
|
||||
label: '错误铃声(无效码)',
|
||||
path: _errorPath,
|
||||
onPick: () => _pickSound('error'),
|
||||
onPreview: _errorPath != null
|
||||
? () => _previewSound(_errorPath!)
|
||||
: null,
|
||||
onClear: _errorPath != null ? () => _clearSound('error') : null,
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 警报铃声(重复上架)
|
||||
_buildSoundRow(
|
||||
label: '警报铃声(重复上架)',
|
||||
path: _alertPath,
|
||||
onPick: () => _pickSound('alert'),
|
||||
onPreview: _alertPath != null
|
||||
? () => _previewSound(_alertPath!)
|
||||
: null,
|
||||
onClear: _alertPath != null ? () => _clearSound('alert') : null,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user