mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-01-29 08:45:41 +08:00
Merge branch 'fix-migrate-tauri2-errors'
* fix-migrate-tauri2-errors: (288 commits) # Conflicts: # .github/ISSUE_TEMPLATE/bug_report.yml
This commit is contained in:
@@ -10,7 +10,7 @@ type Props = {
|
||||
};
|
||||
|
||||
export const BaseFieldset: React.FC<Props> = (props: Props) => {
|
||||
const Fieldset = styled(Box)(() => ({
|
||||
const Fieldset = styled(Box)<{ component?: string }>(() => ({
|
||||
position: "relative",
|
||||
border: "1px solid #bbb",
|
||||
borderRadius: "5px",
|
||||
|
||||
33
src/components/base/base-loading-overlay.tsx
Normal file
33
src/components/base/base-loading-overlay.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import React from "react";
|
||||
import { Box, CircularProgress } from "@mui/material";
|
||||
|
||||
export interface BaseLoadingOverlayProps {
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export const BaseLoadingOverlay: React.FC<BaseLoadingOverlayProps> = ({
|
||||
isLoading,
|
||||
}) => {
|
||||
if (!isLoading) return null;
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
backgroundColor: "rgba(255, 255, 255, 0.7)",
|
||||
zIndex: 1000,
|
||||
}}
|
||||
>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default BaseLoadingOverlay;
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { ReactNode } from "react";
|
||||
import { Typography } from "@mui/material";
|
||||
import { BaseErrorBoundary } from "./base-error-boundary";
|
||||
import { useCustomTheme } from "@/components/layout/use-custom-theme";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
|
||||
interface Props {
|
||||
title?: React.ReactNode; // the page title
|
||||
@@ -13,7 +13,7 @@ interface Props {
|
||||
|
||||
export const BasePage: React.FC<Props> = (props) => {
|
||||
const { title, header, contentStyle, full, children } = props;
|
||||
const { theme } = useCustomTheme();
|
||||
const theme = useTheme();
|
||||
|
||||
const isDark = theme.palette.mode === "dark";
|
||||
|
||||
|
||||
@@ -7,20 +7,19 @@ import matchCaseIcon from "@/assets/image/component/match_case.svg?react";
|
||||
import matchWholeWordIcon from "@/assets/image/component/match_whole_word.svg?react";
|
||||
import useRegularExpressionIcon from "@/assets/image/component/use_regular_expression.svg?react";
|
||||
|
||||
export type SearchState = {
|
||||
text: string;
|
||||
matchCase: boolean;
|
||||
matchWholeWord: boolean;
|
||||
useRegularExpression: boolean;
|
||||
};
|
||||
|
||||
type SearchProps = {
|
||||
placeholder?: string;
|
||||
matchCase?: boolean;
|
||||
matchWholeWord?: boolean;
|
||||
useRegularExpression?: boolean;
|
||||
onSearch: (
|
||||
match: (content: string) => boolean,
|
||||
state: {
|
||||
text: string;
|
||||
matchCase: boolean;
|
||||
matchWholeWord: boolean;
|
||||
useRegularExpression: boolean;
|
||||
}
|
||||
) => void;
|
||||
onSearch: (match: (content: string) => boolean, state: SearchState) => void;
|
||||
};
|
||||
|
||||
export const BaseSearchBox = styled((props: SearchProps) => {
|
||||
@@ -28,10 +27,10 @@ export const BaseSearchBox = styled((props: SearchProps) => {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [matchCase, setMatchCase] = useState(props.matchCase ?? false);
|
||||
const [matchWholeWord, setMatchWholeWord] = useState(
|
||||
props.matchWholeWord ?? false
|
||||
props.matchWholeWord ?? false,
|
||||
);
|
||||
const [useRegularExpression, setUseRegularExpression] = useState(
|
||||
props.useRegularExpression ?? false
|
||||
props.useRegularExpression ?? false,
|
||||
);
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
|
||||
@@ -60,7 +59,7 @@ export const BaseSearchBox = styled((props: SearchProps) => {
|
||||
matchCase,
|
||||
matchWholeWord,
|
||||
useRegularExpression,
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -5,3 +5,4 @@ export { BaseLoading } from "./base-loading";
|
||||
export { BaseErrorBoundary } from "./base-error-boundary";
|
||||
export { Notice } from "./base-notice";
|
||||
export { Switch } from "./base-switch";
|
||||
export { BaseLoadingOverlay } from "./base-loading-overlay";
|
||||
|
||||
@@ -30,6 +30,7 @@ export const ConnectionDetail = forwardRef<ConnectionDetailRef>(
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
sx={{ maxWidth: "520px" }}
|
||||
message={
|
||||
detail ? (
|
||||
<InnerConnectionDetail data={detail} onClose={onClose} />
|
||||
@@ -37,7 +38,7 @@ export const ConnectionDetail = forwardRef<ConnectionDetailRef>(
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
interface InnerProps {
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
import { useClashInfo } from "@/hooks/use-clash";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { TrafficGraph, type TrafficRef } from "./traffic-graph";
|
||||
import { useLogData } from "@/hooks/use-log-data";
|
||||
import { useVisibility } from "@/hooks/use-visibility";
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
import useSWRSubscription from "swr/subscription";
|
||||
@@ -39,11 +38,6 @@ export const LayoutTraffic = () => {
|
||||
return () => {};
|
||||
}, [isDebug]);
|
||||
|
||||
// https://swr.vercel.app/docs/subscription#deduplication
|
||||
// useSWRSubscription auto deduplicates to one subscription per key per entire app
|
||||
// So we can simply invoke it here acting as preconnect
|
||||
useLogData();
|
||||
|
||||
const { data: traffic = { up: 0, down: 0 } } = useSWRSubscription<
|
||||
ITrafficItem,
|
||||
any,
|
||||
@@ -65,7 +59,7 @@ export const LayoutTraffic = () => {
|
||||
this.close();
|
||||
next(event, { up: 0, down: 0 });
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return () => {
|
||||
@@ -75,7 +69,7 @@ export const LayoutTraffic = () => {
|
||||
{
|
||||
fallbackData: { up: 0, down: 0 },
|
||||
keepPreviousData: true,
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
/* --------- meta memory information --------- */
|
||||
@@ -102,7 +96,7 @@ export const LayoutTraffic = () => {
|
||||
this.close();
|
||||
next(event, { inuse: 0 });
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return () => {
|
||||
@@ -112,7 +106,7 @@ export const LayoutTraffic = () => {
|
||||
{
|
||||
fallbackData: { inuse: 0 },
|
||||
keepPreviousData: true,
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const [up, upUnit] = parseTraffic(traffic.up);
|
||||
|
||||
35
src/components/layout/scroll-top-button.tsx
Normal file
35
src/components/layout/scroll-top-button.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { IconButton, Fade } from "@mui/material";
|
||||
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
|
||||
|
||||
interface Props {
|
||||
onClick: () => void;
|
||||
show: boolean;
|
||||
}
|
||||
|
||||
export const ScrollTopButton = ({ onClick, show }: Props) => {
|
||||
return (
|
||||
<Fade in={show}>
|
||||
<IconButton
|
||||
onClick={onClick}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
bottom: "20px",
|
||||
right: "20px",
|
||||
backgroundColor: (theme) =>
|
||||
theme.palette.mode === "dark"
|
||||
? "rgba(255,255,255,0.1)"
|
||||
: "rgba(0,0,0,0.1)",
|
||||
"&:hover": {
|
||||
backgroundColor: (theme) =>
|
||||
theme.palette.mode === "dark"
|
||||
? "rgba(255,255,255,0.2)"
|
||||
: "rgba(0,0,0,0.2)",
|
||||
},
|
||||
visibility: show ? "visible" : "hidden",
|
||||
}}
|
||||
>
|
||||
<KeyboardArrowUpIcon />
|
||||
</IconButton>
|
||||
</Fade>
|
||||
);
|
||||
};
|
||||
@@ -4,6 +4,7 @@ import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||
import { useSetThemeMode, useThemeMode } from "@/services/states";
|
||||
import { defaultTheme, defaultDarkTheme } from "@/pages/_theme";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
const appWindow = getCurrentWebviewWindow();
|
||||
|
||||
/**
|
||||
@@ -103,7 +104,7 @@ export const useCustomTheme = () => {
|
||||
rootEle.style.setProperty("--primary-main", theme.palette.primary.main);
|
||||
rootEle.style.setProperty(
|
||||
"--background-color-alpha",
|
||||
alpha(theme.palette.primary.main, 0.1)
|
||||
alpha(theme.palette.primary.main, 0.1),
|
||||
);
|
||||
|
||||
// inject css
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { styled, Box } from "@mui/material";
|
||||
import { SearchState } from "@/components/base/base-search-box";
|
||||
|
||||
const Item = styled(Box)(({ theme: { palette, typography } }) => ({
|
||||
padding: "8px 0",
|
||||
@@ -32,14 +33,54 @@ const Item = styled(Box)(({ theme: { palette, typography } }) => ({
|
||||
color: palette.text.primary,
|
||||
overflowWrap: "anywhere",
|
||||
},
|
||||
"& .highlight": {
|
||||
backgroundColor: palette.mode === "dark" ? "#ffeb3b40" : "#ffeb3b90",
|
||||
borderRadius: 2,
|
||||
padding: "0 2px",
|
||||
},
|
||||
}));
|
||||
|
||||
interface Props {
|
||||
value: ILogItem;
|
||||
searchState?: SearchState;
|
||||
}
|
||||
|
||||
const LogItem = (props: Props) => {
|
||||
const { value } = props;
|
||||
const LogItem = ({ value, searchState }: Props) => {
|
||||
const renderHighlightText = (text: string) => {
|
||||
if (!searchState?.text.trim()) return text;
|
||||
|
||||
try {
|
||||
const searchText = searchState.text;
|
||||
let pattern: string;
|
||||
|
||||
if (searchState.useRegularExpression) {
|
||||
try {
|
||||
new RegExp(searchText);
|
||||
pattern = searchText;
|
||||
} catch {
|
||||
pattern = searchText.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
} else {
|
||||
const escaped = searchText.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
pattern = searchState.matchWholeWord ? `\\b${escaped}\\b` : escaped;
|
||||
}
|
||||
|
||||
const flags = searchState.matchCase ? "g" : "gi";
|
||||
const parts = text.split(new RegExp(`(${pattern})`, flags));
|
||||
|
||||
return parts.map((part, index) => {
|
||||
return index % 2 === 1 ? (
|
||||
<span key={index} className="highlight">
|
||||
{part}
|
||||
</span>
|
||||
) : (
|
||||
part
|
||||
);
|
||||
});
|
||||
} catch {
|
||||
return text;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Item>
|
||||
@@ -50,7 +91,7 @@ const LogItem = (props: Props) => {
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="data">{value.payload}</span>
|
||||
<span className="data">{renderHighlightText(value.payload)}</span>
|
||||
</div>
|
||||
</Item>
|
||||
);
|
||||
|
||||
@@ -34,7 +34,7 @@ export const ProviderButton = () => {
|
||||
|
||||
const hasProvider = Object.keys(data || {}).length > 0;
|
||||
const [updating, setUpdating] = useState(
|
||||
Object.keys(data || {}).map(() => false)
|
||||
Object.keys(data || {}).map(() => false),
|
||||
);
|
||||
|
||||
const setUpdatingAt = (status: boolean, index: number) => {
|
||||
@@ -107,7 +107,7 @@ export const ProviderButton = () => {
|
||||
const expire = sub?.Expire || 0;
|
||||
const progress = Math.min(
|
||||
Math.round(((download + upload) * 100) / (total + 0.01)) + 1,
|
||||
100
|
||||
100,
|
||||
);
|
||||
return (
|
||||
<>
|
||||
@@ -190,7 +190,7 @@ export const ProviderButton = () => {
|
||||
</>
|
||||
);
|
||||
};
|
||||
const TypeBox = styled(Box)(({ theme }) => ({
|
||||
const TypeBox = styled(Box)<{ component?: React.ElementType }>(({ theme }) => ({
|
||||
display: "inline-block",
|
||||
border: "1px solid #ccc",
|
||||
borderColor: alpha(theme.palette.secondary.main, 0.5),
|
||||
@@ -202,17 +202,19 @@ const TypeBox = styled(Box)(({ theme }) => ({
|
||||
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,
|
||||
}));
|
||||
const StyledTypeBox = styled(Box)<{ component?: React.ElementType }>(
|
||||
({ 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,
|
||||
}),
|
||||
);
|
||||
|
||||
const boxStyle = {
|
||||
height: 26,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useRef } from "react";
|
||||
import { useRef, useState, useEffect } from "react";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { Virtuoso, type VirtuosoHandle } from "react-virtuoso";
|
||||
import {
|
||||
@@ -15,6 +15,7 @@ import { useRenderList } from "./use-render-list";
|
||||
import { ProxyRender } from "./proxy-render";
|
||||
import delayManager from "@/services/delay";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ScrollTopButton } from "../layout/scroll-top-button";
|
||||
|
||||
interface Props {
|
||||
mode: string;
|
||||
@@ -32,6 +33,22 @@ export const ProxyGroups = (props: Props) => {
|
||||
|
||||
const virtuosoRef = useRef<VirtuosoHandle>(null);
|
||||
|
||||
const [showScrollTop, setShowScrollTop] = useState(false);
|
||||
|
||||
// 添加滚动处理函数
|
||||
const handleScroll = (e: any) => {
|
||||
const scrollTop = e.target.scrollTop;
|
||||
setShowScrollTop(scrollTop > 100);
|
||||
};
|
||||
|
||||
// 滚动到顶部
|
||||
const scrollToTop = () => {
|
||||
virtuosoRef.current?.scrollTo?.({
|
||||
top: 0,
|
||||
behavior: "smooth",
|
||||
});
|
||||
};
|
||||
|
||||
// 切换分组的节点代理
|
||||
const handleChangeProxy = useLockFn(
|
||||
async (group: IProxyGroupItem, proxy: IProxyItem) => {
|
||||
@@ -57,7 +74,7 @@ export const ProxyGroups = (props: Props) => {
|
||||
if (!current.selected) current.selected = [];
|
||||
|
||||
const index = current.selected.findIndex(
|
||||
(item) => item.name === group.name
|
||||
(item) => item.name === group.name,
|
||||
);
|
||||
|
||||
if (index < 0) {
|
||||
@@ -66,14 +83,14 @@ export const ProxyGroups = (props: Props) => {
|
||||
current.selected[index] = { name, now: proxy.name };
|
||||
}
|
||||
await patchCurrent({ selected: current.selected });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// 测全部延迟
|
||||
const handleCheckAll = useLockFn(async (groupName: string) => {
|
||||
const proxies = renderList
|
||||
.filter(
|
||||
(e) => e.group?.name === groupName && (e.type === 2 || e.type === 4)
|
||||
(e) => e.group?.name === groupName && (e.type === 2 || e.type === 4),
|
||||
)
|
||||
.flatMap((e) => e.proxyCol || e.proxy!)
|
||||
.filter(Boolean);
|
||||
@@ -82,7 +99,7 @@ export const ProxyGroups = (props: Props) => {
|
||||
|
||||
if (providers.size) {
|
||||
Promise.allSettled(
|
||||
[...providers].map((p) => providerHealthCheck(p))
|
||||
[...providers].map((p) => providerHealthCheck(p)),
|
||||
).then(() => onProxies());
|
||||
}
|
||||
|
||||
@@ -105,7 +122,7 @@ export const ProxyGroups = (props: Props) => {
|
||||
(e) =>
|
||||
e.group?.name === name &&
|
||||
((e.type === 2 && e.proxy?.name === now) ||
|
||||
(e.type === 4 && e.proxyCol?.some((p) => p.name === now)))
|
||||
(e.type === 4 && e.proxyCol?.some((p) => p.name === now))),
|
||||
);
|
||||
|
||||
if (index >= 0) {
|
||||
@@ -122,22 +139,33 @@ export const ProxyGroups = (props: Props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Virtuoso
|
||||
ref={virtuosoRef}
|
||||
style={{ height: "calc(100% - 16px)" }}
|
||||
totalCount={renderList.length}
|
||||
increaseViewportBy={256}
|
||||
itemContent={(index) => (
|
||||
<ProxyRender
|
||||
key={renderList[index].key}
|
||||
item={renderList[index]}
|
||||
indent={mode === "rule" || mode === "script"}
|
||||
onLocation={handleLocation}
|
||||
onCheckAll={handleCheckAll}
|
||||
onHeadState={onHeadState}
|
||||
onChangeProxy={handleChangeProxy}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<div style={{ position: "relative", height: "100%" }}>
|
||||
<Virtuoso
|
||||
ref={virtuosoRef}
|
||||
style={{ height: "calc(100% - 16px)" }}
|
||||
totalCount={renderList.length}
|
||||
increaseViewportBy={256}
|
||||
scrollerRef={(ref) => {
|
||||
if (ref) {
|
||||
ref.addEventListener("scroll", handleScroll);
|
||||
}
|
||||
}}
|
||||
itemContent={(index) => (
|
||||
<>
|
||||
<ProxyRender
|
||||
key={renderList[index].key}
|
||||
item={renderList[index]}
|
||||
indent={mode === "rule" || mode === "script"}
|
||||
onLocation={handleLocation}
|
||||
onCheckAll={handleCheckAll}
|
||||
onHeadState={onHeadState}
|
||||
onChangeProxy={handleChangeProxy}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
|
||||
<ScrollTopButton show={showScrollTop} onClick={scrollToTop} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -160,6 +160,16 @@ export const ProxyItemMini = (props: Props) => {
|
||||
TFO
|
||||
</TypeBox>
|
||||
)}
|
||||
{proxy.mptcp && (
|
||||
<TypeBox color="text.secondary" component="span">
|
||||
MPTCP
|
||||
</TypeBox>
|
||||
)}
|
||||
{proxy.smux && (
|
||||
<TypeBox color="text.secondary" component="span">
|
||||
SMUX
|
||||
</TypeBox>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
@@ -239,7 +249,9 @@ const Widget = styled(Box)(({ theme: { typography } }) => ({
|
||||
borderRadius: "4px",
|
||||
}));
|
||||
|
||||
const TypeBox = styled(Box)(({ theme: { palette, typography } }) => ({
|
||||
const TypeBox = styled(Box, {
|
||||
shouldForwardProp: (prop) => prop !== "component",
|
||||
})<{ component?: React.ElementType }>(({ theme: { palette, typography } }) => ({
|
||||
display: "inline-block",
|
||||
border: "1px solid #ccc",
|
||||
borderColor: "text.secondary",
|
||||
|
||||
@@ -31,7 +31,7 @@ const Widget = styled(Box)(() => ({
|
||||
borderRadius: "4px",
|
||||
}));
|
||||
|
||||
const TypeBox = styled(Box)(({ theme }) => ({
|
||||
const TypeBox = styled("span")(({ theme }) => ({
|
||||
display: "inline-block",
|
||||
border: "1px solid #ccc",
|
||||
borderColor: alpha(theme.palette.text.secondary, 0.36),
|
||||
@@ -121,14 +121,14 @@ export const ProxyItem = (props: Props) => {
|
||||
{showType && proxy.now && ` - ${proxy.now}`}
|
||||
</Box>
|
||||
{showType && !!proxy.provider && (
|
||||
<TypeBox component="span">{proxy.provider}</TypeBox>
|
||||
<TypeBox>{proxy.provider}</TypeBox>
|
||||
)}
|
||||
{showType && <TypeBox component="span">{proxy.type}</TypeBox>}
|
||||
{showType && proxy.udp && <TypeBox component="span">UDP</TypeBox>}
|
||||
{showType && proxy.xudp && (
|
||||
<TypeBox component="span">XUDP</TypeBox>
|
||||
)}
|
||||
{showType && proxy.tfo && <TypeBox component="span">TFO</TypeBox>}
|
||||
{showType && <TypeBox>{proxy.type}</TypeBox>}
|
||||
{showType && proxy.udp && <TypeBox>UDP</TypeBox>}
|
||||
{showType && proxy.xudp && <TypeBox>XUDP</TypeBox>}
|
||||
{showType && proxy.tfo && <TypeBox>TFO</TypeBox>}
|
||||
{showType && proxy.mptcp && <TypeBox>MPTCP</TypeBox>}
|
||||
{showType && proxy.smux && <TypeBox>SMUX</TypeBox>}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -59,7 +59,7 @@ export const ProxyRender = (props: RenderProps) => {
|
||||
return url.substring(url.lastIndexOf("/") + 1);
|
||||
}
|
||||
|
||||
if (type === 0 && !group.hidden) {
|
||||
if (type === 0) {
|
||||
return (
|
||||
<ListItemButton
|
||||
dense
|
||||
@@ -125,7 +125,7 @@ export const ProxyRender = (props: RenderProps) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 1 && !group.hidden) {
|
||||
if (type === 1) {
|
||||
return (
|
||||
<ProxyHead
|
||||
sx={{ pl: 2, pr: 3, mt: indent ? 1 : 0.5, mb: 1 }}
|
||||
@@ -139,7 +139,7 @@ export const ProxyRender = (props: RenderProps) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 2 && !group.hidden) {
|
||||
if (type === 2) {
|
||||
return (
|
||||
<ProxyItem
|
||||
group={group}
|
||||
@@ -152,7 +152,7 @@ export const ProxyRender = (props: RenderProps) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 3 && !group.hidden) {
|
||||
if (type === 3) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
@@ -170,7 +170,7 @@ export const ProxyRender = (props: RenderProps) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 4 && !group.hidden) {
|
||||
if (type === 4) {
|
||||
const proxyColItemsMemo = useMemo(() => {
|
||||
return proxyCol?.map((proxy) => (
|
||||
<ProxyItemMini
|
||||
|
||||
@@ -25,7 +25,7 @@ export const useRenderList = (mode: string) => {
|
||||
const { data: proxiesData, mutate: mutateProxies } = useSWR(
|
||||
"getProxies",
|
||||
getProxies,
|
||||
{ refreshInterval: 45000 }
|
||||
{ refreshInterval: 45000 },
|
||||
);
|
||||
|
||||
const { verge } = useVerge();
|
||||
@@ -78,7 +78,7 @@ export const useRenderList = (mode: string) => {
|
||||
group.all,
|
||||
group.name,
|
||||
headState.filterText,
|
||||
headState.sortType
|
||||
headState.sortType,
|
||||
);
|
||||
|
||||
ret.push({ type: 1, key: `head-${group.name}`, group, headState });
|
||||
@@ -97,7 +97,7 @@ export const useRenderList = (mode: string) => {
|
||||
headState,
|
||||
col,
|
||||
proxyCol,
|
||||
}))
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -108,14 +108,14 @@ export const useRenderList = (mode: string) => {
|
||||
group,
|
||||
proxy,
|
||||
headState,
|
||||
}))
|
||||
})),
|
||||
);
|
||||
}
|
||||
return ret;
|
||||
});
|
||||
|
||||
if (!useRule) return retList.slice(1);
|
||||
return retList;
|
||||
return retList.filter((item) => item.group.hidden === false);
|
||||
}, [headStates, proxiesData, mode, col]);
|
||||
|
||||
return {
|
||||
|
||||
@@ -32,7 +32,7 @@ export const ProviderButton = () => {
|
||||
|
||||
const hasProvider = Object.keys(data || {}).length > 0;
|
||||
const [updating, setUpdating] = useState(
|
||||
Object.keys(data || {}).map(() => false)
|
||||
Object.keys(data || {}).map(() => false),
|
||||
);
|
||||
|
||||
const setUpdatingAt = (status: boolean, index: number) => {
|
||||
@@ -162,7 +162,9 @@ export const ProviderButton = () => {
|
||||
</>
|
||||
);
|
||||
};
|
||||
const TypeBox = styled(Box)(({ theme }) => ({
|
||||
const TypeBox = styled(Box, {
|
||||
shouldForwardProp: (prop) => prop !== "component",
|
||||
})<{ component?: React.ElementType }>(({ theme }) => ({
|
||||
display: "inline-block",
|
||||
border: "1px solid #ccc",
|
||||
borderColor: alpha(theme.palette.secondary.main, 0.5),
|
||||
@@ -174,7 +176,9 @@ const TypeBox = styled(Box)(({ theme }) => ({
|
||||
lineHeight: 1.25,
|
||||
}));
|
||||
|
||||
const StyledTypeBox = styled(Box)(({ theme }) => ({
|
||||
const StyledTypeBox = styled(Box, {
|
||||
shouldForwardProp: (prop) => prop !== "component",
|
||||
})<{ component?: React.ElementType }>(({ theme }) => ({
|
||||
display: "inline-block",
|
||||
border: "1px solid #ccc",
|
||||
borderColor: alpha(theme.palette.primary.main, 0.5),
|
||||
|
||||
245
src/components/setting/mods/backup-config-viewer.tsx
Normal file
245
src/components/setting/mods/backup-config-viewer.tsx
Normal file
@@ -0,0 +1,245 @@
|
||||
import { useState, useRef, memo, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { Notice } from "@/components/base";
|
||||
import { isValidUrl } from "@/utils/helper";
|
||||
import { useLockFn } from "ahooks";
|
||||
import {
|
||||
TextField,
|
||||
Button,
|
||||
Grid2,
|
||||
Box,
|
||||
Stack,
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
} from "@mui/material";
|
||||
import Visibility from "@mui/icons-material/Visibility";
|
||||
import VisibilityOff from "@mui/icons-material/VisibilityOff";
|
||||
import { saveWebdavConfig, createWebdavBackup } from "@/services/cmds";
|
||||
|
||||
export interface BackupConfigViewerProps {
|
||||
onBackupSuccess: () => Promise<void>;
|
||||
onSaveSuccess: () => Promise<void>;
|
||||
onRefresh: () => Promise<void>;
|
||||
onInit: () => Promise<void>;
|
||||
setLoading: (loading: boolean) => void;
|
||||
}
|
||||
|
||||
export const BackupConfigViewer = memo(
|
||||
({
|
||||
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 usernameRef = useRef<HTMLInputElement>(null);
|
||||
const passwordRef = useRef<HTMLInputElement>(null);
|
||||
const urlRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const { register, handleSubmit, watch } = useForm<IWebDavConfig>({
|
||||
defaultValues: {
|
||||
url: webdav_url,
|
||||
username: webdav_username,
|
||||
password: webdav_password,
|
||||
},
|
||||
});
|
||||
const url = watch("url");
|
||||
const username = watch("username");
|
||||
const password = watch("password");
|
||||
|
||||
const webdavChanged =
|
||||
webdav_url !== url ||
|
||||
webdav_username !== username ||
|
||||
webdav_password !== password;
|
||||
|
||||
console.log(
|
||||
"webdavChanged",
|
||||
webdavChanged,
|
||||
webdav_url,
|
||||
webdav_username,
|
||||
webdav_password,
|
||||
);
|
||||
|
||||
const handleClickShowPassword = () => {
|
||||
setShowPassword((prev) => !prev);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (webdav_url && webdav_username && webdav_password) {
|
||||
onInit();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const checkForm = () => {
|
||||
const username = usernameRef.current?.value;
|
||||
const password = passwordRef.current?.value;
|
||||
const url = urlRef.current?.value;
|
||||
|
||||
if (!url) {
|
||||
urlRef.current?.focus();
|
||||
Notice.error(t("WebDAV URL Required"));
|
||||
throw new Error(t("WebDAV URL Required"));
|
||||
} else if (!isValidUrl(url)) {
|
||||
urlRef.current?.focus();
|
||||
Notice.error(t("Invalid WebDAV URL"));
|
||||
throw new Error(t("Invalid WebDAV URL"));
|
||||
}
|
||||
if (!username) {
|
||||
usernameRef.current?.focus();
|
||||
Notice.error(t("WebDAV URL Required"));
|
||||
throw new Error(t("Username Required"));
|
||||
}
|
||||
if (!password) {
|
||||
passwordRef.current?.focus();
|
||||
Notice.error(t("WebDAV URL Required"));
|
||||
throw new Error(t("Password Required"));
|
||||
}
|
||||
};
|
||||
|
||||
const save = useLockFn(async (data: IWebDavConfig) => {
|
||||
checkForm();
|
||||
try {
|
||||
setLoading(true);
|
||||
await saveWebdavConfig(
|
||||
data.url.trim(),
|
||||
data.username.trim(),
|
||||
data.password,
|
||||
).then(() => {
|
||||
Notice.success(t("WebDAV Config Saved"));
|
||||
onSaveSuccess();
|
||||
});
|
||||
} catch (error) {
|
||||
Notice.error(t("WebDAV Config Save Failed", { error }), 3000);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
const handleBackup = useLockFn(async () => {
|
||||
checkForm();
|
||||
try {
|
||||
setLoading(true);
|
||||
await createWebdavBackup().then(async () => {
|
||||
await onBackupSuccess();
|
||||
Notice.success(t("Backup Created"));
|
||||
});
|
||||
} catch (error) {
|
||||
Notice.error(t("Backup Failed", { error }));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<form onSubmit={(e) => e.preventDefault()}>
|
||||
<Grid2 container spacing={2}>
|
||||
<Grid2 size={{ xs: 12, sm: 9 }}>
|
||||
<Grid2 container spacing={2}>
|
||||
<Grid2 size={{ xs: 12 }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label={t("WebDAV Server URL")}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
{...register("url")}
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
inputRef={urlRef}
|
||||
/>
|
||||
</Grid2>
|
||||
<Grid2 size={{ xs: 6 }}>
|
||||
<TextField
|
||||
label={t("Username")}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
{...register("username")}
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
inputRef={usernameRef}
|
||||
/>
|
||||
</Grid2>
|
||||
<Grid2 size={{ xs: 6 }}>
|
||||
<TextField
|
||||
label={t("Password")}
|
||||
type={showPassword ? "text" : "password"}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
inputRef={passwordRef}
|
||||
{...register("password")}
|
||||
slotProps={{
|
||||
input: {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
onClick={handleClickShowPassword}
|
||||
edge="end"
|
||||
>
|
||||
{showPassword ? <VisibilityOff /> : <Visibility />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
<Grid2 size={{ xs: 12, sm: 3 }}>
|
||||
<Stack
|
||||
direction="column"
|
||||
justifyContent="space-between"
|
||||
alignItems="stretch"
|
||||
sx={{ height: "100%" }}
|
||||
>
|
||||
{webdavChanged ||
|
||||
webdav_url === undefined ||
|
||||
webdav_username === undefined ||
|
||||
webdav_password === undefined ? (
|
||||
<Button
|
||||
variant="contained"
|
||||
color={"primary"}
|
||||
sx={{ height: "100%" }}
|
||||
type="button"
|
||||
onClick={handleSubmit(save)}
|
||||
>
|
||||
{t("Save")}
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="success"
|
||||
onClick={handleBackup}
|
||||
type="button"
|
||||
size="large"
|
||||
>
|
||||
{t("Backup")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={onRefresh}
|
||||
type="button"
|
||||
size="large"
|
||||
>
|
||||
{t("Refresh")}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Grid2>
|
||||
</Grid2>
|
||||
</form>
|
||||
);
|
||||
},
|
||||
);
|
||||
266
src/components/setting/mods/backup-table-viewer.tsx
Normal file
266
src/components/setting/mods/backup-table-viewer.tsx
Normal file
@@ -0,0 +1,266 @@
|
||||
import { SVGProps, memo } from "react";
|
||||
import {
|
||||
Box,
|
||||
Paper,
|
||||
IconButton,
|
||||
Divider,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TablePagination,
|
||||
} from "@mui/material";
|
||||
import { Notice } from "@/components/base";
|
||||
import { Typography } from "@mui/material";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Dayjs } from "dayjs";
|
||||
import {
|
||||
deleteWebdavBackup,
|
||||
restoreWebDavBackup,
|
||||
restartApp,
|
||||
} from "@/services/cmds";
|
||||
import DeleteIcon from "@mui/icons-material/Delete";
|
||||
import RestoreIcon from "@mui/icons-material/Restore";
|
||||
|
||||
export type BackupFile = IWebDavFile & {
|
||||
platform: string;
|
||||
backup_time: Dayjs;
|
||||
allow_apply: boolean;
|
||||
};
|
||||
|
||||
export const DEFAULT_ROWS_PER_PAGE = 5;
|
||||
|
||||
export interface BackupTableViewerProps {
|
||||
datasource: BackupFile[];
|
||||
page: number;
|
||||
onPageChange: (
|
||||
event: React.MouseEvent<HTMLButtonElement> | null,
|
||||
page: number,
|
||||
) => void;
|
||||
total: number;
|
||||
onRefresh: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const BackupTableViewer = memo(
|
||||
({
|
||||
datasource,
|
||||
page,
|
||||
onPageChange,
|
||||
total,
|
||||
onRefresh,
|
||||
}: BackupTableViewerProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleDelete = useLockFn(async (filename: string) => {
|
||||
await deleteWebdavBackup(filename);
|
||||
await onRefresh();
|
||||
});
|
||||
|
||||
const handleRestore = useLockFn(async (filename: string) => {
|
||||
await restoreWebDavBackup(filename).then(() => {
|
||||
Notice.success(t("Restore Success, App will restart in 1s"));
|
||||
});
|
||||
await restartApp();
|
||||
});
|
||||
|
||||
return (
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>{t("Filename")}</TableCell>
|
||||
<TableCell>{t("Backup Time")}</TableCell>
|
||||
<TableCell align="right">{t("Actions")}</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{datasource.length > 0 ? (
|
||||
datasource?.map((file, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell component="th" scope="row">
|
||||
{file.platform === "windows" ? (
|
||||
<WindowsIcon className="h-full w-full" />
|
||||
) : file.platform === "linux" ? (
|
||||
<LinuxIcon className="h-full w-full" />
|
||||
) : (
|
||||
<MacIcon className="h-full w-full" />
|
||||
)}
|
||||
{file.filename}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{file.backup_time.fromNow()}
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-end",
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
color="secondary"
|
||||
aria-label={t("Delete")}
|
||||
size="small"
|
||||
title={t("Delete Backup")}
|
||||
onClick={async (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
const confirmed = await window.confirm(
|
||||
t("Confirm to delete this backup file?"),
|
||||
);
|
||||
if (confirmed) {
|
||||
await handleDelete(file.filename);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
<Divider
|
||||
orientation="vertical"
|
||||
flexItem
|
||||
sx={{ mx: 1, height: 24 }}
|
||||
/>
|
||||
<IconButton
|
||||
color="primary"
|
||||
aria-label={t("Restore")}
|
||||
size="small"
|
||||
title={t("Restore Backup")}
|
||||
disabled={!file.allow_apply}
|
||||
onClick={async (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
const confirmed = await window.confirm(
|
||||
t("Confirm to restore this backup file?"),
|
||||
);
|
||||
if (confirmed) {
|
||||
await handleRestore(file.filename);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<RestoreIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={3} align="center">
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: 150,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body1"
|
||||
color="textSecondary"
|
||||
align="center"
|
||||
>
|
||||
{t("No Backups")}
|
||||
</Typography>
|
||||
</Box>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<TablePagination
|
||||
rowsPerPageOptions={[]}
|
||||
component="div"
|
||||
count={total}
|
||||
rowsPerPage={DEFAULT_ROWS_PER_PAGE}
|
||||
page={page}
|
||||
onPageChange={onPageChange}
|
||||
labelRowsPerPage={t("Rows per page")}
|
||||
/>
|
||||
</TableContainer>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
function LinuxIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 48 48"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill="#ECEFF1"
|
||||
d="m20.1 16.2l.1 2.3l-1.6 3l-2.5 4.9l-.5 4.1l1.8 5.8l4.1 2.3h6.2l5.8-4.4l2.6-6.9l-6-7.3l-1.7-4.1z"
|
||||
/>
|
||||
<path
|
||||
fill="#263238"
|
||||
d="M34.3 21.9c-1.6-2.3-2.9-3.7-3.6-6.6s.2-2.1-.4-4.6c-.3-1.3-.8-2.2-1.3-2.9c-.6-.7-1.3-1.1-1.7-1.2c-.9-.5-3-1.3-5.6.1c-2.7 1.4-2.4 4.4-1.9 10.5c0 .4-.1.9-.3 1.3c-.4.9-1.1 1.7-1.7 2.4c-.7 1-1.4 2-1.9 3.1c-1.2 2.3-2.3 5.2-2 6.3c.5-.1 6.8 9.5 6.8 9.7c.4-.1 2.1-.1 3.6-.1c2.1-.1 3.3-.2 5 .2c0-.3-.1-.6-.1-.9c0-.6.1-1.1.2-1.8c.1-.5.2-1 .3-1.6c-1 .9-2.8 1.9-4.5 2.2c-1.5.3-4-.2-5.2-1.7c.1 0 .3 0 .4-.1c.3-.1.6-.2.7-.4c.3-.5.1-1-.1-1.3s-1.7-1.4-2.4-2s-1.1-.9-1.5-1.3l-.8-.8c-.2-.2-.3-.4-.4-.5c-.2-.5-.3-1.1-.2-1.9c.1-1.1.5-2 1-3c.2-.4.7-1.2.7-1.2s-1.7 4.2-.8 5.5c0 0 .1-1.3.5-2.6c.3-.9.8-2.2 1.4-2.9s2.1-3.3 2.2-4.9c0-.7.1-1.4.1-1.9c-.4-.4 6.6-1.4 7-.3c.1.4 1.5 4 2.3 5.9c.4.9.9 1.7 1.2 2.7c.3 1.1.5 2.6.5 4.1c0 .3 0 .8-.1 1.3c.2 0 4.1-4.2-.5-7.7c0 0 2.8 1.3 2.9 3.9c.1 2.1-.8 3.8-1 4.1c.1 0 2.1.9 2.2.9c.4 0 1.2-.3 1.2-.3c.1-.3.4-1.1.4-1.4c.7-2.3-1-6-2.6-8.3"
|
||||
/>
|
||||
<g fill="#ECEFF1" transform="translate(0 -2)">
|
||||
<ellipse cx="21.6" cy="15.3" rx="1.3" ry="2" />
|
||||
<ellipse cx="26.1" cy="15.2" rx="1.7" ry="2.3" />
|
||||
</g>
|
||||
<g fill="#212121" transform="translate(0 -2)">
|
||||
<ellipse
|
||||
cx="21.7"
|
||||
cy="15.5"
|
||||
rx="1.2"
|
||||
ry=".7"
|
||||
transform="rotate(-97.204 21.677 15.542)"
|
||||
/>
|
||||
<ellipse cx="26" cy="15.6" rx="1" ry="1.3" />
|
||||
</g>
|
||||
<path
|
||||
fill="#FFC107"
|
||||
d="M39.3 35.6c-.4-.2-1.1-.5-1.7-1.4c-.3-.5-.2-1.9-.7-2.5c-.3-.4-.7-.2-.8-.2c-.9.2-3 1.6-4.4 0c-.2-.2-.5-.5-1-.5s-.7.2-.9.6s-.2.7-.2 1.7c0 .8 0 1.7-.1 2.4c-.2 1.7-.5 2.7-.5 3.7c0 1.1.3 1.8.7 2.1c.3.3.8.5 1.9.5s1.8-.4 2.5-1.1c.5-.5.9-.7 2.3-1.7c1.1-.7 2.8-1.6 3.1-1.9c.2-.2.5-.3.5-.9c0-.5-.4-.7-.7-.8m-20.1.3c-1-1.6-1.1-1.9-1.8-2.9c-.6-1-1.9-2.9-2.7-2.9c-.6 0-.9.3-1.3.7s-.8 1.3-1.5 1.8c-.6.5-2.3.4-2.7 1s.4 1.5.4 3c0 .6-.5 1-.6 1.4c-.1.5-.2.8 0 1.2c.4.6.9.8 4.3 1.5c1.8.4 3.5 1.4 4.6 1.5s3 0 3-2.7c.1-1.6-.8-2-1.7-3.6m1.9-18.1c-.6-.4-1.1-.8-1.1-1.4s.4-.8 1-1.3c.1-.1 1.2-1.1 2.3-1.1s2.4.7 2.9.9c.9.2 1.8.4 1.7 1.1c-.1 1-.2 1.2-1.2 1.7c-.7.2-2 1.3-2.9 1.3c-.4 0-1 0-1.4-.1c-.3-.1-.8-.6-1.3-1.1"
|
||||
/>
|
||||
<path
|
||||
fill="#634703"
|
||||
d="M20.9 17c.2.2.5.4.8.5c.2.1.5.2.5.2h.9c.5 0 1.2-.2 1.9-.6c.7-.3.8-.5 1.3-.7c.5-.3 1-.6.8-.7s-.4 0-1.1.4c-.6.4-1.1.6-1.7.9c-.3.1-.7.3-1 .3h-.9c-.3 0-.5-.1-.8-.2c-.2-.1-.3-.2-.4-.2c-.2-.1-.6-.5-.8-.6c0 0-.2 0-.1.1zm3-2.2c.1.2.3.2.4.3s.2.1.2.1c.1-.1 0-.3-.1-.3c0-.2-.5-.2-.5-.1m-1.6.2c0 .1.2.2.2.1c.1-.1.2-.2.3-.2c.2-.1.1-.2-.2-.2c-.2.1-.2.2-.3.3"
|
||||
/>
|
||||
<path
|
||||
fill="#455A64"
|
||||
d="M32 32.7v.3c.2.4.7.5 1.1.5c.6 0 1.2-.4 1.5-.8c0-.1.1-.2.2-.3c.2-.3.3-.5.4-.6c0 0-.1-.1-.1-.2c-.1-.2-.4-.4-.8-.5c-.3-.1-.8-.2-1-.2c-.9-.1-1.4.2-1.7.5c0 0 .1 0 .1.1c.2.2.3.4.3.7c.1.2 0 .3 0 .5"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function WindowsIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 16 16"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill="#0284c7"
|
||||
d="M6.555 1.375L0 2.237v5.45h6.555zM0 13.795l6.555.933V8.313H0zm7.278-5.4l.026 6.378L16 16V8.395zM16 0L7.33 1.244v6.414H16z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function MacIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 26 26"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill="#000"
|
||||
d="M23.934 18.947c-.598 1.324-.884 1.916-1.652 3.086c-1.073 1.634-2.588 3.673-4.461 3.687c-1.666.014-2.096-1.087-4.357-1.069c-2.261.011-2.732 1.089-4.4 1.072c-1.873-.017-3.307-1.854-4.381-3.485c-3.003-4.575-3.32-9.937-1.464-12.79C4.532 7.425 6.61 6.237 8.561 6.237c1.987 0 3.236 1.092 4.879 1.092c1.594 0 2.565-1.095 4.863-1.095c1.738 0 3.576.947 4.889 2.581c-4.296 2.354-3.598 8.49.742 10.132M16.559 4.408c.836-1.073 1.47-2.587 1.24-4.131c-1.364.093-2.959.964-3.891 2.092c-.844 1.027-1.544 2.553-1.271 4.029c1.488.048 3.028-.839 3.922-1.99"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
144
src/components/setting/mods/backup-viewer.tsx
Normal file
144
src/components/setting/mods/backup-viewer.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
import {
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
useState,
|
||||
useCallback,
|
||||
useEffect,
|
||||
} from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { BaseDialog, DialogRef } from "@/components/base";
|
||||
import getSystem from "@/utils/get-system";
|
||||
import { BaseLoadingOverlay } from "@/components/base";
|
||||
import dayjs from "dayjs";
|
||||
import customParseFormat from "dayjs/plugin/customParseFormat";
|
||||
import {
|
||||
BackupTableViewer,
|
||||
BackupFile,
|
||||
DEFAULT_ROWS_PER_PAGE,
|
||||
} from "./backup-table-viewer";
|
||||
import { BackupConfigViewer } from "./backup-config-viewer";
|
||||
import { Box, Paper, Divider } from "@mui/material";
|
||||
import { listWebDavBackup } from "@/services/cmds";
|
||||
dayjs.extend(customParseFormat);
|
||||
|
||||
const DATE_FORMAT = "YYYY-MM-DD_HH-mm-ss";
|
||||
const FILENAME_PATTERN = /\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}/;
|
||||
|
||||
export const BackupViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [backupFiles, setBackupFiles] = useState<BackupFile[]>([]);
|
||||
const [dataSource, setDataSource] = useState<BackupFile[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(0);
|
||||
|
||||
const OS = getSystem();
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
open: () => {
|
||||
setOpen(true);
|
||||
},
|
||||
close: () => setOpen(false),
|
||||
}));
|
||||
|
||||
// Handle page change
|
||||
const handleChangePage = useCallback(
|
||||
(_: React.MouseEvent<HTMLButtonElement> | null, page: number) => {
|
||||
setPage(page);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const fetchAndSetBackupFiles = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const files = await getAllBackupFiles();
|
||||
setBackupFiles(files);
|
||||
setTotal(files.length);
|
||||
} catch (error) {
|
||||
setBackupFiles([]);
|
||||
setTotal(0);
|
||||
console.error(error);
|
||||
// Notice.error(t("Failed to fetch backup files"));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getAllBackupFiles = async () => {
|
||||
const files = await listWebDavBackup();
|
||||
return files
|
||||
.map((file) => {
|
||||
const platform = file.filename.split("-")[0];
|
||||
const fileBackupTimeStr = file.filename.match(FILENAME_PATTERN)!;
|
||||
|
||||
if (fileBackupTimeStr === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const backupTime = dayjs(fileBackupTimeStr[0], DATE_FORMAT);
|
||||
const allowApply = OS === platform;
|
||||
return {
|
||||
...file,
|
||||
platform,
|
||||
backup_time: backupTime,
|
||||
allow_apply: allowApply,
|
||||
} as BackupFile;
|
||||
})
|
||||
.filter((item) => item !== null)
|
||||
.sort((a, b) => (a.backup_time.isAfter(b.backup_time) ? -1 : 1));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setDataSource(
|
||||
backupFiles.slice(
|
||||
page * DEFAULT_ROWS_PER_PAGE,
|
||||
page * DEFAULT_ROWS_PER_PAGE + DEFAULT_ROWS_PER_PAGE,
|
||||
),
|
||||
);
|
||||
}, [page, backupFiles]);
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
open={open}
|
||||
title={t("Backup Setting")}
|
||||
contentSx={{ width: 600, maxHeight: 800 }}
|
||||
okBtn={t("")}
|
||||
cancelBtn={t("Close")}
|
||||
onClose={() => setOpen(false)}
|
||||
onCancel={() => setOpen(false)}
|
||||
disableOk
|
||||
>
|
||||
<Box sx={{ maxWidth: 800 }}>
|
||||
<BaseLoadingOverlay isLoading={isLoading} />
|
||||
<Paper elevation={2} sx={{ padding: 2 }}>
|
||||
<BackupConfigViewer
|
||||
setLoading={setIsLoading}
|
||||
onBackupSuccess={async () => {
|
||||
fetchAndSetBackupFiles();
|
||||
}}
|
||||
onSaveSuccess={async () => {
|
||||
fetchAndSetBackupFiles();
|
||||
}}
|
||||
onRefresh={async () => {
|
||||
fetchAndSetBackupFiles();
|
||||
}}
|
||||
onInit={async () => {
|
||||
fetchAndSetBackupFiles();
|
||||
}}
|
||||
/>
|
||||
<Divider sx={{ marginY: 2 }} />
|
||||
<BackupTableViewer
|
||||
datasource={dataSource}
|
||||
page={page}
|
||||
onPageChange={handleChangePage}
|
||||
total={total}
|
||||
onRefresh={fetchAndSetBackupFiles}
|
||||
/>
|
||||
</Paper>
|
||||
</Box>
|
||||
</BaseDialog>
|
||||
);
|
||||
});
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
ListItemButton,
|
||||
ListItemText,
|
||||
} from "@mui/material";
|
||||
import { changeClashCore, restartSidecar } from "@/services/cmds";
|
||||
import { changeClashCore, restartCore } from "@/services/cmds";
|
||||
import { closeAllConnections, upgradeCore } from "@/services/api";
|
||||
|
||||
const VALID_CORE = [
|
||||
@@ -59,7 +59,7 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
|
||||
const onRestart = useLockFn(async () => {
|
||||
try {
|
||||
await restartSidecar();
|
||||
await restartCore();
|
||||
Notice.success(t(`Clash Core Restarted`), 1000);
|
||||
} catch (err: any) {
|
||||
Notice.error(err?.message || err.toString());
|
||||
|
||||
@@ -41,7 +41,6 @@ export function GuardState<T>(props: Props<T>) {
|
||||
childProps[onChangeProps] = async (...args: any[]) => {
|
||||
// 多次操作无效
|
||||
if (lockRef.current) return;
|
||||
|
||||
lockRef.current = true;
|
||||
|
||||
try {
|
||||
|
||||
@@ -196,8 +196,8 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
},
|
||||
],
|
||||
});
|
||||
if (selected?.path.length) {
|
||||
await copyIconFile(`${selected.path}`, "common");
|
||||
if (selected) {
|
||||
await copyIconFile(`${selected}`, "common");
|
||||
await initIconPath();
|
||||
onChangeData({ common_tray_icon: true });
|
||||
patchVerge({ common_tray_icon: true });
|
||||
@@ -242,8 +242,8 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
},
|
||||
],
|
||||
});
|
||||
if (selected?.path.length) {
|
||||
await copyIconFile(`${selected.path}`, "sysproxy");
|
||||
if (selected) {
|
||||
await copyIconFile(`${selected}`, "sysproxy");
|
||||
await initIconPath();
|
||||
onChangeData({ sysproxy_tray_icon: true });
|
||||
patchVerge({ sysproxy_tray_icon: true });
|
||||
@@ -281,13 +281,13 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
multiple: false,
|
||||
filters: [
|
||||
{
|
||||
name: "Tray Icon Image",
|
||||
name: "Tun Icon Image",
|
||||
extensions: ["png", "ico"],
|
||||
},
|
||||
],
|
||||
});
|
||||
if (selected?.path.length) {
|
||||
await copyIconFile(`${selected.path}`, "tun");
|
||||
if (selected) {
|
||||
await copyIconFile(`${selected}`, "tun");
|
||||
await initIconPath();
|
||||
onChangeData({ tun_tray_icon: true });
|
||||
patchVerge({ tun_tray_icon: true });
|
||||
|
||||
@@ -205,7 +205,7 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
spellCheck="false"
|
||||
sx={{ width: 250, marginLeft: "auto" }}
|
||||
value={values.defaultLatencyTest}
|
||||
placeholder="http://1.1.1.1"
|
||||
placeholder="http://cp.cloudflare.com/generate_204"
|
||||
onChange={(e) =>
|
||||
setValues((v) => ({ ...v, defaultLatencyTest: e.target.value }))
|
||||
}
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
import { KeyedMutator } from "swr";
|
||||
import { useState } from "react";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { installService, uninstallService } from "@/services/cmds";
|
||||
import { Notice } from "@/components/base";
|
||||
import { LoadingButton } from "@mui/lab";
|
||||
import { PasswordInput } from "./password-input";
|
||||
import getSystem from "@/utils/get-system";
|
||||
|
||||
interface Props {
|
||||
status: "active" | "installed" | "unknown" | "uninstall";
|
||||
mutate: KeyedMutator<"active" | "installed" | "unknown" | "uninstall">;
|
||||
patchVerge: (value: Partial<IVergeConfig>) => Promise<void>;
|
||||
onChangeData: (patch: Partial<IVergeConfig>) => void;
|
||||
}
|
||||
|
||||
export const ServiceSwitcher = (props: Props) => {
|
||||
const { status, mutate, patchVerge, onChangeData } = props;
|
||||
const isWindows = getSystem() === "windows";
|
||||
const isActive = status === "active";
|
||||
const isInstalled = status === "installed";
|
||||
const isUninstall = status === "uninstall" || status === "unknown";
|
||||
|
||||
const { t } = useTranslation();
|
||||
const [serviceLoading, setServiceLoading] = useState(false);
|
||||
const [uninstallServiceLoaing, setUninstallServiceLoading] = useState(false);
|
||||
const [openInstall, setOpenInstall] = useState(false);
|
||||
const [openUninstall, setOpenUninstall] = useState(false);
|
||||
|
||||
async function install(passwd: string) {
|
||||
try {
|
||||
setOpenInstall(false);
|
||||
await installService(passwd);
|
||||
await mutate();
|
||||
setTimeout(() => {
|
||||
mutate();
|
||||
}, 2000);
|
||||
Notice.success(t("Service Installed Successfully"));
|
||||
setServiceLoading(false);
|
||||
} catch (err: any) {
|
||||
await mutate();
|
||||
setTimeout(() => {
|
||||
mutate();
|
||||
}, 2000);
|
||||
Notice.error(err.message || err.toString());
|
||||
setServiceLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function uninstall(passwd: string) {
|
||||
try {
|
||||
setOpenUninstall(false);
|
||||
await uninstallService(passwd);
|
||||
await mutate();
|
||||
setTimeout(() => {
|
||||
mutate();
|
||||
}, 2000);
|
||||
Notice.success(t("Service Uninstalled Successfully"));
|
||||
setUninstallServiceLoading(false);
|
||||
} catch (err: any) {
|
||||
await mutate();
|
||||
setTimeout(() => {
|
||||
mutate();
|
||||
}, 2000);
|
||||
Notice.error(err.message || err.toString());
|
||||
setUninstallServiceLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
const onInstallOrEnableService = useLockFn(async () => {
|
||||
setServiceLoading(true);
|
||||
if (isUninstall) {
|
||||
// install service
|
||||
if (isWindows) {
|
||||
await install("");
|
||||
} else {
|
||||
setOpenInstall(true);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
// enable or disable service
|
||||
await patchVerge({ enable_service_mode: !isActive });
|
||||
onChangeData({ enable_service_mode: !isActive });
|
||||
await mutate();
|
||||
setTimeout(() => {
|
||||
mutate();
|
||||
}, 2000);
|
||||
setServiceLoading(false);
|
||||
} catch (err: any) {
|
||||
await mutate();
|
||||
Notice.error(err.message || err.toString());
|
||||
setServiceLoading(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const onUninstallService = useLockFn(async () => {
|
||||
setUninstallServiceLoading(true);
|
||||
if (isWindows) {
|
||||
await uninstall("");
|
||||
} else {
|
||||
setOpenUninstall(true);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{openInstall && <PasswordInput onConfirm={install} />}
|
||||
{openUninstall && <PasswordInput onConfirm={uninstall} />}
|
||||
|
||||
<LoadingButton
|
||||
size="small"
|
||||
variant={isUninstall ? "outlined" : "contained"}
|
||||
onClick={onInstallOrEnableService}
|
||||
loading={serviceLoading}
|
||||
>
|
||||
{isActive ? t("Disable") : isInstalled ? t("Enable") : t("Install")}
|
||||
</LoadingButton>
|
||||
{isInstalled && (
|
||||
<LoadingButton
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color="error"
|
||||
sx={{ ml: 1 }}
|
||||
onClick={onUninstallService}
|
||||
loading={uninstallServiceLoaing}
|
||||
>
|
||||
{t("Uninstall")}
|
||||
</LoadingButton>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -28,7 +28,7 @@ export const ThemeViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
useImperativeHandle(ref, () => ({
|
||||
open: () => {
|
||||
setOpen(true);
|
||||
setTheme({ ...theme_setting } || {});
|
||||
setTheme({ ...theme_setting });
|
||||
},
|
||||
close: () => setOpen(false),
|
||||
}));
|
||||
|
||||
@@ -7,10 +7,11 @@ import { relaunch } from "@tauri-apps/plugin-process";
|
||||
import { check as checkUpdate } from "@tauri-apps/plugin-updater";
|
||||
import { BaseDialog, DialogRef, Notice } from "@/components/base";
|
||||
import { useUpdateState, useSetUpdateState } from "@/services/states";
|
||||
import { listen, Event, UnlistenFn } from "@tauri-apps/api/event";
|
||||
import { Event, UnlistenFn } from "@tauri-apps/api/event";
|
||||
import { portableFlag } from "@/pages/_layout";
|
||||
import { open as openUrl } from "@tauri-apps/plugin-shell";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import { useListen } from "@/hooks/use-listen";
|
||||
|
||||
let eventListener: UnlistenFn | null = null;
|
||||
|
||||
@@ -21,6 +22,7 @@ export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
|
||||
const updateState = useUpdateState();
|
||||
const setUpdateState = useSetUpdateState();
|
||||
const { addListener } = useListen();
|
||||
|
||||
const { data: updateInfo } = useSWR("checkUpdate", checkUpdate, {
|
||||
errorRetryCount: 2,
|
||||
@@ -66,7 +68,7 @@ export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
if (eventListener !== null) {
|
||||
eventListener();
|
||||
}
|
||||
eventListener = await listen(
|
||||
eventListener = await addListener(
|
||||
"tauri://update-download-progress",
|
||||
(e: Event<any>) => {
|
||||
setTotal(e.payload.contentLength);
|
||||
@@ -74,7 +76,7 @@ export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
setDownloaded((a) => {
|
||||
return a + e.payload.chunkLength;
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
try {
|
||||
await updateInfo.install();
|
||||
@@ -98,7 +100,7 @@ export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
size="small"
|
||||
onClick={() => {
|
||||
openUrl(
|
||||
`https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/v${updateInfo?.version}`
|
||||
`https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/v${updateInfo?.version}`,
|
||||
);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TextField, Select, MenuItem, Typography } from "@mui/material";
|
||||
|
||||
import {
|
||||
SettingsRounded,
|
||||
ShuffleRounded,
|
||||
@@ -34,7 +33,12 @@ const SettingClash = ({ onError }: Props) => {
|
||||
const { clash, version, mutateClash, patchClash } = useClash();
|
||||
const { verge, mutateVerge, patchVerge } = useVerge();
|
||||
|
||||
const { ipv6, "allow-lan": allowLan, "log-level": logLevel } = clash ?? {};
|
||||
const {
|
||||
ipv6,
|
||||
"allow-lan": allowLan,
|
||||
"log-level": logLevel,
|
||||
"unified-delay": unifiedDelay,
|
||||
} = clash ?? {};
|
||||
|
||||
const { enable_random_port = false, verge_mixed_port } = verge ?? {};
|
||||
|
||||
@@ -106,10 +110,36 @@ const SettingClash = ({ onError }: Props) => {
|
||||
</GuardState>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t("Log Level")}>
|
||||
<SettingItem
|
||||
label={t("Unified Delay")}
|
||||
extra={
|
||||
<TooltipIcon
|
||||
title={t("Unified Delay Info")}
|
||||
sx={{ opacity: "0.7" }}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<GuardState
|
||||
value={unifiedDelay ?? false}
|
||||
valueProps="checked"
|
||||
onCatch={onError}
|
||||
onFormat={onSwitchFormat}
|
||||
onChange={(e) => onChangeData({ "unified-delay": e })}
|
||||
onGuard={(e) => patchClash({ "unified-delay": e })}
|
||||
>
|
||||
<Switch edge="end" />
|
||||
</GuardState>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem
|
||||
label={t("Log Level")}
|
||||
extra={
|
||||
<TooltipIcon title={t("Log Level Info")} sx={{ opacity: "0.7" }} />
|
||||
}
|
||||
>
|
||||
<GuardState
|
||||
// clash premium 2022.08.26 值为warn
|
||||
value={logLevel === "warn" ? "warning" : logLevel ?? "info"}
|
||||
value={logLevel === "warn" ? "warning" : (logLevel ?? "info")}
|
||||
onCatch={onError}
|
||||
onFormat={(e: any) => e.target.value}
|
||||
onChange={(e) => onChangeData({ "log-level": e })}
|
||||
@@ -135,7 +165,7 @@ const SettingClash = ({ onError }: Props) => {
|
||||
onClick={() => {
|
||||
Notice.success(
|
||||
t("Restart Application to Apply Modifications"),
|
||||
1000
|
||||
1000,
|
||||
);
|
||||
onChangeVerge({ enable_random_port: !enable_random_port });
|
||||
patchVerge({ enable_random_port: !enable_random_port });
|
||||
|
||||
@@ -2,12 +2,10 @@ import useSWR from "swr";
|
||||
import { useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { SettingsRounded } from "@mui/icons-material";
|
||||
import { checkService } from "@/services/cmds";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { DialogRef, Notice, Switch } from "@/components/base";
|
||||
import { SettingList, SettingItem } from "./mods/setting-comp";
|
||||
import { GuardState } from "./mods/guard-state";
|
||||
import { ServiceSwitcher } from "./mods/service-switcher";
|
||||
import { SysproxyViewer } from "./mods/sysproxy-viewer";
|
||||
import { TunViewer } from "./mods/tun-viewer";
|
||||
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
|
||||
@@ -20,16 +18,6 @@ const SettingSystem = ({ onError }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { verge, mutateVerge, patchVerge } = useVerge();
|
||||
// service mode
|
||||
const { data: serviceStatus, mutate: mutateServiceStatus } = useSWR(
|
||||
"checkService",
|
||||
checkService,
|
||||
{
|
||||
revalidateIfStale: false,
|
||||
shouldRetryOnError: false,
|
||||
focusThrottleInterval: 36e5, // 1 hour
|
||||
}
|
||||
);
|
||||
|
||||
const sysproxyRef = useRef<DialogRef>(null);
|
||||
const tunRef = useRef<DialogRef>(null);
|
||||
@@ -67,34 +55,15 @@ const SettingSystem = ({ onError }: Props) => {
|
||||
onCatch={onError}
|
||||
onFormat={onSwitchFormat}
|
||||
onChange={(e) => {
|
||||
if (serviceStatus !== "active") {
|
||||
onChangeData({ enable_tun_mode: false });
|
||||
} else {
|
||||
onChangeData({ enable_tun_mode: e });
|
||||
}
|
||||
onChangeData({ enable_tun_mode: e });
|
||||
}}
|
||||
onGuard={(e) => {
|
||||
if (serviceStatus !== "active" && e) {
|
||||
Notice.error(t("Please Enable Service Mode"));
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
return patchVerge({ enable_tun_mode: e });
|
||||
}
|
||||
return patchVerge({ enable_tun_mode: e });
|
||||
}}
|
||||
>
|
||||
<Switch edge="end" />
|
||||
</GuardState>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t("Service Mode")}>
|
||||
<ServiceSwitcher
|
||||
status={serviceStatus ?? "unknown"}
|
||||
mutate={mutateServiceStatus}
|
||||
patchVerge={patchVerge}
|
||||
onChangeData={onChangeData}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem
|
||||
label={t("System Proxy")}
|
||||
extra={
|
||||
@@ -134,7 +103,9 @@ const SettingSystem = ({ onError }: Props) => {
|
||||
|
||||
<SettingItem
|
||||
label={t("Silent Start")}
|
||||
extra={<TooltipIcon title={t("Silent Start Info")} />}
|
||||
extra={
|
||||
<TooltipIcon title={t("Silent Start Info")} sx={{ opacity: "0.7" }} />
|
||||
}
|
||||
>
|
||||
<GuardState
|
||||
value={enable_silent_start ?? false}
|
||||
|
||||
@@ -23,6 +23,7 @@ import { ThemeViewer } from "./mods/theme-viewer";
|
||||
import { GuardState } from "./mods/guard-state";
|
||||
import { LayoutViewer } from "./mods/layout-viewer";
|
||||
import { UpdateViewer } from "./mods/update-viewer";
|
||||
import { BackupViewer } from "./mods/backup-viewer";
|
||||
import getSystem from "@/utils/get-system";
|
||||
import { routers } from "@/pages/_routers";
|
||||
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
|
||||
@@ -52,6 +53,7 @@ const SettingVerge = ({ onError }: Props) => {
|
||||
const themeRef = useRef<DialogRef>(null);
|
||||
const layoutRef = useRef<DialogRef>(null);
|
||||
const updateRef = useRef<DialogRef>(null);
|
||||
const backupRef = useRef<DialogRef>(null);
|
||||
|
||||
const onChangeData = (patch: Partial<IVergeConfig>) => {
|
||||
mutateVerge({ ...verge, ...patch }, false);
|
||||
@@ -83,6 +85,7 @@ const SettingVerge = ({ onError }: Props) => {
|
||||
<MiscViewer ref={miscRef} />
|
||||
<LayoutViewer ref={layoutRef} />
|
||||
<UpdateViewer ref={updateRef} />
|
||||
<BackupViewer ref={backupRef} />
|
||||
|
||||
<SettingItem label={t("Language")}>
|
||||
<GuardState
|
||||
@@ -194,9 +197,9 @@ const SettingVerge = ({ onError }: Props) => {
|
||||
},
|
||||
],
|
||||
});
|
||||
if (selected?.path.length) {
|
||||
onChangeData({ startup_script: `${selected.path}` });
|
||||
patchVerge({ startup_script: `${selected.path}` });
|
||||
if (selected) {
|
||||
onChangeData({ startup_script: `${selected}` });
|
||||
patchVerge({ startup_script: `${selected}` });
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -238,12 +241,23 @@ const SettingVerge = ({ onError }: Props) => {
|
||||
label={t("Hotkey Setting")}
|
||||
/>
|
||||
|
||||
<SettingItem
|
||||
onClick={() => backupRef.current?.open()}
|
||||
label={t("Backup Setting")}
|
||||
extra={
|
||||
<TooltipIcon
|
||||
title={t("Backup Setting Info")}
|
||||
sx={{ opacity: "0.7" }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<SettingItem
|
||||
onClick={() => configRef.current?.open()}
|
||||
label={t("Runtime Config")}
|
||||
/>
|
||||
|
||||
<SettingItem onClick={openAppDir} label={t("Open App Dir")} />
|
||||
<SettingItem onClick={openAppDir} label={t("Open Conf Dir")} />
|
||||
|
||||
<SettingItem onClick={openCoreDir} label={t("Open Core Dir")} />
|
||||
|
||||
|
||||
@@ -18,9 +18,9 @@ import { Notice } from "@/components/base";
|
||||
import { TestBox } from "./test-box";
|
||||
import delayManager from "@/services/delay";
|
||||
import { cmdTestDelay, downloadIconCache } from "@/services/cmds";
|
||||
import { listen, UnlistenFn } from "@tauri-apps/api/event";
|
||||
import { UnlistenFn } from "@tauri-apps/api/event";
|
||||
import { convertFileSrc } from "@tauri-apps/api/core";
|
||||
|
||||
import { useListen } from "@/hooks/use-listen";
|
||||
interface Props {
|
||||
id: string;
|
||||
itemData: IVergeTestItem;
|
||||
@@ -47,6 +47,7 @@ export const TestItem = (props: Props) => {
|
||||
const [delay, setDelay] = useState(-1);
|
||||
const { uid, name, icon, url } = itemData;
|
||||
const [iconCachePath, setIconCachePath] = useState("");
|
||||
const { addListener } = useListen();
|
||||
|
||||
useEffect(() => {
|
||||
initIconCachePath();
|
||||
@@ -91,7 +92,7 @@ export const TestItem = (props: Props) => {
|
||||
|
||||
const listenTsetEvent = async () => {
|
||||
eventListener();
|
||||
eventListener = await listen("verge://test-all", () => {
|
||||
eventListener = await addListener("verge://test-all", () => {
|
||||
onDelay();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -67,6 +67,16 @@ export const TestViewer = forwardRef<TestViewerRef, Props>((props, ref) => {
|
||||
let newList;
|
||||
let uid;
|
||||
|
||||
if (form.icon && form.icon.startsWith("<svg")) {
|
||||
const doc = new DOMParser().parseFromString(
|
||||
form.icon,
|
||||
"image/svg+xml",
|
||||
);
|
||||
if (doc.querySelector("parsererror")) {
|
||||
throw new Error("`Icon`svg format error");
|
||||
}
|
||||
}
|
||||
|
||||
if (openType === "new") {
|
||||
uid = nanoid();
|
||||
const item = { ...form, uid };
|
||||
@@ -87,7 +97,7 @@ export const TestViewer = forwardRef<TestViewerRef, Props>((props, ref) => {
|
||||
Notice.error(err.message || err.toString());
|
||||
setLoading(false);
|
||||
}
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
const handleClose = () => {
|
||||
|
||||
Reference in New Issue
Block a user