fix(settings): save all config atomically and strip trailing slash from API URL

- Save API URL and sound paths in a single load-modify-write cycle to
  prevent race conditions that wiped previously saved config
- Strip trailing slashes from API URL to avoid double-slash 404 errors
- Make AppConfigService.saveConfig public for batch config writes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Misaka_Company
2026-05-13 16:58:39 +08:00
parent 42a9cc7204
commit 077a1ab1b9
2 changed files with 17 additions and 28 deletions

View File

@@ -57,14 +57,27 @@ class _SettingsPageState extends State<SettingsPage> {
} }
Future<void> _saveUrl() async { Future<void> _saveUrl() async {
final url = _controller.text.trim(); final url = _controller.text.trim().replaceAll(RegExp(r'/+$'), '');
if (url.isEmpty) { if (url.isEmpty) {
_showSnackBar('请输入 API 地址', isError: true); _showSnackBar('请输入 API 地址', isError: true);
return; return;
} }
setState(() => _saving = true); setState(() => _saving = true);
// Load config once, apply all changes, then save once to avoid race conditions
final configService = AppConfigService(); final configService = AppConfigService();
await configService.setString('api_url', url); 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); setState(() => _saving = false);
if (mounted) { if (mounted) {
_showSnackBar('设置已保存'); _showSnackBar('设置已保存');
@@ -91,18 +104,6 @@ class _SettingsPageState extends State<SettingsPage> {
); );
if (result != null && result.files.single.path != null) { if (result != null && result.files.single.path != null) {
final path = result.files.single.path!; final path = result.files.single.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(() { setState(() {
switch (key) { switch (key) {
case 'success': case 'success':
@@ -121,18 +122,6 @@ class _SettingsPageState extends State<SettingsPage> {
} }
Future<void> _clearSound(String key) async { 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(() { setState(() {
switch (key) { switch (key) {
case 'success': case 'success':

View File

@@ -38,11 +38,11 @@ class AppConfigService {
Future<void> setString(String key, String value) async { Future<void> setString(String key, String value) async {
final config = await loadConfig(); final config = await loadConfig();
config[key.toString()] = value.toString(); config[key.toString()] = value.toString();
await _saveConfig(config); await saveConfig(config);
} }
/// 完整写入 JSON 文件 /// 完整写入 JSON 文件
Future<void> _saveConfig(Map<String, dynamic> config) async { Future<void> saveConfig(Map<String, dynamic> config) async {
try { try {
final file = File(await _filePath); final file = File(await _filePath);
await file.writeAsString(jsonEncode(config)); await file.writeAsString(jsonEncode(config));