feat: add sound and vibration feedback for registration actions

Integrate audioplayers, vibration, and file_picker packages to provide
audio/haptic feedback on registration success/failure. Add sound
settings UI in SettingsPage for customizing notification sounds.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Misaka
2026-05-11 20:13:03 +08:00
parent 28006f19a5
commit 230e6ee07a
11 changed files with 596 additions and 7 deletions

View File

@@ -1,8 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:vibration/vibration.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:pad_scanner/services/scanner_service.dart';
import 'package:pad_scanner/services/code_parser.dart';
import 'package:pad_scanner/services/api_service.dart';
import 'package:pad_scanner/services/sound_service.dart';
import 'package:pad_scanner/pages/settings_page.dart';
class RegistrationPage extends StatefulWidget {
@@ -15,6 +18,8 @@ class RegistrationPage extends StatefulWidget {
class _RegistrationPageState extends State<RegistrationPage> {
final _scannerService = ScannerService();
final _apiService = ApiService();
final _soundService = SoundService();
final _focusNode = FocusNode();
final _zongpaiNos = <String>[];
String? _locationCode;
@@ -30,6 +35,23 @@ class _RegistrationPageState extends State<RegistrationPage> {
void initState() {
super.initState();
_scannerService.scanResults.listen(_onScan);
WidgetsBinding.instance.addPostFrameCallback((_) {
_focusNode.requestFocus();
});
}
@override
void dispose() {
_focusNode.dispose();
_soundService.dispose();
super.dispose();
}
void _onKeyEvent(KeyEvent event) {
if (event is KeyDownEvent &&
event.logicalKey == LogicalKeyboardKey.enter) {
_submit();
}
}
void _onScan(ScanResult result) {
@@ -137,6 +159,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
setState(() => _isSubmitting = false);
if (result.success) {
_vibrateSuccess();
setState(() {
_zongpaiNos.remove(zongpaiNo);
if (!_isLocked) {
@@ -151,8 +174,10 @@ class _RegistrationPageState extends State<RegistrationPage> {
if (mounted) setState(() => _successMessage = null);
});
} else if (result.isDuplicate) {
_vibrateError();
_showDuplicateDialog(zongpaiNo, result.duplicateInfo);
} else {
_vibrateError();
_showFeedback(result.errorMessage ?? '提交失败', isError: true);
}
}
@@ -193,12 +218,24 @@ class _RegistrationPageState extends State<RegistrationPage> {
});
if (failed.isEmpty) {
_vibrateSuccess();
_showFeedback('批量上架成功($successCount 条)', isError: false);
} else {
_vibrateError();
_showFeedback('成功 $successCount 条,失败 ${failed.length}', isError: true);
}
}
void _vibrateSuccess() {
Vibration.vibrate(duration: 200);
_soundService.playSuccess();
}
void _vibrateError() {
Vibration.vibrate(duration: 1000);
_soundService.playFailure();
}
void _showDuplicateDialog(String zongpaiNo, Map<String, dynamic>? info) {
showDialog(
context: context,
@@ -266,8 +303,11 @@ class _RegistrationPageState extends State<RegistrationPage> {
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Scaffold(
appBar: AppBar(
return KeyboardListener(
focusNode: _focusNode,
onKeyEvent: _onKeyEvent,
child: Scaffold(
appBar: AppBar(
title: const Text('上架登记'),
actions: [
Padding(
@@ -551,7 +591,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
: Text(
_isLocked && _zongpaiNos.length > 1
? '批量上架(${_zongpaiNos.length} 条)'
: ' 认 上 架',
: '认上架(P1)',
style: const TextStyle(fontSize: 18),
),
),
@@ -593,6 +633,7 @@ class _RegistrationPageState extends State<RegistrationPage> {
),
],
),
),
);
}
}