import 'package:flutter/material.dart'; import 'package:pad_scanner/services/app_config_service.dart'; import 'package:file_picker/file_picker.dart'; import 'package:pad_scanner/services/api_service.dart'; import 'package:pad_scanner/services/sound_service.dart'; class SettingsPage extends StatefulWidget { const SettingsPage({super.key}); @override State createState() => _SettingsPageState(); } class _SettingsPageState extends State { final _controller = TextEditingController(); final _apiService = ApiService(); final _soundService = SoundService(); bool _saving = false; bool _testing = false; String? _successPath; String? _failurePath; String? _beepPath; String? _errorPath; String? _alertPath; @override void initState() { super.initState(); _loadUrl(); _loadSoundPaths(); } Future _loadUrl() async { final configService = AppConfigService(); final url = await configService.getString('api_url') ?? ''; _controller.text = url; } Future _loadSoundPaths() async { final results = await Future.wait([ _soundService.getSuccessPath(), _soundService.getFailurePath(), _soundService.getBeepPath(), _soundService.getErrorPath(), _soundService.getAlertPath(), ]); if (mounted) { setState(() { _successPath = results[0]; _failurePath = results[1]; _beepPath = results[2]; _errorPath = results[3]; _alertPath = results[4]; }); } } Future _saveUrl() async { final url = _controller.text.trim().replaceAll(RegExp(r'/+$'), ''); if (url.isEmpty) { _showSnackBar('请输入 API 地址', isError: true); return; } setState(() => _saving = true); // Load config once, apply all changes, then save once to avoid race conditions final configService = AppConfigService(); final config = await configService.loadConfig(); config['api_url'] = url; if (_successPath != null) config['sound_success'] = _successPath; else config.remove('sound_success'); if (_failurePath != null) config['sound_failure'] = _failurePath; else config.remove('sound_failure'); if (_beepPath != null) config['sound_beep'] = _beepPath; else config.remove('sound_beep'); if (_errorPath != null) config['sound_error'] = _errorPath; else config.remove('sound_error'); if (_alertPath != null) config['sound_alert'] = _alertPath; else config.remove('sound_alert'); await configService.saveConfig(config); setState(() => _saving = false); if (mounted) { _showSnackBar('设置已保存'); } } Future _testConnection() async { final url = _controller.text.trim(); if (url.isEmpty) { _showSnackBar('请先输入 API 地址', isError: true); return; } setState(() => _testing = true); final ok = await _apiService.testConnection(url); setState(() => _testing = false); if (mounted) { _showSnackBar(ok ? '连接成功' : '连接失败,请检查地址和网络', isError: !ok); } } Future _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!; setState(() { switch (key) { case 'success': _successPath = path; case 'failure': _failurePath = path; case 'beep': _beepPath = path; case 'error': _errorPath = path; case 'alert': _alertPath = path; } }); } } Future _clearSound(String key) async { setState(() { switch (key) { case 'success': _successPath = null; case 'failure': _failurePath = null; case 'beep': _beepPath = null; case 'error': _errorPath = null; case 'alert': _alertPath = null; } }); } Future _previewSound(String path) async { await _soundService.play(path); } String _fileName(String? path) { if (path == null) return '未设置'; return path.split('/').last; } void _showSnackBar(String message, {bool isError = false}) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(message), backgroundColor: isError ? Colors.red.shade700 : Colors.green.shade700, duration: const Duration(seconds: 2), ), ); } @override void dispose() { _controller.dispose(); _soundService.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('设置')), body: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // ---- API 地址 ---- const Text('API 服务器地址', style: TextStyle(fontSize: 14)), const SizedBox(height: 8), TextField( controller: _controller, decoration: const InputDecoration( hintText: 'http://192.168.1.100:8000', border: OutlineInputBorder(), ), keyboardType: TextInputType.url, ), const SizedBox(height: 20), ElevatedButton.icon( onPressed: _saving ? null : _saveUrl, icon: _saving ? const SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.save), label: const Text('保存设置'), ), const SizedBox(height: 8), OutlinedButton.icon( onPressed: _testing ? null : _testConnection, icon: _testing ? const SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.wifi), label: const Text('测试连接'), ), const SizedBox(height: 24), // ---- 铃声设置 ---- const Divider(), const SizedBox(height: 8), const Text('铃声设置', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), const SizedBox(height: 16), // 扫码提示音 _buildSoundRow( label: '扫码提示音', path: _beepPath, onPick: () => _pickSound('beep'), onPreview: _beepPath != null ? () => _previewSound(_beepPath!) : null, onClear: _beepPath != null ? () => _clearSound('beep') : null, ), const SizedBox(height: 12), // 成功铃声 _buildSoundRow( 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('failure'), onPreview: _failurePath != null ? () => _previewSound(_failurePath!) : 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, ), ], ), ), ); } Widget _buildSoundRow({ required String label, required String? path, required VoidCallback onPick, required VoidCallback? onPreview, required VoidCallback? onClear, }) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: const TextStyle(fontSize: 14)), const SizedBox(height: 6), Row( children: [ Expanded( child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration( border: Border.all(color: Colors.grey.shade400), borderRadius: BorderRadius.circular(8), ), child: Text( _fileName(path), style: TextStyle( fontSize: 13, color: path != null ? Colors.black87 : Colors.grey, ), overflow: TextOverflow.ellipsis, ), ), ), const SizedBox(width: 8), IconButton( icon: const Icon(Icons.folder_open), tooltip: '选择文件', onPressed: onPick, ), if (onPreview != null) IconButton( icon: const Icon(Icons.play_arrow), tooltip: '试听', onPressed: onPreview, ), if (onClear != null) IconButton( icon: const Icon(Icons.clear, size: 20), tooltip: '清除', onPressed: onClear, ), ], ), ], ); } }