diff --git a/src/components/proxy/provider-button.tsx b/src/components/proxy/provider-button.tsx index 5a63072e..d5c93781 100644 --- a/src/components/proxy/provider-button.tsx +++ b/src/components/proxy/provider-button.tsx @@ -7,25 +7,30 @@ import { List, ListItem, ListItemText, + styled, + Box, + alpha, + Typography, + Divider, } from "@mui/material"; import { RefreshRounded } from "@mui/icons-material"; import { useTranslation } from "react-i18next"; import { useLockFn } from "ahooks"; -import { getProviders, providerUpdate } from "@/services/api"; +import { getProxyProviders, proxyProviderUpdate } from "@/services/api"; import { BaseDialog } from "../base"; export const ProviderButton = () => { const { t } = useTranslation(); - const { data } = useSWR("getProviders", getProviders); + const { data } = useSWR("getProxyProviders", getProxyProviders); const [open, setOpen] = useState(false); const hasProvider = Object.keys(data || {}).length > 0; const handleUpdate = useLockFn(async (key: string) => { - await providerUpdate(key); + await proxyProviderUpdate(key); await mutate("getProxies"); - await mutate("getProviders"); + await mutate("getProxyProviders"); }); if (!hasProvider) return null; @@ -43,7 +48,23 @@ export const ProviderButton = () => { + {t("Proxy Provider")} + + + } contentSx={{ width: 400 }} disableOk cancelBtn={t("Cancel")} @@ -54,29 +75,43 @@ export const ProviderButton = () => { {Object.entries(data || {}).map(([key, item]) => { const time = dayjs(item.updatedAt); return ( - - - - Type: {item.vehicleType} - - - Updated: {time.fromNow()} - - - } - /> - handleUpdate(key)} - > - - - + <> + + + + {key} + + + } + secondary={ + <> + + {item.vehicleType} + + + {t("Update At")} {time.fromNow()} + + + } + /> + handleUpdate(key)} + > + + + + + ); })} @@ -84,3 +119,15 @@ export const ProviderButton = () => { ); }; + +const StyledTypeBox = styled(Box)(({ theme }) => ({ + display: "inline-block", + border: "1px solid #ccc", + borderColor: alpha(theme.palette.primary.main, 0.5), + color: alpha(theme.palette.primary.main, 0.8), + borderRadius: 4, + fontSize: 10, + marginRight: "4px", + padding: "0 2px", + lineHeight: 1.25, +})); diff --git a/src/components/rule/provider-button.tsx b/src/components/rule/provider-button.tsx new file mode 100644 index 00000000..0379f8cf --- /dev/null +++ b/src/components/rule/provider-button.tsx @@ -0,0 +1,150 @@ +import dayjs from "dayjs"; +import useSWR, { mutate } from "swr"; +import { useState } from "react"; +import { + Button, + IconButton, + List, + ListItem, + ListItemText, + Typography, + styled, + Box, + alpha, + Divider, +} from "@mui/material"; +import { RefreshRounded } from "@mui/icons-material"; +import { useTranslation } from "react-i18next"; +import { useLockFn } from "ahooks"; +import { getRuleProviders, ruleProviderUpdate } from "@/services/api"; +import { BaseDialog } from "../base"; + +export const ProviderButton = () => { + const { t } = useTranslation(); + const { data } = useSWR("getRuleProviders", getRuleProviders); + + const [open, setOpen] = useState(false); + + const hasProvider = Object.keys(data || {}).length > 0; + + const handleUpdate = useLockFn(async (key: string) => { + await ruleProviderUpdate(key); + await mutate("getRules"); + await mutate("getRuleProviders"); + }); + + if (!hasProvider) return null; + + return ( + <> + + + + {t("Rule Provider")} + + + } + contentSx={{ width: 400 }} + disableOk + cancelBtn={t("Cancel")} + onClose={() => setOpen(false)} + onCancel={() => setOpen(false)} + > + + {Object.entries(data || {}).map(([key, item]) => { + const time = dayjs(item.updatedAt); + return ( + <> + + + + {key} + + + {item.ruleCount} + + + } + secondary={ + <> + + {item.vehicleType} + + + {item.behavior} + + + {t("Update At")} {time.fromNow()} + + + } + /> + handleUpdate(key)} + > + + + + + + ); + })} + + + + ); +}; +const TypeBox = styled(Box)(({ theme }) => ({ + display: "inline-block", + border: "1px solid #ccc", + borderColor: alpha(theme.palette.secondary.main, 0.5), + color: alpha(theme.palette.secondary.main, 0.8), + borderRadius: 4, + fontSize: 10, + marginRight: "4px", + padding: "0 2px", + lineHeight: 1.25, +})); + +const StyledTypeBox = styled(Box)(({ theme }) => ({ + display: "inline-block", + border: "1px solid #ccc", + borderColor: alpha(theme.palette.primary.main, 0.5), + color: alpha(theme.palette.primary.main, 0.8), + borderRadius: 4, + fontSize: 10, + marginRight: "4px", + padding: "0 2px", + lineHeight: 1.25, +})); diff --git a/src/locales/en.json b/src/locales/en.json index f4f6455a..415ca9b0 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -57,6 +57,8 @@ "Filter conditions": "Filter conditions", "Refresh profiles": "Refresh profiles", "Rules": "Rules", + "Update All": "Update All", + "Update At": "Update At", "Type": "Type", "Name": "Name", diff --git a/src/locales/ru.json b/src/locales/ru.json index 8bf853a7..224483db 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -56,6 +56,9 @@ "Filter": "Фильтр", "Filter conditions": "Условия фильтрации", "Refresh profiles": "Обновить профили", + "Rules": "Правила", + "Update All": "Обновить все", + "Update At": "Обновлено в", "Type": "Тип", "Name": "Название", diff --git a/src/locales/zh.json b/src/locales/zh.json index 4abe3ad4..06faae3a 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -57,6 +57,8 @@ "Filter conditions": "过滤条件", "Refresh profiles": "刷新订阅", "Rules": "规则", + "Update All": "更新全部", + "Update At": "更新于", "Type": "类型", "Name": "名称", diff --git a/src/pages/_layout.tsx b/src/pages/_layout.tsx index 2376c439..0da18859 100644 --- a/src/pages/_layout.tsx +++ b/src/pages/_layout.tsx @@ -54,7 +54,7 @@ const Layout = () => { mutate("getProxies"); mutate("getVersion"); mutate("getClashConfig"); - mutate("getProviders"); + mutate("getProxyProviders"); }); // update the verge config diff --git a/src/pages/rules.tsx b/src/pages/rules.tsx index feab0340..e197e69e 100644 --- a/src/pages/rules.tsx +++ b/src/pages/rules.tsx @@ -2,10 +2,11 @@ import useSWR from "swr"; import { useState, useMemo } from "react"; import { useTranslation } from "react-i18next"; import { Virtuoso } from "react-virtuoso"; -import { Box, Paper, TextField } from "@mui/material"; +import { Box, TextField } from "@mui/material"; import { getRules } from "@/services/api"; import { BaseEmpty, BasePage } from "@/components/base"; import RuleItem from "@/components/rule/rule-item"; +import { ProviderButton } from "@/components/rule/provider-button"; const RulesPage = () => { const { t } = useTranslation(); @@ -18,7 +19,16 @@ const RulesPage = () => { }, [data, filterText]); return ( - + + + + } + > { export const getProxies = async () => { const [proxyRecord, providerRecord] = await Promise.all([ getProxiesInner(), - getProviders(), + getProxyProviders(), ]); // provider name map @@ -166,11 +166,31 @@ export const getProxies = async () => { }; // get proxy providers -export const getProviders = async () => { +export const getProxyProviders = async () => { const instance = await getAxios(); const response = await instance.get("/providers/proxies"); - const providers = (response.providers || {}) as Record; + const providers = (response.providers || {}) as Record< + string, + IProxyProviderItem + >; + + return Object.fromEntries( + Object.entries(providers).filter(([key, item]) => { + const type = item.vehicleType.toLowerCase(); + return type === "http" || type === "file"; + }) + ); +}; + +export const getRuleProviders = async () => { + const instance = await getAxios(); + const response = await instance.get("/providers/rules"); + + const providers = (response.providers || {}) as Record< + string, + IRuleProviderItem + >; return Object.fromEntries( Object.entries(providers).filter(([key, item]) => { @@ -188,11 +208,16 @@ export const providerHealthCheck = async (name: string) => { ); }; -export const providerUpdate = async (name: string) => { +export const proxyProviderUpdate = async (name: string) => { const instance = await getAxios(); return instance.put(`/providers/proxies/${encodeURIComponent(name)}`); }; +export const ruleProviderUpdate = async (name: string) => { + const instance = await getAxios(); + return instance.put(`/providers/rules/${encodeURIComponent(name)}`); +}; + export const getConnections = async () => { const instance = await getAxios(); const result = await instance.get("/connections"); diff --git a/src/services/types.d.ts b/src/services/types.d.ts index 1fe07020..c080965d 100644 --- a/src/services/types.d.ts +++ b/src/services/types.d.ts @@ -61,7 +61,7 @@ type IProxyGroupItem = Omit & { all: IProxyItem[]; }; -interface IProviderItem { +interface IProxyProviderItem { name: string; type: string; proxies: IProxyItem[]; @@ -69,6 +69,16 @@ interface IProviderItem { vehicleType: string; } +interface IRuleProviderItem { + name: string; + behavior: string; + format: string; + ruleCount: number; + type: string; + updatedAt: string; + vehicleType: string; +} + interface ITrafficItem { up: number; down: number;