Files
Rtty/desktop/lib/src/connection_bar.dart

320 lines
8.5 KiB
Dart

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,
),
),
),
);
}
}