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, } 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 { 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 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(() => { try { new URL(importUrl); setIsUrlValid(true); } catch (error) { setIsUrlValid(false); } finally { setIsCheckingUrl(false); } }, 500); return () => { clearTimeout(handler); }; }, [importUrl]); const handleImport = useLockFn(async () => { if (!importUrl) return; setIsImporting(true); try { await importProfile(importUrl); showNotice("success", t("Profile Imported Successfully")); props.onChange(); await enhanceProfiles(); setOpen(false); } catch (err) { 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()}`, ); } } 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("Please enter a valid URL")}

)}
)} {(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")}