import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:forui/forui.dart'; import '../constants/breakpoints.dart'; import 'icon_picker_dialog.dart'; import 'app_inline_alert.dart'; import 'app_loading_ring.dart'; import 'server_avatar.dart'; import 'setting_select.dart'; class ConnectionForm extends StatefulWidget { final String mode; final String? initialUrl; final String? initialUsername; final String? initialPassword; final String? initialDisplayName; final String? initialIconUrl; final void Function( String url, String username, String password, String? displayName, String iconUrl, ) onConnect; final VoidCallback? onCancel; final bool loading; final String? error; const ConnectionForm({ super.key, this.mode = 'add', this.initialUrl, this.initialUsername, this.initialPassword, this.initialDisplayName, this.initialIconUrl, required this.onConnect, this.onCancel, this.loading = false, this.error, }); @override State createState() => _ConnectionFormState(); } class _ConnectionFormState extends State { final _formKey = GlobalKey(); late String _protocol; late final TextEditingController _host; late final TextEditingController _port; late final TextEditingController _displayName; late final TextEditingController _username; late final TextEditingController _password; bool _obscurePassword = true; late String _iconUrl; @override void initState() { super.initState(); final parsed = _parseUrl(widget.initialUrl ?? ''); _protocol = parsed['protocol'] ?? 'https'; _host = TextEditingController(text: parsed['host'] ?? ''); _port = TextEditingController(text: parsed['port'] ?? ''); _displayName = TextEditingController(text: widget.initialDisplayName ?? ''); _username = TextEditingController(text: widget.initialUsername ?? ''); _password = TextEditingController(text: widget.initialPassword ?? ''); _iconUrl = widget.initialIconUrl ?? ''; } @override void dispose() { _host.dispose(); _port.dispose(); _displayName.dispose(); _username.dispose(); _password.dispose(); super.dispose(); } static Map _parseUrl(String input) { final trimmed = input.trim(); if (trimmed.isEmpty) return {'protocol': 'https', 'host': '', 'port': null}; try { final toParse = trimmed.contains('://') ? trimmed : 'https://$trimmed'; final uri = Uri.parse(toParse); final scheme = (uri.scheme == 'http' || uri.scheme == 'https') ? uri.scheme : 'https'; return { 'protocol': scheme, 'host': uri.host.isEmpty ? trimmed : uri.host, 'port': uri.hasPort ? uri.port.toString() : null, }; } catch (_) { return {'protocol': 'https', 'host': trimmed, 'port': null}; } } void _syncFromUrl(String value) { if (!value.contains('://')) return; final parsed = _parseUrl(value); final host = parsed['host'] ?? ''; if (host.isEmpty || host == value.trim()) return; setState(() { if (parsed['protocol'] != null) _protocol = parsed['protocol']!; }); WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; _host.text = host; _host.selection = TextSelection.fromPosition( TextPosition(offset: host.length), ); _port.text = parsed['port'] ?? ''; }); } String _buildUrl() { final host = _host.text.trim(); final port = _port.text.trim(); return port.isNotEmpty ? '$_protocol://$host:$port' : '$_protocol://$host'; } void _submit() { if (!_formKey.currentState!.validate()) return; final url = _buildUrl(); final displayName = _displayName.text.trim(); widget.onConnect( url, _username.text.trim(), _password.text, displayName.isEmpty ? null : displayName, _iconUrl, ); } Future _pickIcon() async { if (widget.loading) return; final result = await showIconPickerDialog( context, currentIconUrl: _iconUrl, ); if (result == null) return; if (!mounted) return; setState(() => _iconUrl = result); } @override Widget build(BuildContext context) { final theme = Theme.of(context); final isEdit = widget.mode == 'edit'; final isCompact = Breakpoints.isCompact(MediaQuery.sizeOf(context).width); final protocolTextStyle = theme.textTheme.bodyLarge?.copyWith( color: theme.colorScheme.onSurface, fontWeight: FontWeight.w600, ); return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text(isEdit ? '编辑服务器' : '添加服务器', style: theme.textTheme.titleLarge), const SizedBox(height: 4), Text( isEdit ? '修改 Emby 服务器信息并重新登录' : '输入 Emby 服务器信息以连接', style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 20), Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ if (isCompact) _buildCompactEndpointFields(protocolTextStyle) else _buildDesktopEndpointFields(protocolTextStyle), const SizedBox(height: 10), _DisplayNameWithAvatar( iconUrl: _iconUrl, serverName: _displayName.text.trim().isNotEmpty ? _displayName.text.trim() : (_host.text.trim().isNotEmpty ? _host.text.trim() : 'S'), controller: _displayName, enabled: !widget.loading, onPick: _pickIcon, ), const SizedBox(height: 10), TextFormField( controller: _username, decoration: const InputDecoration(labelText: '用户名'), enabled: !widget.loading, autofillHints: const [AutofillHints.username], textInputAction: TextInputAction.next, autocorrect: false, enableSuggestions: false, validator: (v) => (v == null || v.trim().isEmpty) ? '用户名不能为空' : null, ), const SizedBox(height: 10), TextFormField( controller: _password, decoration: InputDecoration( labelText: '密码', suffixIcon: IconButton( icon: Icon( _obscurePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined, ), onPressed: widget.loading ? null : () => setState( () => _obscurePassword = !_obscurePassword, ), ), ), enabled: !widget.loading, obscureText: _obscurePassword, autofillHints: const [AutofillHints.password], textInputAction: TextInputAction.done, autocorrect: false, enableSuggestions: false, onFieldSubmitted: (_) { if (!widget.loading) _submit(); }, ), if (widget.error != null) ...[ const SizedBox(height: 12), AppInlineAlert(message: widget.error!), ], const SizedBox(height: 20), _buildActions(isCompact), ], ), ), ], ); } Widget _buildDesktopEndpointFields(TextStyle? protocolTextStyle) { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 124, child: _ProtocolSelect( value: _protocol, enabled: !widget.loading, onChanged: (v) { if (v != null) setState(() => _protocol = v); }, ), ), const SizedBox(width: 8), Expanded(child: _buildHostField()), const SizedBox(width: 8), SizedBox(width: 80, child: _buildPortField()), ], ); } Widget _buildCompactEndpointFields(TextStyle? protocolTextStyle) { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _buildHostField(), const SizedBox(height: 10), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( flex: 5, child: _ProtocolSelect( value: _protocol, enabled: !widget.loading, onChanged: (v) { if (v != null) setState(() => _protocol = v); }, ), ), const SizedBox(width: 12), Expanded(flex: 6, child: _buildPortField()), ], ), ], ); } Widget _buildHostField() { return Focus( onFocusChange: (hasFocus) { if (!hasFocus) _syncFromUrl(_host.text); }, child: TextFormField( controller: _host, decoration: const InputDecoration( labelText: '服务器地址', hintText: 'example.com', ), enabled: !widget.loading, keyboardType: TextInputType.url, textInputAction: TextInputAction.next, autocorrect: false, enableSuggestions: false, onChanged: _syncFromUrl, validator: (v) => (v == null || v.trim().isEmpty) ? '服务器地址不能为空' : null, ), ); } Widget _buildPortField() { return TextFormField( controller: _port, decoration: const InputDecoration(labelText: '端口', hintText: '443'), enabled: !widget.loading, keyboardType: TextInputType.number, textInputAction: TextInputAction.next, autocorrect: false, enableSuggestions: false, inputFormatters: [FilteringTextInputFormatter.digitsOnly], validator: (v) { if (v == null || v.trim().isEmpty) return null; final n = int.tryParse(v.trim()); if (n == null || n < 1 || n > 65535) return '1-65535'; return null; }, ); } Widget _buildActions(bool isCompact) { final saveButton = FButton( variant: FButtonVariant.primary, onPress: widget.loading ? null : _submit, child: widget.loading ? const AppLoadingRing(size: 16, color: Colors.white) : const Text('保存'), ); if (!isCompact) { return Row( mainAxisAlignment: MainAxisAlignment.end, children: [ if (widget.onCancel != null) FButton( variant: FButtonVariant.outline, onPress: widget.loading ? null : widget.onCancel, child: const Text('取消'), ), const SizedBox(width: 8), saveButton, ], ); } return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ SizedBox(height: 48, child: saveButton), if (widget.onCancel != null) ...[ const SizedBox(height: 8), SizedBox( height: 44, child: FButton( variant: FButtonVariant.outline, onPress: widget.loading ? null : widget.onCancel, child: const Text('取消'), ), ), ], ], ); } } class _ProtocolSelect extends StatelessWidget { final String value; final bool enabled; final ValueChanged onChanged; const _ProtocolSelect({ required this.value, required this.enabled, required this.onChanged, }); static const _protocols = ['https', 'http']; @override Widget build(BuildContext context) { final theme = Theme.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( '协议', style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 6), SettingSelect( value: value, enabled: enabled, options: _protocols, labelOf: (value) => value.toUpperCase(), onChanged: onChanged, ), ], ); } } class _DisplayNameWithAvatar extends StatelessWidget { final String iconUrl; final String serverName; final TextEditingController controller; final bool enabled; final VoidCallback onPick; const _DisplayNameWithAvatar({ required this.iconUrl, required this.serverName, required this.controller, required this.enabled, required this.onPick, }); @override Widget build(BuildContext context) { final theme = Theme.of(context); return Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Tooltip( message: '点击更换图标', child: InkWell( onTap: enabled ? onPick : null, borderRadius: BorderRadius.circular(10), child: SizedBox( width: 62, height: 62, child: Stack( children: [ Positioned( left: 0, top: 0, child: ServerAvatar( name: serverName, iconUrl: iconUrl, size: 56, ), ), Positioned( right: 0, bottom: 0, child: Container( padding: const EdgeInsets.all(3), decoration: BoxDecoration( color: theme.colorScheme.surface, shape: BoxShape.circle, border: Border.all( color: theme.dividerColor, width: 0.5, ), ), child: Icon( Icons.edit_outlined, size: 11, color: theme.colorScheme.onSurfaceVariant, ), ), ), ], ), ), ), ), const SizedBox(width: 12), Expanded( child: TextFormField( controller: controller, decoration: const InputDecoration( labelText: '展示名称', hintText: '留空则使用服务器名称', ), enabled: enabled, textInputAction: TextInputAction.next, autocorrect: false, enableSuggestions: false, ), ), ], ); } }