From bca1288bd1ba2413ad5f0ae948c4ba81d56ed50b Mon Sep 17 00:00:00 2001 From: lxh2875931338 Date: Thu, 27 Aug 2026 21:52:45 +0800 Subject: [PATCH] =?UTF-8?q?=E8=BF=BD=E5=8A=A0webdav=E9=87=8D=E6=96=B0?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E7=9A=84button=EF=BC=8C=E8=BF=98=E5=9C=A8?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E8=BE=B9=E7=95=8C=E6=83=85=E5=86=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/base/base_state.dart | 63 ++++++++++++++++++++++++++ lib/pages/home_page.dart | 68 +++++++++++++++++++++++++++- lib/pages/player_page.dart | 3 +- lib/pages/webdav_file_list_page.dart | 27 +++++++---- lib/pages/webdav_setup_page.dart | 3 +- 5 files changed, 151 insertions(+), 13 deletions(-) create mode 100644 lib/base/base_state.dart diff --git a/lib/base/base_state.dart b/lib/base/base_state.dart new file mode 100644 index 0000000..2f34ed3 --- /dev/null +++ b/lib/base/base_state.dart @@ -0,0 +1,63 @@ +// lib/base/base_state.dart +import 'package:flutter/material.dart'; + +/// 所有页面的基类,提供安全的 setState 封装 +/// +/// 使用方式: +/// 1. 将页面的 State 类从 `State` 改为 `BaseState` +/// 2. 将所有 `setState(() { ... })` 替换为 `safeSetState(() { ... })` +abstract class BaseState extends State { + /// 安全的 setState,自动检查 mounted + /// + /// 用法: + /// ```dart + /// safeSetState(() { + /// _isLoading = false; + /// }); + /// ``` + void safeSetState(VoidCallback fn) { + if (mounted) { + setState(fn); + } + } + + /// 安全执行异步操作,完成后自动刷新 + /// + /// 用法: + /// ```dart + /// await safeAsync( + /// () => _loadData(), // 异步操作 + /// () => _isLoading = false, // 完成后执行的 setState + /// ); + /// ``` + Future safeAsync( + Future Function() action, + VoidCallback onComplete, { + VoidCallback? onError, + }) async { + try { + await action(); + if (mounted) { + setState(onComplete); + } + } catch (e) { + debugPrint('⚠️ [BaseState] safeAsync error: $e'); + if (mounted) { + setState(onComplete); + } + onError?.call(); + } + } + + /// 显示 SnackBar(封装,确保 mounted) + void showSafeSnackBar(String message, {Color? backgroundColor}) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + backgroundColor: backgroundColor ?? Colors.grey, + duration: const Duration(seconds: 2), + ), + ); + } +} diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index e6a3b53..5e1584b 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -7,6 +7,7 @@ import '../widgets/magnetic_scroll_physics.dart'; import 'webdav_setup_page.dart'; import 'webdav_file_list_page.dart'; import '../constants/ui_constants.dart'; +import '../base/base_state.dart'; // ================================================== // 歌曲数据模型 @@ -149,7 +150,7 @@ class HomePage extends StatefulWidget { State createState() => _HomePageState(); } -class _HomePageState extends State { +class _HomePageState extends BaseState { List _favorites = []; bool _isLoading = true; @@ -282,6 +283,52 @@ class _HomePageState extends State { } } + // ⭐ 退出/重新登录对话框 + void _showReauthDialog() { + showDialog( + context: context, + builder: (context) => AlertDialog( + backgroundColor: const Color(0xFF1A1F1E), + title: const Text( + '重新登录 WebDAV', + style: TextStyle(color: Colors.white), + ), + content: const Text( + '退出当前账号并重新登录?', + style: TextStyle(color: Colors.grey), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('取消'), + ), + TextButton( + onPressed: () async { + Navigator.pop(context); + await WebDAVService.instance.clearCredentials(); + setState(() { + _favorites = []; + _isLoading = false; + }); + if (mounted) { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (_) => const WebDAVSetupPage(), + ), + ); + } + }, + child: const Text( + '重新登录', + style: TextStyle(color: Colors.redAccent), + ), + ), + ], + ), + ); + } + void _resetSnapState() { if (_scrollController.hasClients) { _scrollController.jumpTo(0.0); @@ -413,6 +460,25 @@ class _HomePageState extends State { fontWeight: FontWeight.w400, ), ), + // ⭐ 退出按钮(已连接时显示) + if (isConnected) ...[ + const Spacer(), + IconButton( + icon: const Icon( + Icons.logout, + color: Colors.grey, + size: 18, + ), + tooltip: '重新登录', + onPressed: _showReauthDialog, + padding: EdgeInsets.zero, + constraints: const BoxConstraints( + minWidth: 32, + minHeight: 32, + ), + ), + const SizedBox(width: 4), + ], ], ), const SizedBox(height: 4), diff --git a/lib/pages/player_page.dart b/lib/pages/player_page.dart index b8eeabc..0d051b7 100644 --- a/lib/pages/player_page.dart +++ b/lib/pages/player_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../services/audio_service.dart'; +import '../base/base_state.dart'; class PlayerPage extends StatefulWidget { final VoidCallback? onClose; @@ -17,7 +18,7 @@ class PlayerPage extends StatefulWidget { State createState() => _PlayerPageState(); } -class _PlayerPageState extends State { +class _PlayerPageState extends BaseState { double? _dragProgress; String _formatDuration(Duration d) { diff --git a/lib/pages/webdav_file_list_page.dart b/lib/pages/webdav_file_list_page.dart index 50c7cbd..b5677ef 100644 --- a/lib/pages/webdav_file_list_page.dart +++ b/lib/pages/webdav_file_list_page.dart @@ -7,6 +7,7 @@ import '../services/webdav_service.dart'; import '../services/playback_service.dart'; import '../services/audio_service.dart'; import '../constants/ui_constants.dart'; +import '../base/base_state.dart'; class WebDAVFileListPage extends StatefulWidget { final String currentPath; @@ -17,7 +18,7 @@ class WebDAVFileListPage extends StatefulWidget { State createState() => _WebDAVFileListPageState(); } -class _WebDAVFileListPageState extends State { +class _WebDAVFileListPageState extends BaseState { List _items = []; bool _isLoading = true; String _errorMessage = ''; @@ -43,7 +44,7 @@ class _WebDAVFileListPageState extends State { // 加载目录 // ============================================================ Future _loadDirectory() async { - setState(() { + safeSetState(() { _isLoading = true; _errorMessage = ''; }); @@ -51,15 +52,21 @@ class _WebDAVFileListPageState extends State { try { final items = await WebDAVService.instance.listDirectory(path: _currentPath); - setState(() { - _items = items; - _isLoading = false; - }); + if (mounted) { + // ⭐ 加这个 + setState(() { + _items = items; + _isLoading = false; + }); + } } catch (e) { - setState(() { - _errorMessage = '加载失败: $e'; - _isLoading = false; - }); + if (mounted) { + // ⭐ 加这个 + setState(() { + _errorMessage = '加载失败: $e'; + _isLoading = false; + }); + } } } diff --git a/lib/pages/webdav_setup_page.dart b/lib/pages/webdav_setup_page.dart index bfeadb7..bd14ae5 100644 --- a/lib/pages/webdav_setup_page.dart +++ b/lib/pages/webdav_setup_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import '../services/webdav_service.dart'; import 'webdav_file_list_page.dart'; +import '../base/base_state.dart'; class WebDAVSetupPage extends StatefulWidget { const WebDAVSetupPage({super.key}); @@ -9,7 +10,7 @@ class WebDAVSetupPage extends StatefulWidget { State createState() => _WebDAVSetupPageState(); } -class _WebDAVSetupPageState extends State { +class _WebDAVSetupPageState extends BaseState { final _formKey = GlobalKey(); final _baseUrlController = TextEditingController(); final _usernameController = TextEditingController();