import 'package:flutter/material.dart'; import 'rtty_pty_backend.dart'; import 'theme.dart'; /// 顶部连接控制条。 /// /// 精炼的工业风控制条:状态指示灯 + 服务器地址输入 + 会话 ID + 连接按钮。 /// 断开状态下可编辑,连接成功后锁定并显示会话信息。 class ConnectionBar extends StatelessWidget { const ConnectionBar({ super.key, required this.state, required this.hostController, required this.portController, required this.sessionController, required this.sessionId, required this.terminalSize, required this.onConnect, required this.onDisconnect, }); final ConnState state; final TextEditingController hostController; final TextEditingController portController; final TextEditingController sessionController; final String? sessionId; final String terminalSize; final VoidCallback onConnect; final VoidCallback onDisconnect; bool get _connected => state == ConnState.connected || state == ConnState.connecting; @override Widget build(BuildContext context) { return Container( height: 56, decoration: BoxDecoration( color: RttyTheme.surface, border: Border(bottom: BorderSide(color: RttyTheme.border)), ), padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( children: [ _StatusLight(state: state), const SizedBox(width: 12), _ConnectionLabel(state: state), const SizedBox(width: 16), _Divider(), const SizedBox(width: 16), _EditableField( label: 'HOST', controller: hostController, enabled: !_connected, width: 150, ), const SizedBox(width: 8), _EditableField( label: 'PORT', controller: portController, enabled: !_connected, width: 64, numeric: true, ), const SizedBox(width: 8), if (!_connected) ...[ _EditableField( label: 'SESSION', controller: sessionController, enabled: true, width: 120, hint: 'auto', ), ] else ...[ _SessionBadge(sessionId: sessionId), ], const Spacer(), _TerminalSizeBadge(text: terminalSize), const SizedBox(width: 12), _ConnectButton( state: state, onConnect: onConnect, onDisconnect: onDisconnect, ), ], ), ); } } class _StatusLight extends StatelessWidget { const _StatusLight({required this.state}); final ConnState state; Color get _color => switch (state) { ConnState.disconnected => RttyTheme.textFaint, ConnState.connecting => RttyTheme.accent, ConnState.connected => RttyTheme.primary, ConnState.error => RttyTheme.danger, }; @override Widget build(BuildContext context) { return Container( width: 10, height: 10, decoration: BoxDecoration( color: _color, shape: BoxShape.circle, boxShadow: [ BoxShadow( color: _color.withValues(alpha: 0.55), blurRadius: 6, spreadRadius: 1, ), ], ), ); } } class _ConnectionLabel extends StatelessWidget { const _ConnectionLabel({required this.state}); final ConnState state; @override Widget build(BuildContext context) { final (label, color) = switch (state) { ConnState.disconnected => ('DISCONNECTED', RttyTheme.textFaint), ConnState.connecting => ('CONNECTING', RttyTheme.accent), ConnState.connected => ('CONNECTED', RttyTheme.primary), ConnState.error => ('CONNECTION ERROR', RttyTheme.danger), }; return Text( label, style: TextStyle( color: color, fontSize: 11, fontWeight: FontWeight.w700, letterSpacing: 1.4, ), ); } } class _Divider extends StatelessWidget { const _Divider(); @override Widget build(BuildContext context) { return Container(width: 1, height: 24, color: RttyTheme.border); } } class _EditableField extends StatelessWidget { const _EditableField({ required this.label, required this.controller, required this.enabled, required this.width, this.numeric = false, this.hint, }); final String label; final TextEditingController controller; final bool enabled; final double width; final bool numeric; final String? hint; @override Widget build(BuildContext context) { return SizedBox( width: width, child: TextField( controller: controller, enabled: enabled, keyboardType: numeric ? TextInputType.number : TextInputType.text, style: const TextStyle( color: RttyTheme.text, fontSize: 12, fontFamily: 'Consolas', decorationThickness: 0, ), cursorColor: RttyTheme.primary, decoration: InputDecoration( labelText: label, labelStyle: const TextStyle( color: RttyTheme.textFaint, fontSize: 10, letterSpacing: 1.2, fontWeight: FontWeight.w600, ), hintText: hint, hintStyle: const TextStyle(color: RttyTheme.textFaint, fontSize: 12), isDense: true, contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(6), borderSide: const BorderSide(color: RttyTheme.border), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(6), borderSide: const BorderSide(color: RttyTheme.primaryDim), ), disabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(6), borderSide: BorderSide( color: RttyTheme.border.withValues(alpha: 0.5), ), ), ), ), ); } } class _SessionBadge extends StatelessWidget { const _SessionBadge({required this.sessionId}); final String? sessionId; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( color: RttyTheme.surfaceHi, borderRadius: BorderRadius.circular(6), border: Border.all(color: RttyTheme.border), ), child: Text( 'SID ${sessionId ?? '—'}', style: const TextStyle( color: RttyTheme.textDim, fontSize: 11, fontFamily: 'Consolas', letterSpacing: 0.4, ), ), ); } } class _TerminalSizeBadge extends StatelessWidget { const _TerminalSizeBadge({required this.text}); final String text; @override Widget build(BuildContext context) { return Text( text, style: const TextStyle( color: RttyTheme.textFaint, fontSize: 11, fontFamily: 'Consolas', ), ); } } class _ConnectButton extends StatelessWidget { const _ConnectButton({ required this.state, required this.onConnect, required this.onDisconnect, }); final ConnState state; final VoidCallback onConnect; final VoidCallback onDisconnect; @override Widget build(BuildContext context) { final connected = state == ConnState.connected; final connecting = state == ConnState.connecting; final bg = connected ? RttyTheme.primaryDim.withValues(alpha: 0.25) : RttyTheme.primary.withValues(alpha: 0.14); final fg = connected ? RttyTheme.danger : RttyTheme.primary; final label = connected ? 'DISCONNECT' : connecting ? 'CONNECTING…' : 'CONNECT'; return SizedBox( height: 34, child: FilledButton( onPressed: connected ? onDisconnect : onConnect, style: FilledButton.styleFrom( backgroundColor: bg, foregroundColor: fg, padding: const EdgeInsets.symmetric(horizontal: 18), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(6), side: BorderSide(color: fg.withValues(alpha: 0.5)), ), ), child: Text( label, style: const TextStyle( fontSize: 11, fontWeight: FontWeight.w700, letterSpacing: 1.2, ), ), ), ); } }