@@ -103,17 +103,23 @@ class _RemoteAccessPageState extends State<RemoteAccessPage> {
103103 final server = ServiceProvider .webServer;
104104 if (enabled) {
105105 final success = await server.startServer (port: _currentPort);
106+ if (! mounted) return ;
106107 if (success) {
107108 BlurSnackBar .show (context, '远程访问服务已启动' );
108109 _updateAccessUrls ();
109110 } else {
110- BlurSnackBar .show (context, '远程访问服务启动失败' );
111111 setState (() {
112112 _webServerEnabled = false ;
113+ _accessUrls = [];
114+ _publicIpUrl = null ;
113115 });
116+ _showStartServerErrorDialog (
117+ server.lastStartErrorMessage ?? '未知原因' ,
118+ );
114119 }
115120 } else {
116121 await server.stopServer ();
122+ if (! mounted) return ;
117123 BlurSnackBar .show (context, '远程访问服务已停止' );
118124 setState (() {
119125 _accessUrls = [];
@@ -145,6 +151,22 @@ class _RemoteAccessPageState extends State<RemoteAccessPage> {
145151 BlurSnackBar .show (context, '访问地址已复制到剪贴板' );
146152 }
147153
154+ void _showStartServerErrorDialog (String message) {
155+ final colorScheme = Theme .of (context).colorScheme;
156+ BlurDialog .show (
157+ context: context,
158+ title: '远程访问服务启动失败' ,
159+ content: message,
160+ actions: [
161+ HoverScaleTextButton (
162+ text: '确定' ,
163+ idleColor: colorScheme.onSurface,
164+ onPressed: () => Navigator .of (context).pop (),
165+ ),
166+ ],
167+ );
168+ }
169+
148170 void _showPortDialog () async {
149171 final colorScheme = Theme .of (context).colorScheme;
150172 final portController = TextEditingController (text: _currentPort.toString ());
@@ -190,12 +212,30 @@ class _RemoteAccessPageState extends State<RemoteAccessPage> {
190212 );
191213
192214 if (newPort != null && newPort != _currentPort) {
215+ final wasRunning = _webServerEnabled;
193216 setState (() {
194217 _currentPort = newPort;
195218 });
196- await ServiceProvider .webServer.setPort (newPort);
197- BlurSnackBar .show (context, '远程访问端口已更新,正在重启服务...' );
198- _updateAccessUrls ();
219+ final server = ServiceProvider .webServer;
220+ await server.setPort (newPort);
221+ if (! mounted) return ;
222+ if (wasRunning) {
223+ if (server.isRunning) {
224+ BlurSnackBar .show (context, '远程访问端口已更新,服务已重启' );
225+ _updateAccessUrls ();
226+ } else {
227+ setState (() {
228+ _webServerEnabled = false ;
229+ _accessUrls = [];
230+ _publicIpUrl = null ;
231+ });
232+ _showStartServerErrorDialog (
233+ server.lastStartErrorMessage ?? '未知原因' ,
234+ );
235+ }
236+ } else {
237+ BlurSnackBar .show (context, '远程访问端口已更新' );
238+ }
199239 }
200240 }
201241
@@ -276,30 +316,31 @@ class _RemoteAccessPageState extends State<RemoteAccessPage> {
276316 onChanged: _toggleAutoStart,
277317 ),
278318 ),
279-
319+
320+ const SizedBox (height: 8 ),
321+ Divider (color: colorScheme.onSurface.withOpacity (0.12 ), height: 1 ),
322+ const SizedBox (height: 8 ),
323+
280324 if (_webServerEnabled) ...[
281- const SizedBox (height: 8 ),
282- Divider (color: colorScheme.onSurface.withOpacity (0.12 ), height: 1 ),
283- const SizedBox (height: 8 ),
284-
285325 // 访问地址
286326 _buildAccessAddressSection (),
287-
327+
288328 const SizedBox (height: 8 ),
289329 Divider (color: colorScheme.onSurface.withOpacity (0.12 ), height: 1 ),
290330 const SizedBox (height: 8 ),
291-
292- // 端口设置
293- _buildSettingItem (
294- icon: Icons .settings_ethernet,
295- title: '端口设置' ,
296- subtitle: '当前端口: $_currentPort ' ,
297- trailing: IconButton (
298- icon: Icon (Icons .edit, color: colorScheme.onSurface),
299- onPressed: _showPortDialog,
300- ),
301- ),
302331 ],
332+
333+ // 端口设置
334+ _buildSettingItem (
335+ icon: Icons .settings_ethernet,
336+ title: '端口设置' ,
337+ subtitle: '当前端口: $_currentPort ' ,
338+ trailing: _HoverScaleIconButton (
339+ icon: Icons .edit,
340+ onPressed: _showPortDialog,
341+ idleColor: colorScheme.onSurface,
342+ ),
343+ ),
303344 ],
304345 );
305346 }
@@ -475,4 +516,57 @@ class _RemoteAccessPageState extends State<RemoteAccessPage> {
475516 ),
476517 );
477518 }
478- }
519+ }
520+
521+ class _HoverScaleIconButton extends StatefulWidget {
522+ final IconData icon;
523+ final VoidCallback onPressed;
524+ final Color ? idleColor;
525+ final Color hoverColor;
526+ final double size;
527+ final double hoverScale;
528+ final EdgeInsetsGeometry padding;
529+
530+ const _HoverScaleIconButton ({
531+ required this .icon,
532+ required this .onPressed,
533+ this .idleColor,
534+ this .hoverColor = const Color (0xFFFF2E55 ),
535+ this .size = 20 ,
536+ this .hoverScale = 1.1 ,
537+ this .padding = const EdgeInsets .all (6 ),
538+ });
539+
540+ @override
541+ State <_HoverScaleIconButton > createState () => _HoverScaleIconButtonState ();
542+ }
543+
544+ class _HoverScaleIconButtonState extends State <_HoverScaleIconButton > {
545+ bool _isHovered = false ;
546+
547+ @override
548+ Widget build (BuildContext context) {
549+ final baseColor =
550+ widget.idleColor ?? Theme .of (context).colorScheme.onSurface;
551+ final color = _isHovered ? widget.hoverColor : baseColor;
552+
553+ return MouseRegion (
554+ onEnter: (_) => setState (() => _isHovered = true ),
555+ onExit: (_) => setState (() => _isHovered = false ),
556+ cursor: SystemMouseCursors .click,
557+ child: GestureDetector (
558+ behavior: HitTestBehavior .opaque,
559+ onTap: widget.onPressed,
560+ child: AnimatedScale (
561+ scale: _isHovered ? widget.hoverScale : 1.0 ,
562+ duration: const Duration (milliseconds: 200 ),
563+ curve: Curves .easeOutBack,
564+ child: Padding (
565+ padding: widget.padding,
566+ child: Icon (widget.icon, size: widget.size, color: color),
567+ ),
568+ ),
569+ ),
570+ );
571+ }
572+ }
0 commit comments