import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState, } from "react"; import { useLockFn } from "ahooks"; import { useTranslation } from "react-i18next"; import { useForm } from "react-hook-form"; import { createProfile, patchProfile, importProfile, enhanceProfiles, createProfileFromShareLink, } from "@/services/cmds"; import { useProfiles } from "@/hooks/use-profiles"; import { showNotice } from "@/services/noticeService"; import { version } from "@root/package.json"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogClose, } from "@/components/ui/dialog"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; import { Textarea } from "@/components/ui/textarea"; import { ClipboardPaste, Loader2, X } from "lucide-react"; import { readText } from "@tauri-apps/plugin-clipboard-manager"; import { cn } from "@root/lib/utils"; interface Props { onChange: (isActivating?: boolean) => void; } export interface ProfileViewerRef { create: () => void; edit: (item: IProfileItem) => void; } export const ProfileViewer = forwardRef( (props, ref) => { const { t } = useTranslation(); const [open, setOpen] = useState(false); const [openType, setOpenType] = useState<"new" | "edit">("new"); const { profiles } = useProfiles(); const fileDataRef = useRef(null); const [showAdvanced, setShowAdvanced] = useState(false); const [importUrl, setImportUrl] = useState(""); const [isUrlValid, setIsUrlValid] = useState(true); const [isCheckingUrl, setIsCheckingUrl] = useState(false); const [isImporting, setIsImporting] = useState(false); const [loading, setLoading] = useState(false); const [selectedTemplate, setSelectedTemplate] = useState("default"); const form = useForm({ defaultValues: { type: "remote", name: "", desc: "", url: "", option: { with_proxy: false, self_proxy: false, danger_accept_invalid_certs: false, }, }, }); const { control, watch, handleSubmit, reset, setValue } = form; useImperativeHandle(ref, () => ({ create: () => { reset({ type: "remote", name: "", desc: "", url: "", option: { with_proxy: false, self_proxy: false, danger_accept_invalid_certs: false, }, }); fileDataRef.current = null; setImportUrl(""); setShowAdvanced(false); setOpenType("new"); setOpen(true); }, edit: (item) => { reset(item); fileDataRef.current = null; setImportUrl(item.url || ""); setShowAdvanced(true); setOpenType("edit"); setOpen(true); }, })); const selfProxy = watch("option.self_proxy"); const withProxy = watch("option.with_proxy"); useEffect(() => { if (selfProxy) setValue("option.with_proxy", false); }, [selfProxy, setValue]); useEffect(() => { if (withProxy) setValue("option.self_proxy", false); }, [withProxy, setValue]); useEffect(() => { if (!importUrl) { setIsUrlValid(true); setIsCheckingUrl(false); return; } setIsCheckingUrl(true); const handler = setTimeout(() => { const isValid = /^(https?|vmess|vless|ss|socks|trojan):\/\//.test( importUrl, ); setIsUrlValid(isValid); setIsCheckingUrl(false); }, 500); return () => { clearTimeout(handler); }; }, [importUrl]); const handleImport = useLockFn(async () => { if (!importUrl || !isUrlValid) return; setIsImporting(true); const isShareLink = /^(vmess|vless|ss|socks|trojan):\/\//.test(importUrl); try { if (isShareLink) { await createProfileFromShareLink(importUrl, selectedTemplate); showNotice("success", t("Profile created from link successfully")); } else { await importProfile(importUrl); showNotice("success", t("Profile Imported Successfully")); } props.onChange(); await enhanceProfiles(); setOpen(false); } catch (err: any) { const errorMessage = typeof err === "string" ? err : err.message || String(err); const lowerErrorMessage = errorMessage.toLowerCase(); if ( lowerErrorMessage.includes("device") || lowerErrorMessage.includes("устройств") ) { window.dispatchEvent( new CustomEvent("show-hwid-error", { detail: errorMessage }), ); } else if (!isShareLink && errorMessage.includes("failed to fetch")) { showNotice("info", t("Import failed, retrying with Clash proxy...")); try { await importProfile(importUrl, { with_proxy: false, self_proxy: true, }); showNotice("success", t("Profile Imported with Clash proxy")); props.onChange(); await enhanceProfiles(); setOpen(false); } catch (retryErr: any) { showNotice( "error", `${t("Import failed even with Clash proxy")}: ${retryErr?.message || retryErr.toString()}`, ); } } else { showNotice("error", errorMessage); } } finally { setIsImporting(false); } }); const onCopyLink = async () => { const text = await readText(); if (text) setImportUrl(text); }; const handleSaveAdvanced = useLockFn( handleSubmit(async (formData) => { const form = { ...formData, url: formData.url || importUrl }; setLoading(true); try { if (!form.type) throw new Error("`Type` should not be null"); if (form.type === "remote" && !form.url) throw new Error("The URL should not be null"); if (form.option?.update_interval) form.option.update_interval = +form.option.update_interval; else delete form.option?.update_interval; if (form.option?.user_agent === "") delete form.option.user_agent; const name = form.name || `${form.type} file`; const item = { ...form, name }; const isUpdate = openType === "edit"; const isActivating = isUpdate && form.uid === (profiles?.current ?? ""); if (openType === "new") { await createProfile(item, fileDataRef.current); } else { if (!form.uid) throw new Error("UID not found"); await patchProfile(form.uid, item); } setOpen(false); props.onChange(isActivating); } catch (err: any) { showNotice("error", err.message || err.toString()); } finally { setLoading(false); } }), ); const formType = watch("type"); const isRemote = formType === "remote"; const isLocal = formType === "local"; return ( {openType === "new" ? t("Create Profile") : t("Edit Profile")} {openType === "new" && (
setImportUrl(e.target.value)} disabled={isImporting} className={cn( "h-9 min-w-[200px] flex-grow sm:w-65", !isUrlValid && "border-destructive focus-visible:ring-destructive", )} /> {importUrl ? ( ) : ( )}
{!isUrlValid && importUrl && (

{t("Invalid Profile URL")}

)}
{/^(vmess|vless|ss|socks|trojan):\/\//.test(importUrl) && (
)}
)} {(openType === "edit" || showAdvanced) && (
{ e.preventDefault(); handleSaveAdvanced(); }} className="space-y-4 max-h-[60vh] overflow-y-auto px-1 pt-4" > ( {t("Type")} )} /> ( {t("Name")} )} /> ( {t("Descriptions")} )} /> {isRemote && ( ( {t("Subscription URL")}