Files
Netcatty/components/ProtocolSelectDialog.tsx
T
LAPTOP-O016UC3M\Qi Chen d8347582b4 Adds full UI internationalization and language switching
Implements comprehensive i18n support, replacing hardcoded UI strings with translation keys and introducing an i18n provider across the app. Adds persistent UI language selection, cross-window sync, and system menu localization. Updates settings and state to handle locale, propagates language changes to Electron, and adjusts components for runtime translation.

Enables a user-friendly, multi-language experience and prepares the app for further language expansion.
2025-12-17 12:06:28 +08:00

212 lines
9.0 KiB
TypeScript

import {
Globe,
Terminal as TerminalIcon,
Wifi
} from 'lucide-react';
import React,{ useMemo,useState } from 'react';
import { useI18n } from '../application/i18n/I18nProvider';
import { cn } from '../lib/utils';
import { Host,HostProtocol } from '../types';
import { DistroAvatar } from './DistroAvatar';
import { Button } from './ui/button';
import { Input } from './ui/input';
interface ProtocolOption {
protocol: HostProtocol;
port: number;
label: string;
icon: React.ReactNode;
description: string;
enabled: boolean;
}
interface ProtocolSelectDialogProps {
host: Host;
onSelect: (protocol: HostProtocol, port: number) => void;
onCancel: () => void;
}
const ProtocolSelectDialog: React.FC<ProtocolSelectDialogProps> = ({
host,
onSelect,
onCancel,
}) => {
const { t } = useI18n();
// Build protocol options from host configuration
const protocolOptions = useMemo<ProtocolOption[]>(() => {
const options: ProtocolOption[] = [];
// SSH (always available if not explicitly disabled)
const sshEnabled = host.protocol === 'ssh' || !host.protocol || host.protocols?.some(p => p.protocol === 'ssh' && p.enabled);
if (sshEnabled !== false) {
const sshConfig = host.protocols?.find(p => p.protocol === 'ssh');
options.push({
protocol: 'ssh',
port: sshConfig?.port || host.port || 22,
label: 'SSH',
icon: <TerminalIcon size={18} />,
description: `ssh ${host.hostname}`,
enabled: true,
});
}
// Mosh (if enabled)
if (host.moshEnabled || host.protocols?.some(p => p.protocol === 'mosh' && p.enabled)) {
const moshConfig = host.protocols?.find(p => p.protocol === 'mosh');
options.push({
protocol: 'mosh',
port: moshConfig?.port || host.port || 22,
label: 'Mosh',
icon: <Wifi size={18} />,
description: `mosh ${host.hostname}`,
enabled: true,
});
}
// Telnet (if enabled)
if (host.telnetEnabled || host.protocol === 'telnet' || host.protocols?.some(p => p.protocol === 'telnet' && p.enabled)) {
const telnetConfig = host.protocols?.find(p => p.protocol === 'telnet');
options.push({
protocol: 'telnet',
port: telnetConfig?.port || host.telnetPort || 23,
label: 'Telnet',
icon: <Globe size={18} />,
description: `telnet ${host.hostname}`,
enabled: true,
});
}
return options;
}, [host]);
// State for custom ports (allow editing inline)
const [ports, setPorts] = useState<Record<HostProtocol, number>>(() => {
const initial: Record<string, number> = {};
protocolOptions.forEach(opt => {
initial[opt.protocol] = opt.port;
});
return initial as Record<HostProtocol, number>;
});
const [selectedProtocol, setSelectedProtocol] = useState<HostProtocol>(
protocolOptions[0]?.protocol || 'ssh'
);
const handlePortChange = (protocol: HostProtocol, value: string) => {
const port = parseInt(value, 10);
if (!isNaN(port) && port > 0 && port <= 65535) {
setPorts(prev => ({ ...prev, [protocol]: port }));
}
};
const handleContinue = () => {
onSelect(selectedProtocol, ports[selectedProtocol] || 22);
};
// If only one protocol, auto-select and proceed
React.useEffect(() => {
if (protocolOptions.length === 1) {
// Don't auto-proceed, let user confirm
}
}, [protocolOptions]);
return (
<div className="fixed inset-0 z-50 flex items-center justify-center" onClick={onCancel}>
<div className="w-[560px] max-w-[90vw] bg-background border border-border rounded-2xl animate-in fade-in-0 zoom-in-95 duration-200" style={{ boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.25), 0 12px 24px -8px rgba(0, 0, 0, 0.15)' }} onClick={(e) => e.stopPropagation()}>
{/* Header */}
<div className="px-6 py-5 border-b border-border/50">
<div className="flex items-center gap-3">
<DistroAvatar
host={host}
fallback={host.label.slice(0, 2).toUpperCase()}
className="h-12 w-12"
/>
<div>
<h2 className="text-base font-semibold">{host.label}</h2>
<p className="text-xs text-muted-foreground font-mono">
{host.hostname}
</p>
</div>
</div>
</div>
{/* Progress indicator */}
<div className="px-6 py-4">
<div className="flex items-center gap-3">
<div className="h-8 w-8 rounded-full bg-primary/20 text-primary flex items-center justify-center">
<TerminalIcon size={14} />
</div>
<div className="flex-1 h-0.5 bg-muted" />
<div className="h-8 w-8 rounded-full bg-muted text-muted-foreground flex items-center justify-center text-xs font-mono">
{'>_'}
</div>
</div>
</div>
{/* Protocol selection */}
<div className="px-6 py-4 space-y-4">
<h3 className="text-base font-semibold">{t("protocolSelect.chooseProtocol")}</h3>
<div className="space-y-3">
{protocolOptions.map((option) => (
<button
key={option.protocol}
className={cn(
"w-full flex items-center justify-between px-4 py-3 rounded-xl border-2 transition-all text-left",
selectedProtocol === option.protocol
? "border-primary bg-primary/5"
: "border-border/60 hover:border-border hover:bg-secondary/50"
)}
onClick={() => setSelectedProtocol(option.protocol)}
>
<div className="flex items-center gap-3">
<div className={cn(
"h-10 w-10 rounded-lg flex items-center justify-center",
selectedProtocol === option.protocol
? "bg-primary/20 text-primary"
: "bg-muted text-muted-foreground"
)}>
{option.icon}
</div>
<div>
<div className="font-medium">{option.label}</div>
<div className="text-xs text-muted-foreground font-mono">
{option.description}
</div>
</div>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground">{t("protocolSelect.port")}</span>
<Input
type="number"
value={ports[option.protocol] || option.port}
onChange={(e) => handlePortChange(option.protocol, e.target.value)}
onClick={(e) => {
e.stopPropagation();
setSelectedProtocol(option.protocol);
}}
className="w-16 h-7 text-xs text-center"
min={1}
max={65535}
/>
</div>
</button>
))}
</div>
</div>
{/* Footer */}
<div className="px-6 py-4 border-t border-border/50 flex items-center justify-between">
<Button variant="secondary" onClick={onCancel}>
{t("common.close")}
</Button>
<Button onClick={handleContinue}>
{t("common.continue")}
</Button>
</div>
</div>
</div>
);
};
export default ProtocolSelectDialog;