diff --git a/src/assets/image/component/match_case.svg b/src/assets/image/component/match_case.svg new file mode 100644 index 00000000..cb59388f --- /dev/null +++ b/src/assets/image/component/match_case.svg @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/src/assets/image/component/match_whole_word.svg b/src/assets/image/component/match_whole_word.svg new file mode 100644 index 00000000..5701ad48 --- /dev/null +++ b/src/assets/image/component/match_whole_word.svg @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/src/assets/image/component/use_regular_expression.svg b/src/assets/image/component/use_regular_expression.svg new file mode 100644 index 00000000..31165959 --- /dev/null +++ b/src/assets/image/component/use_regular_expression.svg @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/src/components/base/base-search-box.tsx b/src/components/base/base-search-box.tsx new file mode 100644 index 00000000..35028122 --- /dev/null +++ b/src/components/base/base-search-box.tsx @@ -0,0 +1,145 @@ +import { Box, SvgIcon, TextField, Theme, styled } from "@mui/material"; +import Tooltip from "@mui/material/Tooltip"; +import { ChangeEvent, useState } from "react"; + +import { useTranslation } from "react-i18next"; +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"; + +type SearchProps = { + placeholder?: string; + onSearch: ( + match: (content: string) => boolean, + search: (contents: string[]) => string[], + state: { + input: string; + matchCase: boolean; + matchWholeWord: boolean; + useRegularExpression: boolean; + } + ) => void; +}; + +export const BaseSearchBox = styled((props: SearchProps) => { + const { t } = useTranslation(); + const [matchCase, setMatchCase] = useState(true); + const [matchWholeWord, setMatchWholeWord] = useState(false); + const [useRegularExpression, setUseRegularExpression] = useState(false); + const [errorMessage, setErrorMessage] = useState(""); + + const iconStyle = { + style: { + height: "24px", + width: "24px", + cursor: "pointer", + } as React.CSSProperties, + inheritViewBox: true, + }; + const active = "var(--primary-main)"; + + const onChange = (e: ChangeEvent) => { + props.onSearch( + (content) => doSearch([content], e.target?.value ?? "").length > 0, + (contents: string[]) => doSearch(contents, e.target?.value ?? ""), + { + input: e.target?.value ?? "", + matchCase, + matchWholeWord, + useRegularExpression, + } + ); + }; + + const doSearch = (searchList: string[], searchItem: string) => { + setErrorMessage(""); + return searchList.filter((item) => { + try { + let searchItemCopy = searchItem; + if (!matchCase) { + item = item.toLowerCase(); + searchItemCopy = searchItemCopy.toLowerCase(); + } + if (matchWholeWord) { + const regex = new RegExp(`\\b${searchItemCopy}\\b`); + if (useRegularExpression) { + const regexWithOptions = new RegExp(searchItemCopy); + return regexWithOptions.test(item) && regex.test(item); + } else { + return regex.test(item); + } + } else if (useRegularExpression) { + const regex = new RegExp(searchItemCopy); + return regex.test(item); + } else { + return item.includes(searchItemCopy); + } + } catch (e) { + setErrorMessage(`${e}`); + } + }); + }; + + return ( + + + +
+ { + setMatchCase(!matchCase); + }} + /> +
+
+ +
+ { + setMatchWholeWord(!matchWholeWord); + }} + /> +
+
+ +
+ { + setUseRegularExpression(!useRegularExpression); + }} + />{" "} +
+
+ + ), + }} + /> +
+ ); +})(({ theme }) => ({ + "& .MuiInputBase-root": { + background: theme.palette.mode === "light" ? "#fff" : undefined, + }, +})); diff --git a/src/locales/en.json b/src/locales/en.json index 209be797..f02974d8 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -214,6 +214,8 @@ "System and Mixed Can Only be Used in Service Mode": "System and Mixed Can Only be Used in Service Mode", "Information: Please make sure that the Clash Verge Service is installed and enabled": "Information: Please make sure that the Clash Verge Service is installed and enabled", + "Match Case": "Match Case", + "Match Whole Word": "Match Whole Word", "Use Regular Expression": "Use Regular Expression", "External Controller Address Modified": "External Controller Address Modified", diff --git a/src/locales/ru.json b/src/locales/ru.json index d9f9dcd3..e8e5428f 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -214,7 +214,9 @@ "System and Mixed Can Only be Used in Service Mode": "Система и смешанные могут использоваться только в сервисном режиме", "Information: Please make sure that the Clash Verge Service is installed and enabled": "Информация: Пожалуйста, убедитесь, что сервис Clash Verge Service установлен и включен", - "Use Regular Expression": "Использование регулярных выражений", + "Match Case": "Учитывать регистр", + "Match Whole Word": "Полное совпадение слова", + "Use Regular Expression": "Использовать регулярные выражения", "External Controller Address Modified": "Изменен адрес внешнего контроллера", "Clash Port Modified": "Clash порт изменен", diff --git a/src/locales/zh.json b/src/locales/zh.json index 24feb0d9..7b6e76e7 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -214,6 +214,8 @@ "System and Mixed Can Only be Used in Service Mode": "System 和 Mixed 只能在服务模式下使用", "Information: Please make sure that the Clash Verge Service is installed and enabled": "提示信息: 请确保 Clash Verge Service 已安装并启用", + "Match Case": "区分大小写", + "Match Whole Word": "全字匹配", "Use Regular Expression": "使用正则表达式", "External Controller Address Modified": "外部控制器监听地址已修改", diff --git a/src/pages/logs.tsx b/src/pages/logs.tsx index fcfcf642..274ed813 100644 --- a/src/pages/logs.tsx +++ b/src/pages/logs.tsx @@ -19,7 +19,7 @@ import { atomEnableLog, atomLogData } from "@/services/states"; import { BaseEmpty, BasePage } from "@/components/base"; import LogItem from "@/components/log/log-item"; import { useCustomTheme } from "@/components/layout/use-custom-theme"; -import { BaseStyledTextField } from "@/components/base/base-styled-text-field"; +import { BaseSearchBox } from "@/components/base/base-search-box"; const StyledSelect = styled((props: SelectProps) => { return ( @@ -46,35 +46,15 @@ const LogPage = () => { const { theme } = useCustomTheme(); const isDark = theme.palette.mode === "dark"; const [logState, setLogState] = useState("all"); - const [filterText, setFilterText] = useState(""); - const [useRegexSearch, setUseRegexSearch] = useState(true); - const [hasInputError, setInputError] = useState(false); - const [inputHelperText, setInputHelperText] = useState(""); + const [match, setMatch] = useState(() => (_: string) => true); + const filterLogs = useMemo(() => { - setInputHelperText(""); - setInputError(false); - if (useRegexSearch) { - try { - const regex = new RegExp(filterText); - return logData.filter((data) => { - return ( - regex.test(data.payload) && - (logState === "all" ? true : data.type.includes(logState)) - ); - }); - } catch (err: any) { - setInputHelperText(err.message.substring(0, 60)); - setInputError(true); - return logData; - } - } - return logData.filter((data) => { - return ( - data.payload.includes(filterText) && - (logState === "all" ? true : data.type.includes(logState)) - ); - }); - }, [logData, logState, filterText]); + return logData + .filter((data) => + logState === "all" ? true : data.type.includes(logState) + ) + .filter((data) => match(data.payload)); + }, [logData, logState, match]); return ( { WARN ERROR - - setFilterText(e.target.value)} - helperText={inputHelperText} - placeholder={t("Filter conditions")} - InputProps={{ - sx: { pr: 1 }, - endAdornment: ( - setUseRegexSearch(!useRegexSearch)} - > - .* - - ), - }} - /> + setMatch(() => match)} />