mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-09-24 15:49:09 +00:00
d8347582b4
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.
212 lines
9.0 KiB
TypeScript
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;
|