From 18b73662582b34970065561dbd0f8098f025d98a Mon Sep 17 00:00:00 2001 From: coolcoala Date: Mon, 14 Jul 2025 01:13:20 +0300 Subject: [PATCH] simplified the proxy import menu --- src/components/profile/profile-viewer.tsx | 358 ++++++++++++---------- src/locales/en.json | 4 +- src/locales/ru.json | 4 +- 3 files changed, 208 insertions(+), 158 deletions(-) diff --git a/src/components/profile/profile-viewer.tsx b/src/components/profile/profile-viewer.tsx index 4f4043e1..640947b2 100644 --- a/src/components/profile/profile-viewer.tsx +++ b/src/components/profile/profile-viewer.tsx @@ -1,13 +1,12 @@ -import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react"; +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 } from "@/services/cmds"; +import { createProfile, patchProfile, importProfile } 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, @@ -35,7 +34,9 @@ import { } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; import { Textarea } from "@/components/ui/textarea"; -import { Loader2 } from "lucide-react"; +import {ClipboardPaste, Loader2, X} from "lucide-react"; +import {readText} from "@tauri-apps/plugin-clipboard-manager"; +import { cn } from "@root/lib/utils"; interface Props { @@ -51,10 +52,16 @@ export const ProfileViewer = forwardRef((props, ref) => const { t } = useTranslation(); const [open, setOpen] = useState(false); const [openType, setOpenType] = useState<"new" | "edit">("new"); - const [loading, setLoading] = useState(false); 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", @@ -75,12 +82,16 @@ export const ProfileViewer = forwardRef((props, 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); }, @@ -88,71 +99,82 @@ export const ProfileViewer = forwardRef((props, ref) => 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 (selfProxy) setValue("option.with_proxy", false); - }, [selfProxy, setValue]); + if (!importUrl) { + setIsUrlValid(true); + setIsCheckingUrl(false); + return; + } + setIsCheckingUrl(true); - useEffect(() => { - if (withProxy) setValue("option.self_proxy", false); - }, [withProxy, setValue]); - - const handleOk = useLockFn( - handleSubmit(async (form) => { - if (form.option?.timeout_seconds) { - form.option.timeout_seconds = +form.option.timeout_seconds; + 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(); + 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(); + 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; - } + 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 isRemote = form.type === "remote"; const isUpdate = openType === "edit"; const isActivating = isUpdate && form.uid === (profiles?.current ?? ""); - const originalOptions = { with_proxy: form.option?.with_proxy, self_proxy: form.option?.self_proxy }; - if (!isRemote) { - if (openType === "new") { - await createProfile(item, fileDataRef.current); - } else { - if (!form.uid) throw new Error("UID not found"); - await patchProfile(form.uid, item); - } + if (openType === "new") { + await createProfile(item, fileDataRef.current); } else { - try { - if (openType === "new") { - await createProfile(item, fileDataRef.current); - } else { - if (!form.uid) throw new Error("UID not found"); - await patchProfile(form.uid, item); - } - } catch (err) { - showNotice("info", t("Profile creation failed, retrying with Clash proxy...")); - const retryItem = { ...item, option: { ...item.option, with_proxy: false, self_proxy: true } }; - if (openType === "new") { - await createProfile(retryItem, fileDataRef.current); - } else { - if (!form.uid) throw new Error("UID not found"); - await patchProfile(form.uid, retryItem); - await patchProfile(form.uid, { option: originalOptions }); - } - showNotice("success", t("Profile creation succeeded with Clash proxy")); - } + if (!form.uid) throw new Error("UID not found"); + await patchProfile(form.uid, item); } setOpen(false); @@ -171,127 +193,151 @@ export const ProfileViewer = forwardRef((props, ref) => return ( - + {openType === "new" ? t("Create Profile") : t("Edit Profile")} -
- { e.preventDefault(); handleOk(); }} className="space-y-4 max-h-[70vh] overflow-y-auto px-1"> - ( - - {t("Type")} - - - - )}/> + {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")} +

+ )} +
- ( - - {t("Name")} - - - - )}/> + +
+ )} - ( - - {t("Descriptions")} - - - - )}/> - {isRemote && ( - <> - ( - - {t("Subscription URL")} -