feat(proxy): unify filter search box and sync filter options (#5819)

* feat(proxy): unify filter search box and sync filter options

* docs: fix JSDoc placement

* refactor(search): unify string matcher and stabilize regex filtering

* fix(search): treat invalid regex as matching nothing

* docs: update changelog to include advanced filter search feature for proxy page

---------

Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com>
This commit is contained in:
Sline
2025-12-19 17:12:36 +08:00
committed by GitHub
co-authored by Tunglies
parent aa72fa9a42
commit b4e25951b4
7 changed files with 240 additions and 140 deletions
+30 -12
View File
@@ -15,6 +15,7 @@ import { Box, IconButton, TextField, SxProps } from "@mui/material";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { BaseSearchBox } from "@/components/base/base-search-box";
import { useVerge } from "@/hooks/use-verge";
import delayManager from "@/services/delay";
import { debugLog } from "@/utils/debug";
@@ -43,7 +44,16 @@ export const ProxyHead = ({
onLocation,
onCheckDelay,
}: Props) => {
const { showType, sortType, filterText, textState, testUrl } = headState;
const {
showType,
sortType,
filterText,
textState,
testUrl,
filterMatchCase,
filterMatchWholeWord,
filterUseRegularExpression,
} = headState;
const { t } = useTranslation();
const [autoFocus, setAutoFocus] = useState(false);
@@ -154,17 +164,25 @@ export const ProxyHead = ({
</IconButton>
{textState === "filter" && (
<TextField
autoComplete="new-password"
autoFocus={autoFocus}
hiddenLabel
value={filterText}
size="small"
variant="outlined"
placeholder={t("shared.placeholders.filter")}
onChange={(e) => onHeadState({ filterText: e.target.value })}
sx={{ ml: 0.5, flex: "1 1 auto", input: { py: 0.65, px: 1 } }}
/>
<Box sx={{ ml: 0.5, flex: "1 1 auto" }}>
<BaseSearchBox
autoFocus={autoFocus}
value={filterText}
searchState={{
matchCase: filterMatchCase,
matchWholeWord: filterMatchWholeWord,
useRegularExpression: filterUseRegularExpression,
}}
onSearch={(_, state) =>
onHeadState({
filterText: state.text,
filterMatchCase: state.matchCase,
filterMatchWholeWord: state.matchWholeWord,
filterUseRegularExpression: state.useRegularExpression,
})
}
/>
</Box>
)}
{textState === "url" && (
+30 -6
View File
@@ -2,15 +2,23 @@ import { useEffect, useMemo, useReducer } from "react";
import { useVerge } from "@/hooks/use-verge";
import delayManager from "@/services/delay";
import { compileStringMatcher } from "@/utils/search-matcher";
// default | delay | alphabet
export type ProxySortType = 0 | 1 | 2;
export type ProxySearchState = {
matchCase?: boolean;
matchWholeWord?: boolean;
useRegularExpression?: boolean;
};
export default function useFilterSort(
proxies: IProxyItem[],
groupName: string,
filterText: string,
sortType: ProxySortType,
searchState?: ProxySearchState,
) {
const { verge } = useVerge();
const [_, bumpRefresh] = useReducer((count: number) => count + 1, 0);
@@ -33,7 +41,7 @@ export default function useFilterSort(
}, [groupName]);
return useMemo(() => {
const fp = filterProxies(proxies, groupName, filterText);
const fp = filterProxies(proxies, groupName, filterText, searchState);
const sp = sortProxies(
fp,
groupName,
@@ -46,6 +54,7 @@ export default function useFilterSort(
groupName,
filterText,
sortType,
searchState,
verge?.default_latency_timeout,
]);
}
@@ -56,8 +65,9 @@ export function filterSort(
filterText: string,
sortType: ProxySortType,
latencyTimeout?: number,
searchState?: ProxySearchState,
) {
const fp = filterProxies(proxies, groupName, filterText);
const fp = filterProxies(proxies, groupName, filterText, searchState);
const sp = sortProxies(fp, groupName, sortType, latencyTimeout);
return sp;
}
@@ -76,10 +86,12 @@ function filterProxies(
proxies: IProxyItem[],
groupName: string,
filterText: string,
searchState?: ProxySearchState,
) {
if (!filterText) return proxies;
const query = filterText.trim();
if (!query) return proxies;
const res1 = regex1.exec(filterText);
const res1 = regex1.exec(query);
if (res1) {
const symbol = res1[1];
const symbol2 = res1[2].toLowerCase();
@@ -100,13 +112,25 @@ function filterProxies(
});
}
const res2 = regex2.exec(filterText);
const res2 = regex2.exec(query);
if (res2) {
const type = res2[1].toLowerCase();
return proxies.filter((p) => p.type.toLowerCase().includes(type));
}
return proxies.filter((p) => p.name.includes(filterText.trim()));
const {
matchCase = false,
matchWholeWord = false,
useRegularExpression = false,
} = searchState ?? {};
const compiled = compileStringMatcher(query, {
matchCase,
matchWholeWord,
useRegularExpression,
});
if (!compiled.isValid) return [];
return proxies.filter((p) => compiled.matcher(p.name));
}
/**
+6
View File
@@ -9,6 +9,9 @@ export interface HeadState {
showType: boolean;
sortType: ProxySortType;
filterText: string;
filterMatchCase?: boolean;
filterMatchWholeWord?: boolean;
filterUseRegularExpression?: boolean;
textState: "url" | "filter" | null;
testUrl: string;
}
@@ -21,6 +24,9 @@ export const DEFAULT_STATE: HeadState = {
showType: true,
sortType: 0,
filterText: "",
filterMatchCase: false,
filterMatchWholeWord: false,
filterUseRegularExpression: false,
textState: null,
testUrl: "",
};
+5
View File
@@ -390,6 +390,11 @@ export const useRenderList = (
headState.filterText,
headState.sortType,
latencyTimeout,
{
matchCase: headState.filterMatchCase,
matchWholeWord: headState.filterMatchWholeWord,
useRegularExpression: headState.filterUseRegularExpression,
},
);
ret.push({