refactor: vpn refresh and remove openvpn support

This commit is contained in:
zhom
2026-04-26 23:28:54 +04:00
parent 91218e08f9
commit ce76c1381f
36 changed files with 613 additions and 2570 deletions
+4 -8
View File
@@ -1068,7 +1068,7 @@ export function CreateProfileDialog({
v.id === selectedProxyId.slice(4),
);
return vpn
? `${vpn.vpn_type === "WireGuard" ? "WG" : "OVPN"}${vpn.name}`
? `WG${vpn.name}`
: "No proxy / VPN";
}
const proxy = storedProxies.find(
@@ -1154,9 +1154,7 @@ export function CreateProfileDialog({
variant="outline"
className="text-[10px] px-1 py-0 leading-tight mr-1"
>
{vpn.vpn_type === "WireGuard"
? "WG"
: "OVPN"}
WG
</Badge>
{vpn.name}
</CommandItem>
@@ -1417,7 +1415,7 @@ export function CreateProfileDialog({
v.id === selectedProxyId.slice(4),
);
return vpn
? `${vpn.vpn_type === "WireGuard" ? "WG" : "OVPN"} — ${vpn.name}`
? `WG — ${vpn.name}`
: "No proxy / VPN";
}
const proxy = storedProxies.find(
@@ -1503,9 +1501,7 @@ export function CreateProfileDialog({
variant="outline"
className="text-[10px] px-1 py-0 leading-tight mr-1"
>
{vpn.vpn_type === "WireGuard"
? "WG"
: "OVPN"}
WG
</Badge>
{vpn.name}
</CommandItem>
+2 -6
View File
@@ -2219,11 +2219,7 @@ export function ProfilesDataTable({
: effectiveProxy
? effectiveProxy.name
: "Not Selected";
const vpnBadge = effectiveVpn
? effectiveVpn.vpn_type === "WireGuard"
? "WG"
: "OVPN"
: null;
const vpnBadge = effectiveVpn ? "WG" : null;
const tooltipText = hasAssignment ? displayName : null;
const isSelectorOpen = meta.openProxySelectorFor === profile.id;
const selectedId = effectiveVpnId ?? effectiveProxyId ?? null;
@@ -2385,7 +2381,7 @@ export function ProfilesDataTable({
variant="outline"
className="text-[10px] px-1 py-0 leading-tight mr-1"
>
{vpn.vpn_type === "WireGuard" ? "WG" : "OVPN"}
WG
</Badge>
{vpn.name}
</CommandItem>
+2 -4
View File
@@ -179,9 +179,7 @@ export function ProxyAssignmentDialog({
if (selectionType === "none") return "None";
if (selectionType === "vpn") {
const vpn = vpnConfigs.find((v) => v.id === selectedId);
return vpn
? `${vpn.vpn_type === "WireGuard" ? "WG" : "OVPN"}${vpn.name}`
: "None";
return vpn ? `WG — ${vpn.name}` : "None";
}
const proxy = storedProxies.find(
(p) => p.id === selectedId,
@@ -264,7 +262,7 @@ export function ProxyAssignmentDialog({
variant="outline"
className="text-[10px] px-1 py-0 leading-tight mr-1"
>
{vpn.vpn_type === "WireGuard" ? "WG" : "OVPN"}
WG
</Badge>
{vpn.name}
</CommandItem>
+1 -5
View File
@@ -670,11 +670,7 @@ export function ProxyManagementDialog({
</div>
</TableCell>
<TableCell>
<Badge variant="outline">
{vpn.vpn_type === "WireGuard"
? "WG"
: "OVPN"}
</Badge>
<Badge variant="outline">WG</Badge>
</TableCell>
<TableCell>
<Badge variant="secondary">
+1 -1
View File
@@ -111,7 +111,7 @@ function CommandGroup({
<CommandPrimitive.Group
data-slot="command-group"
className={cn(
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-x-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium overflow-y-scroll",
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-x-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
className,
)}
{...props}
+162 -381
View File
@@ -3,10 +3,8 @@
import { invoke } from "@tauri-apps/api/core";
import { emit } from "@tauri-apps/api/event";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { LoadingButton } from "@/components/loading-button";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import {
Dialog,
DialogContent,
@@ -19,15 +17,7 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { RippleButton } from "@/components/ui/ripple";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import type { VpnConfig, VpnType } from "@/types";
import type { VpnConfig } from "@/types";
interface VpnFormDialogProps {
isOpen: boolean;
@@ -48,19 +38,6 @@ interface WireGuardFormData {
presharedKey: string;
}
interface OpenVpnFormData {
name: string;
rawConfig: string;
}
interface VpnDependencyStatus {
isAvailable: boolean;
requiresExternalInstall: boolean;
missingBinary: boolean;
missingWindowsAdapter: boolean;
dependencyCheckFailed: boolean;
}
const defaultWireGuardForm: WireGuardFormData = {
name: "",
privateKey: "",
@@ -74,11 +51,6 @@ const defaultWireGuardForm: WireGuardFormData = {
presharedKey: "",
};
const defaultOpenVpnForm: OpenVpnFormData = {
name: "",
rawConfig: "",
};
function buildWireGuardConfig(form: WireGuardFormData): string {
const lines: string[] = ["[Interface]"];
lines.push(`PrivateKey = ${form.privateKey.trim()}`);
@@ -102,63 +74,24 @@ export function VpnFormDialog({
onClose,
editingVpn,
}: VpnFormDialogProps) {
const { t } = useTranslation();
const [isSubmitting, setIsSubmitting] = useState(false);
const [vpnType, setVpnType] = useState<VpnType>("WireGuard");
const [wireGuardForm, setWireGuardForm] =
useState<WireGuardFormData>(defaultWireGuardForm);
const [openVpnForm, setOpenVpnForm] =
useState<OpenVpnFormData>(defaultOpenVpnForm);
const [vpnDependencyStatus, setVpnDependencyStatus] =
useState<VpnDependencyStatus | null>(null);
const resetForms = useCallback(() => {
setVpnType("WireGuard");
setWireGuardForm(defaultWireGuardForm);
setOpenVpnForm(defaultOpenVpnForm);
}, []);
useEffect(() => {
if (isOpen) {
if (editingVpn) {
setVpnType(editingVpn.vpn_type);
if (editingVpn.vpn_type === "WireGuard") {
setWireGuardForm({ ...defaultWireGuardForm, name: editingVpn.name });
} else {
setOpenVpnForm({ name: editingVpn.name, rawConfig: "" });
}
setWireGuardForm({ ...defaultWireGuardForm, name: editingVpn.name });
} else {
resetForms();
}
}
}, [isOpen, editingVpn, resetForms]);
useEffect(() => {
if (!isOpen) {
setVpnDependencyStatus(null);
return;
}
let cancelled = false;
void invoke<VpnDependencyStatus>("get_vpn_dependency_status", { vpnType })
.then((status) => {
if (!cancelled) {
setVpnDependencyStatus(status);
}
})
.catch((error) => {
console.error("Failed to load VPN dependency status:", error);
if (!cancelled) {
setVpnDependencyStatus(null);
}
});
return () => {
cancelled = true;
};
}, [isOpen, vpnType]);
const handleClose = useCallback(() => {
if (!isSubmitting) {
onClose();
@@ -167,10 +100,7 @@ export function VpnFormDialog({
const handleSubmit = useCallback(async () => {
if (editingVpn) {
const name =
vpnType === "WireGuard"
? wireGuardForm.name.trim()
: openVpnForm.name.trim();
const name = wireGuardForm.name.trim();
if (!name) {
toast.error("VPN name is required");
@@ -196,80 +126,49 @@ export function VpnFormDialog({
return;
}
if (vpnType === "WireGuard") {
const { name, privateKey, address, peerPublicKey, peerEndpoint } =
wireGuardForm;
const { name, privateKey, address, peerPublicKey, peerEndpoint } =
wireGuardForm;
if (!name.trim()) {
toast.error("VPN name is required");
return;
}
if (!privateKey.trim()) {
toast.error("Private key is required");
return;
}
if (!address.trim()) {
toast.error("Address is required");
return;
}
if (!peerPublicKey.trim()) {
toast.error("Peer public key is required");
return;
}
if (!peerEndpoint.trim()) {
toast.error("Peer endpoint is required");
return;
}
setIsSubmitting(true);
try {
const configData = buildWireGuardConfig(wireGuardForm);
await invoke("create_vpn_config_manual", {
name: name.trim(),
vpnType: "WireGuard",
configData,
});
await emit("vpn-configs-changed");
toast.success("WireGuard VPN created successfully");
onClose();
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
toast.error(`Failed to create VPN: ${errorMessage}`);
} finally {
setIsSubmitting(false);
}
} else {
const { name, rawConfig } = openVpnForm;
if (!name.trim()) {
toast.error("VPN name is required");
return;
}
if (!rawConfig.trim()) {
toast.error("OpenVPN config content is required");
return;
}
setIsSubmitting(true);
try {
await invoke("create_vpn_config_manual", {
name: name.trim(),
vpnType: "OpenVPN",
configData: rawConfig,
});
await emit("vpn-configs-changed");
toast.success("OpenVPN configuration created successfully");
onClose();
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
toast.error(`Failed to create VPN: ${errorMessage}`);
} finally {
setIsSubmitting(false);
}
if (!name.trim()) {
toast.error("VPN name is required");
return;
}
}, [editingVpn, vpnType, wireGuardForm, openVpnForm, onClose]);
if (!privateKey.trim()) {
toast.error("Private key is required");
return;
}
if (!address.trim()) {
toast.error("Address is required");
return;
}
if (!peerPublicKey.trim()) {
toast.error("Peer public key is required");
return;
}
if (!peerEndpoint.trim()) {
toast.error("Peer endpoint is required");
return;
}
setIsSubmitting(true);
try {
const configData = buildWireGuardConfig(wireGuardForm);
await invoke("create_vpn_config_manual", {
name: name.trim(),
vpnType: "WireGuard",
configData,
});
await emit("vpn-configs-changed");
toast.success("WireGuard VPN created successfully");
onClose();
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
toast.error(`Failed to create VPN: ${errorMessage}`);
} finally {
setIsSubmitting(false);
}
}, [editingVpn, wireGuardForm, onClose]);
const updateWireGuard = useCallback(
(field: keyof WireGuardFormData, value: string) => {
@@ -278,54 +177,10 @@ export function VpnFormDialog({
[],
);
const updateOpenVpn = useCallback(
(field: keyof OpenVpnFormData, value: string) => {
setOpenVpnForm((prev) => ({ ...prev, [field]: value }));
},
[],
);
const dialogTitle = editingVpn
? "Edit VPN"
: vpnType === "WireGuard"
? "Create WireGuard VPN"
: "Create OpenVPN Configuration";
const dialogTitle = editingVpn ? "Edit VPN" : "Create WireGuard VPN";
const dialogDescription = editingVpn
? "Update the name of your VPN configuration."
: vpnType === "WireGuard"
? "Enter your WireGuard interface and peer details."
: "Paste your .ovpn configuration file content.";
let dependencyWarningTitle: string | null = null;
let dependencyWarningDescription: string | null = null;
if (
vpnType === "OpenVPN" &&
vpnDependencyStatus?.requiresExternalInstall &&
!vpnDependencyStatus.isAvailable
) {
if (vpnDependencyStatus.missingBinary) {
dependencyWarningTitle = t("vpnForm.dependencies.openVpnMissingTitle");
dependencyWarningDescription = t(
"vpnForm.dependencies.openVpnMissingDescription",
);
} else if (vpnDependencyStatus.missingWindowsAdapter) {
dependencyWarningTitle = t(
"vpnForm.dependencies.openVpnAdapterMissingTitle",
);
dependencyWarningDescription = t(
"vpnForm.dependencies.openVpnAdapterMissingDescription",
);
} else if (vpnDependencyStatus.dependencyCheckFailed) {
dependencyWarningTitle = t(
"vpnForm.dependencies.openVpnCheckFailedTitle",
);
dependencyWarningDescription = t(
"vpnForm.dependencies.openVpnCheckFailedDescription",
);
}
}
: "Enter your WireGuard interface and peer details.";
return (
<Dialog open={isOpen} onOpenChange={handleClose}>
@@ -337,221 +192,147 @@ export function VpnFormDialog({
<ScrollArea className="max-h-[60vh] pr-4">
<div className="grid gap-4 py-2">
{dependencyWarningTitle && dependencyWarningDescription && (
<Alert className="border-warning/50 bg-warning/10">
<AlertTitle className="text-warning">
{dependencyWarningTitle}
</AlertTitle>
<AlertDescription className="text-warning">
{dependencyWarningDescription}
</AlertDescription>
</Alert>
)}
<div className="grid gap-2">
<Label htmlFor="wg-name">Name</Label>
<Input
id="wg-name"
value={wireGuardForm.name}
onChange={(e) => {
updateWireGuard("name", e.target.value);
}}
placeholder="e.g. Home WireGuard"
disabled={isSubmitting}
/>
</div>
{!editingVpn && (
<div className="grid gap-2">
<Label>VPN Type</Label>
<Select
value={vpnType}
onValueChange={(value) => {
setVpnType(value as VpnType);
}}
disabled={isSubmitting}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select VPN type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="WireGuard">WireGuard</SelectItem>
<SelectItem value="OpenVPN">OpenVPN</SelectItem>
</SelectContent>
</Select>
</div>
)}
{vpnType === "WireGuard" && (
<>
<div className="grid gap-2">
<Label htmlFor="wg-name">Name</Label>
<Label htmlFor="wg-private-key">Private Key</Label>
<Input
id="wg-name"
value={wireGuardForm.name}
id="wg-private-key"
value={wireGuardForm.privateKey}
onChange={(e) => {
updateWireGuard("name", e.target.value);
updateWireGuard("privateKey", e.target.value);
}}
placeholder="e.g. Home WireGuard"
placeholder="Base64-encoded private key"
disabled={isSubmitting}
/>
</div>
{!editingVpn && (
<>
<div className="grid gap-2">
<Label htmlFor="wg-private-key">Private Key</Label>
<Input
id="wg-private-key"
value={wireGuardForm.privateKey}
onChange={(e) => {
updateWireGuard("privateKey", e.target.value);
}}
placeholder="Base64-encoded private key"
disabled={isSubmitting}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="wg-address">Address</Label>
<Input
id="wg-address"
value={wireGuardForm.address}
onChange={(e) => {
updateWireGuard("address", e.target.value);
}}
placeholder="e.g. 10.0.0.2/24"
disabled={isSubmitting}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="grid gap-2">
<Label htmlFor="wg-dns">DNS (optional)</Label>
<Input
id="wg-dns"
value={wireGuardForm.dns}
onChange={(e) => {
updateWireGuard("dns", e.target.value);
}}
placeholder="e.g. 1.1.1.1"
disabled={isSubmitting}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="wg-mtu">MTU (optional)</Label>
<Input
id="wg-mtu"
type="number"
value={wireGuardForm.mtu}
onChange={(e) => {
updateWireGuard("mtu", e.target.value);
}}
placeholder="e.g. 1420"
disabled={isSubmitting}
/>
</div>
</div>
<div className="grid gap-2">
<Label htmlFor="wg-peer-public-key">
Peer Public Key
</Label>
<Input
id="wg-peer-public-key"
value={wireGuardForm.peerPublicKey}
onChange={(e) => {
updateWireGuard("peerPublicKey", e.target.value);
}}
placeholder="Base64-encoded peer public key"
disabled={isSubmitting}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="wg-peer-endpoint">Peer Endpoint</Label>
<Input
id="wg-peer-endpoint"
value={wireGuardForm.peerEndpoint}
onChange={(e) => {
updateWireGuard("peerEndpoint", e.target.value);
}}
placeholder="e.g. vpn.example.com:51820"
disabled={isSubmitting}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="wg-allowed-ips">Allowed IPs</Label>
<Input
id="wg-allowed-ips"
value={wireGuardForm.allowedIps}
onChange={(e) => {
updateWireGuard("allowedIps", e.target.value);
}}
placeholder="e.g. 0.0.0.0/0, ::/0"
disabled={isSubmitting}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="grid gap-2">
<Label htmlFor="wg-keepalive">
Persistent Keepalive (optional)
</Label>
<Input
id="wg-keepalive"
type="number"
value={wireGuardForm.persistentKeepalive}
onChange={(e) => {
updateWireGuard(
"persistentKeepalive",
e.target.value,
);
}}
placeholder="e.g. 25"
disabled={isSubmitting}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="wg-preshared-key">
Preshared Key (optional)
</Label>
<Input
id="wg-preshared-key"
value={wireGuardForm.presharedKey}
onChange={(e) => {
updateWireGuard("presharedKey", e.target.value);
}}
placeholder="Base64-encoded preshared key"
disabled={isSubmitting}
/>
</div>
</div>
</>
)}
</>
)}
{vpnType === "OpenVPN" && (
<>
<div className="grid gap-2">
<Label htmlFor="ovpn-name">Name</Label>
<Label htmlFor="wg-address">Address</Label>
<Input
id="ovpn-name"
value={openVpnForm.name}
id="wg-address"
value={wireGuardForm.address}
onChange={(e) => {
updateOpenVpn("name", e.target.value);
updateWireGuard("address", e.target.value);
}}
placeholder="e.g. Work OpenVPN"
placeholder="e.g. 10.0.0.2/24"
disabled={isSubmitting}
/>
</div>
{!editingVpn && (
<div className="grid grid-cols-2 gap-4">
<div className="grid gap-2">
<Label htmlFor="ovpn-config">Raw Config</Label>
<Textarea
id="ovpn-config"
value={openVpnForm.rawConfig}
<Label htmlFor="wg-dns">DNS (optional)</Label>
<Input
id="wg-dns"
value={wireGuardForm.dns}
onChange={(e) => {
updateOpenVpn("rawConfig", e.target.value);
updateWireGuard("dns", e.target.value);
}}
placeholder="Paste your .ovpn file content here..."
className="min-h-[200px] font-mono text-xs"
placeholder="e.g. 1.1.1.1"
disabled={isSubmitting}
/>
</div>
)}
<div className="grid gap-2">
<Label htmlFor="wg-mtu">MTU (optional)</Label>
<Input
id="wg-mtu"
type="number"
value={wireGuardForm.mtu}
onChange={(e) => {
updateWireGuard("mtu", e.target.value);
}}
placeholder="e.g. 1420"
disabled={isSubmitting}
/>
</div>
</div>
<div className="grid gap-2">
<Label htmlFor="wg-peer-public-key">Peer Public Key</Label>
<Input
id="wg-peer-public-key"
value={wireGuardForm.peerPublicKey}
onChange={(e) => {
updateWireGuard("peerPublicKey", e.target.value);
}}
placeholder="Base64-encoded peer public key"
disabled={isSubmitting}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="wg-peer-endpoint">Peer Endpoint</Label>
<Input
id="wg-peer-endpoint"
value={wireGuardForm.peerEndpoint}
onChange={(e) => {
updateWireGuard("peerEndpoint", e.target.value);
}}
placeholder="e.g. vpn.example.com:51820"
disabled={isSubmitting}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="wg-allowed-ips">Allowed IPs</Label>
<Input
id="wg-allowed-ips"
value={wireGuardForm.allowedIps}
onChange={(e) => {
updateWireGuard("allowedIps", e.target.value);
}}
placeholder="e.g. 0.0.0.0/0, ::/0"
disabled={isSubmitting}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="grid gap-2">
<Label htmlFor="wg-keepalive">
Persistent Keepalive (optional)
</Label>
<Input
id="wg-keepalive"
type="number"
value={wireGuardForm.persistentKeepalive}
onChange={(e) => {
updateWireGuard("persistentKeepalive", e.target.value);
}}
placeholder="e.g. 25"
disabled={isSubmitting}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="wg-preshared-key">
Preshared Key (optional)
</Label>
<Input
id="wg-preshared-key"
value={wireGuardForm.presharedKey}
onChange={(e) => {
updateWireGuard("presharedKey", e.target.value);
}}
placeholder="Base64-encoded preshared key"
disabled={isSubmitting}
/>
</div>
</div>
</>
)}
</div>
+6 -23
View File
@@ -52,17 +52,6 @@ const detectVpnType = (
endpoint: endpointMatch ? endpointMatch[1] : null,
};
}
if (
lowerFilename.endsWith(".ovpn") ||
(content.includes("remote ") &&
(content.includes("client") || content.includes("dev tun")))
) {
const remoteMatch = content.match(/remote\s+(\S+)(?:\s+(\d+))?/i);
const endpoint = remoteMatch
? `${remoteMatch[1]}${remoteMatch[2] ? `:${remoteMatch[2]}` : ""}`
: null;
return { isVpn: true, type: "OpenVPN", endpoint };
}
return { isVpn: false, type: null, endpoint: null };
};
@@ -105,7 +94,7 @@ export function VpnImportDialog({ isOpen, onClose }: VpnImportDialogProps) {
endpoint: detection.endpoint,
});
const baseName = filename
.replace(/\.(conf|ovpn)$/i, "")
.replace(/\.conf$/i, "")
.replace(/_/g, " ")
.replace(/-/g, " ");
setVpnName(baseName || `${detection.type} VPN`);
@@ -132,13 +121,11 @@ export function VpnImportDialog({ isOpen, onClose }: VpnImportDialogProps) {
e.preventDefault();
setIsDragOver(false);
const files = Array.from(e.dataTransfer.files);
const validFile = files.find(
(f) => f.name.endsWith(".conf") || f.name.endsWith(".ovpn"),
);
const validFile = files.find((f) => f.name.endsWith(".conf"));
if (validFile) {
handleFileRead(validFile);
} else {
toast.error("Please drop a .conf or .ovpn file");
toast.error("Please drop a WireGuard .conf file");
}
},
[handleFileRead],
@@ -200,7 +187,7 @@ export function VpnImportDialog({ isOpen, onClose }: VpnImportDialogProps) {
<DialogTitle>Import VPN Config</DialogTitle>
<DialogDescription>
{step === "dropzone" &&
"Import a WireGuard (.conf) or OpenVPN (.ovpn) configuration file"}
"Import a WireGuard (.conf) configuration file"}
{step === "vpn-preview" && "Review the VPN configuration to import"}
{step === "vpn-result" && "VPN import completed"}
</DialogDescription>
@@ -230,16 +217,12 @@ export function VpnImportDialog({ isOpen, onClose }: VpnImportDialogProps) {
>
<LuUpload className="w-10 h-10 text-muted-foreground mb-4" />
<p className="text-sm text-muted-foreground text-center">
Drop a VPN config file here or click to browse
<br />
<span className="text-xs">
(.conf for WireGuard, .ovpn for OpenVPN)
</span>
Drop a WireGuard .conf file here or click to browse
</p>
<input
id="vpn-file-input"
type="file"
accept=".conf,.ovpn"
accept=".conf"
className="hidden"
onChange={(e) => {
const file = e.target.files?.[0];
-10
View File
@@ -812,16 +812,6 @@
"button": "Clone"
}
},
"vpnForm": {
"dependencies": {
"openVpnMissingTitle": "OpenVPN is not installed",
"openVpnMissingDescription": "You can save this configuration, but Donut Browser cannot connect it until OpenVPN is installed on this device.",
"openVpnAdapterMissingTitle": "OpenVPN adapter is missing",
"openVpnAdapterMissingDescription": "OpenVPN is installed, but no TAP/Wintun/ovpn-dco adapter was found. Repair or reinstall OpenVPN before connecting on Windows.",
"openVpnCheckFailedTitle": "OpenVPN install could not be verified",
"openVpnCheckFailedDescription": "Donut Browser could not inspect the local OpenVPN installation. Repair or reinstall OpenVPN before connecting on Windows."
}
},
"extensions": {
"title": "Extensions",
"description": "Manage browser extensions and extension groups for your profiles.",
-10
View File
@@ -812,16 +812,6 @@
"button": "Clonar"
}
},
"vpnForm": {
"dependencies": {
"openVpnMissingTitle": "OpenVPN no está instalado",
"openVpnMissingDescription": "Puedes guardar esta configuración, pero Donut Browser no podrá conectarse hasta que OpenVPN esté instalado en este dispositivo.",
"openVpnAdapterMissingTitle": "Falta el adaptador de OpenVPN",
"openVpnAdapterMissingDescription": "OpenVPN está instalado, pero no se encontró ningún adaptador TAP/Wintun/ovpn-dco. Repara o reinstala OpenVPN antes de conectarte en Windows.",
"openVpnCheckFailedTitle": "No se pudo verificar la instalación de OpenVPN",
"openVpnCheckFailedDescription": "Donut Browser no pudo inspeccionar la instalación local de OpenVPN. Repara o reinstala OpenVPN antes de conectarte en Windows."
}
},
"extensions": {
"title": "Extensiones",
"description": "Administra extensiones de navegador y grupos de extensiones para tus perfiles.",
-10
View File
@@ -812,16 +812,6 @@
"button": "Cloner"
}
},
"vpnForm": {
"dependencies": {
"openVpnMissingTitle": "OpenVPN n'est pas installé",
"openVpnMissingDescription": "Vous pouvez enregistrer cette configuration, mais Donut Browser ne pourra pas s'y connecter tant qu'OpenVPN n'est pas installé sur cet appareil.",
"openVpnAdapterMissingTitle": "L'adaptateur OpenVPN est manquant",
"openVpnAdapterMissingDescription": "OpenVPN est installé, mais aucun adaptateur TAP/Wintun/ovpn-dco n'a été trouvé. Réparez ou réinstallez OpenVPN avant de vous connecter sous Windows.",
"openVpnCheckFailedTitle": "L'installation d'OpenVPN n'a pas pu être vérifiée",
"openVpnCheckFailedDescription": "Donut Browser n'a pas pu inspecter l'installation locale d'OpenVPN. Réparez ou réinstallez OpenVPN avant de vous connecter sous Windows."
}
},
"extensions": {
"title": "Extensions",
"description": "Gérez les extensions de navigateur et les groupes d'extensions pour vos profils.",
-10
View File
@@ -812,16 +812,6 @@
"button": "複製"
}
},
"vpnForm": {
"dependencies": {
"openVpnMissingTitle": "OpenVPN がインストールされていません",
"openVpnMissingDescription": "この設定は保存できますが、このデバイスに OpenVPN がインストールされるまで Donut Browser では接続できません。",
"openVpnAdapterMissingTitle": "OpenVPN アダプターが見つかりません",
"openVpnAdapterMissingDescription": "OpenVPN はインストールされていますが、TAP/Wintun/ovpn-dco アダプターが見つかりませんでした。Windows で接続する前に OpenVPN を修復または再インストールしてください。",
"openVpnCheckFailedTitle": "OpenVPN のインストールを確認できませんでした",
"openVpnCheckFailedDescription": "Donut Browser はローカルの OpenVPN インストールを確認できませんでした。Windows で接続する前に OpenVPN を修復または再インストールしてください。"
}
},
"extensions": {
"title": "拡張機能",
"description": "プロファイル用のブラウザ拡張機能と拡張機能グループを管理します。",
-10
View File
@@ -812,16 +812,6 @@
"button": "Clonar"
}
},
"vpnForm": {
"dependencies": {
"openVpnMissingTitle": "OpenVPN não está instalado",
"openVpnMissingDescription": "Você pode salvar esta configuração, mas o Donut Browser não poderá se conectar até que o OpenVPN esteja instalado neste dispositivo.",
"openVpnAdapterMissingTitle": "O adaptador do OpenVPN está ausente",
"openVpnAdapterMissingDescription": "O OpenVPN está instalado, mas nenhum adaptador TAP/Wintun/ovpn-dco foi encontrado. Repare ou reinstale o OpenVPN antes de se conectar no Windows.",
"openVpnCheckFailedTitle": "Não foi possível verificar a instalação do OpenVPN",
"openVpnCheckFailedDescription": "O Donut Browser não conseguiu inspecionar a instalação local do OpenVPN. Repare ou reinstale o OpenVPN antes de se conectar no Windows."
}
},
"extensions": {
"title": "Extensões",
"description": "Gerencie extensões de navegador e grupos de extensões para seus perfis.",
-10
View File
@@ -812,16 +812,6 @@
"button": "Клонировать"
}
},
"vpnForm": {
"dependencies": {
"openVpnMissingTitle": "OpenVPN не установлен",
"openVpnMissingDescription": "Вы можете сохранить эту конфигурацию, но Donut Browser не сможет подключиться, пока OpenVPN не будет установлен на этом устройстве.",
"openVpnAdapterMissingTitle": "Отсутствует адаптер OpenVPN",
"openVpnAdapterMissingDescription": "OpenVPN установлен, но адаптер TAP/Wintun/ovpn-dco не найден. Восстановите или переустановите OpenVPN перед подключением в Windows.",
"openVpnCheckFailedTitle": "Не удалось проверить установку OpenVPN",
"openVpnCheckFailedDescription": "Donut Browser не смог проверить локальную установку OpenVPN. Восстановите или переустановите OpenVPN перед подключением в Windows."
}
},
"extensions": {
"title": "Расширения",
"description": "Управляйте расширениями браузера и группами расширений для ваших профилей.",
-10
View File
@@ -812,16 +812,6 @@
"button": "克隆"
}
},
"vpnForm": {
"dependencies": {
"openVpnMissingTitle": "未安装 OpenVPN",
"openVpnMissingDescription": "你现在可以保存这个配置,但在此设备上安装 OpenVPN 之前,Donut Browser 无法连接它。",
"openVpnAdapterMissingTitle": "缺少 OpenVPN 适配器",
"openVpnAdapterMissingDescription": "已安装 OpenVPN,但未找到 TAP/Wintun/ovpn-dco 适配器。在 Windows 上连接前,请修复或重新安装 OpenVPN。",
"openVpnCheckFailedTitle": "无法验证 OpenVPN 安装",
"openVpnCheckFailedDescription": "Donut Browser 无法检查本机 OpenVPN 安装。在 Windows 上连接前,请修复或重新安装 OpenVPN。"
}
},
"extensions": {
"title": "扩展程序",
"description": "管理配置文件的浏览器扩展程序和扩展程序组。",
+1 -1
View File
@@ -667,7 +667,7 @@ export type ProxyParseResult =
| { status: "invalid"; line: string; reason: string };
// VPN types
export type VpnType = "WireGuard" | "OpenVPN";
export type VpnType = "WireGuard";
export interface VpnConfig {
id: string;