Skip to content

Commit 97f9543

Browse files
committed
移除了手动匹配弹幕页面剧集列表的涟漪效果以匹配新UI。现在在开启远程访问之前就可以修改端口了,以防止端口冲突后无法开启远程访问。
1 parent a11781b commit 97f9543

File tree

3 files changed

+148
-22
lines changed

3 files changed

+148
-22
lines changed

lib/services/web_server_service.dart

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@ class WebServerService {
1717
int _port = 1180;
1818
bool _isRunning = false;
1919
bool _autoStart = false;
20+
String? _lastStartErrorMessage;
2021
final WebApiService _webApiService = WebApiService();
2122
final NipaPlayLanDiscoveryResponder _lanDiscoveryResponder =
2223
NipaPlayLanDiscoveryResponder();
2324

2425
bool get isRunning => _isRunning;
2526
int get port => _port;
2627
bool get autoStart => _autoStart;
28+
String? get lastStartErrorMessage => _lastStartErrorMessage;
2729

2830
Future<void> loadSettings() async {
2931
final prefs = await SharedPreferences.getInstance();
@@ -48,8 +50,34 @@ class WebServerService {
4850
await prefs.setInt(_portKey, _port);
4951
}
5052

53+
String _formatStartError(Object error) {
54+
if (error is SocketException) {
55+
final osError = error.osError;
56+
final errorCode = osError?.errorCode;
57+
final rawMessage = (osError?.message ?? error.message).trim();
58+
final lowerMessage = rawMessage.toLowerCase();
59+
if (errorCode == 48 ||
60+
errorCode == 98 ||
61+
errorCode == 10048 ||
62+
lowerMessage.contains('address already in use')) {
63+
return '端口 $_port 已被占用,请修改端口后重试。';
64+
}
65+
if (errorCode == 13 ||
66+
errorCode == 10013 ||
67+
lowerMessage.contains('permission denied') ||
68+
lowerMessage.contains('access is denied')) {
69+
return '没有权限绑定端口 $_port,请尝试 1024 以上端口或以更高权限运行。';
70+
}
71+
if (rawMessage.isNotEmpty) {
72+
return '无法监听端口 $_port:$rawMessage';
73+
}
74+
}
75+
return '远程访问服务启动失败:$error';
76+
}
77+
5178
Future<bool> startServer({int? port}) async {
5279
if (_isRunning) {
80+
_lastStartErrorMessage = null;
5381
print('Remote access server is already running.');
5482
return true;
5583
}
@@ -78,12 +106,15 @@ class WebServerService {
78106

79107
_server = await shelf_io.serve(handler, '0.0.0.0', _port);
80108
_isRunning = true;
109+
_lastStartErrorMessage = null;
81110
print('Remote access server started on port ${_server!.port}');
82111
await _lanDiscoveryResponder.start(webPort: _server!.port);
83112
await saveSettings();
84113
return true;
85114
} catch (e) {
86115
_isRunning = false;
116+
_server = null;
117+
_lastStartErrorMessage = _formatStartError(e);
87118
await _lanDiscoveryResponder.stop();
88119
return false;
89120
}

lib/themes/nipaplay/pages/settings/remote_access_page.dart

Lines changed: 116 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}

lib/themes/nipaplay/widgets/manual_danmaku_dialog.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -597,6 +597,7 @@ class _ManualDanmakuMatchDialogState extends State<ManualDanmakuMatchDialog>
597597
return Material(
598598
color: Colors.transparent,
599599
child: InkWell(
600+
splashFactory: NoSplash.splashFactory,
600601
onTap: () {
601602
setState(() {
602603
_selectedEpisode = episode;

0 commit comments

Comments
 (0)