simplified the proxy import menu
This commit is contained in:
@@ -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 { useLockFn } from "ahooks";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useForm } from "react-hook-form";
|
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 { useProfiles } from "@/hooks/use-profiles";
|
||||||
import { showNotice } from "@/services/noticeService";
|
import { showNotice } from "@/services/noticeService";
|
||||||
import { version } from "@root/package.json";
|
import { version } from "@root/package.json";
|
||||||
|
|
||||||
// --- Новые импорты ---
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -35,7 +34,9 @@ import {
|
|||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
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 {
|
interface Props {
|
||||||
@@ -51,10 +52,16 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>((props, ref) =>
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [openType, setOpenType] = useState<"new" | "edit">("new");
|
const [openType, setOpenType] = useState<"new" | "edit">("new");
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const { profiles } = useProfiles();
|
const { profiles } = useProfiles();
|
||||||
const fileDataRef = useRef<string | null>(null);
|
const fileDataRef = useRef<string | null>(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<IProfileItem>({
|
const form = useForm<IProfileItem>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
type: "remote",
|
type: "remote",
|
||||||
@@ -75,12 +82,16 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>((props, ref) =>
|
|||||||
create: () => {
|
create: () => {
|
||||||
reset({ type: "remote", name: "", desc: "", url: "", option: { with_proxy: false, self_proxy: false, danger_accept_invalid_certs: false } });
|
reset({ type: "remote", name: "", desc: "", url: "", option: { with_proxy: false, self_proxy: false, danger_accept_invalid_certs: false } });
|
||||||
fileDataRef.current = null;
|
fileDataRef.current = null;
|
||||||
|
setImportUrl("");
|
||||||
|
setShowAdvanced(false);
|
||||||
setOpenType("new");
|
setOpenType("new");
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
},
|
},
|
||||||
edit: (item) => {
|
edit: (item) => {
|
||||||
reset(item);
|
reset(item);
|
||||||
fileDataRef.current = null;
|
fileDataRef.current = null;
|
||||||
|
setImportUrl(item.url || "");
|
||||||
|
setShowAdvanced(true);
|
||||||
setOpenType("edit");
|
setOpenType("edit");
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
},
|
},
|
||||||
@@ -88,72 +99,83 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>((props, ref) =>
|
|||||||
|
|
||||||
const selfProxy = watch("option.self_proxy");
|
const selfProxy = watch("option.self_proxy");
|
||||||
const withProxy = watch("option.with_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(() => {
|
useEffect(() => {
|
||||||
if (selfProxy) setValue("option.with_proxy", false);
|
if (!importUrl) {
|
||||||
}, [selfProxy, setValue]);
|
setIsUrlValid(true);
|
||||||
|
setIsCheckingUrl(false);
|
||||||
useEffect(() => {
|
return;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
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();
|
||||||
|
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);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
if (!form.type) throw new Error("`Type` should not be null");
|
if (!form.type) throw new Error("`Type` should not be null");
|
||||||
if (form.type === "remote" && !form.url) {
|
if (form.type === "remote" && !form.url) throw new Error("The URL should not be null");
|
||||||
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.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 name = form.name || `${form.type} file`;
|
||||||
const item = { ...form, name };
|
const item = { ...form, name };
|
||||||
const isRemote = form.type === "remote";
|
|
||||||
const isUpdate = openType === "edit";
|
const isUpdate = openType === "edit";
|
||||||
const isActivating = isUpdate && form.uid === (profiles?.current ?? "");
|
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") {
|
if (openType === "new") {
|
||||||
await createProfile(item, fileDataRef.current);
|
await createProfile(item, fileDataRef.current);
|
||||||
} else {
|
} else {
|
||||||
if (!form.uid) throw new Error("UID not found");
|
if (!form.uid) throw new Error("UID not found");
|
||||||
await patchProfile(form.uid, item);
|
await patchProfile(form.uid, item);
|
||||||
}
|
}
|
||||||
} 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"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
props.onChange(isActivating);
|
props.onChange(isActivating);
|
||||||
@@ -171,113 +193,134 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>((props, ref) =>
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogContent className="sm:max-w-[425px]">
|
<DialogContent className="w-[95vw] sm:max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{openType === "new" ? t("Create Profile") : t("Edit Profile")}</DialogTitle>
|
<DialogTitle>{openType === "new" ? t("Create Profile") : t("Edit Profile")}</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
|
{openType === "new" && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<div className="flex items-center gap-1 flex-grow sm:flex-grow-0">
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder={t("Profile URL")}
|
||||||
|
value={importUrl}
|
||||||
|
onChange={(e) => 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 ? (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
title={t("Clear")}
|
||||||
|
onClick={() => setImportUrl("")}
|
||||||
|
className="h-9 w-9 flex-shrink-0"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
title={t("Paste")}
|
||||||
|
onClick={onCopyLink}
|
||||||
|
className="h-9 w-9 flex-shrink-0"
|
||||||
|
>
|
||||||
|
<ClipboardPaste className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={handleImport}
|
||||||
|
disabled={!importUrl || isCheckingUrl || !isUrlValid || isImporting}
|
||||||
|
className="flex-shrink-0 min-w-[5.5rem]"
|
||||||
|
>
|
||||||
|
{(isCheckingUrl || isImporting) ? (
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
t("Import")
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
{!isUrlValid && importUrl && (
|
||||||
|
<p className="text-sm text-destructive px-1">
|
||||||
|
{t("Please enter a valid URL")}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button variant="outline" onClick={() => setShowAdvanced(!showAdvanced)}>
|
||||||
|
{showAdvanced ? t("Hide Advanced Settings") : t("Show Advanced Settings")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
{(openType === 'edit' || showAdvanced) && (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={e => { e.preventDefault(); handleOk(); }} className="space-y-4 max-h-[70vh] overflow-y-auto px-1">
|
<form onSubmit={e => { e.preventDefault(); handleSaveAdvanced(); }} className="space-y-4 max-h-[60vh] overflow-y-auto px-1 pt-4">
|
||||||
<FormField control={control} name="type" render={({ field }) => (
|
<FormField control={control} name="type" render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>{t("Type")}</FormLabel>
|
<FormLabel>{t("Type")}</FormLabel>
|
||||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
<Select onValueChange={field.onChange} defaultValue={field.value} disabled={openType === "edit"}>
|
||||||
<FormControl><SelectTrigger><SelectValue placeholder="Select type" /></SelectTrigger></FormControl>
|
<FormControl><SelectTrigger><SelectValue /></SelectTrigger></FormControl>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="remote">Remote</SelectItem>
|
<SelectItem value="remote">Remote</SelectItem>
|
||||||
<SelectItem value="local">Local</SelectItem>
|
<SelectItem value="local">Local</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}/>
|
)}/>
|
||||||
|
|
||||||
<FormField control={control} name="name" render={({ field }) => (
|
<FormField control={control} name="name" render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem><FormLabel>{t("Name")}</FormLabel><FormControl><Input placeholder={t("Profile Name")} {...field} /></FormControl></FormItem>
|
||||||
<FormLabel>{t("Name")}</FormLabel>
|
|
||||||
<FormControl><Input placeholder={t("Profile Name")} {...field} /></FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}/>
|
)}/>
|
||||||
|
|
||||||
<FormField control={control} name="desc" render={({ field }) => (
|
<FormField control={control} name="desc" render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem><FormLabel>{t("Descriptions")}</FormLabel><FormControl><Input placeholder={t("Profile Description")} {...field} /></FormControl></FormItem>
|
||||||
<FormLabel>{t("Descriptions")}</FormLabel>
|
|
||||||
<FormControl><Input placeholder={t("Profile Description")} {...field} /></FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}/>
|
)}/>
|
||||||
|
|
||||||
{isRemote && (
|
{isRemote && (
|
||||||
<>
|
|
||||||
<FormField control={control} name="url" render={({ field }) => (
|
<FormField control={control} name="url" render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem><FormLabel>{t("Subscription URL")}</FormLabel><FormControl><Textarea placeholder={t("Leave blank to use the URL above")} {...field} /></FormControl></FormItem>
|
||||||
<FormLabel>{t("Subscription URL")}</FormLabel>
|
|
||||||
<FormControl><Textarea placeholder="https://example.com/profile.yaml" {...field} /></FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}/>
|
)}/>
|
||||||
<FormField control={control} name="option.user_agent" render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>User Agent</FormLabel>
|
|
||||||
<FormControl><Input placeholder={`clash-verge/v${version}`} {...field} /></FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}/>
|
|
||||||
<FormField control={control} name="option.update_interval" render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>{t("Update Interval")}</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Input type="number" placeholder="1440" {...field} onChange={event => field.onChange(parseInt(event.target.value, 10) || 0)} />
|
|
||||||
<span className="text-sm text-muted-foreground">{t("mins")}</span>
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}/>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isLocal && openType === "new" && (
|
{isLocal && openType === "new" && (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>{t("File")}</FormLabel>
|
<FormLabel>{t("File")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl><Input type="file" accept=".yml,.yaml" onChange={(e) => {
|
||||||
<Input type="file" accept=".yml,.yaml" onChange={(e) => {
|
|
||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
setValue("name", form.getValues("name") || file.name);
|
setValue("name", form.getValues("name") || file.name);
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = (event) => {
|
reader.onload = (event) => { fileDataRef.current = event.target?.result as string; };
|
||||||
fileDataRef.current = event.target?.result as string;
|
|
||||||
};
|
|
||||||
reader.readAsText(file);
|
reader.readAsText(file);
|
||||||
}
|
}
|
||||||
}} />
|
}} /></FormControl>
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isRemote && (
|
{isRemote && (
|
||||||
<div className="space-y-4 rounded-md border p-4">
|
<div className="space-y-4 rounded-md border p-4">
|
||||||
|
<FormField control={control} name="option.update_interval" render={({ field }) => (
|
||||||
|
<FormItem><FormLabel>{t("Update Interval (mins)")}</FormLabel><FormControl><Input type="number" placeholder="1440" {...field} onChange={e => field.onChange(+e.target.value)} /></FormControl></FormItem>
|
||||||
|
)}/>
|
||||||
|
<FormField control={control} name="option.user_agent" render={({ field }) => (
|
||||||
|
<FormItem><FormLabel>User Agent</FormLabel><FormControl><Input placeholder={`clash-verge/v${version}`} {...field} /></FormControl></FormItem>
|
||||||
|
)}/>
|
||||||
<FormField control={control} name="option.with_proxy" render={({ field }) => (
|
<FormField control={control} name="option.with_proxy" render={({ field }) => (
|
||||||
<FormItem className="flex flex-row items-center justify-between">
|
<FormItem className="flex items-center justify-between"><FormLabel>{t("Use System Proxy")}</FormLabel><FormControl><Switch checked={field.value} onCheckedChange={field.onChange} /></FormControl></FormItem>
|
||||||
<FormLabel>{t("Use System Proxy")}</FormLabel>
|
|
||||||
<FormControl><Switch checked={field.value} onCheckedChange={field.onChange} /></FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}/>
|
)}/>
|
||||||
<FormField control={control} name="option.self_proxy" render={({ field }) => (
|
<FormField control={control} name="option.self_proxy" render={({ field }) => (
|
||||||
<FormItem className="flex flex-row items-center justify-between">
|
<FormItem className="flex items-center justify-between"><FormLabel>{t("Use Clash Proxy")}</FormLabel><FormControl><Switch checked={field.value} onCheckedChange={field.onChange} /></FormControl></FormItem>
|
||||||
<FormLabel>{t("Use Clash Proxy")}</FormLabel>
|
|
||||||
<FormControl><Switch checked={field.value} onCheckedChange={field.onChange} /></FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}/>
|
)}/>
|
||||||
<FormField control={control} name="option.danger_accept_invalid_certs" render={({ field }) => (
|
<FormField control={control} name="option.danger_accept_invalid_certs" render={({ field }) => (
|
||||||
<FormItem className="flex flex-row items-center justify-between">
|
<FormItem className="flex items-center justify-between"><FormLabel className="text-destructive">{t("Accept Invalid Certs (Danger)")}</FormLabel><FormControl><Switch checked={field.value} onCheckedChange={field.onChange} /></FormControl></FormItem>
|
||||||
<FormLabel className="text-destructive">{t("Accept Invalid Certs (Danger)")}</FormLabel>
|
|
||||||
<FormControl><Switch checked={field.value} onCheckedChange={field.onChange} /></FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}/>
|
)}/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -285,13 +328,16 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>((props, ref) =>
|
|||||||
<button type="submit" className="hidden" />
|
<button type="submit" className="hidden" />
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
)}
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<DialogClose asChild><Button type="button" variant="outline">{t("Cancel")}</Button></DialogClose>
|
<DialogClose asChild><Button type="button" variant="outline">{t("Cancel")}</Button></DialogClose>
|
||||||
<Button type="button" onClick={handleOk} disabled={loading}>
|
{(openType === 'edit' || showAdvanced) && (
|
||||||
|
<Button type="button" onClick={handleSaveAdvanced} disabled={loading}>
|
||||||
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||||
{t("Save")}
|
{t("Save")}
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -656,5 +656,7 @@
|
|||||||
"Expires in": "Expires in {{duration}}",
|
"Expires in": "Expires in {{duration}}",
|
||||||
"Expired": "Expired",
|
"Expired": "Expired",
|
||||||
"Get Started": "Get Started",
|
"Get Started": "Get Started",
|
||||||
"You don't have any profiles yet. Add your first one to begin.": "You don't have any profiles yet.\nAdd your first one to begin."
|
"You don't have any profiles yet. Add your first one to begin.": "You don't have any profiles yet.\nAdd your first one to begin.",
|
||||||
|
"Show Advanced Settings": "Show Advanced Settings",
|
||||||
|
"Hide Advanced Settings": "Hide Advanced Settings"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -608,5 +608,7 @@
|
|||||||
"Expires in": "Истекает через {{duration}}",
|
"Expires in": "Истекает через {{duration}}",
|
||||||
"Expired": "Истекло",
|
"Expired": "Истекло",
|
||||||
"Get Started": "Приступить к работе",
|
"Get Started": "Приступить к работе",
|
||||||
"You don't have any profiles yet. Add your first one to begin.": "У вас еще нет профилей.\nДобавьте свой первый профиль, чтобы начать."
|
"You don't have any profiles yet. Add your first one to begin.": "У вас еще нет профилей.\nДобавьте свой первый профиль, чтобы начать.",
|
||||||
|
"Show Advanced Settings": "Показать дополнительные настройки",
|
||||||
|
"Hide Advanced Settings": "Скрыть дополнительные настройки"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user