code formatting with prettier

This commit is contained in:
coolcoala
2025-07-14 05:23:32 +03:00
parent eb1e4fe0c3
commit 5cdc5075f8
58 changed files with 5163 additions and 1846 deletions

View File

@@ -10,11 +10,17 @@ import { saveWebdavConfig, createWebdavBackup } from "@/services/cmds";
import { showNotice } from "@/services/noticeService";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Eye, EyeOff } from "lucide-react";
import { cn } from "@root/lib/utils";
export interface BackupConfigViewerProps {
onBackupSuccess: () => Promise<void>;
onSaveSuccess: () => Promise<void>;
@@ -24,23 +30,29 @@ export interface BackupConfigViewerProps {
}
export const BackupConfigViewer = memo(
({ onBackupSuccess, onSaveSuccess, onRefresh, onInit, setLoading }: BackupConfigViewerProps) => {
({
onBackupSuccess,
onSaveSuccess,
onRefresh,
onInit,
setLoading,
}: BackupConfigViewerProps) => {
const { t } = useTranslation();
const { verge } = useVerge();
const { webdav_url, webdav_username, webdav_password } = verge || {};
const [showPassword, setShowPassword] = useState(false);
const form = useForm<IWebDavConfig>({
defaultValues: { url: '', username: '', password: '' },
defaultValues: { url: "", username: "", password: "" },
});
// Синхронизируем форму с данными из verge
useEffect(() => {
form.reset({
url: webdav_url,
username: webdav_username,
password: webdav_password
});
form.reset({
url: webdav_url,
username: webdav_username,
password: webdav_password,
});
}, [webdav_url, webdav_username, webdav_password, form.reset]);
const { register, handleSubmit, watch, getValues } = form;
@@ -48,47 +60,77 @@ export const BackupConfigViewer = memo(
const username = watch("username");
const password = watch("password");
const webdavChanged = webdav_url !== url || webdav_username !== username || webdav_password !== password;
const webdavChanged =
webdav_url !== url ||
webdav_username !== username ||
webdav_password !== password;
const checkForm = () => {
const values = getValues();
if (!values.url) { showNotice("error", t("WebDAV URL Required")); throw new Error("URL Required"); }
if (!isValidUrl(values.url)) { showNotice("error", t("Invalid WebDAV URL")); throw new Error("Invalid URL"); }
if (!values.username) { showNotice("error", t("Username Required")); throw new Error("Username Required"); }
if (!values.password) { showNotice("error", t("Password Required")); throw new Error("Password Required"); }
const values = getValues();
if (!values.url) {
showNotice("error", t("WebDAV URL Required"));
throw new Error("URL Required");
}
if (!isValidUrl(values.url)) {
showNotice("error", t("Invalid WebDAV URL"));
throw new Error("Invalid URL");
}
if (!values.username) {
showNotice("error", t("Username Required"));
throw new Error("Username Required");
}
if (!values.password) {
showNotice("error", t("Password Required"));
throw new Error("Password Required");
}
};
const save = useLockFn(async (data: IWebDavConfig) => {
try { checkForm(); } catch { return; }
try {
setLoading(true);
await saveWebdavConfig(data.url.trim(), data.username.trim(), data.password);
showNotice("success", t("WebDAV Config Saved"));
await onSaveSuccess();
} catch (error) {
showNotice("error", t("WebDAV Config Save Failed", { error }), 3000);
} finally {
setLoading(false);
}
try {
checkForm();
} catch {
return;
}
try {
setLoading(true);
await saveWebdavConfig(
data.url.trim(),
data.username.trim(),
data.password,
);
showNotice("success", t("WebDAV Config Saved"));
await onSaveSuccess();
} catch (error) {
showNotice("error", t("WebDAV Config Save Failed", { error }), 3000);
} finally {
setLoading(false);
}
});
const handleBackup = useLockFn(async () => {
try { checkForm(); } catch { return; }
try {
setLoading(true);
await createWebdavBackup();
showNotice("success", t("Backup Created"));
await onBackupSuccess();
} catch (error) {
showNotice("error", t("Backup Failed", { error }));
} finally {
setLoading(false);
}
try {
checkForm();
} catch {
return;
}
try {
setLoading(true);
await createWebdavBackup();
showNotice("success", t("Backup Created"));
await onBackupSuccess();
} catch (error) {
showNotice("error", t("Backup Failed", { error }));
} finally {
setLoading(false);
}
});
return (
<Form {...form}>
<form onSubmit={e => e.preventDefault()} className="flex flex-col sm:flex-row gap-4">
<form
onSubmit={(e) => e.preventDefault()}
className="flex flex-col sm:flex-row gap-4"
>
{/* Левая часть: поля ввода */}
<div className="flex-1 space-y-4">
<FormField
@@ -97,7 +139,9 @@ export const BackupConfigViewer = memo(
render={({ field }) => (
<FormItem>
<FormLabel>{t("WebDAV Server URL")}</FormLabel>
<FormControl><Input {...field} /></FormControl>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
@@ -109,7 +153,9 @@ export const BackupConfigViewer = memo(
render={({ field }) => (
<FormItem>
<FormLabel>{t("Username")}</FormLabel>
<FormControl><Input {...field} /></FormControl>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
@@ -122,7 +168,11 @@ export const BackupConfigViewer = memo(
<FormLabel>{t("Password")}</FormLabel>
<div className="relative">
<FormControl>
<Input type={showPassword ? "text" : "password"} {...field} className="pr-10" />
<Input
type={showPassword ? "text" : "password"}
{...field}
className="pr-10"
/>
</FormControl>
<Button
type="button"
@@ -131,7 +181,11 @@ export const BackupConfigViewer = memo(
className="absolute top-0 right-0 h-full px-3"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
{showPassword ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</Button>
</div>
<FormMessage />
@@ -144,7 +198,11 @@ export const BackupConfigViewer = memo(
{/* Правая часть: кнопки действий */}
<div className="flex sm:flex-col gap-2">
{webdavChanged || !webdav_url ? (
<Button type="button" className="w-full h-full" onClick={handleSubmit(save)}>
<Button
type="button"
className="w-full h-full"
onClick={handleSubmit(save)}
>
{t("Save")}
</Button>
) : (
@@ -152,7 +210,12 @@ export const BackupConfigViewer = memo(
<Button type="button" className="w-full" onClick={handleBackup}>
{t("Backup")}
</Button>
<Button type="button" variant="outline" className="w-full" onClick={onRefresh}>
<Button
type="button"
variant="outline"
className="w-full"
onClick={onRefresh}
>
{t("Refresh")}
</Button>
</>
@@ -161,5 +224,5 @@ export const BackupConfigViewer = memo(
</form>
</Form>
);
}
},
);

View File

@@ -16,10 +16,14 @@ import {
TableRow,
} from "@/components/ui/table";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Trash2, History } from "lucide-react";
export type BackupFile = IWebDavFile & {
platform: string;
backup_time: dayjs.Dayjs;
@@ -31,7 +35,10 @@ export const DEFAULT_ROWS_PER_PAGE = 5;
export interface BackupTableViewerProps {
datasource: BackupFile[];
page: number;
onPageChange: (event: React.MouseEvent<HTMLButtonElement> | null, page: number) => void;
onPageChange: (
event: React.MouseEvent<HTMLButtonElement> | null,
page: number,
) => void;
total: number;
onRefresh: () => Promise<void>;
}
@@ -118,9 +125,14 @@ function MacIcon(props: SVGProps<SVGSVGElement>) {
);
}
export const BackupTableViewer = memo(
({ datasource, page, onPageChange, total, onRefresh }: BackupTableViewerProps) => {
({
datasource,
page,
onPageChange,
total,
onRefresh,
}: BackupTableViewerProps) => {
const { t } = useTranslation();
const handleDelete = useLockFn(async (filename: string) => {
@@ -151,50 +163,66 @@ export const BackupTableViewer = memo(
<TableRow key={index}>
<TableCell className="font-medium">
<div className="flex items-center gap-2">
{file.platform === "windows" ? ( <WindowsIcon className="h-5 w-5" />
) : file.platform === "linux" ? ( <LinuxIcon className="h-5 w-5" />
) : ( <MacIcon className="h-5 w-5" /> )}
<span>{file.filename}</span>
{file.platform === "windows" ? (
<WindowsIcon className="h-5 w-5" />
) : file.platform === "linux" ? (
<LinuxIcon className="h-5 w-5" />
) : (
<MacIcon className="h-5 w-5" />
)}
<span>{file.filename}</span>
</div>
</TableCell>
<TableCell className="text-center">{file.backup_time.fromNow()}</TableCell>
<TableCell className="text-center">
{file.backup_time.fromNow()}
</TableCell>
<TableCell>
<div className="flex items-center justify-end gap-2">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-destructive hover:text-destructive"
onClick={async () => {
const confirmed = window.confirm(t("Confirm to delete this backup file?"));
if (confirmed) await handleDelete(file.filename);
}}
>
<Trash2 className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent><p>{t("Delete Backup")}</p></TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
disabled={!file.allow_apply}
onClick={async () => {
const confirmed = window.confirm(t("Confirm to restore this backup file?"));
if (confirmed) await handleRestore(file.filename);
}}
>
<History className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent><p>{t("Restore Backup")}</p></TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-destructive hover:text-destructive"
onClick={async () => {
const confirmed = window.confirm(
t("Confirm to delete this backup file?"),
);
if (confirmed)
await handleDelete(file.filename);
}}
>
<Trash2 className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t("Delete Backup")}</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
disabled={!file.allow_apply}
onClick={async () => {
const confirmed = window.confirm(
t("Confirm to restore this backup file?"),
);
if (confirmed)
await handleRestore(file.filename);
}}
>
<History className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t("Restore Backup")}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</TableCell>
</TableRow>
@@ -210,27 +238,27 @@ export const BackupTableViewer = memo(
</Table>
{/* Новая кастомная пагинация */}
<div className="flex items-center justify-end space-x-2 p-2 border-t border-border">
<div className="flex-1 text-sm text-muted-foreground">
{t("Total")} {total}
</div>
<Button
variant="outline"
size="sm"
onClick={(e) => onPageChange(e, page - 1)}
disabled={page === 0}
>
{t("Previous")}
</Button>
<Button
variant="outline"
size="sm"
onClick={(e) => onPageChange(e, page + 1)}
disabled={(page + 1) * DEFAULT_ROWS_PER_PAGE >= total}
>
{t("Next")}
</Button>
<div className="flex-1 text-sm text-muted-foreground">
{t("Total")} {total}
</div>
<Button
variant="outline"
size="sm"
onClick={(e) => onPageChange(e, page - 1)}
disabled={page === 0}
>
{t("Previous")}
</Button>
<Button
variant="outline"
size="sm"
onClick={(e) => onPageChange(e, page + 1)}
disabled={(page + 1) * DEFAULT_ROWS_PER_PAGE >= total}
>
{t("Next")}
</Button>
</div>
</div>
);
}
},
);

View File

@@ -1,4 +1,10 @@
import { forwardRef, useImperativeHandle, useState, useCallback, useEffect } from "react";
import {
forwardRef,
useImperativeHandle,
useState,
useCallback,
useEffect,
} from "react";
import { useTranslation } from "react-i18next";
import dayjs, { Dayjs } from "dayjs";
import customParseFormat from "dayjs/plugin/customParseFormat";
@@ -6,11 +12,22 @@ import { useLockFn } from "ahooks";
// Новые импорты
import { listWebDavBackup } from "@/services/cmds";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogClose } from "@/components/ui/dialog";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
DialogClose,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { BaseLoadingOverlay } from "@/components/base"; // Наш рефакторенный компонент
import { BackupTableViewer, BackupFile, DEFAULT_ROWS_PER_PAGE } from "./backup-table-viewer"; // Наш рефакторенный компонент
import {
BackupTableViewer,
BackupFile,
DEFAULT_ROWS_PER_PAGE,
} from "./backup-table-viewer"; // Наш рефакторенный компонент
import { BackupConfigViewer } from "./backup-config-viewer"; // Наш рефакторенный компонент
dayjs.extend(customParseFormat);
@@ -118,7 +135,9 @@ export const BackupViewer = forwardRef<DialogRef>((props, ref) => {
<DialogFooter>
<DialogClose asChild>
<Button type="button" variant="outline">{t("Close")}</Button>
<Button type="button" variant="outline">
{t("Close")}
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>

View File

@@ -24,7 +24,6 @@ import { changeClashCore, restartCore } from "@/services/cmds";
import { closeAllConnections, upgradeCore } from "@/services/api";
import { showNotice } from "@/services/noticeService";
// Константы и интерфейсы
const VALID_CORE = [
{ name: "Mihomo", core: "verge-mihomo", chip: "Release Version" },
@@ -107,12 +106,28 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
<div className="flex justify-between items-center">
<DialogTitle>{t("Clash Core")}</DialogTitle>
<div className="flex items-center gap-2">
<Button size="sm" disabled={restarting || changingCore !== null} onClick={onUpgrade}>
{upgrading ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Replace className="mr-2 h-4 w-4" />}
<Button
size="sm"
disabled={restarting || changingCore !== null}
onClick={onUpgrade}
>
{upgrading ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<Replace className="mr-2 h-4 w-4" />
)}
{t("Upgrade")}
</Button>
<Button size="sm" disabled={upgrading || changingCore !== null} onClick={onRestart}>
{restarting ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <RotateCw className="mr-2 h-4 w-4" />}
<Button
size="sm"
disabled={upgrading || changingCore !== null}
onClick={onRestart}
>
{restarting ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<RotateCw className="mr-2 h-4 w-4" />
)}
{t("Restart")}
</Button>
</div>
@@ -133,8 +148,10 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
onClick={() => !isDisabled && onCoreChange(each.core)}
className={cn(
"flex items-center justify-between p-3 rounded-md transition-colors",
isDisabled ? "cursor-not-allowed opacity-50" : "cursor-pointer hover:bg-accent",
isSelected && "bg-accent"
isDisabled
? "cursor-not-allowed opacity-50"
: "cursor-pointer hover:bg-accent",
isSelected && "bg-accent",
)}
>
<div>
@@ -145,7 +162,9 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
{isChanging ? (
<Loader2 className="h-5 w-5 animate-spin" />
) : (
<Badge variant={isSelected ? "default" : "secondary"}>{t(each.chip)}</Badge>
<Badge variant={isSelected ? "default" : "secondary"}>
{t(each.chip)}
</Badge>
)}
</div>
</div>
@@ -155,7 +174,9 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
<DialogFooter>
<DialogClose asChild>
<Button type="button" variant="outline">{t("Close")}</Button>
<Button type="button" variant="outline">
{t("Close")}
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>

View File

@@ -18,7 +18,12 @@ import {
DialogFooter,
DialogClose,
} from "@/components/ui/dialog";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Shuffle, Loader2 } from "lucide-react";
const OS = getSystem();
@@ -28,7 +33,8 @@ interface ClashPortViewerRef {
close: () => void;
}
const generateRandomPort = () => Math.floor(Math.random() * (65535 - 1025 + 1)) + 1025;
const generateRandomPort = () =>
Math.floor(Math.random() * (65535 - 1025 + 1)) + 1025;
// Компонент для одной строки настроек порта
const PortSettingRow = ({
@@ -85,7 +91,9 @@ const PortSettingRow = ({
<Shuffle className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent><p>{t("Random Port")}</p></TooltipContent>
<TooltipContent>
<p>{t("Random Port")}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<Switch
@@ -98,7 +106,6 @@ const PortSettingRow = ({
);
};
export const ClashPortViewer = forwardRef<ClashPortViewerRef>((props, ref) => {
const { t } = useTranslation();
const { clashInfo, patchInfo } = useClashInfo();
@@ -171,7 +178,7 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((props, ref) => {
const clashConfig = {
"mixed-port": mixedPort,
"socks-port": socksEnabled ? socksPort : 0,
"port": httpEnabled ? httpPort : 0,
port: httpEnabled ? httpPort : 0,
"redir-port": redirEnabled ? redirPort : 0,
"tproxy-port": tproxyEnabled ? tproxyPort : 0,
};
@@ -199,19 +206,53 @@ export const ClashPortViewer = forwardRef<ClashPortViewerRef>((props, ref) => {
</DialogHeader>
<div className="py-4 space-y-1">
<PortSettingRow label={t("Mixed Port")} port={mixedPort} setPort={setMixedPort} isEnabled={true} isFixed={true} />
<PortSettingRow label={t("Socks Port")} port={socksPort} setPort={setSocksPort} isEnabled={socksEnabled} setIsEnabled={setSocksEnabled} />
<PortSettingRow label={t("Http Port")} port={httpPort} setPort={setHttpPort} isEnabled={httpEnabled} setIsEnabled={setHttpEnabled} />
<PortSettingRow
label={t("Mixed Port")}
port={mixedPort}
setPort={setMixedPort}
isEnabled={true}
isFixed={true}
/>
<PortSettingRow
label={t("Socks Port")}
port={socksPort}
setPort={setSocksPort}
isEnabled={socksEnabled}
setIsEnabled={setSocksEnabled}
/>
<PortSettingRow
label={t("Http Port")}
port={httpPort}
setPort={setHttpPort}
isEnabled={httpEnabled}
setIsEnabled={setHttpEnabled}
/>
{OS !== "windows" && (
<PortSettingRow label={t("Redir Port")} port={redirPort} setPort={setRedirPort} isEnabled={redirEnabled} setIsEnabled={setRedirEnabled} />
<PortSettingRow
label={t("Redir Port")}
port={redirPort}
setPort={setRedirPort}
isEnabled={redirEnabled}
setIsEnabled={setRedirEnabled}
/>
)}
{OS === "linux" && (
<PortSettingRow label={t("Tproxy Port")} port={tproxyPort} setPort={setTproxyPort} isEnabled={tproxyEnabled} setIsEnabled={setTproxyEnabled} />
<PortSettingRow
label={t("Tproxy Port")}
port={tproxyPort}
setPort={setTproxyPort}
isEnabled={tproxyEnabled}
setIsEnabled={setTproxyEnabled}
/>
)}
</div>
<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={onSave} disabled={loading}>
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{t("Save")}

View File

@@ -17,10 +17,14 @@ import {
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Copy, Loader2 } from "lucide-react";
export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
@@ -55,24 +59,31 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
showNotice("success", t("Configuration saved successfully"));
setOpen(false);
} catch (err: any) {
showNotice("error", err.message || t("Failed to save configuration"), 4000);
showNotice(
"error",
err.message || t("Failed to save configuration"),
4000,
);
} finally {
setIsSaving(false);
}
});
const handleCopyToClipboard = useLockFn(async (text: string, type: string) => {
try {
await navigator.clipboard.writeText(text);
// --- ИЗМЕНЕНИЕ: Используем showNotice вместо Snackbar ---
const message = type === "controller"
? t("Controller address copied to clipboard")
: t("Secret copied to clipboard");
showNotice("success", message);
} catch (err) {
showNotice("error", t("Failed to copy"));
}
});
const handleCopyToClipboard = useLockFn(
async (text: string, type: string) => {
try {
await navigator.clipboard.writeText(text);
// --- ИЗМЕНЕНИЕ: Используем showNotice вместо Snackbar ---
const message =
type === "controller"
? t("Controller address copied to clipboard")
: t("Secret copied to clipboard");
showNotice("success", message);
} catch (err) {
showNotice("error", t("Failed to copy"));
}
},
);
return (
<Dialog open={open} onOpenChange={setOpen}>
@@ -83,7 +94,9 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
<div className="space-y-4 py-4">
<div className="grid gap-2">
<Label htmlFor="controller-address">{t("External Controller")}</Label>
<Label htmlFor="controller-address">
{t("External Controller")}
</Label>
<div className="flex items-center gap-2">
<Input
id="controller-address"
@@ -95,11 +108,20 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" onClick={() => handleCopyToClipboard(controller, "controller")} disabled={isSaving}>
<Button
variant="ghost"
size="icon"
onClick={() =>
handleCopyToClipboard(controller, "controller")
}
disabled={isSaving}
>
<Copy className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent><p>{t("Copy to clipboard")}</p></TooltipContent>
<TooltipContent>
<p>{t("Copy to clipboard")}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
@@ -118,11 +140,18 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" onClick={() => handleCopyToClipboard(secret, "secret")} disabled={isSaving}>
<Button
variant="ghost"
size="icon"
onClick={() => handleCopyToClipboard(secret, "secret")}
disabled={isSaving}
>
<Copy className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent><p>{t("Copy to clipboard")}</p></TooltipContent>
<TooltipContent>
<p>{t("Copy to clipboard")}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
@@ -131,7 +160,9 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
<DialogFooter>
<DialogClose asChild>
<Button type="button" variant="outline">{t("Cancel")}</Button>
<Button type="button" variant="outline">
{t("Cancel")}
</Button>
</DialogClose>
<Button type="button" onClick={onSave} disabled={isSaving}>
{isSaving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}

View File

@@ -34,7 +34,6 @@ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Separator } from "@/components/ui/separator";
import { AlertTriangle, RotateCcw, Code } from "lucide-react";
const DEFAULT_DNS_CONFIG = {
enable: true,
listen: ":53",
@@ -46,15 +45,44 @@ const DEFAULT_DNS_CONFIG = {
"use-hosts": false,
"use-system-hosts": false,
ipv6: true,
"fake-ip-filter": ["*.lan", "*.local", "*.arpa", "time.*.com", "ntp.*.com", "+.market.xiaomi.com", "localhost.ptlogin2.qq.com", "*.msftncsi.com", "www.msftconnecttest.com"],
"default-nameserver": ["system", "223.6.6.6", "8.8.8.8", "2400:3200::1", "2001:4860:4860::8888"],
nameserver: ["8.8.8.8", "https://doh.pub/dns-query", "https://dns.alidns.com/dns-query"],
"fake-ip-filter": [
"*.lan",
"*.local",
"*.arpa",
"time.*.com",
"ntp.*.com",
"+.market.xiaomi.com",
"localhost.ptlogin2.qq.com",
"*.msftncsi.com",
"www.msftconnecttest.com",
],
"default-nameserver": [
"system",
"223.6.6.6",
"8.8.8.8",
"2400:3200::1",
"2001:4860:4860::8888",
],
nameserver: [
"8.8.8.8",
"https://doh.pub/dns-query",
"https://dns.alidns.com/dns-query",
],
fallback: [],
"nameserver-policy": {},
"proxy-server-nameserver": ["https://doh.pub/dns-query", "https://dns.alidns.com/dns-query", "tls://223.5.5.5"],
"proxy-server-nameserver": [
"https://doh.pub/dns-query",
"https://dns.alidns.com/dns-query",
"tls://223.5.5.5",
],
"direct-nameserver": [],
"direct-nameserver-follow-policy": false,
"fallback-filter": { geoip: true, "geoip-code": "CN", ipcidr: ["240.0.0.0/4", "0.0.0.0/32"], domain: ["+.google.com", "+.facebook.com", "+.youtube.com"] },
"fallback-filter": {
geoip: true,
"geoip-code": "CN",
ipcidr: ["240.0.0.0/4", "0.0.0.0/32"],
domain: ["+.google.com", "+.facebook.com", "+.youtube.com"],
},
};
interface Props {
@@ -63,74 +91,193 @@ interface Props {
// Функция-помощник, которая всегда возвращает состояние в правильном формате (со строками)
const formatValues = (config: any = {}): any => {
const dnsConfig = config.dns || {};
const hostsConfig = config.hosts || {};
const formatList = (arr: any[] | undefined = []): string => (arr || []).join(", ");
const formatHosts = (hosts: any): string => !hosts ? "" : Object.entries(hosts).map(([domain, value]) => `${domain}=${Array.isArray(value) ? value.join(';') : value}`).join(", ");
const formatNameserverPolicy = (policy: any): string => !policy ? "" : Object.entries(policy).map(([domain, servers]) => `${domain}=${Array.isArray(servers) ? servers.join(';') : servers}`).join(", ");
const dnsConfig = config.dns || {};
const hostsConfig = config.hosts || {};
const formatList = (arr: any[] | undefined = []): string =>
(arr || []).join(", ");
const formatHosts = (hosts: any): string =>
!hosts
? ""
: Object.entries(hosts)
.map(
([domain, value]) =>
`${domain}=${Array.isArray(value) ? value.join(";") : value}`,
)
.join(", ");
const formatNameserverPolicy = (policy: any): string =>
!policy
? ""
: Object.entries(policy)
.map(
([domain, servers]) =>
`${domain}=${Array.isArray(servers) ? servers.join(";") : servers}`,
)
.join(", ");
const enhancedMode = dnsConfig["enhanced-mode"];
const validEnhancedMode = ["fake-ip", "redir-host"].includes(enhancedMode) ? enhancedMode : DEFAULT_DNS_CONFIG["enhanced-mode"];
const fakeIpFilterMode = dnsConfig["fake-ip-filter-mode"];
const validFakeIpFilterMode = ["blacklist", "whitelist"].includes(fakeIpFilterMode) ? fakeIpFilterMode : DEFAULT_DNS_CONFIG["fake-ip-filter-mode"];
const enhancedMode = dnsConfig["enhanced-mode"];
const validEnhancedMode = ["fake-ip", "redir-host"].includes(enhancedMode)
? enhancedMode
: DEFAULT_DNS_CONFIG["enhanced-mode"];
const fakeIpFilterMode = dnsConfig["fake-ip-filter-mode"];
const validFakeIpFilterMode = ["blacklist", "whitelist"].includes(
fakeIpFilterMode,
)
? fakeIpFilterMode
: DEFAULT_DNS_CONFIG["fake-ip-filter-mode"];
return {
enable: dnsConfig.enable ?? DEFAULT_DNS_CONFIG.enable,
listen: dnsConfig.listen ?? DEFAULT_DNS_CONFIG.listen,
enhancedMode: validEnhancedMode,
fakeIpRange: dnsConfig["fake-ip-range"] ?? DEFAULT_DNS_CONFIG["fake-ip-range"],
fakeIpFilterMode: validFakeIpFilterMode,
preferH3: dnsConfig["prefer-h3"] ?? DEFAULT_DNS_CONFIG["prefer-h3"],
respectRules: dnsConfig["respect-rules"] ?? DEFAULT_DNS_CONFIG["respect-rules"],
useHosts: dnsConfig["use-hosts"] ?? DEFAULT_DNS_CONFIG["use-hosts"],
useSystemHosts: dnsConfig["use-system-hosts"] ?? DEFAULT_DNS_CONFIG["use-system-hosts"],
ipv6: dnsConfig.ipv6 ?? DEFAULT_DNS_CONFIG.ipv6,
fakeIpFilter: formatList(dnsConfig["fake-ip-filter"] ?? DEFAULT_DNS_CONFIG["fake-ip-filter"]),
defaultNameserver: formatList(dnsConfig["default-nameserver"] ?? DEFAULT_DNS_CONFIG["default-nameserver"]),
nameserver: formatList(dnsConfig.nameserver ?? DEFAULT_DNS_CONFIG.nameserver),
fallback: formatList(dnsConfig.fallback ?? DEFAULT_DNS_CONFIG.fallback),
proxyServerNameserver: formatList(dnsConfig["proxy-server-nameserver"] ?? DEFAULT_DNS_CONFIG["proxy-server-nameserver"]),
directNameserver: formatList(dnsConfig["direct-nameserver"] ?? DEFAULT_DNS_CONFIG["direct-nameserver"]),
directNameserverFollowPolicy: dnsConfig["direct-nameserver-follow-policy"] ?? DEFAULT_DNS_CONFIG["direct-nameserver-follow-policy"],
fallbackGeoip: dnsConfig["fallback-filter"]?.geoip ?? DEFAULT_DNS_CONFIG["fallback-filter"].geoip,
fallbackGeoipCode: dnsConfig["fallback-filter"]?.["geoip-code"] ?? DEFAULT_DNS_CONFIG["fallback-filter"]["geoip-code"],
fallbackIpcidr: formatList(dnsConfig["fallback-filter"]?.ipcidr) ?? formatList(DEFAULT_DNS_CONFIG["fallback-filter"].ipcidr),
fallbackDomain: formatList(dnsConfig["fallback-filter"]?.domain) ?? formatList(DEFAULT_DNS_CONFIG["fallback-filter"].domain),
nameserverPolicy: formatNameserverPolicy(dnsConfig["nameserver-policy"]) || "",
hosts: formatHosts(hostsConfig) || "",
};
return {
enable: dnsConfig.enable ?? DEFAULT_DNS_CONFIG.enable,
listen: dnsConfig.listen ?? DEFAULT_DNS_CONFIG.listen,
enhancedMode: validEnhancedMode,
fakeIpRange:
dnsConfig["fake-ip-range"] ?? DEFAULT_DNS_CONFIG["fake-ip-range"],
fakeIpFilterMode: validFakeIpFilterMode,
preferH3: dnsConfig["prefer-h3"] ?? DEFAULT_DNS_CONFIG["prefer-h3"],
respectRules:
dnsConfig["respect-rules"] ?? DEFAULT_DNS_CONFIG["respect-rules"],
useHosts: dnsConfig["use-hosts"] ?? DEFAULT_DNS_CONFIG["use-hosts"],
useSystemHosts:
dnsConfig["use-system-hosts"] ?? DEFAULT_DNS_CONFIG["use-system-hosts"],
ipv6: dnsConfig.ipv6 ?? DEFAULT_DNS_CONFIG.ipv6,
fakeIpFilter: formatList(
dnsConfig["fake-ip-filter"] ?? DEFAULT_DNS_CONFIG["fake-ip-filter"],
),
defaultNameserver: formatList(
dnsConfig["default-nameserver"] ??
DEFAULT_DNS_CONFIG["default-nameserver"],
),
nameserver: formatList(
dnsConfig.nameserver ?? DEFAULT_DNS_CONFIG.nameserver,
),
fallback: formatList(dnsConfig.fallback ?? DEFAULT_DNS_CONFIG.fallback),
proxyServerNameserver: formatList(
dnsConfig["proxy-server-nameserver"] ??
DEFAULT_DNS_CONFIG["proxy-server-nameserver"],
),
directNameserver: formatList(
dnsConfig["direct-nameserver"] ?? DEFAULT_DNS_CONFIG["direct-nameserver"],
),
directNameserverFollowPolicy:
dnsConfig["direct-nameserver-follow-policy"] ??
DEFAULT_DNS_CONFIG["direct-nameserver-follow-policy"],
fallbackGeoip:
dnsConfig["fallback-filter"]?.geoip ??
DEFAULT_DNS_CONFIG["fallback-filter"].geoip,
fallbackGeoipCode:
dnsConfig["fallback-filter"]?.["geoip-code"] ??
DEFAULT_DNS_CONFIG["fallback-filter"]["geoip-code"],
fallbackIpcidr:
formatList(dnsConfig["fallback-filter"]?.ipcidr) ??
formatList(DEFAULT_DNS_CONFIG["fallback-filter"].ipcidr),
fallbackDomain:
formatList(dnsConfig["fallback-filter"]?.domain) ??
formatList(DEFAULT_DNS_CONFIG["fallback-filter"].domain),
nameserverPolicy:
formatNameserverPolicy(dnsConfig["nameserver-policy"]) || "",
hosts: formatHosts(hostsConfig) || "",
};
};
export const DnsViewer = forwardRef<DialogRef, Props>(({ onSave }, ref) => {
const { t } = useTranslation();
const themeMode = useThemeMode();
const [open, setOpen] = useState(false);
const [visualization, setVisualization] = useState(true);
const [values, setValues] = useState(() => formatValues({ dns: DEFAULT_DNS_CONFIG, hosts: {} }));
const [values, setValues] = useState(() =>
formatValues({ dns: DEFAULT_DNS_CONFIG, hosts: {} }),
);
const [yamlContent, setYamlContent] = useState("");
const [prevData, setPrevData] = useState("");
const parseList = (str: string = ""): string[] => str ? str.split(",").map(s => s.trim()).filter(Boolean) : [];
const parseHosts = (str: string): Record<string, any> => str.split(",").reduce((acc, item) => { const parts = item.trim().split("="); if (parts.length >= 2) { const domain = parts[0].trim(); const valueStr = parts.slice(1).join("=").trim(); acc[domain] = valueStr.includes(";") ? valueStr.split(";").map(s => s.trim()).filter(Boolean) : valueStr; } return acc; }, {} as Record<string, any>);
const parseNameserverPolicy = (str: string): Record<string, any> => str.split(",").reduce((acc, item) => { const parts = item.trim().split("="); if (parts.length >= 2) { const domain = parts[0].trim(); const serversStr = parts.slice(1).join("=").trim(); acc[domain] = serversStr.includes(";") ? serversStr.split(";").map(s => s.trim()).filter(Boolean) : serversStr; } return acc; }, {} as Record<string, any>);
const parseList = (str: string = ""): string[] =>
str
? str
.split(",")
.map((s) => s.trim())
.filter(Boolean)
: [];
const parseHosts = (str: string): Record<string, any> =>
str.split(",").reduce(
(acc, item) => {
const parts = item.trim().split("=");
if (parts.length >= 2) {
const domain = parts[0].trim();
const valueStr = parts.slice(1).join("=").trim();
acc[domain] = valueStr.includes(";")
? valueStr
.split(";")
.map((s) => s.trim())
.filter(Boolean)
: valueStr;
}
return acc;
},
{} as Record<string, any>,
);
const parseNameserverPolicy = (str: string): Record<string, any> =>
str.split(",").reduce(
(acc, item) => {
const parts = item.trim().split("=");
if (parts.length >= 2) {
const domain = parts[0].trim();
const serversStr = parts.slice(1).join("=").trim();
acc[domain] = serversStr.includes(";")
? serversStr
.split(";")
.map((s) => s.trim())
.filter(Boolean)
: serversStr;
}
return acc;
},
{} as Record<string, any>,
);
const generateDnsConfig = () => {
const dnsConfig: any = { enable: values.enable, listen: values.listen, "enhanced-mode": values.enhancedMode, "fake-ip-range": values.fakeIpRange, "fake-ip-filter-mode": values.fakeIpFilterMode, "prefer-h3": values.preferH3, "respect-rules": values.respectRules, "use-hosts": values.useHosts, "use-system-hosts": values.useSystemHosts, ipv6: values.ipv6, "fake-ip-filter": parseList(values.fakeIpFilter), "default-nameserver": parseList(values.defaultNameserver), nameserver: parseList(values.nameserver), "direct-nameserver-follow-policy": values.directNameserverFollowPolicy, "fallback-filter": { geoip: values.fallbackGeoip, "geoip-code": values.fallbackGeoipCode, ipcidr: parseList(values.fallbackIpcidr), domain: parseList(values.fallbackDomain) }};
const dnsConfig: any = {
enable: values.enable,
listen: values.listen,
"enhanced-mode": values.enhancedMode,
"fake-ip-range": values.fakeIpRange,
"fake-ip-filter-mode": values.fakeIpFilterMode,
"prefer-h3": values.preferH3,
"respect-rules": values.respectRules,
"use-hosts": values.useHosts,
"use-system-hosts": values.useSystemHosts,
ipv6: values.ipv6,
"fake-ip-filter": parseList(values.fakeIpFilter),
"default-nameserver": parseList(values.defaultNameserver),
nameserver: parseList(values.nameserver),
"direct-nameserver-follow-policy": values.directNameserverFollowPolicy,
"fallback-filter": {
geoip: values.fallbackGeoip,
"geoip-code": values.fallbackGeoipCode,
ipcidr: parseList(values.fallbackIpcidr),
domain: parseList(values.fallbackDomain),
},
};
if (values.fallback) dnsConfig["fallback"] = parseList(values.fallback);
const policy = parseNameserverPolicy(values.nameserverPolicy);
if (Object.keys(policy).length > 0) dnsConfig["nameserver-policy"] = policy;
if (values.proxyServerNameserver) dnsConfig["proxy-server-nameserver"] = parseList(values.proxyServerNameserver);
if (values.directNameserver) dnsConfig["direct-nameserver"] = parseList(values.directNameserver);
if (values.proxyServerNameserver)
dnsConfig["proxy-server-nameserver"] = parseList(
values.proxyServerNameserver,
);
if (values.directNameserver)
dnsConfig["direct-nameserver"] = parseList(values.directNameserver);
return dnsConfig;
};
const updateYamlFromValues = () => {
const config: Record<string, any> = {};
const dnsConfig = generateDnsConfig();
if (Object.keys(dnsConfig).length > 0) { config.dns = dnsConfig; }
if (Object.keys(dnsConfig).length > 0) {
config.dns = dnsConfig;
}
const hosts = parseHosts(values.hosts);
if (Object.keys(hosts).length > 0) { config.hosts = hosts; }
if (Object.keys(hosts).length > 0) {
config.hosts = hosts;
}
setYamlContent(yaml.dump(config, { forceQuotes: true }));
};
@@ -162,20 +309,30 @@ export const DnsViewer = forwardRef<DialogRef, Props>(({ onSave }, ref) => {
try {
let finalConfig: Record<string, any>;
if (visualization) {
finalConfig = { dns: generateDnsConfig(), hosts: parseHosts(values.hosts) };
finalConfig = {
dns: generateDnsConfig(),
hosts: parseHosts(values.hosts),
};
} else {
const parsed = yaml.load(yamlContent);
if (typeof parsed !== "object" || parsed === null) throw new Error(t("Invalid configuration"));
if (typeof parsed !== "object" || parsed === null)
throw new Error(t("Invalid configuration"));
finalConfig = parsed as Record<string, any>;
}
const currentData = yaml.dump(finalConfig, { forceQuotes: true });
await invoke("save_dns_config", { dnsConfig: finalConfig });
const [isValid, errorMsg] = await invoke<[boolean, string]>("validate_dns_config", {});
const [isValid, errorMsg] = await invoke<[boolean, string]>(
"validate_dns_config",
{},
);
if (!isValid) {
const cleanErrorMsg = errorMsg.split(/msg="([^"]+)"/)[1] || errorMsg;
showNotice("error", t("DNS configuration error") + ": " + cleanErrorMsg);
showNotice(
"error",
t("DNS configuration error") + ": " + cleanErrorMsg,
);
return;
}
@@ -187,21 +344,32 @@ export const DnsViewer = forwardRef<DialogRef, Props>(({ onSave }, ref) => {
}
});
const handleChange = (field: keyof typeof values) => (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement> | string) => {
const value = typeof e === 'string' ? e : (e.target.type === "checkbox" ? (e.target as any).checked : e.target.value);
setValues((prev: any) => ({ ...prev, [field]: value }));
};
const handleChange =
(field: keyof typeof values) =>
(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement> | string) => {
const value =
typeof e === "string"
? e
: e.target.type === "checkbox"
? (e.target as any).checked
: e.target.value;
setValues((prev: any) => ({ ...prev, [field]: value }));
};
const handleSwitchChange = (field: keyof typeof values) => (checked: boolean) => {
setValues((prev: any) => ({ ...prev, [field]: checked }));
};
const handleSwitchChange =
(field: keyof typeof values) => (checked: boolean) => {
setValues((prev: any) => ({ ...prev, [field]: checked }));
};
useEffect(() => {
if (visualization && open) updateYamlFromValues();
}, [values, visualization, open]);
useImperativeHandle(ref, () => ({
open: () => { setOpen(true); initDnsConfig(); },
open: () => {
setOpen(true);
initDnsConfig();
},
close: () => setOpen(false),
}));
@@ -214,8 +382,18 @@ export const DnsViewer = forwardRef<DialogRef, Props>(({ onSave }, ref) => {
<div className="flex flex-wrap items-center justify-between gap-x-4 gap-y-2 pr-12">
<DialogTitle>{t("DNS Overwrite")}</DialogTitle>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={resetToDefaults}><RotateCcw className="mr-2 h-4 w-4"/>{t("Reset to Default")}</Button>
<Button variant="secondary" size="sm" onClick={() => setVisualization(prev => !prev)}><Code className="mr-2 h-4 w-4"/>{visualization ? t("Advanced") : t("Visualization")}</Button>
<Button variant="outline" size="sm" onClick={resetToDefaults}>
<RotateCcw className="mr-2 h-4 w-4" />
{t("Reset to Default")}
</Button>
<Button
variant="secondary"
size="sm"
onClick={() => setVisualization((prev) => !prev)}
>
<Code className="mr-2 h-4 w-4" />
{visualization ? t("Advanced") : t("Visualization")}
</Button>
</div>
</div>
{/* --- КОНЕЦ ИЗМЕНЕНИЙ --- */}
@@ -224,61 +402,289 @@ export const DnsViewer = forwardRef<DialogRef, Props>(({ onSave }, ref) => {
<div className="flex-1 min-h-0 py-4">
{visualization ? (
<div className="h-full pr-4 -mr-4 space-y-6 overflow-y-auto">
<Alert variant="destructive" className="bg-amber-500/10 border-amber-500/50 text-amber-700 dark:text-amber-400">
<Alert
variant="destructive"
className="bg-amber-500/10 border-amber-500/50 text-amber-700 dark:text-amber-400"
>
<AlertTriangle className="h-4 w-4 !text-amber-500" />
<AlertTitle>{t("Warning")}</AlertTitle>
<AlertDescription>{t("DNS Settings Warning")}</AlertDescription>
</Alert>
<div className="space-y-4">
<h4 className="font-semibold">{t("DNS Settings")}</h4>
<div className="flex items-center justify-between"><Label htmlFor="dns-enable">{t("Enable DNS")}</Label><Switch id="dns-enable" checked={values.enable} onCheckedChange={handleSwitchChange("enable")} /></div>
<div className="grid gap-2"><Label htmlFor="dns-listen">{t("DNS Listen")}</Label><Input id="dns-listen" value={values.listen || ''} onChange={handleChange("listen")} /></div>
<div className="grid gap-2"><Label>{t("Enhanced Mode")}</Label><Select value={values.enhancedMode} onValueChange={handleChange("enhancedMode")}><SelectTrigger><SelectValue/></SelectTrigger><SelectContent><SelectItem value="fake-ip">fake-ip</SelectItem><SelectItem value="redir-host">redir-host</SelectItem></SelectContent></Select></div>
<div className="grid gap-2"><Label htmlFor="dns-fake-ip-range">{t("Fake IP Range")}</Label><Input id="dns-fake-ip-range" value={values.fakeIpRange || ''} onChange={handleChange("fakeIpRange")} /></div>
<div className="grid gap-2"><Label>{t("Fake IP Filter Mode")}</Label><Select value={values.fakeIpFilterMode} onValueChange={handleChange("fakeIpFilterMode")}><SelectTrigger><SelectValue/></SelectTrigger><SelectContent><SelectItem value="blacklist">blacklist</SelectItem><SelectItem value="whitelist">whitelist</SelectItem></SelectContent></Select></div>
<div className="flex items-center justify-between"><Label>{t("IPv6")}</Label><Switch checked={values.ipv6} onCheckedChange={handleSwitchChange("ipv6")} /></div>
<div className="flex items-center justify-between"><Label>{t("Prefer H3")}</Label><Switch checked={values.preferH3} onCheckedChange={handleSwitchChange("preferH3")} /></div>
<div className="flex items-center justify-between"><Label>{t("Respect Rules")}</Label><Switch checked={values.respectRules} onCheckedChange={handleSwitchChange("respectRules")} /></div>
<div className="flex items-center justify-between"><Label>{t("Use Hosts")}</Label><Switch checked={values.useHosts} onCheckedChange={handleSwitchChange("useHosts")} /></div>
<div className="flex items-center justify-between"><Label>{t("Use System Hosts")}</Label><Switch checked={values.useSystemHosts} onCheckedChange={handleSwitchChange("useSystemHosts")} /></div>
<div className="flex items-center justify-between"><Label>{t("Direct Nameserver Follow Policy")}</Label><Switch checked={values.directNameserverFollowPolicy} onCheckedChange={handleSwitchChange("directNameserverFollowPolicy")} /></div>
<div className="grid gap-2"><Label htmlFor="dns-default-nameserver">{t("Default Nameserver")}</Label><Textarea id="dns-default-nameserver" value={values.defaultNameserver || ''} onChange={handleChange("defaultNameserver")} rows={3} /></div>
<div className="grid gap-2"><Label htmlFor="dns-nameserver">{t("Nameserver")}</Label><Textarea id="dns-nameserver" value={values.nameserver || ''} onChange={handleChange("nameserver")} rows={3} /></div>
<div className="grid gap-2"><Label htmlFor="dns-fallback">{t("Fallback")}</Label><Textarea id="dns-fallback" value={values.fallback || ''} onChange={handleChange("fallback")} rows={3} /></div>
<div className="grid gap-2"><Label htmlFor="dns-proxy-server">{t("Proxy Server Nameserver")}</Label><Textarea id="dns-proxy-server" value={values.proxyServerNameserver || ''} onChange={handleChange("proxyServerNameserver")} rows={3} /></div>
<div className="grid gap-2"><Label htmlFor="dns-direct-server">{t("Direct Nameserver")}</Label><Textarea id="dns-direct-server" value={values.directNameserver || ''} onChange={handleChange("directNameserver")} rows={3} /></div>
<div className="grid gap-2"><Label htmlFor="dns-fake-ip-filter">{t("Fake IP Filter")}</Label><Textarea id="dns-fake-ip-filter" value={values.fakeIpFilter || ''} onChange={handleChange("fakeIpFilter")} rows={3} /></div>
<div className="grid gap-2"><Label htmlFor="dns-policy">{t("Nameserver Policy")}</Label><Textarea id="dns-policy" value={values.nameserverPolicy || ''} onChange={handleChange("nameserverPolicy")} rows={3} /></div>
<h4 className="font-semibold">{t("DNS Settings")}</h4>
<div className="flex items-center justify-between">
<Label htmlFor="dns-enable">{t("Enable DNS")}</Label>
<Switch
id="dns-enable"
checked={values.enable}
onCheckedChange={handleSwitchChange("enable")}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="dns-listen">{t("DNS Listen")}</Label>
<Input
id="dns-listen"
value={values.listen || ""}
onChange={handleChange("listen")}
/>
</div>
<div className="grid gap-2">
<Label>{t("Enhanced Mode")}</Label>
<Select
value={values.enhancedMode}
onValueChange={handleChange("enhancedMode")}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="fake-ip">fake-ip</SelectItem>
<SelectItem value="redir-host">redir-host</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid gap-2">
<Label htmlFor="dns-fake-ip-range">
{t("Fake IP Range")}
</Label>
<Input
id="dns-fake-ip-range"
value={values.fakeIpRange || ""}
onChange={handleChange("fakeIpRange")}
/>
</div>
<div className="grid gap-2">
<Label>{t("Fake IP Filter Mode")}</Label>
<Select
value={values.fakeIpFilterMode}
onValueChange={handleChange("fakeIpFilterMode")}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="blacklist">blacklist</SelectItem>
<SelectItem value="whitelist">whitelist</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center justify-between">
<Label>{t("IPv6")}</Label>
<Switch
checked={values.ipv6}
onCheckedChange={handleSwitchChange("ipv6")}
/>
</div>
<div className="flex items-center justify-between">
<Label>{t("Prefer H3")}</Label>
<Switch
checked={values.preferH3}
onCheckedChange={handleSwitchChange("preferH3")}
/>
</div>
<div className="flex items-center justify-between">
<Label>{t("Respect Rules")}</Label>
<Switch
checked={values.respectRules}
onCheckedChange={handleSwitchChange("respectRules")}
/>
</div>
<div className="flex items-center justify-between">
<Label>{t("Use Hosts")}</Label>
<Switch
checked={values.useHosts}
onCheckedChange={handleSwitchChange("useHosts")}
/>
</div>
<div className="flex items-center justify-between">
<Label>{t("Use System Hosts")}</Label>
<Switch
checked={values.useSystemHosts}
onCheckedChange={handleSwitchChange("useSystemHosts")}
/>
</div>
<div className="flex items-center justify-between">
<Label>{t("Direct Nameserver Follow Policy")}</Label>
<Switch
checked={values.directNameserverFollowPolicy}
onCheckedChange={handleSwitchChange(
"directNameserverFollowPolicy",
)}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="dns-default-nameserver">
{t("Default Nameserver")}
</Label>
<Textarea
id="dns-default-nameserver"
value={values.defaultNameserver || ""}
onChange={handleChange("defaultNameserver")}
rows={3}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="dns-nameserver">{t("Nameserver")}</Label>
<Textarea
id="dns-nameserver"
value={values.nameserver || ""}
onChange={handleChange("nameserver")}
rows={3}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="dns-fallback">{t("Fallback")}</Label>
<Textarea
id="dns-fallback"
value={values.fallback || ""}
onChange={handleChange("fallback")}
rows={3}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="dns-proxy-server">
{t("Proxy Server Nameserver")}
</Label>
<Textarea
id="dns-proxy-server"
value={values.proxyServerNameserver || ""}
onChange={handleChange("proxyServerNameserver")}
rows={3}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="dns-direct-server">
{t("Direct Nameserver")}
</Label>
<Textarea
id="dns-direct-server"
value={values.directNameserver || ""}
onChange={handleChange("directNameserver")}
rows={3}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="dns-fake-ip-filter">
{t("Fake IP Filter")}
</Label>
<Textarea
id="dns-fake-ip-filter"
value={values.fakeIpFilter || ""}
onChange={handleChange("fakeIpFilter")}
rows={3}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="dns-policy">{t("Nameserver Policy")}</Label>
<Textarea
id="dns-policy"
value={values.nameserverPolicy || ""}
onChange={handleChange("nameserverPolicy")}
rows={3}
/>
</div>
</div>
<Separator />
<div className="space-y-4">
<h4 className="font-semibold">{t("Fallback Filter Settings")}</h4>
<div className="flex items-center justify-between"><Label htmlFor="fallback-geoip">{t("GeoIP Filtering")}</Label><Switch id="fallback-geoip" checked={values.fallbackGeoip} onCheckedChange={handleSwitchChange("fallbackGeoip")} /></div>
<div className="grid gap-2"><Label htmlFor="fallback-geoip-code">{t("GeoIP Code")}</Label><Input id="fallback-geoip-code" value={values.fallbackGeoipCode || ''} onChange={handleChange("fallbackGeoipCode")} /></div>
<div className="grid gap-2"><Label htmlFor="fallback-ip-cidr">{t("Fallback IP CIDR")}</Label><Textarea id="fallback-ip-cidr" value={values.fallbackIpcidr || ''} onChange={handleChange("fallbackIpcidr")} rows={3} /></div>
<div className="grid gap-2"><Label htmlFor="fallback-domain">{t("Fallback Domain")}</Label><Textarea id="fallback-domain" value={values.fallbackDomain || ''} onChange={handleChange("fallbackDomain")} rows={3} /></div>
<h4 className="font-semibold">
{t("Fallback Filter Settings")}
</h4>
<div className="flex items-center justify-between">
<Label htmlFor="fallback-geoip">{t("GeoIP Filtering")}</Label>
<Switch
id="fallback-geoip"
checked={values.fallbackGeoip}
onCheckedChange={handleSwitchChange("fallbackGeoip")}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="fallback-geoip-code">{t("GeoIP Code")}</Label>
<Input
id="fallback-geoip-code"
value={values.fallbackGeoipCode || ""}
onChange={handleChange("fallbackGeoipCode")}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="fallback-ip-cidr">
{t("Fallback IP CIDR")}
</Label>
<Textarea
id="fallback-ip-cidr"
value={values.fallbackIpcidr || ""}
onChange={handleChange("fallbackIpcidr")}
rows={3}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="fallback-domain">
{t("Fallback Domain")}
</Label>
<Textarea
id="fallback-domain"
value={values.fallbackDomain || ""}
onChange={handleChange("fallbackDomain")}
rows={3}
/>
</div>
</div>
<Separator />
<div className="space-y-4">
<h4 className="font-semibold">{t("Hosts Settings")}</h4>
<div className="grid gap-2"><Label htmlFor="hosts-settings">{t("Hosts")}</Label><Textarea id="hosts-settings" value={values.hosts || ''} onChange={handleChange("hosts")} rows={4} /></div>
<h4 className="font-semibold">{t("Hosts Settings")}</h4>
<div className="grid gap-2">
<Label htmlFor="hosts-settings">{t("Hosts")}</Label>
<Textarea
id="hosts-settings"
value={values.hosts || ""}
onChange={handleChange("hosts")}
rows={4}
/>
</div>
</div>
</div>
) : (
<div className="h-full rounded-md border">
<MonacoEditor height="100%" language="yaml" value={yamlContent} theme={themeMode === "light" ? "vs" : "vs-dark"} options={{ tabSize: 2, minimap: { enabled: document.documentElement.clientWidth >= 1500 }, mouseWheelZoom: true, quickSuggestions: { strings: true, comments: true, other: true }, padding: { top: 16 }, fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${getSystem() === "windows" ? ", twemoji mozilla" : ""}`, fontLigatures: false, smoothScrolling: true }} onChange={(value) => setYamlContent(value || "")} />
<MonacoEditor
height="100%"
language="yaml"
value={yamlContent}
theme={themeMode === "light" ? "vs" : "vs-dark"}
options={{
tabSize: 2,
minimap: {
enabled: document.documentElement.clientWidth >= 1500,
},
mouseWheelZoom: true,
quickSuggestions: {
strings: true,
comments: true,
other: true,
},
padding: { top: 16 },
fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${getSystem() === "windows" ? ", twemoji mozilla" : ""}`,
fontLigatures: false,
smoothScrolling: true,
}}
onChange={(value) => setYamlContent(value || "")}
/>
</div>
)}
</div>
<DialogFooter className="mt-4">
<DialogClose asChild><Button type="button" variant="outline">{t("Cancel")}</Button></DialogClose>
<Button type="button" onClick={handleSave}>{t("Save")}</Button>
<DialogClose asChild>
<Button type="button" variant="outline">
{t("Cancel")}
</Button>
</DialogClose>
<Button type="button" onClick={handleSave}>
{t("Save")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>

View File

@@ -9,7 +9,6 @@ import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { X } from "lucide-react";
interface Props {
value: string[];
onChange: (value: string[]) => void;

View File

@@ -47,7 +47,10 @@ export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
verge?.hotkeys?.forEach((text) => {
const [func, key] = text.split(",").map((e) => e.trim());
if (!func || !key) return;
map[func] = key.split("+").map((e) => e.trim()).map((k) => (k === "PLUS" ? "+" : k));
map[func] = key
.split("+")
.map((e) => e.trim())
.map((k) => (k === "PLUS" ? "+" : k));
});
setHotkeyMap(map);
},
@@ -58,7 +61,11 @@ export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
const hotkeys = Object.entries(hotkeyMap)
.map(([func, keys]) => {
if (!func || !keys?.length) return "";
const key = keys.map((k) => k.trim()).filter(Boolean).map((k) => (k === "+" ? "PLUS" : k)).join("+");
const key = keys
.map((k) => k.trim())
.filter(Boolean)
.map((k) => (k === "+" ? "PLUS" : k))
.join("+");
if (!key) return "";
return `${func},${key}`;
})
@@ -110,9 +117,13 @@ export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
<DialogFooter>
<DialogClose asChild>
<Button type="button" variant="outline">{t("Cancel")}</Button>
<Button type="button" variant="outline">
{t("Cancel")}
</Button>
</DialogClose>
<Button type="button" onClick={onSave}>{t("Save")}</Button>
<Button type="button" onClick={onSave}>
{t("Save")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>

View File

@@ -1,4 +1,10 @@
import { forwardRef, useImperativeHandle, useState, useEffect, useCallback } from "react";
import {
forwardRef,
useImperativeHandle,
useState,
useEffect,
useCallback,
} from "react";
import { useTranslation } from "react-i18next";
import { useLockFn } from "ahooks";
import { open as openDialog } from "@tauri-apps/plugin-dialog";
@@ -14,8 +20,21 @@ import { GuardState } from "./guard-state";
import { copyIconFile, getAppDir } from "@/services/cmds";
import { showNotice } from "@/services/noticeService";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogClose } from "@/components/ui/dialog";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
DialogClose,
} from "@/components/ui/dialog";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import getSystem from "@/utils/get-system";
const OS = getSystem();
@@ -28,11 +47,22 @@ const getIcons = async (icon_dir: string, name: string) => {
};
// Наш переиспользуемый компонент для строки настроек
const SettingRow = ({ label, extra, children }: { label: React.ReactNode; extra?: React.ReactNode; children?: React.ReactNode; }) => (
<div className="flex items-center justify-between py-3 border-b border-border last:border-b-0">
<div className="flex items-center gap-2"><p className="text-sm font-medium">{label}</p>{extra && <div className="text-muted-foreground">{extra}</div>}</div>
<div>{children}</div>
const SettingRow = ({
label,
extra,
children,
}: {
label: React.ReactNode;
extra?: React.ReactNode;
children?: React.ReactNode;
}) => (
<div className="flex items-center justify-between py-3 border-b border-border last:border-b-0">
<div className="flex items-center gap-2">
<p className="text-sm font-medium">{label}</p>
{extra && <div className="text-muted-foreground">{extra}</div>}
</div>
<div>{children}</div>
</div>
);
export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
@@ -47,13 +77,22 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
const initIconPath = useCallback(async () => {
const appDir = await getAppDir();
const icon_dir = await join(appDir, "icons");
const { icon_png: common_icon_png, icon_ico: common_icon_ico } = await getIcons(icon_dir, "common");
const { icon_png: sysproxy_icon_png, icon_ico: sysproxy_icon_ico } = await getIcons(icon_dir, "sysproxy");
const { icon_png: tun_icon_png, icon_ico: tun_icon_ico } = await getIcons(icon_dir, "tun");
const { icon_png: common_icon_png, icon_ico: common_icon_ico } =
await getIcons(icon_dir, "common");
const { icon_png: sysproxy_icon_png, icon_ico: sysproxy_icon_ico } =
await getIcons(icon_dir, "sysproxy");
const { icon_png: tun_icon_png, icon_ico: tun_icon_ico } = await getIcons(
icon_dir,
"tun",
);
setCommonIcon(await exists(common_icon_ico) ? common_icon_ico : common_icon_png);
setSysproxyIcon(await exists(sysproxy_icon_ico) ? sysproxy_icon_ico : sysproxy_icon_png);
setTunIcon(await exists(tun_icon_ico) ? tun_icon_ico : tun_icon_png);
setCommonIcon(
(await exists(common_icon_ico)) ? common_icon_ico : common_icon_png,
);
setSysproxyIcon(
(await exists(sysproxy_icon_ico)) ? sysproxy_icon_ico : sysproxy_icon_png,
);
setTunIcon((await exists(tun_icon_ico)) ? tun_icon_ico : tun_icon_png);
}, []);
useEffect(() => {
@@ -73,25 +112,28 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
mutateVerge({ ...verge, ...patch }, false);
};
const handleIconChange = useLockFn(async (type: 'common' | 'sysproxy' | 'tun') => {
const key = `${type}_tray_icon` as keyof IVergeConfig;
if (verge?.[key]) {
const handleIconChange = useLockFn(
async (type: "common" | "sysproxy" | "tun") => {
const key = `${type}_tray_icon` as keyof IVergeConfig;
if (verge?.[key]) {
onChangeData({ [key]: false });
await patchVerge({ [key]: false });
} else {
} else {
const selected = await openDialog({
directory: false, multiple: false,
filters: [{ name: "Tray Icon Image", extensions: ["png", "ico"] }],
directory: false,
multiple: false,
filters: [{ name: "Tray Icon Image", extensions: ["png", "ico"] }],
});
if (selected) {
const path = Array.isArray(selected) ? selected[0] : selected;
await copyIconFile(path, type);
await initIconPath();
onChangeData({ [key]: true });
await patchVerge({ [key]: true });
const path = Array.isArray(selected) ? selected[0] : selected;
await copyIconFile(path, type);
await initIconPath();
onChangeData({ [key]: true });
await patchVerge({ [key]: true });
}
}
});
}
},
);
return (
<Dialog open={open} onOpenChange={setOpen}>
@@ -101,99 +143,196 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
</DialogHeader>
<div className="py-4 space-y-1">
<SettingRow label={t("Traffic Graph")}>
<GuardState value={verge?.traffic_graph ?? true} valueProps="checked" onCatch={onError} onFormat={onSwitchFormat} onChange={(e) => onChangeData({ traffic_graph: e })} onGuard={(e) => patchVerge({ traffic_graph: e })}>
<Switch />
<SettingRow label={t("Traffic Graph")}>
<GuardState
value={verge?.traffic_graph ?? true}
valueProps="checked"
onCatch={onError}
onFormat={onSwitchFormat}
onChange={(e) => onChangeData({ traffic_graph: e })}
onGuard={(e) => patchVerge({ traffic_graph: e })}
>
<Switch />
</GuardState>
</SettingRow>
<SettingRow label={t("Memory Usage")}>
<GuardState
value={verge?.enable_memory_usage ?? true}
valueProps="checked"
onCatch={onError}
onFormat={onSwitchFormat}
onChange={(e) => onChangeData({ enable_memory_usage: e })}
onGuard={(e) => patchVerge({ enable_memory_usage: e })}
>
<Switch />
</GuardState>
</SettingRow>
<SettingRow label={t("Proxy Group Icon")}>
<GuardState
value={verge?.enable_group_icon ?? true}
valueProps="checked"
onCatch={onError}
onFormat={onSwitchFormat}
onChange={(e) => onChangeData({ enable_group_icon: e })}
onGuard={(e) => patchVerge({ enable_group_icon: e })}
>
<Switch />
</GuardState>
</SettingRow>
<SettingRow
label={t("Hover Jump Navigator")}
extra={<TooltipIcon tooltip={t("Hover Jump Navigator Info")} />}
>
<GuardState
value={verge?.enable_hover_jump_navigator ?? true}
valueProps="checked"
onCatch={onError}
onFormat={onSwitchFormat}
onChange={(e) => onChangeData({ enable_hover_jump_navigator: e })}
onGuard={(e) => patchVerge({ enable_hover_jump_navigator: e })}
>
<Switch />
</GuardState>
</SettingRow>
<SettingRow label={t("Nav Icon")}>
<GuardState
value={verge?.menu_icon ?? "monochrome"}
onCatch={onError}
onFormat={(v) => v}
onChange={(e) => onChangeData({ menu_icon: e })}
onGuard={(e) => patchVerge({ menu_icon: e })}
>
{/* --- НАЧАЛО ИЗМЕНЕНИЙ 1 --- */}
<Select
onValueChange={(value) =>
onChangeData({ menu_icon: value as any })
}
value={verge?.menu_icon}
>
{/* --- КОНЕЦ ИЗМЕНЕНИЙ 1 --- */}
<SelectTrigger className="w-40 h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="monochrome">{t("Monochrome")}</SelectItem>
<SelectItem value="colorful">{t("Colorful")}</SelectItem>
<SelectItem value="disable">{t("Disable")}</SelectItem>
</SelectContent>
</Select>
</GuardState>
</SettingRow>
{OS === "macos" && (
<>
<SettingRow label={t("Tray Icon")}>
<GuardState
value={verge?.tray_icon ?? "monochrome"}
onCatch={onError}
onFormat={(v) => v}
onChange={(e) => onChangeData({ tray_icon: e })}
onGuard={(e) => patchVerge({ tray_icon: e })}
>
{/* --- НАЧАЛО ИЗМЕНЕНИЙ 2 --- */}
<Select
onValueChange={(value) =>
onChangeData({ tray_icon: value as any })
}
value={verge?.tray_icon}
>
{/* --- КОНЕЦ ИЗМЕНЕНИЙ 2 --- */}
<SelectTrigger className="w-40 h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="monochrome">
{t("Monochrome")}
</SelectItem>
<SelectItem value="colorful">{t("Colorful")}</SelectItem>
</SelectContent>
</Select>
</GuardState>
</SettingRow>
</SettingRow>
<SettingRow label={t("Memory Usage")}>
<GuardState value={verge?.enable_memory_usage ?? true} valueProps="checked" onCatch={onError} onFormat={onSwitchFormat} onChange={(e) => onChangeData({ enable_memory_usage: e })} onGuard={(e) => patchVerge({ enable_memory_usage: e })}>
<Switch />
<SettingRow label={t("Enable Tray Icon")}>
<GuardState
value={verge?.enable_tray_icon ?? true}
valueProps="checked"
onCatch={onError}
onFormat={onSwitchFormat}
onChange={(e) => onChangeData({ enable_tray_icon: e })}
onGuard={(e) => patchVerge({ enable_tray_icon: e })}
>
<Switch />
</GuardState>
</SettingRow>
</SettingRow>
</>
)}
<SettingRow label={t("Proxy Group Icon")}>
<GuardState value={verge?.enable_group_icon ?? true} valueProps="checked" onCatch={onError} onFormat={onSwitchFormat} onChange={(e) => onChangeData({ enable_group_icon: e })} onGuard={(e) => patchVerge({ enable_group_icon: e })}>
<Switch />
</GuardState>
</SettingRow>
<SettingRow label={t("Common Tray Icon")}>
<Button
variant="outline"
size="sm"
className="h-8"
onClick={() => handleIconChange("common")}
>
{verge?.common_tray_icon && commonIcon && (
<img
src={convertFileSrc(commonIcon)}
className="h-5 mr-2"
alt="common tray icon"
/>
)}
{verge?.common_tray_icon ? t("Clear") : t("Browse")}
</Button>
</SettingRow>
<SettingRow label={t("Hover Jump Navigator")} extra={<TooltipIcon tooltip={t("Hover Jump Navigator Info")} />}>
<GuardState value={verge?.enable_hover_jump_navigator ?? true} valueProps="checked" onCatch={onError} onFormat={onSwitchFormat} onChange={(e) => onChangeData({ enable_hover_jump_navigator: e })} onGuard={(e) => patchVerge({ enable_hover_jump_navigator: e })}>
<Switch />
</GuardState>
</SettingRow>
<SettingRow label={t("System Proxy Tray Icon")}>
<Button
variant="outline"
size="sm"
className="h-8"
onClick={() => handleIconChange("sysproxy")}
>
{verge?.sysproxy_tray_icon && sysproxyIcon && (
<img
src={convertFileSrc(sysproxyIcon)}
className="h-5 mr-2"
alt="system proxy tray icon"
/>
)}
{verge?.sysproxy_tray_icon ? t("Clear") : t("Browse")}
</Button>
</SettingRow>
<SettingRow label={t("Nav Icon")}>
<GuardState value={verge?.menu_icon ?? "monochrome"} onCatch={onError} onFormat={v => v} onChange={(e) => onChangeData({ menu_icon: e })} onGuard={(e) => patchVerge({ menu_icon: e })}>
{/* --- НАЧАЛО ИЗМЕНЕНИЙ 1 --- */}
<Select
onValueChange={(value) => onChangeData({ menu_icon: value as any })}
value={verge?.menu_icon}
>
{/* --- КОНЕЦ ИЗМЕНЕНИЙ 1 --- */}
<SelectTrigger className="w-40 h-8"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="monochrome">{t("Monochrome")}</SelectItem>
<SelectItem value="colorful">{t("Colorful")}</SelectItem>
<SelectItem value="disable">{t("Disable")}</SelectItem>
</SelectContent>
</Select>
</GuardState>
</SettingRow>
{OS === "macos" && (
<>
<SettingRow label={t("Tray Icon")}>
<GuardState value={verge?.tray_icon ?? "monochrome"} onCatch={onError} onFormat={v => v} onChange={(e) => onChangeData({ tray_icon: e })} onGuard={(e) => patchVerge({ tray_icon: e })}>
{/* --- НАЧАЛО ИЗМЕНЕНИЙ 2 --- */}
<Select
onValueChange={(value) => onChangeData({ tray_icon: value as any })}
value={verge?.tray_icon}
>
{/* --- КОНЕЦ ИЗМЕНЕНИЙ 2 --- */}
<SelectTrigger className="w-40 h-8"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="monochrome">{t("Monochrome")}</SelectItem>
<SelectItem value="colorful">{t("Colorful")}</SelectItem>
</SelectContent>
</Select>
</GuardState>
</SettingRow>
<SettingRow label={t("Enable Tray Icon")}>
<GuardState value={verge?.enable_tray_icon ?? true} valueProps="checked" onCatch={onError} onFormat={onSwitchFormat} onChange={(e) => onChangeData({ enable_tray_icon: e })} onGuard={(e) => patchVerge({ enable_tray_icon: e })}>
<Switch />
</GuardState>
</SettingRow>
</>
)}
<SettingRow label={t("Common Tray Icon")}>
<Button variant="outline" size="sm" className="h-8" onClick={() => handleIconChange('common')}>
{verge?.common_tray_icon && commonIcon && <img src={convertFileSrc(commonIcon)} className="h-5 mr-2" alt="common tray icon" />}
{verge?.common_tray_icon ? t("Clear") : t("Browse")}
</Button>
</SettingRow>
<SettingRow label={t("System Proxy Tray Icon")}>
<Button variant="outline" size="sm" className="h-8" onClick={() => handleIconChange('sysproxy')}>
{verge?.sysproxy_tray_icon && sysproxyIcon && <img src={convertFileSrc(sysproxyIcon)} className="h-5 mr-2" alt="system proxy tray icon"/>}
{verge?.sysproxy_tray_icon ? t("Clear") : t("Browse")}
</Button>
</SettingRow>
<SettingRow label={t("Tun Tray Icon")}>
<Button variant="outline" size="sm" className="h-8" onClick={() => handleIconChange('tun')}>
{verge?.tun_tray_icon && tunIcon && <img src={convertFileSrc(tunIcon)} className="h-5 mr-2" alt="tun mode tray icon"/>}
{verge?.tun_tray_icon ? t("Clear") : t("Browse")}
</Button>
</SettingRow>
<SettingRow label={t("Tun Tray Icon")}>
<Button
variant="outline"
size="sm"
className="h-8"
onClick={() => handleIconChange("tun")}
>
{verge?.tun_tray_icon && tunIcon && (
<img
src={convertFileSrc(tunIcon)}
className="h-5 mr-2"
alt="tun mode tray icon"
/>
)}
{verge?.tun_tray_icon ? t("Clear") : t("Browse")}
</Button>
</SettingRow>
</div>
<DialogFooter>
<DialogClose asChild><Button type="button" variant="outline">{t("Close")}</Button></DialogClose>
<DialogClose asChild>
<Button type="button" variant="outline">
{t("Close")}
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>

View File

@@ -20,19 +20,25 @@ import {
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
// Наш переиспользуемый компонент для строки настроек
const SettingRow = ({ label, extra, children }: { label: React.ReactNode; extra?: React.ReactNode; children?: React.ReactNode; }) => (
<div className="flex items-center justify-between py-3">
<div className="flex items-center gap-2">
<p className="text-sm font-medium">{label}</p>
{extra && <div className="text-muted-foreground">{extra}</div>}
</div>
<div>{children}</div>
const SettingRow = ({
label,
extra,
children,
}: {
label: React.ReactNode;
extra?: React.ReactNode;
children?: React.ReactNode;
}) => (
<div className="flex items-center justify-between py-3">
<div className="flex items-center gap-2">
<p className="text-sm font-medium">{label}</p>
{extra && <div className="text-muted-foreground">{extra}</div>}
</div>
<div>{children}</div>
</div>
);
export const LiteModeViewer = forwardRef<DialogRef>((props, ref) => {
const { t } = useTranslation();
const { verge, patchVerge } = useVerge();
@@ -75,57 +81,73 @@ export const LiteModeViewer = forwardRef<DialogRef>((props, ref) => {
</DialogHeader>
<div className="py-4 space-y-2">
<SettingRow label={t("Enter LightWeight Mode Now")}>
{/* --- НАЧАЛО ИЗМЕНЕНИЙ --- */}
{/* Меняем variant="link" на "outline" для вида кнопки */}
<Button variant="outline" size="sm" onClick={entry_lightweight_mode}>
{t("Enable")}
</Button>
{/* --- КОНЕЦ ИЗМЕНЕНИЙ --- */}
</SettingRow>
<SettingRow
label={t("Auto Enter LightWeight Mode")}
extra={<TooltipIcon tooltip={t("Auto Enter LightWeight Mode Info")} />}
<SettingRow label={t("Enter LightWeight Mode Now")}>
{/* --- НАЧАЛО ИЗМЕНЕНИЙ --- */}
{/* Меняем variant="link" на "outline" для вида кнопки */}
<Button
variant="outline"
size="sm"
onClick={entry_lightweight_mode}
>
<Switch
checked={values.autoEnterLiteMode}
onCheckedChange={(c) => setValues((v) => ({ ...v, autoEnterLiteMode: c }))}
/>
</SettingRow>
{t("Enable")}
</Button>
{/* --- КОНЕЦ ИЗМЕНЕНИЙ --- */}
</SettingRow>
{values.autoEnterLiteMode && (
<div className="pl-4">
<SettingRow label={t("Auto Enter LightWeight Mode Delay")}>
<div className="flex items-center gap-2">
<Input
type="number"
className="w-24 h-8"
value={values.autoEnterLiteModeDelay}
onChange={(e) =>
setValues((v) => ({
...v,
autoEnterLiteModeDelay: parseInt(e.target.value) || 1,
}))
}
/>
<span className="text-sm text-muted-foreground">{t("mins")}</span>
</div>
</SettingRow>
<SettingRow
label={t("Auto Enter LightWeight Mode")}
extra={
<TooltipIcon tooltip={t("Auto Enter LightWeight Mode Info")} />
}
>
<Switch
checked={values.autoEnterLiteMode}
onCheckedChange={(c) =>
setValues((v) => ({ ...v, autoEnterLiteMode: c }))
}
/>
</SettingRow>
<p className="text-xs text-muted-foreground italic mt-2">
{t(
"When closing the window, LightWeight Mode will be automatically activated after _n minutes",
{ n: values.autoEnterLiteModeDelay }
)}
</p>
{values.autoEnterLiteMode && (
<div className="pl-4">
<SettingRow label={t("Auto Enter LightWeight Mode Delay")}>
<div className="flex items-center gap-2">
<Input
type="number"
className="w-24 h-8"
value={values.autoEnterLiteModeDelay}
onChange={(e) =>
setValues((v) => ({
...v,
autoEnterLiteModeDelay: parseInt(e.target.value) || 1,
}))
}
/>
<span className="text-sm text-muted-foreground">
{t("mins")}
</span>
</div>
)}
</SettingRow>
<p className="text-xs text-muted-foreground italic mt-2">
{t(
"When closing the window, LightWeight Mode will be automatically activated after _n minutes",
{ n: values.autoEnterLiteModeDelay },
)}
</p>
</div>
)}
</div>
<DialogFooter>
<DialogClose asChild><Button type="button" variant="outline">{t("Cancel")}</Button></DialogClose>
<Button type="button" onClick={onSave}>{t("Save")}</Button>
<DialogClose asChild>
<Button type="button" variant="outline">
{t("Cancel")}
</Button>
</DialogClose>
<Button type="button" onClick={onSave}>
{t("Save")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>

View File

@@ -8,34 +8,71 @@ import { DialogRef, Switch } from "@/components/base";
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
import { showNotice } from "@/services/noticeService";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogClose } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import {
FileText, Unplug, RefreshCw, Zap, Columns, ArchiveRestore, Link as LinkIcon, Timer
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
DialogClose,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
FileText,
Unplug,
RefreshCw,
Zap,
Columns,
ArchiveRestore,
Link as LinkIcon,
Timer,
} from "lucide-react";
interface Props {}
// Наш переиспользуемый компонент для строки настроек
const SettingRow = ({ label, extra, children }: { label: React.ReactNode; extra?: React.ReactNode; children?: React.ReactNode; }) => (
<div className="flex items-center justify-between py-3 border-b border-border last:border-b-0">
<div className="flex items-center gap-2">
<div className="text-sm font-medium">{label}</div>
{extra && <div className="text-muted-foreground">{extra}</div>}
</div>
<div>{children}</div>
const SettingRow = ({
label,
extra,
children,
}: {
label: React.ReactNode;
extra?: React.ReactNode;
children?: React.ReactNode;
}) => (
<div className="flex items-center justify-between py-3 border-b border-border last:border-b-0">
<div className="flex items-center gap-2">
<div className="text-sm font-medium">{label}</div>
{extra && <div className="text-muted-foreground">{extra}</div>}
</div>
<div>{children}</div>
</div>
);
// Вспомогательная функция для создания лейбла с иконкой
const LabelWithIcon = ({ icon, text }: { icon: React.ElementType, text: string }) => {
const Icon = icon;
return ( <span className="flex items-center gap-3"><Icon className="h-4 w-4 text-muted-foreground" />{text}</span> );
const LabelWithIcon = ({
icon,
text,
}: {
icon: React.ElementType;
text: string;
}) => {
const Icon = icon;
return (
<span className="flex items-center gap-3">
<Icon className="h-4 w-4 text-muted-foreground" />
{text}
</span>
);
};
export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
const { t } = useTranslation();
const { verge, patchVerge } = useVerge();
@@ -89,7 +126,7 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
});
const handleValueChange = (key: keyof typeof values, value: any) => {
setValues(v => ({ ...v, [key]: value }));
setValues((v) => ({ ...v, [key]: value }));
};
return (
@@ -100,42 +137,106 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
</DialogHeader>
<div className="max-h-[70vh] overflow-y-auto px-1 space-y-1">
<SettingRow label={<LabelWithIcon icon={FileText} text={t("App Log Level")} />}>
<Select value={values.appLogLevel} onValueChange={(v) => handleValueChange("appLogLevel", v)}>
<SelectTrigger className="w-28 h-8"><SelectValue /></SelectTrigger>
<SettingRow
label={<LabelWithIcon icon={FileText} text={t("App Log Level")} />}
>
<Select
value={values.appLogLevel}
onValueChange={(v) => handleValueChange("appLogLevel", v)}
>
<SelectTrigger className="w-28 h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
{["trace", "debug", "info", "warn", "error", "silent"].map((i) => (
<SelectItem value={i} key={i}>{i[0].toUpperCase() + i.slice(1).toLowerCase()}</SelectItem>
{["trace", "debug", "info", "warn", "error", "silent"].map(
(i) => (
<SelectItem value={i} key={i}>
{i[0].toUpperCase() + i.slice(1).toLowerCase()}
</SelectItem>
),
)}
</SelectContent>
</Select>
</SettingRow>
<SettingRow
label={
<LabelWithIcon icon={Unplug} text={t("Auto Close Connections")} />
}
extra={<TooltipIcon tooltip={t("Auto Close Connections Info")} />}
>
<Switch
checked={values.autoCloseConnection}
onCheckedChange={(c) =>
handleValueChange("autoCloseConnection", c)
}
/>
</SettingRow>
<SettingRow
label={
<LabelWithIcon icon={RefreshCw} text={t("Auto Check Update")} />
}
>
<Switch
checked={values.autoCheckUpdate}
onCheckedChange={(c) => handleValueChange("autoCheckUpdate", c)}
/>
</SettingRow>
<SettingRow
label={
<LabelWithIcon icon={Zap} text={t("Enable Builtin Enhanced")} />
}
extra={<TooltipIcon tooltip={t("Enable Builtin Enhanced Info")} />}
>
<Switch
checked={values.enableBuiltinEnhanced}
onCheckedChange={(c) =>
handleValueChange("enableBuiltinEnhanced", c)
}
/>
</SettingRow>
<SettingRow
label={
<LabelWithIcon icon={Columns} text={t("Proxy Layout Columns")} />
}
>
<Select
value={String(values.proxyLayoutColumn)}
onValueChange={(v) =>
handleValueChange("proxyLayoutColumn", Number(v))
}
>
<SelectTrigger className="w-36 h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="6">{t("Auto Columns")}</SelectItem>
{[1, 2, 3, 4, 5].map((i) => (
<SelectItem value={String(i)} key={i}>
{i}
</SelectItem>
))}
</SelectContent>
</Select>
</SettingRow>
<SettingRow label={<LabelWithIcon icon={Unplug} text={t("Auto Close Connections")} />} extra={<TooltipIcon tooltip={t("Auto Close Connections Info")} />}>
<Switch checked={values.autoCloseConnection} onCheckedChange={(c) => handleValueChange("autoCloseConnection", c)} />
</SettingRow>
<SettingRow label={<LabelWithIcon icon={RefreshCw} text={t("Auto Check Update")} />}>
<Switch checked={values.autoCheckUpdate} onCheckedChange={(c) => handleValueChange("autoCheckUpdate", c)} />
</SettingRow>
<SettingRow label={<LabelWithIcon icon={Zap} text={t("Enable Builtin Enhanced")} />} extra={<TooltipIcon tooltip={t("Enable Builtin Enhanced Info")} />}>
<Switch checked={values.enableBuiltinEnhanced} onCheckedChange={(c) => handleValueChange("enableBuiltinEnhanced", c)} />
</SettingRow>
<SettingRow label={<LabelWithIcon icon={Columns} text={t("Proxy Layout Columns")} />}>
<Select value={String(values.proxyLayoutColumn)} onValueChange={(v) => handleValueChange("proxyLayoutColumn", Number(v))}>
<SelectTrigger className="w-36 h-8"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="6">{t("Auto Columns")}</SelectItem>
{[1, 2, 3, 4, 5].map((i) => (<SelectItem value={String(i)} key={i}>{i}</SelectItem>))}
</SelectContent>
</Select>
</SettingRow>
<SettingRow label={<LabelWithIcon icon={ArchiveRestore} text={t("Auto Log Clean")} />}>
<Select value={String(values.autoLogClean)} onValueChange={(v) => handleValueChange("autoLogClean", Number(v))}>
<SelectTrigger className="w-48 h-8"><SelectValue /></SelectTrigger>
<SettingRow
label={
<LabelWithIcon icon={ArchiveRestore} text={t("Auto Log Clean")} />
}
>
<Select
value={String(values.autoLogClean)}
onValueChange={(v) =>
handleValueChange("autoLogClean", Number(v))
}
>
<SelectTrigger className="w-48 h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
{[
{ key: t("Never Clean"), value: 0 },
@@ -143,37 +244,65 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
{ key: t("Retain _n Days", { n: 7 }), value: 2 },
{ key: t("Retain _n Days", { n: 30 }), value: 3 },
{ key: t("Retain _n Days", { n: 90 }), value: 4 },
].map((i) => (<SelectItem key={i.value} value={String(i.value)}>{i.key}</SelectItem>))}
].map((i) => (
<SelectItem key={i.value} value={String(i.value)}>
{i.key}
</SelectItem>
))}
</SelectContent>
</Select>
</SettingRow>
<SettingRow label={<LabelWithIcon icon={LinkIcon} text={t("Default Latency Test")} />} extra={<TooltipIcon tooltip={t("Default Latency Test Info")} />}>
<SettingRow
label={
<LabelWithIcon icon={LinkIcon} text={t("Default Latency Test")} />
}
extra={<TooltipIcon tooltip={t("Default Latency Test Info")} />}
>
<Input
className="w-75 h-8"
value={values.defaultLatencyTest}
placeholder="https://www.google.com/generate_204"
onChange={(e) => handleValueChange("defaultLatencyTest", e.target.value)}
onChange={(e) =>
handleValueChange("defaultLatencyTest", e.target.value)
}
/>
</SettingRow>
<SettingRow label={<LabelWithIcon icon={Timer} text={t("Default Latency Timeout")} />}>
<div className="flex items-center gap-2">
<Input
type="number"
className="w-24 h-8"
value={values.defaultLatencyTimeout}
placeholder="5000"
onChange={(e) => handleValueChange("defaultLatencyTimeout", Number(e.target.value))}
/>
<span className="text-sm text-muted-foreground">{t("millis")}</span>
</div>
<SettingRow
label={
<LabelWithIcon icon={Timer} text={t("Default Latency Timeout")} />
}
>
<div className="flex items-center gap-2">
<Input
type="number"
className="w-24 h-8"
value={values.defaultLatencyTimeout}
placeholder="5000"
onChange={(e) =>
handleValueChange(
"defaultLatencyTimeout",
Number(e.target.value),
)
}
/>
<span className="text-sm text-muted-foreground">
{t("millis")}
</span>
</div>
</SettingRow>
</div>
<DialogFooter>
<DialogClose asChild><Button type="button" variant="outline">{t("Cancel")}</Button></DialogClose>
<Button type="button" onClick={onSave}>{t("Save")}</Button>
<DialogClose asChild>
<Button type="button" variant="outline">
{t("Cancel")}
</Button>
</DialogClose>
<Button type="button" onClick={onSave}>
{t("Save")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>

View File

@@ -18,10 +18,14 @@ import {
DialogClose,
} from "@/components/ui/dialog";
import { Separator } from "@/components/ui/separator";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Copy } from "lucide-react";
// Дочерний компонент AddressDisplay (без изменений)
const AddressDisplay = (props: { label: string; content: string }) => {
const { t } = useTranslation();
@@ -37,21 +41,27 @@ const AddressDisplay = (props: { label: string; content: string }) => {
<div className="flex items-center gap-2 rounded-md bg-muted px-2 py-1">
<span className="font-mono">{props.content}</span>
<TooltipProvider delayDuration={100}>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" className="h-6 w-6" onClick={handleCopy}>
<Copy className="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent><p>{t("Copy to clipboard")}</p></TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
onClick={handleCopy}
>
<Copy className="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t("Copy to clipboard")}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>
);
};
export const NetworkInterfaceViewer = forwardRef<DialogRef>((props, ref) => {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
@@ -65,7 +75,7 @@ export const NetworkInterfaceViewer = forwardRef<DialogRef>((props, ref) => {
const { data: networkInterfaces } = useSWR(
open ? "clash-verge-rev-internal://network-interfaces" : null,
getNetworkInterfacesInfo,
{ fallbackData: [] }
{ fallbackData: [] },
);
return (
@@ -75,11 +85,25 @@ export const NetworkInterfaceViewer = forwardRef<DialogRef>((props, ref) => {
<div className="flex justify-between items-center pr-12">
<DialogTitle>{t("Network Interface")}</DialogTitle>
<div className="flex items-center rounded-md border bg-muted p-0.5">
{/* --- НАЧАЛО ИЗМЕНЕНИЙ --- */}
{/* Меняем `secondary` на `default` для активной кнопки */}
<Button variant={isV4 ? "default" : "ghost"} size="sm" className="px-3 text-xs" onClick={() => setIsV4(true)}>IPv4</Button>
<Button variant={!isV4 ? "default" : "ghost"} size="sm" className="px-3 text-xs" onClick={() => setIsV4(false)}>IPv6</Button>
{/* --- КОНЕЦ ИЗМЕНЕНИЙ --- */}
{/* --- НАЧАЛО ИЗМЕНЕНИЙ --- */}
{/* Меняем `secondary` на `default` для активной кнопки */}
<Button
variant={isV4 ? "default" : "ghost"}
size="sm"
className="px-3 text-xs"
onClick={() => setIsV4(true)}
>
IPv4
</Button>
<Button
variant={!isV4 ? "default" : "ghost"}
size="sm"
className="px-3 text-xs"
onClick={() => setIsV4(false)}
>
IPv6
</Button>
{/* --- КОНЕЦ ИЗМЕНЕНИЙ --- */}
</div>
</div>
</DialogHeader>
@@ -91,25 +115,53 @@ export const NetworkInterfaceViewer = forwardRef<DialogRef>((props, ref) => {
<div>
{isV4 ? (
<>
{item.addr.map((address) => address.V4 && <AddressDisplay key={address.V4.ip} label={t("Ip Address")} content={address.V4.ip} />)}
<AddressDisplay label={t("Mac Address")} content={item.mac_addr ?? ""} />
{item.addr.map(
(address) =>
address.V4 && (
<AddressDisplay
key={address.V4.ip}
label={t("Ip Address")}
content={address.V4.ip}
/>
),
)}
<AddressDisplay
label={t("Mac Address")}
content={item.mac_addr ?? ""}
/>
</>
) : (
<>
{item.addr.map((address) => address.V6 && <AddressDisplay key={address.V6.ip} label={t("Ip Address")} content={address.V6.ip} />)}
<AddressDisplay label={t("Mac Address")} content={item.mac_addr ?? ""} />
{item.addr.map(
(address) =>
address.V6 && (
<AddressDisplay
key={address.V6.ip}
label={t("Ip Address")}
content={address.V6.ip}
/>
),
)}
<AddressDisplay
label={t("Mac Address")}
content={item.mac_addr ?? ""}
/>
</>
)}
</div>
{index < networkInterfaces.length - 1 && <Separator className="mt-2"/>}
{index < networkInterfaces.length - 1 && (
<Separator className="mt-2" />
)}
</div>
))}
</div>
<DialogFooter>
<DialogClose asChild>
<Button type="button" variant="outline">{t("Close")}</Button>
</DialogClose>
<DialogClose asChild>
<Button type="button" variant="outline">
{t("Close")}
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>

View File

@@ -15,7 +15,6 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
interface Props {
// Компонент теперь сам управляет своим состоянием,
// но вызывает onConfirm при подтверждении
@@ -39,7 +38,9 @@ export const PasswordInput = (props: Props) => {
<AlertDialog open={true}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t("Please enter your root password")}</AlertDialogTitle>
<AlertDialogTitle>
{t("Please enter your root password")}
</AlertDialogTitle>
<AlertDialogDescription>
{t("This action requires administrator privileges.")}
</AlertDialogDescription>

View File

@@ -14,19 +14,16 @@ interface ListProps {
export const SettingList: React.FC<ListProps> = ({ title, children }) => (
<div>
<h3 className="text-lg font-medium mb-4 px-1">{title}</h3>
<div className="flex flex-col">
{children}
</div>
<div className="flex flex-col">{children}</div>
</div>
);
// --- Новый компонент SettingItem ---
interface ItemProps {
label: ReactNode;
extra?: ReactNode; // Для иконок-подсказок рядом с лейблом
children?: ReactNode; // Для элементов управления (Switch, Select и т.д.)
secondary?: ReactNode; // Для текста-описания под лейблом
extra?: ReactNode; // Для иконок-подсказок рядом с лейблом
children?: ReactNode; // Для элементов управления (Switch, Select и т.д.)
secondary?: ReactNode; // Для текста-описания под лейблом
onClick?: () => void | Promise<any>;
}
@@ -54,7 +51,7 @@ export const SettingItem: React.FC<ItemProps> = (props) => {
className={cn(
"flex items-center justify-between py-4 border-b border-border last:border-b-0",
clickable && "cursor-pointer hover:bg-accent/50 -mx-4 px-4",
isLoading && "cursor-default opacity-70"
isLoading && "cursor-default opacity-70",
)}
>
{/* Левая часть: заголовок и описание */}
@@ -63,7 +60,9 @@ export const SettingItem: React.FC<ItemProps> = (props) => {
<p className="text-sm font-medium">{label}</p>
{extra}
</div>
{secondary && <p className="text-sm text-muted-foreground">{secondary}</p>}
{secondary && (
<p className="text-sm text-muted-foreground">{secondary}</p>
)}
</div>
{/* Правая часть: элемент управления или иконка */}

View File

@@ -1,4 +1,11 @@
import { forwardRef, useImperativeHandle, useState, useMemo, useEffect, ReactNode } from "react";
import {
forwardRef,
useImperativeHandle,
useState,
useMemo,
useEffect,
ReactNode,
} from "react";
import { useTranslation } from "react-i18next";
import { useLockFn } from "ahooks";
import useSWR, { mutate } from "swr";
@@ -21,12 +28,29 @@ import {
import { showNotice } from "@/services/noticeService";
import getSystem from "@/utils/get-system";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogClose } from "@/components/ui/dialog";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
DialogClose,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Command, CommandEmpty, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
Command,
CommandEmpty,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import { Check, ChevronsUpDown, Edit, Loader2 } from "lucide-react";
import { cn } from "@root/lib/utils";
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
@@ -34,7 +58,8 @@ import { TooltipIcon } from "@/components/base/base-tooltip-icon";
// --- Вся ваша оригинальная логика, константы и хелперы ---
const DEFAULT_PAC = `function FindProxyForURL(url, host) { return "PROXY %proxy_host%:%mixed-port%; SOCKS5 %proxy_host%:%mixed-port%; DIRECT;"; }`;
const ipv4_part = String.raw`\d{1,3}`;
const rDomainSimple = String.raw`(?:[a-z0-9\-\*]+\.|\*)*` + String.raw`(?:\w{2,64}\*?|\*)`;
const rDomainSimple =
String.raw`(?:[a-z0-9\-\*]+\.|\*)*` + String.raw`(?:\w{2,64}\*?|\*)`;
const ipv6_part = "(?:[a-fA-F0-9:])+";
const rLocal = `localhost|<local>|localdomain`;
const getValidReg = (isWindows: boolean) => {
@@ -49,40 +74,78 @@ const getValidReg = (isWindows: boolean) => {
};
// --- Компонент Combobox для замены Autocomplete ---
const Combobox = ({ options, value, onValueChange, placeholder }: { options: string[], value: string, onValueChange: (value: string) => void, placeholder?: string }) => {
const [open, setOpen] = useState(false);
return (
<Popover open={open} onOpenChange={setOpen} modal={true}>
<PopoverTrigger asChild>
<Button variant="outline" role="combobox" aria-expanded={open} className="w-48 h-8 justify-between font-normal">
{value || placeholder || "Select..."}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[--radix-popover-trigger-width] p-0">
<Command onValueChange={onValueChange}>
<CommandInput placeholder="Search or type..." />
<CommandEmpty>No results found.</CommandEmpty>
<CommandList>
{options.map((option) => (
<CommandItem key={option} value={option} onSelect={(currentValue) => { onValueChange(options.find(opt => opt.toLowerCase() === currentValue) || ''); setOpen(false); }}>
<Check className={cn("mr-2 h-4 w-4", value === option ? "opacity-100" : "opacity-0")} />
{option}
</CommandItem>
))}
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
const Combobox = ({
options,
value,
onValueChange,
placeholder,
}: {
options: string[];
value: string;
onValueChange: (value: string) => void;
placeholder?: string;
}) => {
const [open, setOpen] = useState(false);
return (
<Popover open={open} onOpenChange={setOpen} modal={true}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className="w-48 h-8 justify-between font-normal"
>
{value || placeholder || "Select..."}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[--radix-popover-trigger-width] p-0">
<Command onValueChange={onValueChange}>
<CommandInput placeholder="Search or type..." />
<CommandEmpty>No results found.</CommandEmpty>
<CommandList>
{options.map((option) => (
<CommandItem
key={option}
value={option}
onSelect={(currentValue) => {
onValueChange(
options.find((opt) => opt.toLowerCase() === currentValue) ||
"",
);
setOpen(false);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
value === option ? "opacity-100" : "opacity-0",
)}
/>
{option}
</CommandItem>
))}
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
};
// --- Наш переиспользуемый компонент для строки настроек ---
const SettingRow = ({ label, children }: { label: React.ReactNode; children?: React.ReactNode; }) => (
<div className="flex items-center justify-between py-2">
<Label className="text-sm text-muted-foreground flex items-center gap-2">{label}</Label>
<div>{children}</div>
</div>
const SettingRow = ({
label,
children,
}: {
label: React.ReactNode;
children?: React.ReactNode;
}) => (
<div className="flex items-center justify-between py-2">
<Label className="text-sm text-muted-foreground flex items-center gap-2">
{label}
</Label>
<div>{children}</div>
</div>
);
export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
@@ -102,25 +165,50 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
type AutoProxy = Awaited<ReturnType<typeof getAutotemProxy>>;
const [autoproxy, setAutoproxy] = useState<AutoProxy>();
const { enable_system_proxy: enabled, proxy_auto_config, pac_file_content, enable_proxy_guard, use_default_bypass, system_proxy_bypass, proxy_guard_duration, proxy_host } = verge ?? {};
const {
enable_system_proxy: enabled,
proxy_auto_config,
pac_file_content,
enable_proxy_guard,
use_default_bypass,
system_proxy_bypass,
proxy_guard_duration,
proxy_host,
} = verge ?? {};
const [value, setValue] = useState({
guard: enable_proxy_guard, bypass: system_proxy_bypass, duration: proxy_guard_duration ?? 10,
use_default: use_default_bypass ?? true, pac: proxy_auto_config, pac_content: pac_file_content ?? DEFAULT_PAC,
guard: enable_proxy_guard,
bypass: system_proxy_bypass,
duration: proxy_guard_duration ?? 10,
use_default: use_default_bypass ?? true,
pac: proxy_auto_config,
pac_content: pac_file_content ?? DEFAULT_PAC,
proxy_host: proxy_host ?? "127.0.0.1",
});
const defaultBypass = () => {
if (isWindows) return "localhost;127.*;192.168.*;10.*;172.16.*;172.17.*;172.18.*;172.19.*;172.20.*;172.21.*;172.22.*;172.23.*;172.24.*;172.25.*;172.26.*;172.27.*;172.28.*;172.29.*;172.30.*;172.31.*;<local>";
if (getSystem() === "linux") return "localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,::1";
if (isWindows)
return "localhost;127.*;192.168.*;10.*;172.16.*;172.17.*;172.18.*;172.19.*;172.20.*;172.21.*;172.22.*;172.23.*;172.24.*;172.25.*;172.26.*;172.27.*;172.28.*;172.29.*;172.30.*;172.31.*;<local>";
if (getSystem() === "linux")
return "localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,::1";
return "127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,localhost,*.local,*.crashlytics.com,<local>";
};
const { data: clashConfig } = useSWR("getClashConfig", getClashConfig, { revalidateOnFocus: false, revalidateIfStale: true, dedupingInterval: 1000, errorRetryInterval: 5000 });
const [prevMixedPort, setPrevMixedPort] = useState(clashConfig?.["mixed-port"]);
const { data: clashConfig } = useSWR("getClashConfig", getClashConfig, {
revalidateOnFocus: false,
revalidateIfStale: true,
dedupingInterval: 1000,
errorRetryInterval: 5000,
});
const [prevMixedPort, setPrevMixedPort] = useState(
clashConfig?.["mixed-port"],
);
useEffect(() => {
if (clashConfig?.["mixed-port"] && clashConfig?.["mixed-port"] !== prevMixedPort) {
if (
clashConfig?.["mixed-port"] &&
clashConfig?.["mixed-port"] !== prevMixedPort
) {
setPrevMixedPort(clashConfig?.["mixed-port"]);
resetSystemProxy();
}
@@ -134,9 +222,14 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
await patchVergeConfig({ enable_system_proxy: false });
await new Promise((resolve) => setTimeout(resolve, 200));
await patchVergeConfig({ enable_system_proxy: true });
await Promise.all([ mutate("getSystemProxy"), mutate("getAutotemProxy") ]);
await Promise.all([
mutate("getSystemProxy"),
mutate("getAutotemProxy"),
]);
}
} catch (err: any) { showNotice("error", err.toString()); }
} catch (err: any) {
showNotice("error", err.toString());
}
};
const { systemProxyAddress } = useAppData();
@@ -151,7 +244,13 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
} else {
return systemProxyAddress;
}
}, [value.pac, value.proxy_host, verge?.verge_mixed_port, clashConfig, systemProxyAddress]);
}, [
value.pac,
value.proxy_host,
verge?.verge_mixed_port,
clashConfig,
systemProxyAddress,
]);
const getCurrentPacUrl = useMemo(() => {
const host = value.proxy_host || "127.0.0.1";
@@ -175,7 +274,9 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
if (hostname && hostname !== "localhost" && hostname !== "127.0.0.1") {
hostname = hostname + ".local";
}
} catch (err) { console.error("Failed to get hostname:", err); }
} catch (err) {
console.error("Failed to get hostname:", err);
}
const options = ["127.0.0.1", "localhost"];
if (hostname) options.push(hostname);
options.push(...ipAddresses);
@@ -190,8 +291,12 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
open: () => {
setOpen(true);
setValue({
guard: enable_proxy_guard, bypass: system_proxy_bypass, duration: proxy_guard_duration ?? 10,
use_default: use_default_bypass ?? true, pac: proxy_auto_config, pac_content: pac_file_content ?? DEFAULT_PAC,
guard: enable_proxy_guard,
bypass: system_proxy_bypass,
duration: proxy_guard_duration ?? 10,
use_default: use_default_bypass ?? true,
pac: proxy_auto_config,
pac_content: pac_file_content ?? DEFAULT_PAC,
proxy_host: proxy_host ?? "127.0.0.1",
});
getSystemProxy().then(setSysproxy);
@@ -340,65 +445,170 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
<>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="sm:max-w-lg">
<DialogHeader><DialogTitle>{t("System Proxy Setting")}</DialogTitle></DialogHeader>
<DialogHeader>
<DialogTitle>{t("System Proxy Setting")}</DialogTitle>
</DialogHeader>
<div className="max-h-[70vh] overflow-y-auto space-y-4 py-4 px-1">
<BaseFieldset label={t("Current System Proxy")}>
<div className="text-sm space-y-2">
<div className="flex justify-between"><span className="text-muted-foreground">{t("Enable status")}</span><span>{value.pac ? (autoproxy?.enable ? t("Enabled") : t("Disabled")) : (sysproxy?.enable ? t("Enabled") : t("Disabled"))}</span></div>
{!value.pac && <div className="flex justify-between"><span className="text-muted-foreground">{t("Server Addr")}</span><span className="font-mono">{getSystemProxyAddress}</span></div>}
{value.pac && <div className="flex justify-between"><span className="text-muted-foreground">{t("PAC URL")}</span><span className="font-mono">{getCurrentPacUrl || "-"}</span></div>}
<div className="text-sm space-y-2">
<div className="flex justify-between">
<span className="text-muted-foreground">
{t("Enable status")}
</span>
<span>
{value.pac
? autoproxy?.enable
? t("Enabled")
: t("Disabled")
: sysproxy?.enable
? t("Enabled")
: t("Disabled")}
</span>
</div>
{!value.pac && (
<div className="flex justify-between">
<span className="text-muted-foreground">
{t("Server Addr")}
</span>
<span className="font-mono">{getSystemProxyAddress}</span>
</div>
)}
{value.pac && (
<div className="flex justify-between">
<span className="text-muted-foreground">
{t("PAC URL")}
</span>
<span className="font-mono">{getCurrentPacUrl || "-"}</span>
</div>
)}
</div>
</BaseFieldset>
<SettingRow label={t("Proxy Host")}>
<Combobox options={hostOptions} value={value.proxy_host} onValueChange={(val) => setValue(v => ({...v, proxy_host: val}))} placeholder="127.0.0.1" />
<Combobox
options={hostOptions}
value={value.proxy_host}
onValueChange={(val) =>
setValue((v) => ({ ...v, proxy_host: val }))
}
placeholder="127.0.0.1"
/>
</SettingRow>
<SettingRow label={t("Use PAC Mode")}>
<Switch disabled={!enabled} checked={value.pac} onCheckedChange={(e) => setValue((v) => ({ ...v, pac: e }))} />
<Switch
disabled={!enabled}
checked={value.pac}
onCheckedChange={(e) => setValue((v) => ({ ...v, pac: e }))}
/>
</SettingRow>
<SettingRow label={<>{t("Proxy Guard")} <TooltipIcon tooltip={t("Proxy Guard Info")} /></>}>
<Switch disabled={!enabled} checked={value.guard} onCheckedChange={(e) => setValue((v) => ({ ...v, guard: e }))} />
<SettingRow
label={
<>
{t("Proxy Guard")}{" "}
<TooltipIcon tooltip={t("Proxy Guard Info")} />
</>
}
>
<Switch
disabled={!enabled}
checked={value.guard}
onCheckedChange={(e) => setValue((v) => ({ ...v, guard: e }))}
/>
</SettingRow>
<SettingRow label={t("Guard Duration")}>
<div className="flex items-center gap-2">
<Input disabled={!enabled} type="number" className="w-24 h-8" value={value.duration} onChange={(e) => setValue((v) => ({ ...v, duration: +e.target.value.replace(/\D/, "") }))}/>
<span className="text-sm text-muted-foreground">s</span>
</div>
<div className="flex items-center gap-2">
<Input
disabled={!enabled}
type="number"
className="w-24 h-8"
value={value.duration}
onChange={(e) =>
setValue((v) => ({
...v,
duration: +e.target.value.replace(/\D/, ""),
}))
}
/>
<span className="text-sm text-muted-foreground">s</span>
</div>
</SettingRow>
{!value.pac && (
<SettingRow label={t("Always use Default Bypass")}>
<Switch disabled={!enabled} checked={value.use_default} onCheckedChange={(e) => setValue((v) => ({...v, use_default: e, bypass: !e && !v.bypass ? defaultBypass() : v.bypass}))}/>
</SettingRow>
<SettingRow label={t("Always use Default Bypass")}>
<Switch
disabled={!enabled}
checked={value.use_default}
onCheckedChange={(e) =>
setValue((v) => ({
...v,
use_default: e,
bypass: !e && !v.bypass ? defaultBypass() : v.bypass,
}))
}
/>
</SettingRow>
)}
{!value.pac && !value.use_default && (
<div className="space-y-2">
<Label>{t("Proxy Bypass")}</Label>
<Textarea
id="proxy-bypass"
disabled={!enabled}
rows={4}
value={value.bypass}
onChange={(e) => setValue((v) => ({ ...v, bypass: e.target.value }))}
// Вместо пропса `error` используем условные классы
className={cn(
(value.bypass && !validReg.test(value.bypass)) && "border-destructive focus-visible:ring-destructive"
)}
/>
</div>
<div className="space-y-2">
<Label>{t("Proxy Bypass")}</Label>
<Textarea
id="proxy-bypass"
disabled={!enabled}
rows={4}
value={value.bypass}
onChange={(e) =>
setValue((v) => ({ ...v, bypass: e.target.value }))
}
// Вместо пропса `error` используем условные классы
className={cn(
value.bypass &&
!validReg.test(value.bypass) &&
"border-destructive focus-visible:ring-destructive",
)}
/>
</div>
)}
{value.pac && (
<SettingRow label={t("PAC Script Content")}>
<Button variant="outline" size="sm" onClick={() => setEditorOpen(true)}><Edit className="mr-2 h-4 w-4"/>{t("Edit")} PAC</Button>
</SettingRow>
<SettingRow label={t("PAC Script Content")}>
<Button
variant="outline"
size="sm"
onClick={() => setEditorOpen(true)}
>
<Edit className="mr-2 h-4 w-4" />
{t("Edit")} PAC
</Button>
</SettingRow>
)}
</div>
<DialogFooter>
<DialogClose asChild><Button type="button" variant="outline">{t("Cancel")}</Button></DialogClose>
<Button type="button" onClick={onSave} disabled={saving}>{saving && <Loader2 className="mr-2 h-4 w-4 animate-spin"/>}{t("Save")}</Button>
<DialogClose asChild>
<Button type="button" variant="outline">
{t("Cancel")}
</Button>
</DialogClose>
<Button type="button" onClick={onSave} disabled={saving}>
{saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{t("Save")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{editorOpen && <EditorViewer open={true} title={`${t("Edit")} PAC`} initialData={Promise.resolve(value.pac_content ?? "")} language="javascript" onSave={(_prev, curr) => { let pac = DEFAULT_PAC; if (curr && curr.trim().length > 0) { pac = curr; } setValue((v) => ({ ...v, pac_content: pac })); }} onClose={() => setEditorOpen(false)} />}
{editorOpen && (
<EditorViewer
open={true}
title={`${t("Edit")} PAC`}
initialData={Promise.resolve(value.pac_content ?? "")}
language="javascript"
onSave={(_prev, curr) => {
let pac = DEFAULT_PAC;
if (curr && curr.trim().length > 0) {
pac = curr;
}
setValue((v) => ({ ...v, pac_content: pac }));
}}
onClose={() => setEditorOpen(false)}
/>
)}
</>
);
});

View File

@@ -1,4 +1,10 @@
import { forwardRef, useImperativeHandle, useState, useEffect, useCallback } from "react";
import {
forwardRef,
useImperativeHandle,
useState,
useEffect,
useCallback,
} from "react";
import { useLockFn } from "ahooks";
import { useTranslation } from "react-i18next";
@@ -8,18 +14,33 @@ import { DialogRef } from "@/components/base";
import { EditorViewer } from "@/components/profile/editor-viewer";
import { showNotice } from "@/services/noticeService";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogClose } from "@/components/ui/dialog";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
DialogClose,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Edit } from "lucide-react";
import { useThemeMode } from "@/services/states"; // Наш хук для получения текущего режима
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import {HexColorPicker} from "react-colorful";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { HexColorPicker } from "react-colorful";
interface Props {}
const ColorSettingRow = ({ label, value, placeholder, onChange }: {
const ColorSettingRow = ({
label,
value,
placeholder,
onChange,
}: {
label: string;
value: string;
placeholder: string;
@@ -41,7 +62,10 @@ const ColorSettingRow = ({ label, value, placeholder, onChange }: {
/>
</PopoverTrigger>
<PopoverContent className="w-auto p-0 border-0">
<HexColorPicker color={color} onChange={(newColor) => onChange({ target: { value: newColor } })} />
<HexColorPicker
color={color}
onChange={(newColor) => onChange({ target: { value: newColor } })}
/>
</PopoverContent>
</Popover>
<Input
@@ -55,7 +79,6 @@ const ColorSettingRow = ({ label, value, placeholder, onChange }: {
);
};
export const ThemeViewer = forwardRef<DialogRef>((props, ref) => {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
@@ -65,9 +88,12 @@ export const ThemeViewer = forwardRef<DialogRef>((props, ref) => {
const [theme, setTheme] = useState(theme_setting || {});
const mode = useThemeMode();
const resolvedMode = mode === 'system'
? (window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light")
: mode;
const resolvedMode =
mode === "system"
? window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light"
: mode;
useImperativeHandle(ref, () => ({
open: () => {
@@ -77,9 +103,10 @@ export const ThemeViewer = forwardRef<DialogRef>((props, ref) => {
close: () => setOpen(false),
}));
const handleChange = (field: keyof typeof theme) => (e: { target: { value: string } }) => {
setTheme((t) => ({ ...t, [field]: e.target.value }));
};
const handleChange =
(field: keyof typeof theme) => (e: { target: { value: string } }) => {
setTheme((t) => ({ ...t, [field]: e.target.value }));
};
const onSave = useLockFn(async () => {
try {
@@ -92,7 +119,6 @@ export const ThemeViewer = forwardRef<DialogRef>((props, ref) => {
}
});
const dt = resolvedMode === "light" ? defaultTheme : defaultDarkTheme;
type ThemeKey = keyof typeof theme & keyof typeof defaultTheme;
@@ -128,23 +154,34 @@ export const ThemeViewer = forwardRef<DialogRef>((props, ref) => {
<div className="flex items-center justify-between py-2">
<Label>{t("Font Family")}</Label>
<Input
className="w-48 h-8"
value={theme.font_family ?? ""}
onChange={handleChange("font_family")}
className="w-48 h-8"
value={theme.font_family ?? ""}
onChange={handleChange("font_family")}
/>
</div>
<div className="flex items-center justify-between py-2">
<Label>{t("CSS Injection")}</Label>
<Button variant="outline" size="sm" onClick={() => setEditorOpen(true)}>
<Edit className="mr-2 h-4 w-4" />{t("Edit")} CSS
<Button
variant="outline"
size="sm"
onClick={() => setEditorOpen(true)}
>
<Edit className="mr-2 h-4 w-4" />
{t("Edit")} CSS
</Button>
</div>
</div>
<DialogFooter>
<DialogClose asChild><Button type="button" variant="outline">{t("Cancel")}</Button></DialogClose>
<Button type="button" onClick={onSave}>{t("Save")}</Button>
<DialogClose asChild>
<Button type="button" variant="outline">
{t("Cancel")}
</Button>
</DialogClose>
<Button type="button" onClick={onSave}>
{t("Save")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
@@ -156,7 +193,7 @@ export const ThemeViewer = forwardRef<DialogRef>((props, ref) => {
initialData={Promise.resolve(theme.css_injection ?? "")}
language="css"
onSave={(_prev, curr) => {
setTheme(v => ({ ...v, css_injection: curr }));
setTheme((v) => ({ ...v, css_injection: curr }));
}}
onClose={() => setEditorOpen(false)}
/>

View File

@@ -22,21 +22,49 @@ import {
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { RotateCcw, Layers, Laptop, Route, RouteOff, Network, Dna, Gauge } from "lucide-react";
import {
RotateCcw,
Layers,
Laptop,
Route,
RouteOff,
Network,
Dna,
Gauge,
} from "lucide-react";
const OS = getSystem();
type StackMode = "mixed" | "gvisor" | "system";
// Компоненты-хелперы
const SettingRow = ({ label, children }: { label: React.ReactNode; children?: React.ReactNode; }) => (
<div className="flex items-center justify-between py-3 border-b border-border last:border-b-0">
<div className="flex items-center gap-2"><div className="text-sm font-medium">{label}</div></div>
<div>{children}</div>
const SettingRow = ({
label,
children,
}: {
label: React.ReactNode;
children?: React.ReactNode;
}) => (
<div className="flex items-center justify-between py-3 border-b border-border last:border-b-0">
<div className="flex items-center gap-2">
<div className="text-sm font-medium">{label}</div>
</div>
<div>{children}</div>
</div>
);
const LabelWithIcon = ({ icon, text }: { icon: React.ElementType, text: string }) => {
const Icon = icon;
return ( <span className="flex items-center gap-3"><Icon className="h-4 w-4 text-muted-foreground" />{text}</span> );
const LabelWithIcon = ({
icon,
text,
}: {
icon: React.ElementType;
text: string;
}) => {
const Icon = icon;
return (
<span className="flex items-center gap-3">
<Icon className="h-4 w-4 text-muted-foreground" />
{text}
</span>
);
};
export const TunViewer = forwardRef<DialogRef>((props, ref) => {
@@ -89,7 +117,12 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
try {
const tun = {
stack: values.stack,
device: values.device === "" ? (OS === "macos" ? "utun1024" : "Mihomo") : values.device,
device:
values.device === ""
? OS === "macos"
? "utun1024"
: "Mihomo"
: values.device,
"auto-route": values.autoRoute,
"auto-detect-interface": values.autoDetectInterface,
"dns-hijack": values.dnsHijack[0] === "" ? [] : values.dnsHijack,
@@ -130,29 +163,90 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
onChange={(value) => setValues((v) => ({ ...v, stack: value }))}
/>
</SettingRow>
<SettingRow label={<LabelWithIcon icon={Laptop} text={t("Device")} />}>
<Input className="h-8 w-40" value={values.device} placeholder="Mihomo" onChange={(e) => setValues((v) => ({ ...v, device: e.target.value }))} />
<SettingRow
label={<LabelWithIcon icon={Laptop} text={t("Device")} />}
>
<Input
className="h-8 w-40"
value={values.device}
placeholder="Mihomo"
onChange={(e) =>
setValues((v) => ({ ...v, device: e.target.value }))
}
/>
</SettingRow>
<SettingRow label={<LabelWithIcon icon={Route} text={t("Auto Route")} />}>
<Switch checked={values.autoRoute} onCheckedChange={(c) => setValues((v) => ({ ...v, autoRoute: c }))} />
<SettingRow
label={<LabelWithIcon icon={Route} text={t("Auto Route")} />}
>
<Switch
checked={values.autoRoute}
onCheckedChange={(c) =>
setValues((v) => ({ ...v, autoRoute: c }))
}
/>
</SettingRow>
<SettingRow label={<LabelWithIcon icon={RouteOff} text={t("Strict Route")} />}>
<Switch checked={values.strictRoute} onCheckedChange={(c) => setValues((v) => ({ ...v, strictRoute: c }))} />
<SettingRow
label={<LabelWithIcon icon={RouteOff} text={t("Strict Route")} />}
>
<Switch
checked={values.strictRoute}
onCheckedChange={(c) =>
setValues((v) => ({ ...v, strictRoute: c }))
}
/>
</SettingRow>
<SettingRow label={<LabelWithIcon icon={Network} text={t("Auto Detect Interface")} />}>
<Switch checked={values.autoDetectInterface} onCheckedChange={(c) => setValues((v) => ({ ...v, autoDetectInterface: c }))} />
<SettingRow
label={
<LabelWithIcon icon={Network} text={t("Auto Detect Interface")} />
}
>
<Switch
checked={values.autoDetectInterface}
onCheckedChange={(c) =>
setValues((v) => ({ ...v, autoDetectInterface: c }))
}
/>
</SettingRow>
<SettingRow label={<LabelWithIcon icon={Dna} text={t("DNS Hijack")} />}>
<Input className="h-8 w-40" value={values.dnsHijack.join(",")} placeholder="any:53" onChange={(e) => setValues((v) => ({ ...v, dnsHijack: e.target.value.split(",") }))} />
<SettingRow
label={<LabelWithIcon icon={Dna} text={t("DNS Hijack")} />}
>
<Input
className="h-8 w-40"
value={values.dnsHijack.join(",")}
placeholder="any:53"
onChange={(e) =>
setValues((v) => ({
...v,
dnsHijack: e.target.value.split(","),
}))
}
/>
</SettingRow>
<SettingRow label={<LabelWithIcon icon={Gauge} text={t("MTU")} />}>
<Input type="number" className="h-8 w-40" value={values.mtu} placeholder="1500" onChange={(e) => setValues((v) => ({ ...v, mtu: parseInt(e.target.value, 10) || 0 }))} />
<Input
type="number"
className="h-8 w-40"
value={values.mtu}
placeholder="1500"
onChange={(e) =>
setValues((v) => ({
...v,
mtu: parseInt(e.target.value, 10) || 0,
}))
}
/>
</SettingRow>
</div>
<DialogFooter>
<DialogClose asChild><Button type="button" variant="outline">{t("Cancel")}</Button></DialogClose>
<Button type="button" onClick={onSave}>{t("Save")}</Button>
<DialogClose asChild>
<Button type="button" variant="outline">
{t("Cancel")}
</Button>
</DialogClose>
<Button type="button" onClick={onSave}>
{t("Save")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>

View File

@@ -1,5 +1,11 @@
import useSWR from "swr";
import { forwardRef, useImperativeHandle, useState, useMemo, useEffect } from "react";
import {
forwardRef,
useImperativeHandle,
useState,
useMemo,
useEffect,
} from "react";
import { useLockFn } from "ahooks";
import { useTranslation } from "react-i18next";
import { relaunch } from "@tauri-apps/plugin-process";
@@ -15,17 +21,24 @@ import { portableFlag } from "@/pages/_layout";
import { useListen } from "@/hooks/use-listen";
import { showNotice } from "@/services/noticeService";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogClose } from "@/components/ui/dialog";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
DialogClose,
} from "@/components/ui/dialog";
import { Progress } from "@/components/ui/progress";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { AlertTriangle, ExternalLink } from "lucide-react";
export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const [currentProgressListener, setCurrentProgressListener] = useState<UnlistenFn | null>(null);
const [currentProgressListener, setCurrentProgressListener] =
useState<UnlistenFn | null>(null);
const updateState = useUpdateState();
const setUpdateState = useSetUpdateState();
@@ -55,9 +68,15 @@ export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
}, [updateInfo]);
const onUpdate = useLockFn(async () => {
if (portableFlag) { showNotice("error", t("Portable Updater Error")); return; }
if (portableFlag) {
showNotice("error", t("Portable Updater Error"));
return;
}
if (!updateInfo?.body) return;
if (breakChangeFlag) { showNotice("error", t("Break Change Update Error")); return; }
if (breakChangeFlag) {
showNotice("error", t("Break Change Update Error"));
return;
}
if (updateState) return;
setUpdateState(true);
@@ -66,7 +85,9 @@ export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
if (currentProgressListener) currentProgressListener();
const progressListener = await addListener("tauri://update-download-progress", (e: Event<any>) => {
const progressListener = await addListener(
"tauri://update-download-progress",
(e: Event<any>) => {
setTotal(e.payload.contentLength);
setDownloaded((prev) => prev + e.payload.chunkLength);
},
@@ -86,7 +107,9 @@ export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
});
useEffect(() => {
return () => { currentProgressListener?.(); };
return () => {
currentProgressListener?.();
};
}, [currentProgressListener]);
const downloadProgress = total > 0 ? (downloaded / total) * 100 : 0;
@@ -96,11 +119,17 @@ export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
<DialogContent className="sm:max-w-2xl">
<DialogHeader>
<div className="flex justify-between items-center">
<DialogTitle>{t("New Version")} v{updateInfo?.version}</DialogTitle>
<DialogTitle>
{t("New Version")} v{updateInfo?.version}
</DialogTitle>
<Button
variant="outline"
size="sm"
onClick={() => openUrl(`https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/v${updateInfo?.version}`)}
onClick={() =>
openUrl(
`https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/v${updateInfo?.version}`,
)
}
>
<ExternalLink className="mr-2 h-4 w-4" />
{t("Go to Release Page")}
@@ -110,16 +139,20 @@ export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
<div className="max-h-[60vh] overflow-y-auto my-4 pr-6 -mr-6">
{breakChangeFlag && (
<Alert variant="destructive" className="mb-4">
<AlertTriangle className="h-4 w-4" />
<AlertTitle>{t("Warning")}</AlertTitle>
<AlertDescription>{t("Break Change Warning")}</AlertDescription>
</Alert>
<Alert variant="destructive" className="mb-4">
<AlertTriangle className="h-4 w-4" />
<AlertTitle>{t("Warning")}</AlertTitle>
<AlertDescription>{t("Break Change Warning")}</AlertDescription>
</Alert>
)}
{/* Оборачиваем ReactMarkdown для красивой стилизации */}
<article className="prose prose-sm dark:prose-invert max-w-none">
<ReactMarkdown
components={{ a: ({ node, ...props }) => <a {...props} target="_blank" rel="noopener noreferrer" /> }}
components={{
a: ({ node, ...props }) => (
<a {...props} target="_blank" rel="noopener noreferrer" />
),
}}
>
{markdownContent}
</ReactMarkdown>
@@ -127,15 +160,25 @@ export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
</div>
{updateState && (
<div className="w-full space-y-1">
<Progress value={downloadProgress} />
<p className="text-xs text-muted-foreground text-right">{Math.round(downloadProgress)}%</p>
</div>
<div className="w-full space-y-1">
<Progress value={downloadProgress} />
<p className="text-xs text-muted-foreground text-right">
{Math.round(downloadProgress)}%
</p>
</div>
)}
<DialogFooter>
<DialogClose asChild><Button type="button" variant="outline">{t("Cancel")}</Button></DialogClose>
<Button type="button" onClick={onUpdate} disabled={updateState || breakChangeFlag}>
<DialogClose asChild>
<Button type="button" variant="outline">
{t("Cancel")}
</Button>
</DialogClose>
<Button
type="button"
onClick={onUpdate}
disabled={updateState || breakChangeFlag}
>
{t("Update")}
</Button>
</DialogFooter>

View File

@@ -5,10 +5,14 @@ import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Check, X, Trash2, Edit3, ExternalLink } from "lucide-react";
interface Props {
value?: string;
onlyEdit?: boolean;
@@ -26,17 +30,18 @@ const HighlightedUrl = ({ url }: { url: string }) => {
return (
<p className="truncate text-sm" title={url}>
{parts.map((part, index) =>
part.startsWith('%') && part.endsWith('%') ? (
<span key={index} className="font-semibold text-primary">{part}</span>
part.startsWith("%") && part.endsWith("%") ? (
<span key={index} className="font-semibold text-primary">
{part}
</span>
) : (
<span key={index}>{part}</span>
)
),
)}
</p>
);
};
export const WebUIItem = (props: Props) => {
const {
value,
@@ -71,20 +76,31 @@ export const WebUIItem = (props: Props) => {
placeholder={t("Support %host, %port, %secret")}
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter') handleSave(); if (e.key === 'Escape') handleCancel(); }}
onKeyDown={(e) => {
if (e.key === "Enter") handleSave();
if (e.key === "Escape") handleCancel();
}}
/>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button size="icon" onClick={handleSave}><Check className="h-4 w-4" /></Button>
<Button size="icon" onClick={handleSave}>
<Check className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent><p>{t("Save")}</p></TooltipContent>
<TooltipContent>
<p>{t("Save")}</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button size="icon" variant="ghost" onClick={handleCancel}><X className="h-4 w-4" /></Button>
<Button size="icon" variant="ghost" onClick={handleCancel}>
<X className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent><p>{t("Cancel")}</p></TooltipContent>
<TooltipContent>
<p>{t("Cancel")}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
@@ -96,35 +112,65 @@ export const WebUIItem = (props: Props) => {
// --- Рендер режима просмотра ---
return (
<div className="w-full">
<div className="flex items-center gap-2 mt-1 mb-1 h-10"> {/* h-10 для сохранения высоты */}
<div className="flex items-center gap-2 mt-1 mb-1 h-10">
{" "}
{/* h-10 для сохранения высоты */}
<div className="flex-1 min-w-0">
{value ? <HighlightedUrl url={value} /> : <p className="text-sm text-muted-foreground">NULL</p>}
{value ? (
<HighlightedUrl url={value} />
) : (
<p className="text-sm text-muted-foreground">NULL</p>
)}
</div>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => onOpenUrl?.(value)}>
<ExternalLink className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent><p>{t("Open URL")}</p></TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => { setEditing(true); setEditValue(value); }}>
<Edit3 className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent><p>{t("Edit")}</p></TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8 text-destructive hover:text-destructive" onClick={onDelete}>
<Trash2 className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent><p>{t("Delete")}</p></TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={() => onOpenUrl?.(value)}
>
<ExternalLink className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t("Open URL")}</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={() => {
setEditing(true);
setEditValue(value);
}}
>
<Edit3 className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t("Edit")}</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-destructive hover:text-destructive"
onClick={onDelete}
>
<Trash2 className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t("Delete")}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<Separator />

View File

@@ -20,7 +20,6 @@ import {
} from "@/components/ui/dialog";
import { Plus } from "lucide-react";
export const WebUIViewer = forwardRef<DialogRef>((props, ref) => {
const { t } = useTranslation();
const { clashInfo } = useClashInfo();
@@ -71,9 +70,14 @@ export const WebUIViewer = forwardRef<DialogRef>((props, ref) => {
if (!clashInfo.server?.includes(":")) {
throw new Error(`failed to parse the server "${clashInfo.server}"`);
}
const port = clashInfo.server.slice(clashInfo.server.indexOf(":") + 1).trim();
const port = clashInfo.server
.slice(clashInfo.server.indexOf(":") + 1)
.trim();
url = url.replaceAll("%port", port || "9097");
url = url.replaceAll("%secret", encodeURIComponent(clashInfo.secret || ""));
url = url.replaceAll(
"%secret",
encodeURIComponent(clashInfo.secret || ""),
);
}
await openWebUrl(url);
} catch (e: any) {
@@ -87,7 +91,11 @@ export const WebUIViewer = forwardRef<DialogRef>((props, ref) => {
<DialogHeader className="pr-7">
<div className="flex justify-between items-center">
<DialogTitle>{t("Web UI")}</DialogTitle>
<Button size="sm" disabled={editing} onClick={() => setEditing(true)}>
<Button
size="sm"
disabled={editing}
onClick={() => setEditing(true)}
>
<Plus className="mr-2 h-4 w-4" />
{t("New")}
</Button>
@@ -96,7 +104,9 @@ export const WebUIViewer = forwardRef<DialogRef>((props, ref) => {
<div className="max-h-[60vh] overflow-y-auto -mx-6 px-6">
{!editing && webUIList.length === 0 ? (
<div className="h-40"> {/* Задаем высоту для центрирования */}
<div className="h-40">
{" "}
{/* Задаем высоту для центрирования */}
<BaseEmpty
extra={
<p className="mt-2 text-xs text-center">
@@ -130,9 +140,11 @@ export const WebUIViewer = forwardRef<DialogRef>((props, ref) => {
</div>
<DialogFooter>
<DialogClose asChild>
<Button type="button" variant="outline">{t("Close")}</Button>
</DialogClose>
<DialogClose asChild>
<Button type="button" variant="outline">
{t("Close")}
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>

View File

@@ -28,8 +28,18 @@ import { GuardState } from "./mods/guard-state";
// Иконки
import {
Settings, Network, Dna, Globe2, Timer, FileText, Plug, RadioTower,
LayoutDashboard, Cog, Repeat, Map as MapIcon
Settings,
Network,
Dna,
Globe2,
Timer,
FileText,
Plug,
RadioTower,
LayoutDashboard,
Cog,
Repeat,
Map as MapIcon,
} from "lucide-react";
// Модальные окна
@@ -47,17 +57,44 @@ interface Props {
}
// Компонент для строки настроек
const SettingRow = ({ label, extra, children, onClick }: { label: React.ReactNode; extra?: React.ReactNode; children?: React.ReactNode; onClick?: () => void; }) => (
<div className={`flex items-center justify-between py-3 border-b border-border last:border-b-0 ${onClick ? 'cursor-pointer hover:bg-accent/50 -mx-3 px-3 rounded-md' : ''}`} onClick={onClick}>
<div className="flex items-center gap-2"><div className="text-sm font-medium">{label}</div>{extra && <div className="text-muted-foreground">{extra}</div>}</div>
<div>{children}</div>
const SettingRow = ({
label,
extra,
children,
onClick,
}: {
label: React.ReactNode;
extra?: React.ReactNode;
children?: React.ReactNode;
onClick?: () => void;
}) => (
<div
className={`flex items-center justify-between py-3 border-b border-border last:border-b-0 ${onClick ? "cursor-pointer hover:bg-accent/50 -mx-3 px-3 rounded-md" : ""}`}
onClick={onClick}
>
<div className="flex items-center gap-2">
<div className="text-sm font-medium">{label}</div>
{extra && <div className="text-muted-foreground">{extra}</div>}
</div>
<div>{children}</div>
</div>
);
// Вспомогательная функция для создания лейбла с иконкой
const LabelWithIcon = ({ icon, text }: { icon: React.ElementType, text: string }) => {
const Icon = icon;
return ( <span className="flex items-center gap-3"><Icon className="h-4 w-4 text-muted-foreground" />{text}</span> );
const LabelWithIcon = ({
icon,
text,
}: {
icon: React.ElementType;
text: string;
}) => {
const Icon = icon;
return (
<span className="flex items-center gap-3">
<Icon className="h-4 w-4 text-muted-foreground" />
{text}
</span>
);
};
const SettingClash = ({ onError }: Props) => {
@@ -131,32 +168,93 @@ const SettingClash = ({ onError }: Props) => {
<NetworkInterfaceViewer ref={networkRef} />
<DnsViewer ref={dnsRef} />
<SettingRow label={<LabelWithIcon icon={Network} text={t("Allow Lan")} />} extra={<TooltipIcon tooltip={t("Network Interface")} icon={<Settings className="h-4 w-4"/>} onClick={() => networkRef.current?.open()} />}>
<GuardState value={allowLan ?? false} valueProps="checked" onChangeProps="onCheckedChange" onFormat={onSwitchFormat} onChange={(e) => onChangeData({ "allow-lan": e })} onGuard={(e) => patchClash({ "allow-lan": e })} onCatch={onError}>
<SettingRow
label={<LabelWithIcon icon={Network} text={t("Allow Lan")} />}
extra={
<TooltipIcon
tooltip={t("Network Interface")}
icon={<Settings className="h-4 w-4" />}
onClick={() => networkRef.current?.open()}
/>
}
>
<GuardState
value={allowLan ?? false}
valueProps="checked"
onChangeProps="onCheckedChange"
onFormat={onSwitchFormat}
onChange={(e) => onChangeData({ "allow-lan": e })}
onGuard={(e) => patchClash({ "allow-lan": e })}
onCatch={onError}
>
<Switch />
</GuardState>
</SettingRow>
<SettingRow label={<LabelWithIcon icon={Dna} text={t("DNS Overwrite")} />} extra={<TooltipIcon tooltip={t("DNS Settings")} icon={<Settings className="h-4 w-4"/>} onClick={() => dnsRef.current?.open()} />}>
<Switch checked={dnsSettingsEnabled} onCheckedChange={handleDnsToggle} />
<SettingRow
label={<LabelWithIcon icon={Dna} text={t("DNS Overwrite")} />}
extra={
<TooltipIcon
tooltip={t("DNS Settings")}
icon={<Settings className="h-4 w-4" />}
onClick={() => dnsRef.current?.open()}
/>
}
>
<Switch
checked={dnsSettingsEnabled}
onCheckedChange={handleDnsToggle}
/>
</SettingRow>
<SettingRow label={<LabelWithIcon icon={Globe2} text={t("IPv6")} />}>
<GuardState value={ipv6 ?? false} valueProps="checked" onChangeProps="onCheckedChange" onFormat={onSwitchFormat} onChange={(e) => onChangeData({ ipv6: e })} onGuard={(e) => patchClash({ ipv6: e })} onCatch={onError}>
<GuardState
value={ipv6 ?? false}
valueProps="checked"
onChangeProps="onCheckedChange"
onFormat={onSwitchFormat}
onChange={(e) => onChangeData({ ipv6: e })}
onGuard={(e) => patchClash({ ipv6: e })}
onCatch={onError}
>
<Switch />
</GuardState>
</SettingRow>
<SettingRow label={<LabelWithIcon icon={Timer} text={t("Unified Delay")} />} extra={<TooltipIcon tooltip={t("Unified Delay Info")} />}>
<GuardState value={unifiedDelay ?? false} valueProps="checked" onChangeProps="onCheckedChange" onFormat={onSwitchFormat} onChange={(e) => onChangeData({ "unified-delay": e })} onGuard={(e) => patchClash({ "unified-delay": e })} onCatch={onError}>
<SettingRow
label={<LabelWithIcon icon={Timer} text={t("Unified Delay")} />}
extra={<TooltipIcon tooltip={t("Unified Delay Info")} />}
>
<GuardState
value={unifiedDelay ?? false}
valueProps="checked"
onChangeProps="onCheckedChange"
onFormat={onSwitchFormat}
onChange={(e) => onChangeData({ "unified-delay": e })}
onGuard={(e) => patchClash({ "unified-delay": e })}
onCatch={onError}
>
<Switch />
</GuardState>
</SettingRow>
<SettingRow label={<LabelWithIcon icon={FileText} text={t("Log Level")} />} extra={<TooltipIcon tooltip={t("Log Level Info")} />}>
<GuardState value={logLevel ?? "info"} valueProps="value" onChangeProps="onValueChange" onFormat={onSelectFormat} onChange={(e) => onChangeData({ "log-level": e })} onGuard={(e) => patchClash({ "log-level": e })} onCatch={onError}>
<SettingRow
label={<LabelWithIcon icon={FileText} text={t("Log Level")} />}
extra={<TooltipIcon tooltip={t("Log Level Info")} />}
>
<GuardState
value={logLevel ?? "info"}
valueProps="value"
onChangeProps="onValueChange"
onFormat={onSelectFormat}
onChange={(e) => onChangeData({ "log-level": e })}
onGuard={(e) => patchClash({ "log-level": e })}
onCatch={onError}
>
<Select value={logLevel}>
<SelectTrigger className="w-28 h-8"><SelectValue /></SelectTrigger>
<SelectTrigger className="w-28 h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="info">Info</SelectItem>
<SelectItem value="warning">Warning</SelectItem>
@@ -168,21 +266,67 @@ const SettingClash = ({ onError }: Props) => {
</GuardState>
</SettingRow>
<SettingRow label={<LabelWithIcon icon={Plug} text={t("Port Config")} />}>
<Button variant="outline" className="w-28 h-8 font-mono" onClick={() => portRef.current?.open()}>{verge_mixed_port ?? 7897}</Button>
<SettingRow
label={<LabelWithIcon icon={Plug} text={t("Port Config")} />}
>
<Button
variant="outline"
className="w-28 h-8 font-mono"
onClick={() => portRef.current?.open()}
>
{verge_mixed_port ?? 7897}
</Button>
</SettingRow>
<SettingRow onClick={() => ctrlRef.current?.open()} label={<div className="flex items-center gap-3"><RadioTower className="h-4 w-4 text-muted-foreground" />{t("External Controller")} <TooltipIcon tooltip={t("Enable one-click random API port and key. Click to randomize the port and key")} /></div>} />
<SettingRow
onClick={() => ctrlRef.current?.open()}
label={
<div className="flex items-center gap-3">
<RadioTower className="h-4 w-4 text-muted-foreground" />
{t("External Controller")}{" "}
<TooltipIcon
tooltip={t(
"Enable one-click random API port and key. Click to randomize the port and key",
)}
/>
</div>
}
/>
<SettingRow onClick={() => webRef.current?.open()} label={<LabelWithIcon icon={LayoutDashboard} text={t("Yacd Web UI")} />} />
<SettingRow
onClick={() => webRef.current?.open()}
label={
<LabelWithIcon icon={LayoutDashboard} text={t("Yacd Web UI")} />
}
/>
<SettingRow label={<LabelWithIcon icon={Cog} text={t("Clash Core")} />} extra={<TooltipIcon tooltip={t("Clash Core Settings")} icon={<Settings className="h-4 w-4"/>} onClick={() => coreRef.current?.open()} />}>
<SettingRow
label={<LabelWithIcon icon={Cog} text={t("Clash Core")} />}
extra={
<TooltipIcon
tooltip={t("Clash Core Settings")}
icon={<Settings className="h-4 w-4" />}
onClick={() => coreRef.current?.open()}
/>
}
>
<p className="text-sm font-medium pr-2 font-mono">{version}</p>
</SettingRow>
{isWIN && <SettingRow onClick={useLockFn(invoke_uwp_tool)} label={<LabelWithIcon icon={Repeat} text={t("UWP Loopback Tool")} />} extra={<TooltipIcon tooltip={t("Open UWP tool Info")} />} />}
{isWIN && (
<SettingRow
onClick={useLockFn(invoke_uwp_tool)}
label={
<LabelWithIcon icon={Repeat} text={t("UWP Loopback Tool")} />
}
extra={<TooltipIcon tooltip={t("Open UWP tool Info")} />}
/>
)}
<SettingRow onClick={onUpdateGeo} label={<LabelWithIcon icon={MapIcon} text={t("Update GeoData")} />} />
<SettingRow
onClick={onUpdateGeo}
label={<LabelWithIcon icon={MapIcon} text={t("Update GeoData")} />}
/>
</div>
</div>
);

View File

@@ -46,7 +46,6 @@ import { UpdateViewer } from "./mods/update-viewer";
import { BackupViewer } from "./mods/backup-viewer";
import { LiteModeViewer } from "./mods/lite-mode-viewer";
interface Props {
onError?: (err: Error) => void;
}
@@ -63,22 +62,19 @@ const SettingRow = ({
children?: React.ReactNode;
onClick?: () => void;
}) => (
<div
className={`flex items-center justify-between py-3 border-b border-border last:border-b-0 ${onClick ? 'cursor-pointer hover:bg-accent/50 -mx-3 px-3 rounded-md' : ''}`}
onClick={onClick}
>
<div className="flex items-center gap-2">
{/* Мы ожидаем, что label теперь может быть сложным компонентом */}
<div className="text-sm font-medium">{label}</div>
{extra && <div className="text-muted-foreground">{extra}</div>}
</div>
<div>
{children}
</div>
<div
className={`flex items-center justify-between py-3 border-b border-border last:border-b-0 ${onClick ? "cursor-pointer hover:bg-accent/50 -mx-3 px-3 rounded-md" : ""}`}
onClick={onClick}
>
<div className="flex items-center gap-2">
{/* Мы ожидаем, что label теперь может быть сложным компонентом */}
<div className="text-sm font-medium">{label}</div>
{extra && <div className="text-muted-foreground">{extra}</div>}
</div>
<div>{children}</div>
</div>
);
const SettingVergeAdvanced = ({ onError }: Props) => {
const { t } = useTranslation();
@@ -110,7 +106,13 @@ const SettingVergeAdvanced = ({ onError }: Props) => {
}, []);
// Вспомогательная функция для создания лейбла с иконкой
const LabelWithIcon = ({ icon, text }: { icon: React.ElementType, text: string }) => {
const LabelWithIcon = ({
icon,
text,
}: {
icon: React.ElementType;
text: string;
}) => {
const Icon = icon;
return (
<span className="flex items-center gap-3">
@@ -122,7 +124,9 @@ const SettingVergeAdvanced = ({ onError }: Props) => {
return (
<div>
<h3 className="text-lg font-medium mb-4">{t("Verge Advanced Setting")}</h3>
<h3 className="text-lg font-medium mb-4">
{t("Verge Advanced Setting")}
</h3>
<div className="space-y-1">
<ThemeViewer ref={themeRef} />
<ConfigViewer ref={configRef} />
@@ -134,21 +138,71 @@ const SettingVergeAdvanced = ({ onError }: Props) => {
<LiteModeViewer ref={liteModeRef} />
{/* --- НАЧАЛО ИЗМЕНЕНИЙ 2: Добавляем иконки к каждому пункту --- */}
<SettingRow onClick={() => backupRef.current?.open()} label={<LabelWithIcon icon={Archive} text={t("Backup Setting")} />} extra={<TooltipIcon tooltip={t("Backup Setting Info")} />} />
<SettingRow onClick={() => configRef.current?.open()} label={<LabelWithIcon icon={FileCode} text={t("Runtime Config")} />} />
<SettingRow onClick={openAppDir} label={<LabelWithIcon icon={Folder} text={t("Open Conf Dir")} />} extra={<TooltipIcon tooltip={t("Open Conf Dir Info")} />} />
<SettingRow onClick={openCoreDir} label={<LabelWithIcon icon={FolderCog} text={t("Open Core Dir")} />} />
<SettingRow onClick={openLogsDir} label={<LabelWithIcon icon={FolderClock} text={t("Open Logs Dir")} />} />
<SettingRow onClick={onCheckUpdate} label={<LabelWithIcon icon={RefreshCw} text={t("Check for Updates")} />} />
<SettingRow onClick={openDevTools} label={<LabelWithIcon icon={Terminal} text={t("Open Dev Tools")} />} />
<SettingRow label={<LabelWithIcon icon={Feather} text={t("LightWeight Mode Settings")} />} extra={<TooltipIcon tooltip={t("LightWeight Mode Info")} />} onClick={() => liteModeRef.current?.open()} />
<SettingRow onClick={exitApp} label={<LabelWithIcon icon={LogOut} text={t("Exit")} />} />
<SettingRow
onClick={() => backupRef.current?.open()}
label={<LabelWithIcon icon={Archive} text={t("Backup Setting")} />}
extra={<TooltipIcon tooltip={t("Backup Setting Info")} />}
/>
<SettingRow
onClick={() => configRef.current?.open()}
label={<LabelWithIcon icon={FileCode} text={t("Runtime Config")} />}
/>
<SettingRow
onClick={openAppDir}
label={<LabelWithIcon icon={Folder} text={t("Open Conf Dir")} />}
extra={<TooltipIcon tooltip={t("Open Conf Dir Info")} />}
/>
<SettingRow
onClick={openCoreDir}
label={<LabelWithIcon icon={FolderCog} text={t("Open Core Dir")} />}
/>
<SettingRow
onClick={openLogsDir}
label={<LabelWithIcon icon={FolderClock} text={t("Open Logs Dir")} />}
/>
<SettingRow
onClick={onCheckUpdate}
label={
<LabelWithIcon icon={RefreshCw} text={t("Check for Updates")} />
}
/>
<SettingRow
onClick={openDevTools}
label={<LabelWithIcon icon={Terminal} text={t("Open Dev Tools")} />}
/>
<SettingRow
label={
<LabelWithIcon
icon={Feather}
text={t("LightWeight Mode Settings")}
/>
}
extra={<TooltipIcon tooltip={t("LightWeight Mode Info")} />}
onClick={() => liteModeRef.current?.open()}
/>
<SettingRow
onClick={exitApp}
label={<LabelWithIcon icon={LogOut} text={t("Exit")} />}
/>
<SettingRow label={<LabelWithIcon icon={ClipboardList} text={t("Export Diagnostic Info")} />}>
<TooltipIcon tooltip={t("Copy")} icon={<Copy className="h-4 w-4"/>} onClick={onExportDiagnosticInfo} />
<SettingRow
label={
<LabelWithIcon
icon={ClipboardList}
text={t("Export Diagnostic Info")}
/>
}
>
<TooltipIcon
tooltip={t("Copy")}
icon={<Copy className="h-4 w-4" />}
onClick={onExportDiagnosticInfo}
/>
</SettingRow>
<SettingRow label={<LabelWithIcon icon={Info} text={t("Verge Version")} />}>
<SettingRow
label={<LabelWithIcon icon={Info} text={t("Verge Version")} />}
>
<p className="text-sm font-medium pr-2 font-mono">v{version}</p>
</SettingRow>
{/* --- КОНЕЦ ИЗМЕНЕНИЙ 2 --- */}

View File

@@ -12,13 +12,28 @@ import { DialogRef, Switch } from "@/components/base";
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { GuardState } from "./mods/guard-state";
import { ThemeModeSwitch } from "./mods/theme-mode-switch"; // Импортируем наш новый компонент
import {
Copy, Languages, Palette, MousePointerClick, Terminal, Home, FileTerminal,
SwatchBook, LayoutTemplate, Sparkles, Keyboard
Copy,
Languages,
Palette,
MousePointerClick,
Terminal,
Home,
FileTerminal,
SwatchBook,
LayoutTemplate,
Sparkles,
Keyboard,
} from "lucide-react";
import { ConfigViewer } from "./mods/config-viewer";
@@ -37,28 +52,69 @@ const OS = getSystem();
const languageOptions = Object.entries(languages).map(([code, _]) => {
const labels: { [key: string]: string } = {
en: "English", ru: "Русский", zh: "中文", fa: "فارسی", tt: "Татар", id: "Bahasa Indonesia",
ar: "العربية", ko: "한국어", tr: "Türkçe",
en: "English",
ru: "Русский",
zh: "中文",
fa: "فارسی",
tt: "Татар",
id: "Bahasa Indonesia",
ar: "العربية",
ko: "한국어",
tr: "Türkçe",
};
return { code, label: labels[code] || code };
});
const SettingRow = ({ label, extra, children, onClick }: { label: React.ReactNode; extra?: React.ReactNode; children?: React.ReactNode; onClick?: () => void; }) => (
<div className={`flex items-center justify-between py-3 border-b border-border last:border-b-0 ${onClick ? 'cursor-pointer hover:bg-accent/50 -mx-3 px-3 rounded-md' : ''}`} onClick={onClick}>
<div className="flex items-center gap-2"><div className="text-sm font-medium">{label}</div>{extra && <div className="text-muted-foreground">{extra}</div>}</div>
<div>{children}</div>
const SettingRow = ({
label,
extra,
children,
onClick,
}: {
label: React.ReactNode;
extra?: React.ReactNode;
children?: React.ReactNode;
onClick?: () => void;
}) => (
<div
className={`flex items-center justify-between py-3 border-b border-border last:border-b-0 ${onClick ? "cursor-pointer hover:bg-accent/50 -mx-3 px-3 rounded-md" : ""}`}
onClick={onClick}
>
<div className="flex items-center gap-2">
<div className="text-sm font-medium">{label}</div>
{extra && <div className="text-muted-foreground">{extra}</div>}
</div>
<div>{children}</div>
</div>
);
const LabelWithIcon = ({ icon, text }: { icon: React.ElementType, text: string }) => {
const Icon = icon;
return ( <span className="flex items-center gap-3"><Icon className="h-4 w-4 text-muted-foreground" />{text}</span> );
const LabelWithIcon = ({
icon,
text,
}: {
icon: React.ElementType;
text: string;
}) => {
const Icon = icon;
return (
<span className="flex items-center gap-3">
<Icon className="h-4 w-4 text-muted-foreground" />
{text}
</span>
);
};
const SettingVergeBasic = ({ onError }: Props) => {
const { t } = useTranslation();
const { verge, patchVerge, mutateVerge } = useVerge();
const { theme_mode, language, tray_event, env_type, startup_script, start_page } = verge ?? {};
const {
theme_mode,
language,
tray_event,
env_type,
startup_script,
start_page,
} = verge ?? {};
const configRef = useRef<DialogRef>(null);
const hotkeyRef = useRef<DialogRef>(null);
@@ -91,7 +147,9 @@ const SettingVergeBasic = ({ onError }: Props) => {
<UpdateViewer ref={updateRef} />
<BackupViewer ref={backupRef} />
<SettingRow label={<LabelWithIcon icon={Languages} text={t("Language")} />}>
<SettingRow
label={<LabelWithIcon icon={Languages} text={t("Language")} />}
>
<GuardState
value={language ?? "en"}
onCatch={onError}
@@ -101,90 +159,204 @@ const SettingVergeBasic = ({ onError }: Props) => {
onGuard={(e) => patchVerge({ language: e })}
>
<Select>
<SelectTrigger className="w-32 h-8"><SelectValue /></SelectTrigger>
<SelectContent>{languageOptions.map(({ code, label }) => (<SelectItem key={code} value={code}>{label}</SelectItem>))}</SelectContent>
<SelectTrigger className="w-32 h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
{languageOptions.map(({ code, label }) => (
<SelectItem key={code} value={code}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
</GuardState>
</SettingRow>
<SettingRow label={<LabelWithIcon icon={Palette} text={t("Theme Mode")} />}>
<GuardState
value={theme_mode}
onCatch={onError}
onChange={(e) => onChangeData({ theme_mode: e })}
onGuard={(e) => patchVerge({ theme_mode: e })}
>
<ThemeModeSwitch />
</GuardState>
<SettingRow
label={<LabelWithIcon icon={Palette} text={t("Theme Mode")} />}
>
<GuardState
value={theme_mode}
onCatch={onError}
onChange={(e) => onChangeData({ theme_mode: e })}
onGuard={(e) => patchVerge({ theme_mode: e })}
>
<ThemeModeSwitch />
</GuardState>
</SettingRow>
{OS !== "linux" && (
<SettingRow label={<LabelWithIcon icon={MousePointerClick} text={t("Tray Click Event")} />}>
<GuardState value={tray_event ?? "main_window"} onCatch={onError} onFormat={v => v} onChange={(e) => onChangeData({ tray_event: e })} onGuard={(e) => patchVerge({ tray_event: e })}>
<Select onValueChange={(value) => onChangeData({ tray_event: value })} value={tray_event}>
<SelectTrigger className="w-40 h-8"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="main_window">{t("Show Main Window")}</SelectItem>
<SelectItem value="tray_menu">{t("Show Tray Menu")}</SelectItem>
<SelectItem value="system_proxy">{t("System Proxy")}</SelectItem>
<SelectItem value="tun_mode">{t("Tun Mode")}</SelectItem>
<SelectItem value="disable">{t("Disable")}</SelectItem>
</SelectContent>
</Select>
</GuardState>
</SettingRow>
<SettingRow
label={
<LabelWithIcon
icon={MousePointerClick}
text={t("Tray Click Event")}
/>
}
>
<GuardState
value={tray_event ?? "main_window"}
onCatch={onError}
onFormat={(v) => v}
onChange={(e) => onChangeData({ tray_event: e })}
onGuard={(e) => patchVerge({ tray_event: e })}
>
<Select
onValueChange={(value) => onChangeData({ tray_event: value })}
value={tray_event}
>
<SelectTrigger className="w-40 h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="main_window">
{t("Show Main Window")}
</SelectItem>
<SelectItem value="tray_menu">
{t("Show Tray Menu")}
</SelectItem>
<SelectItem value="system_proxy">
{t("System Proxy")}
</SelectItem>
<SelectItem value="tun_mode">{t("Tun Mode")}</SelectItem>
<SelectItem value="disable">{t("Disable")}</SelectItem>
</SelectContent>
</Select>
</GuardState>
</SettingRow>
)}
<SettingRow label={<LabelWithIcon icon={Copy} text={t("Copy Env Type")} />} extra={<TooltipIcon tooltip={t("Copy")} icon={<Copy className="h-4 w-4"/>} onClick={onCopyClashEnv} />}>
<GuardState value={env_type ?? (OS === "windows" ? "powershell" : "bash")} onCatch={onError} onFormat={v => v} onChange={(e) => onChangeData({ env_type: e })} onGuard={(e) => patchVerge({ env_type: e })}>
<Select onValueChange={(value) => onChangeData({ env_type: value })} value={env_type}>
<SelectTrigger className="w-36 h-8"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="bash">Bash</SelectItem>
<SelectItem value="fish">Fish</SelectItem>
<SelectItem value="nushell">Nushell</SelectItem>
<SelectItem value="cmd">CMD</SelectItem>
<SelectItem value="powershell">PowerShell</SelectItem>
</SelectContent>
</Select>
</GuardState>
</SettingRow>
<SettingRow label={<LabelWithIcon icon={Home} text={t("Start Page")} />}>
<GuardState
value={start_page ?? "/"}
onCatch={onError}
onChangeProps="onValueChange"
onFormat={(value: string) => value}
onChange={(e) => onChangeData({ start_page: e })}
onGuard={(e) => patchVerge({ start_page: e })}
<SettingRow
label={<LabelWithIcon icon={Copy} text={t("Copy Env Type")} />}
extra={
<TooltipIcon
tooltip={t("Copy")}
icon={<Copy className="h-4 w-4" />}
onClick={onCopyClashEnv}
/>
}
>
<GuardState
value={env_type ?? (OS === "windows" ? "powershell" : "bash")}
onCatch={onError}
onFormat={(v) => v}
onChange={(e) => onChangeData({ env_type: e })}
onGuard={(e) => patchVerge({ env_type: e })}
>
<Select
onValueChange={(value) => onChangeData({ env_type: value })}
value={env_type}
>
<Select>
<SelectTrigger className="w-40 h-8"><SelectValue /></SelectTrigger>
<SelectContent>
{routers
.filter(page => page.label && page.path !== '/')
.map(page => (
<SelectItem key={page.path} value={page.path}>{t(page.label!)}</SelectItem>
))
}
</SelectContent>
</Select>
</GuardState>
<SelectTrigger className="w-36 h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="bash">Bash</SelectItem>
<SelectItem value="fish">Fish</SelectItem>
<SelectItem value="nushell">Nushell</SelectItem>
<SelectItem value="cmd">CMD</SelectItem>
<SelectItem value="powershell">PowerShell</SelectItem>
</SelectContent>
</Select>
</GuardState>
</SettingRow>
<SettingRow label={<LabelWithIcon icon={FileTerminal} text={t("Startup Script")} />}>
<div className="flex items-center gap-2">
<Input readOnly value={startup_script ?? ""} placeholder={t("Not Set")} className="h-8 flex-1" />
<Button variant="outline" size="sm" className="h-8" onClick={async () => { const selected = await open({ directory: false, multiple: false, filters: [{ name: "Shell Script", extensions: ["sh", "bat", "ps1"] }] }); if (selected) { const path = Array.isArray(selected) ? selected[0] : selected; onChangeData({ startup_script: path }); patchVerge({ startup_script: path }); } }}>{t("Browse")}</Button>
{startup_script && <Button variant="destructive" size="sm" className="h-8" onClick={async () => { onChangeData({ startup_script: "" }); patchVerge({ startup_script: "" }); }}>{t("Clear")}</Button>}
</div>
<SettingRow
label={<LabelWithIcon icon={Home} text={t("Start Page")} />}
>
<GuardState
value={start_page ?? "/"}
onCatch={onError}
onChangeProps="onValueChange"
onFormat={(value: string) => value}
onChange={(e) => onChangeData({ start_page: e })}
onGuard={(e) => patchVerge({ start_page: e })}
>
<Select>
<SelectTrigger className="w-40 h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
{routers
.filter((page) => page.label && page.path !== "/")
.map((page) => (
<SelectItem key={page.path} value={page.path}>
{t(page.label!)}
</SelectItem>
))}
</SelectContent>
</Select>
</GuardState>
</SettingRow>
<SettingRow onClick={() => themeRef.current?.open()} label={<LabelWithIcon icon={SwatchBook} text={t("Theme Setting")} />} />
<SettingRow onClick={() => layoutRef.current?.open()} label={<LabelWithIcon icon={LayoutTemplate} text={t("Layout Setting")} />} />
<SettingRow onClick={() => miscRef.current?.open()} label={<LabelWithIcon icon={Sparkles} text={t("Miscellaneous")} />} />
<SettingRow onClick={() => hotkeyRef.current?.open()} label={<LabelWithIcon icon={Keyboard} text={t("Hotkey Setting")} />} />
<SettingRow
label={
<LabelWithIcon icon={FileTerminal} text={t("Startup Script")} />
}
>
<div className="flex items-center gap-2">
<Input
readOnly
value={startup_script ?? ""}
placeholder={t("Not Set")}
className="h-8 flex-1"
/>
<Button
variant="outline"
size="sm"
className="h-8"
onClick={async () => {
const selected = await open({
directory: false,
multiple: false,
filters: [
{ name: "Shell Script", extensions: ["sh", "bat", "ps1"] },
],
});
if (selected) {
const path = Array.isArray(selected) ? selected[0] : selected;
onChangeData({ startup_script: path });
patchVerge({ startup_script: path });
}
}}
>
{t("Browse")}
</Button>
{startup_script && (
<Button
variant="destructive"
size="sm"
className="h-8"
onClick={async () => {
onChangeData({ startup_script: "" });
patchVerge({ startup_script: "" });
}}
>
{t("Clear")}
</Button>
)}
</div>
</SettingRow>
<SettingRow
onClick={() => themeRef.current?.open()}
label={<LabelWithIcon icon={SwatchBook} text={t("Theme Setting")} />}
/>
<SettingRow
onClick={() => layoutRef.current?.open()}
label={
<LabelWithIcon icon={LayoutTemplate} text={t("Layout Setting")} />
}
/>
<SettingRow
onClick={() => miscRef.current?.open()}
label={<LabelWithIcon icon={Sparkles} text={t("Miscellaneous")} />}
/>
<SettingRow
onClick={() => hotkeyRef.current?.open()}
label={<LabelWithIcon icon={Keyboard} text={t("Hotkey Setting")} />}
/>
</div>
</div>
);