import { Wifi, WifiOff, AlertCircle, Loader2, HardDrive, Clock, ChevronUp } from 'lucide-react' import { useEffect, useState, useMemo } from 'react' import { useTabStore, type ConnectionStatus } from '../../stores/tabStore' import { usePoolStore } from '../../stores/connection/poolStore' import type { ConnectionState } from '../../stores/connection' import { useConnectionPanelStore } from '../../stores/connectionPanelStore' import { useSessionStore } from '../../stores' import packageJson from '../../../package.json' // ============== Status Display Config ============== interface StatusDisplay { icon: React.ReactNode label: string colorClass: string animateClass: string } const STATUS_CONFIG: Record = { connected: { icon: , label: '已连接', colorClass: 'text-green-400', animateClass: '', }, connecting: { icon: , label: '连接中', colorClass: 'text-yellow-400', animateClass: 'animate-pulse', }, background: { icon: , label: '后台', colorClass: 'text-green-400', animateClass: '', }, suspended: { icon: , label: '已挂起', colorClass: 'text-yellow-400', animateClass: '', }, reconnecting: { icon: , label: '重连中', colorClass: 'text-yellow-400', animateClass: 'animate-pulse', }, error: { icon: , label: '连接错误', colorClass: 'text-red-400', animateClass: '', }, disconnected: { icon: , label: '未连接', colorClass: 'text-txt-muted', animateClass: '', }, } // ============== Helper: format bytes ============== function formatBytes(bytes: number): string { if (bytes === 0) return '0 B' const units = ['B', 'KB', 'MB', 'GB'] const k = 1024 const i = Math.floor(Math.log(bytes) / Math.log(k)) const value = bytes / Math.pow(k, i) return `${value.toFixed(i === 0 ? 0 : 1)} ${units[i]}` } // ============== Helper: map tab connectionStatus to ConnectionState ============== function mapTabStatusToConnectionState(status?: ConnectionStatus): ConnectionState | undefined { if (!status) return undefined // tabStore's ConnectionStatus: 'disconnected' | 'connecting' | 'connected' | 'error' // maps directly to ConnectionState values return status as ConnectionState } // ============== Component ============== export default function StatusBar() { const togglePanel = useConnectionPanelStore((s) => s.toggle) const [version, setVersion] = useState(packageJson.version) const [currentTime, setCurrentTime] = useState(new Date()) // Tab store const activeTabId = useTabStore((s) => s.activeTabId) const tabs = useTabStore((s) => s.tabs) // Connection store (pool) — subscribe to connections to reactively update const connections = usePoolStore((s) => s.connections) const sessions = useSessionStore((s) => s.sessions) // Total connection count for the panel badge const totalConnections = connections.size // Derive the active tab const activeTab = useMemo( () => tabs.find((t) => t.id === activeTabId), [tabs, activeTabId] ) // Determine the connection state and info for the active tab const { connectionState, hostLabel, stats, tabKind } = useMemo(() => { if (!activeTab || activeTab.type === 'welcome' || activeTab.type === 'settings') { return { connectionState: undefined, hostLabel: undefined, stats: undefined, tabKind: activeTab?.type as string | undefined, } } if (activeTab.type === 'serial') { return { connectionState: mapTabStatusToConnectionState(activeTab.connectionStatus), hostLabel: activeTab.serialConfig ? `${activeTab.serialConfig.path} · ${activeTab.serialConfig.baudRate}` : activeTab.title, stats: undefined, tabKind: activeTab.type, } } // 1. Try to get connection from pool via connectionInstance const instance = activeTab.connectionInstance if (instance?.connectionId) { const conn = connections.get(instance.connectionId) if (conn) { return { connectionState: conn.state, hostLabel: `${conn.config.username}@${conn.config.host}:${conn.config.port || 22}`, stats: conn.stats, tabKind: activeTab.type, } } } // 2. Session id → session store if (activeTab.sessionId) { const session = sessions.find((s) => s.id === activeTab.sessionId) if (session) { return { connectionState: mapTabStatusToConnectionState(activeTab.connectionStatus), hostLabel: `${session.username}@${session.host}:${session.port || 22}`, stats: undefined, tabKind: activeTab.type, } } } // 3. Fall back to tab's own connectionConfig const state = mapTabStatusToConnectionState(activeTab.connectionStatus) const config = activeTab.connectionConfig const label = config ? `${config.username}@${config.host}:${config.port || 22}` : activeTab.isLocal ? '本地终端' : undefined return { connectionState: state, hostLabel: label, stats: undefined, tabKind: activeTab.type } }, [activeTab, connections, sessions]) // Clock timer + version useEffect(() => { if (window.opsxApi) { window.opsxApi.getVersion().then((v: string) => { setVersion(v) }).catch(() => undefined) } const timer = setInterval(() => { setCurrentTime(new Date()) }, 1000) return () => clearInterval(timer) }, []) const formatTime = (date: Date) => { return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit', }) } // Resolve status display const statusKey = connectionState ?? 'disconnected' const statusCfg = STATUS_CONFIG[statusKey] ?? STATUS_CONFIG.disconnected // Format transfer info if available const transferLabel = stats && stats.bytesTransferred > 0 ? `↑↓ ${formatBytes(stats.bytesTransferred)}` : null return (
{/* Left: Connection status area — clickable to toggle ConnectionPanel */} {/* Center: Transfer stats */} {transferLabel && (
{transferLabel}
)}
{formatTime(currentTime)}
OpsXShell v{version}
) }