fix: resolve all eslint errors and warnings

This commit is contained in:
xmk23333 2026-01-08 12:56:08 +08:00
parent e70ca694b9
commit a5d2114363
22 changed files with 445 additions and 457 deletions

View File

@ -69,5 +69,12 @@ module.exports = [
'@typescript-eslint/no-explicit-any': 'warn', '@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-non-null-assertion': 'warn' '@typescript-eslint/no-non-null-assertion': 'warn'
} }
},
{
files: ['**/logger.ts'],
rules: {
'no-console': 'off'
}
} }
] ]

View File

@ -41,7 +41,7 @@ export async function setProfileConfig(config: IProfileConfig): Promise<void> {
export async function updateProfileConfig( export async function updateProfileConfig(
updater: (config: IProfileConfig) => IProfileConfig | Promise<IProfileConfig> updater: (config: IProfileConfig) => IProfileConfig | Promise<IProfileConfig>
): Promise<IProfileConfig> { ): Promise<IProfileConfig> {
let result: IProfileConfig let result: IProfileConfig | undefined
profileConfigWriteQueue = profileConfigWriteQueue.then(async () => { profileConfigWriteQueue = profileConfigWriteQueue.then(async () => {
const data = await readFile(profileConfigPath(), 'utf-8') const data = await readFile(profileConfigPath(), 'utf-8')
profileConfig = parse(data) || { items: [] } profileConfig = parse(data) || { items: [] }
@ -51,7 +51,7 @@ export async function updateProfileConfig(
await writeFile(profileConfigPath(), stringify(profileConfig), 'utf-8') await writeFile(profileConfigPath(), stringify(profileConfig), 'utf-8')
}) })
await profileConfigWriteQueue await profileConfigWriteQueue
return structuredClone(result!) return structuredClone(result ?? profileConfig)
} }
export async function getProfileItem(id: string | undefined): Promise<IProfileItem | undefined> { export async function getProfileItem(id: string | undefined): Promise<IProfileItem | undefined> {

View File

@ -41,8 +41,6 @@ import {
getAxios getAxios
} from './mihomoApi' } from './mihomoApi'
import { generateProfile } from './factory' import { generateProfile } from './factory'
// 拆分模块
import { getSessionAdminStatus } from './permissions' import { getSessionAdminStatus } from './permissions'
import { import {
cleanupSocketFile, cleanupSocketFile,

View File

@ -11,7 +11,7 @@ import { getMihomoIpcPath } from './manager'
const mihomoApiLogger = createLogger('MihomoApi') const mihomoApiLogger = createLogger('MihomoApi')
let axiosIns: AxiosInstance = null! let axiosIns: AxiosInstance | null = null
let currentIpcPath: string = '' let currentIpcPath: string = ''
let mihomoTrafficWs: WebSocket | null = null let mihomoTrafficWs: WebSocket | null = null
let trafficRetry = 10 let trafficRetry = 10
@ -27,7 +27,6 @@ const MAX_RETRY = 10
export const getAxios = async (force: boolean = false): Promise<AxiosInstance> => { export const getAxios = async (force: boolean = false): Promise<AxiosInstance> => {
const dynamicIpcPath = getMihomoIpcPath() const dynamicIpcPath = getMihomoIpcPath()
// 如路径改变 强制重新创建实例
if (axiosIns && !force && currentIpcPath === dynamicIpcPath) { if (axiosIns && !force && currentIpcPath === dynamicIpcPath) {
return axiosIns return axiosIns
} }

View File

@ -18,10 +18,8 @@ import { initShortcut } from './resolve/shortcut'
import { initProfileUpdater } from './core/profileUpdater' import { initProfileUpdater } from './core/profileUpdater'
import { startMonitor } from './resolve/trafficMonitor' import { startMonitor } from './resolve/trafficMonitor'
import { showFloatingWindow } from './resolve/floatingWindow' import { showFloatingWindow } from './resolve/floatingWindow'
import { logger , createLogger } from './utils/logger' import { logger, createLogger } from './utils/logger'
import { initWebdavBackupScheduler } from './resolve/backup' import { initWebdavBackupScheduler } from './resolve/backup'
const mainLogger = createLogger('Main')
import { import {
createWindow, createWindow,
mainWindow, mainWindow,
@ -37,6 +35,8 @@ import {
getSystemLanguage getSystemLanguage
} from './lifecycle' } from './lifecycle'
const mainLogger = createLogger('Main')
export { mainWindow, showMainWindow, triggerMainWindow, closeMainWindow } export { mainWindow, showMainWindow, triggerMainWindow, closeMainWindow }
const gotTheLock = app.requestSingleInstanceLock() const gotTheLock = app.requestSingleInstanceLock()

View File

@ -49,7 +49,9 @@ async function createFloatingWindow(): Promise<void> {
if (process.platform === 'win32') { if (process.platform === 'win32') {
windowOptions.hasShadow = !safeMode windowOptions.hasShadow = !safeMode
windowOptions.webPreferences!.offscreen = false if (windowOptions.webPreferences) {
windowOptions.webPreferences.offscreen = false
}
} }
floatingWindow = new BrowserWindow(windowOptions) floatingWindow = new BrowserWindow(windowOptions)
@ -68,7 +70,9 @@ async function createFloatingWindow(): Promise<void> {
}) })
floatingWindow.on('moved', () => { floatingWindow.on('moved', () => {
floatingWindow && floatingWindowState.saveState(floatingWindow) if (floatingWindow) {
floatingWindowState.saveState(floatingWindow)
}
}) })
// IPC 监听器 // IPC 监听器

View File

@ -59,9 +59,8 @@ export async function getGitHubTags(
// 检查缓存 // 检查缓存
if (!forceRefresh && versionCache.has(cacheKey)) { if (!forceRefresh && versionCache.has(cacheKey)) {
const cache = versionCache.get(cacheKey)! const cache = versionCache.get(cacheKey)
// 检查缓存是否过期 if (cache && Date.now() - cache.timestamp < CACHE_EXPIRY) {
if (Date.now() - cache.timestamp < CACHE_EXPIRY) {
log.debug(`Returning cached tags for ${owner}/${repo}`) log.debug(`Returning cached tags for ${owner}/${repo}`)
return cache.data return cache.data
} }

View File

@ -191,7 +191,7 @@ const electronAPI = {
if (!listenerMap.has(channel)) { if (!listenerMap.has(channel)) {
listenerMap.set(channel, new Set()) listenerMap.set(channel, new Set())
} }
listenerMap.get(channel)!.add(listener) listenerMap.get(channel)?.add(listener)
ipcRenderer.on(channel, listener) ipcRenderer.on(channel, listener)
} }
}, },

View File

@ -1,5 +1,5 @@
import { useTheme } from 'next-themes' import { useTheme } from 'next-themes'
import { useEffect, useRef, useState } from 'react' import { useCallback, useEffect, useRef, useState } from 'react'
import { NavigateFunction, useLocation, useNavigate, useRoutes } from 'react-router-dom' import { NavigateFunction, useLocation, useNavigate, useRoutes } from 'react-router-dom'
import OutboundModeSwitcher from '@renderer/components/sider/outbound-mode-switcher' import OutboundModeSwitcher from '@renderer/components/sider/outbound-mode-switcher'
import SysproxySwitcher from '@renderer/components/sider/sysproxy-switcher' import SysproxySwitcher from '@renderer/components/sider/sysproxy-switcher'
@ -32,10 +32,10 @@ import { applyTheme, setNativeTheme, setTitleBarOverlay } from '@renderer/utils/
import { platform } from '@renderer/utils/init' import { platform } from '@renderer/utils/init'
import { TitleBarOverlayOptions } from 'electron' import { TitleBarOverlayOptions } from 'electron'
import SubStoreCard from '@renderer/components/sider/substore-card' import SubStoreCard from '@renderer/components/sider/substore-card'
import MihomoIcon from './components/base/mihomo-icon'
import { createTourDriver, getDriver, startTourIfNeeded } from '@renderer/utils/tour' import { createTourDriver, getDriver, startTourIfNeeded } from '@renderer/utils/tour'
import 'driver.js/dist/driver.css' import 'driver.js/dist/driver.css'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import MihomoIcon from './components/base/mihomo-icon'
let navigate: NavigateFunction let navigate: NavigateFunction
@ -78,7 +78,7 @@ const App: React.FC = () => {
const location = useLocation() const location = useLocation()
const page = useRoutes(routes) const page = useRoutes(routes)
const setTitlebar = (): void => { const setTitlebar = useCallback((): void => {
if (!useWindowFrame && platform !== 'darwin') { if (!useWindowFrame && platform !== 'darwin') {
const options = { height: 48 } as TitleBarOverlayOptions const options = { height: 48 } as TitleBarOverlayOptions
try { try {
@ -89,7 +89,7 @@ const App: React.FC = () => {
// ignore // ignore
} }
} }
} }, [useWindowFrame])
useEffect(() => { useEffect(() => {
setOrder(siderOrder) setOrder(siderOrder)
@ -101,6 +101,13 @@ const App: React.FC = () => {
resizingRef.current = resizing resizingRef.current = resizing
}, [siderWidthValue, resizing]) }, [siderWidthValue, resizing])
const onResizeEnd = useCallback((): void => {
if (resizingRef.current) {
setResizing(false)
patchAppConfig({ siderWidth: siderWidthValueRef.current })
}
}, [patchAppConfig])
useEffect(() => { useEffect(() => {
if (!tourInitialized.current) { if (!tourInitialized.current) {
tourInitialized.current = true tourInitialized.current = true
@ -113,25 +120,18 @@ const App: React.FC = () => {
setNativeTheme(appTheme) setNativeTheme(appTheme)
setTheme(appTheme) setTheme(appTheme)
setTitlebar() setTitlebar()
}, [appTheme, systemTheme]) }, [appTheme, systemTheme, setTheme, setTitlebar])
useEffect(() => { useEffect(() => {
applyTheme(customTheme || 'default.css').then(() => { applyTheme(customTheme || 'default.css').then(() => {
setTitlebar() setTitlebar()
}) })
}, [customTheme]) }, [customTheme, setTitlebar])
useEffect(() => { useEffect(() => {
window.addEventListener('mouseup', onResizeEnd) window.addEventListener('mouseup', onResizeEnd)
return (): void => window.removeEventListener('mouseup', onResizeEnd) return (): void => window.removeEventListener('mouseup', onResizeEnd)
}, []) }, [onResizeEnd])
const onResizeEnd = (): void => {
if (resizingRef.current) {
setResizing(false)
patchAppConfig({ siderWidth: siderWidthValueRef.current })
}
}
const onDragEnd = async (event: DragEndEvent): Promise<void> => { const onDragEnd = async (event: DragEndEvent): Promise<void> => {
const { active, over } = event const { active, over } = event

View File

@ -36,7 +36,7 @@ const BasePage = forwardRef<HTMLDivElement, Props>((props, ref) => {
// ignore // ignore
} }
} }
}, []) }, [useWindowFrame])
const contentRef = useRef<HTMLDivElement>(null) const contentRef = useRef<HTMLDivElement>(null)
useImperativeHandle(ref, () => { useImperativeHandle(ref, () => {

View File

@ -15,13 +15,12 @@ const EditFileModal: React.FC<Props> = (props) => {
const [currData, setCurrData] = useState('') const [currData, setCurrData] = useState('')
const { t } = useTranslation() const { t } = useTranslation()
const getContent = async (): Promise<void> => {
setCurrData(await getOverride(id, language === 'javascript' ? 'js' : 'yaml'))
}
useEffect(() => { useEffect(() => {
getContent() const loadContent = async (): Promise<void> => {
}, []) setCurrData(await getOverride(id, language === 'javascript' ? 'js' : 'yaml'))
}
loadContent()
}, [id, language])
return ( return (
<Modal <Modal

View File

@ -20,13 +20,12 @@ const ExecLogModal: React.FC<Props> = (props) => {
const [logs, setLogs] = useState<string[]>([]) const [logs, setLogs] = useState<string[]>([])
const { t } = useTranslation() const { t } = useTranslation()
const getLog = async (): Promise<void> => {
setLogs((await getOverride(id, 'log')).split('\n').filter(Boolean))
}
useEffect(() => { useEffect(() => {
getLog() const loadLog = async (): Promise<void> => {
}, []) setLogs((await getOverride(id, 'log')).split('\n').filter(Boolean))
}
loadLog()
}, [id])
return ( return (
<Modal <Modal

View File

@ -16,13 +16,12 @@ const EditFileModal: React.FC<Props> = (props) => {
const navigate = useNavigate() const navigate = useNavigate()
const { t } = useTranslation() const { t } = useTranslation()
const getContent = async (): Promise<void> => {
setCurrData(await getProfileStr(id))
}
useEffect(() => { useEffect(() => {
getContent() const loadContent = async (): Promise<void> => {
}, []) setCurrData(await getProfileStr(id))
}
loadContent()
}, [id])
return ( return (
<Modal <Modal

View File

@ -558,188 +558,266 @@ const EditRulesModal: React.FC<Props> = (props) => {
const deferredFilteredRules = useDeferredValue(filteredRules) const deferredFilteredRules = useDeferredValue(filteredRules)
const getContent = async (): Promise<void> => { // 解析规则字符串
setIsLoading(true) const parseRuleString = useCallback((ruleStr: string): RuleItem => {
try { const parts = ruleStr.split(',')
const content = await getProfileStr(id) const firstPartIsNumber =
setProfileContent(content) !isNaN(Number(parts[0])) && parts[0].trim() !== '' && parts.length >= 3
const parsed = yaml.load(content) as Record<string, unknown> | undefined let offset = 0
let initialRules: RuleItem[] = [] let ruleParts = parts
if (parsed && parsed.rules && Array.isArray(parsed.rules)) { if (firstPartIsNumber) {
initialRules = parsed.rules.map((rule: string) => { offset = parseInt(parts[0])
const parts = rule.split(',') ruleParts = parts.slice(1)
if (parts[0] === 'MATCH') { }
return {
type: 'MATCH', if (ruleParts[0] === 'MATCH') {
payload: '', return {
proxy: parts[1] type: 'MATCH',
} payload: '',
proxy: ruleParts[1],
offset: offset > 0 ? offset : undefined
}
} else {
const additionalParams = ruleParts.slice(3).filter((param) => param.trim() !== '') || []
return {
type: ruleParts[0],
payload: ruleParts[1],
proxy: ruleParts[2],
additionalParams,
offset: offset > 0 ? offset : undefined
}
}
}, [])
// 处理前置规则位置
const processRulesWithPositions = useCallback(
(
rulesToProcess: RuleItem[],
allRules: RuleItem[],
positionCalculator: (rule: RuleItem, currentRules: RuleItem[]) => number
): { updatedRules: RuleItem[]; ruleIndices: Set<number> } => {
const updatedRules = [...allRules]
const ruleIndices = new Set<number>()
rulesToProcess.forEach((rule) => {
const targetPosition = positionCalculator(rule, updatedRules)
const actualPosition = Math.min(targetPosition, updatedRules.length)
updatedRules.splice(actualPosition, 0, rule)
const newRuleIndices = new Set<number>()
ruleIndices.forEach((idx) => {
if (idx >= actualPosition) {
newRuleIndices.add(idx + 1)
} else { } else {
const additionalParams = parts.slice(3).filter((param) => param.trim() !== '') || [] newRuleIndices.add(idx)
return {
type: parts[0],
payload: parts[1],
proxy: parts[2],
additionalParams
}
} }
}) })
} newRuleIndices.add(actualPosition)
// 提取代理组 ruleIndices.clear()
if (parsed) { newRuleIndices.forEach((idx) => ruleIndices.add(idx))
const groups: string[] = [] })
// 添加代理组和代理名称 return { updatedRules, ruleIndices }
if (Array.isArray(parsed['proxy-groups'])) { },
groups.push( []
...((parsed['proxy-groups'] as Array<Record<string, unknown>>) )
.map((group) =>
group && typeof group['name'] === 'string' ? (group['name'] as string) : ''
)
.filter(Boolean) as string[])
)
}
if (Array.isArray(parsed['proxies'])) { // 处理后置规则位置
groups.push( const processAppendRulesWithPositions = useCallback(
...((parsed['proxies'] as Array<Record<string, unknown>>) (
.map((proxy) => rulesToProcess: RuleItem[],
proxy && typeof proxy['name'] === 'string' ? (proxy['name'] as string) : '' allRules: RuleItem[],
) positionCalculator: (rule: RuleItem, currentRules: RuleItem[]) => number
.filter(Boolean) as string[]) ): { updatedRules: RuleItem[]; ruleIndices: Set<number> } => {
) const updatedRules = [...allRules]
} const ruleIndices = new Set<number>()
// 预置出站 https://wiki.metacubex.one/config/proxies/built-in/ rulesToProcess.forEach((rule) => {
groups.push('DIRECT', 'REJECT', 'REJECT-DROP', 'PASS', 'COMPATIBLE') const targetPosition = positionCalculator(rule, updatedRules)
const actualPosition = Math.min(targetPosition, updatedRules.length)
updatedRules.splice(actualPosition, 0, rule)
// 去重 const newRuleIndices = new Set<number>()
setProxyGroups([...new Set(groups)]) ruleIndices.forEach((idx) => {
} if (idx >= actualPosition) {
newRuleIndices.add(idx + 1)
} else {
newRuleIndices.add(idx)
}
})
newRuleIndices.add(actualPosition)
// 读取规则文件 ruleIndices.clear()
newRuleIndices.forEach((idx) => ruleIndices.add(idx))
})
return { updatedRules, ruleIndices }
},
[]
)
useEffect(() => {
const loadContent = async (): Promise<void> => {
setIsLoading(true)
try { try {
const ruleContent = await getRuleStr(id) const content = await getProfileStr(id)
const ruleData = yaml.load(ruleContent) as { setProfileContent(content)
prepend?: string[]
append?: string[] const parsed = yaml.load(content) as Record<string, unknown> | undefined
delete?: string[] let initialRules: RuleItem[] = []
if (parsed && parsed.rules && Array.isArray(parsed.rules)) {
initialRules = parsed.rules.map((rule: string) => {
const parts = rule.split(',')
if (parts[0] === 'MATCH') {
return {
type: 'MATCH',
payload: '',
proxy: parts[1]
}
} else {
const additionalParams = parts.slice(3).filter((param) => param.trim() !== '') || []
return {
type: parts[0],
payload: parts[1],
proxy: parts[2],
additionalParams
}
}
})
} }
if (ruleData) { if (parsed) {
let allRules = [...initialRules] const groups: string[] = []
const newPrependRules = new Set<number>()
const newAppendRules = new Set<number>()
const newDeletedRules = new Set<number>()
// 处理前置规则 if (Array.isArray(parsed['proxy-groups'])) {
if (ruleData.prepend && Array.isArray(ruleData.prepend)) { groups.push(
const prependRules: RuleItem[] = [] ...((parsed['proxy-groups'] as Array<Record<string, unknown>>)
ruleData.prepend.forEach((ruleStr: string) => { .map((group) =>
prependRules.push(parseRuleString(ruleStr)) group && typeof group['name'] === 'string' ? (group['name'] as string) : ''
}) )
.filter(Boolean) as string[])
// 插入前置规则
const { updatedRules, ruleIndices } = processRulesWithPositions(
prependRules,
allRules,
(rule, currentRules) => {
if (rule.offset !== undefined && rule.offset < currentRules.length) {
return rule.offset
}
return 0
}
) )
allRules = updatedRules
ruleIndices.forEach((index) => newPrependRules.add(index))
} }
// 处理后置规则 if (Array.isArray(parsed['proxies'])) {
if (ruleData.append && Array.isArray(ruleData.append)) { groups.push(
const appendRules: RuleItem[] = [] ...((parsed['proxies'] as Array<Record<string, unknown>>)
ruleData.append.forEach((ruleStr: string) => { .map((proxy) =>
appendRules.push(parseRuleString(ruleStr)) proxy && typeof proxy['name'] === 'string' ? (proxy['name'] as string) : ''
}) )
.filter(Boolean) as string[])
// 插入后置规则
const { updatedRules, ruleIndices } = processAppendRulesWithPositions(
appendRules,
allRules,
(rule, currentRules) => {
if (rule.offset !== undefined) {
return Math.max(0, currentRules.length - rule.offset)
}
return currentRules.length
}
) )
allRules = updatedRules
// 标记后置规则
ruleIndices.forEach((index) => newAppendRules.add(index))
} }
// 处理删除规则 groups.push('DIRECT', 'REJECT', 'REJECT-DROP', 'PASS', 'COMPATIBLE')
if (ruleData.delete && Array.isArray(ruleData.delete)) { setProxyGroups([...new Set(groups)])
const deleteRules = ruleData.delete.map((ruleStr: string) => { }
return parseRuleString(ruleStr)
})
// 匹配并标记删除规则 try {
deleteRules.forEach((deleteRule) => { const ruleContent = await getRuleStr(id)
const matchedIndex = allRules.findIndex( const ruleData = yaml.load(ruleContent) as {
(rule) => prepend?: string[]
rule.type === deleteRule.type && append?: string[]
rule.payload === deleteRule.payload && delete?: string[]
rule.proxy === deleteRule.proxy && }
JSON.stringify(rule.additionalParams || []) ===
JSON.stringify(deleteRule.additionalParams || []) if (ruleData) {
let allRules = [...initialRules]
const newPrependRules = new Set<number>()
const newAppendRules = new Set<number>()
const newDeletedRules = new Set<number>()
if (ruleData.prepend && Array.isArray(ruleData.prepend)) {
const prependRuleItems: RuleItem[] = []
ruleData.prepend.forEach((ruleStr: string) => {
prependRuleItems.push(parseRuleString(ruleStr))
})
const { updatedRules, ruleIndices } = processRulesWithPositions(
prependRuleItems,
allRules,
(rule, currentRules) => {
if (rule.offset !== undefined && rule.offset < currentRules.length) {
return rule.offset
}
return 0
}
) )
if (matchedIndex !== -1) { allRules = updatedRules
newDeletedRules.add(matchedIndex) ruleIndices.forEach((index) => newPrependRules.add(index))
} }
})
if (ruleData.append && Array.isArray(ruleData.append)) {
const appendRuleItems: RuleItem[] = []
ruleData.append.forEach((ruleStr: string) => {
appendRuleItems.push(parseRuleString(ruleStr))
})
const { updatedRules, ruleIndices } = processAppendRulesWithPositions(
appendRuleItems,
allRules,
(rule, currentRules) => {
if (rule.offset !== undefined) {
return Math.max(0, currentRules.length - rule.offset)
}
return currentRules.length
}
)
allRules = updatedRules
ruleIndices.forEach((index) => newAppendRules.add(index))
}
if (ruleData.delete && Array.isArray(ruleData.delete)) {
const deleteRules = ruleData.delete.map((ruleStr: string) => {
return parseRuleString(ruleStr)
})
deleteRules.forEach((deleteRule) => {
const matchedIndex = allRules.findIndex(
(rule) =>
rule.type === deleteRule.type &&
rule.payload === deleteRule.payload &&
rule.proxy === deleteRule.proxy &&
JSON.stringify(rule.additionalParams || []) ===
JSON.stringify(deleteRule.additionalParams || [])
)
if (matchedIndex !== -1) {
newDeletedRules.add(matchedIndex)
}
})
}
setPrependRules(newPrependRules)
setAppendRules(newAppendRules)
setDeletedRules(newDeletedRules)
setRules(allRules)
} else {
setRules(initialRules)
setPrependRules(new Set())
setAppendRules(new Set())
setDeletedRules(new Set())
} }
} catch {
// 更新状态
setPrependRules(newPrependRules)
setAppendRules(newAppendRules)
setDeletedRules(newDeletedRules)
// 设置规则列表
setRules(allRules)
} else {
// 使用初始规则
setRules(initialRules) setRules(initialRules)
// 清空规则标记
setPrependRules(new Set()) setPrependRules(new Set())
setAppendRules(new Set()) setAppendRules(new Set())
setDeletedRules(new Set()) setDeletedRules(new Set())
} }
} catch (ruleError) { } catch {
// 规则文件读取失败 // 解析配置文件失败,静默处理
console.debug('规则文件读取失败:', ruleError) } finally {
setRules(initialRules) setIsLoading(false)
// 清空规则标记
setPrependRules(new Set())
setAppendRules(new Set())
setDeletedRules(new Set())
} }
} catch (e) {
console.error('Failed to parse profile content', e)
} finally {
setIsLoading(false)
} }
} loadContent()
}, [id, parseRuleString, processRulesWithPositions, processAppendRulesWithPositions])
useEffect(() => {
getContent()
}, [])
const validateRulePayload = useCallback((ruleType: string, payload: string): boolean => { const validateRulePayload = useCallback((ruleType: string, payload: string): boolean => {
if (ruleType === 'MATCH') { if (ruleType === 'MATCH') {
@ -843,6 +921,58 @@ const EditRulesModal: React.FC<Props> = (props) => {
}) })
} }
// 计算插入位置的索引
const getUpdatedIndexForInsertion = (index: number, insertPosition: number): number => {
if (index >= insertPosition) {
return index + 1
} else {
return index
}
}
// 插入规则后更新所有索引
const updateAllRuleIndicesAfterInsertion = useCallback(
(
currentPrependRules: Set<number>,
currentAppendRules: Set<number>,
currentDeletedRules: Set<number>,
insertPosition: number,
isNewPrependRule: boolean = false,
isNewAppendRule: boolean = false
): {
newPrependRules: Set<number>
newAppendRules: Set<number>
newDeletedRules: Set<number>
} => {
const newPrependRules = new Set<number>()
const newAppendRules = new Set<number>()
const newDeletedRules = new Set<number>()
currentPrependRules.forEach((idx) => {
newPrependRules.add(getUpdatedIndexForInsertion(idx, insertPosition))
})
currentAppendRules.forEach((idx) => {
newAppendRules.add(getUpdatedIndexForInsertion(idx, insertPosition))
})
currentDeletedRules.forEach((idx) => {
newDeletedRules.add(getUpdatedIndexForInsertion(idx, insertPosition))
})
if (isNewPrependRule) {
newPrependRules.add(insertPosition)
}
if (isNewAppendRule) {
newAppendRules.add(insertPosition)
}
return { newPrependRules, newAppendRules, newDeletedRules }
},
[]
)
const handleAddRule = useCallback( const handleAddRule = useCallback(
(position: 'prepend' | 'append' = 'append'): void => { (position: 'prepend' | 'append' = 'append'): void => {
if (!(newRule.type === 'MATCH' || newRule.payload.trim() !== '')) { if (!(newRule.type === 'MATCH' || newRule.payload.trim() !== '')) {
@ -917,7 +1047,16 @@ const EditRulesModal: React.FC<Props> = (props) => {
}) })
setNewRule({ type: 'DOMAIN', payload: '', proxy: 'DIRECT', additionalParams: [] }) setNewRule({ type: 'DOMAIN', payload: '', proxy: 'DIRECT', additionalParams: [] })
}, },
[newRule, rules, prependRules, appendRules, deletedRules, validateRulePayload, t] [
newRule,
rules,
prependRules,
appendRules,
deletedRules,
validateRulePayload,
t,
updateAllRuleIndicesAfterInsertion
]
) )
const handleRemoveRule = useCallback((index: number): void => { const handleRemoveRule = useCallback((index: number): void => {
@ -1002,126 +1141,6 @@ const EditRulesModal: React.FC<Props> = (props) => {
[rules, prependRules, appendRules] [rules, prependRules, appendRules]
) )
// 解析规则字符串
const parseRuleString = (ruleStr: string): RuleItem => {
const parts = ruleStr.split(',')
const firstPartIsNumber =
!isNaN(Number(parts[0])) && parts[0].trim() !== '' && parts.length >= 3
let offset = 0
let ruleParts = parts
if (firstPartIsNumber) {
offset = parseInt(parts[0])
ruleParts = parts.slice(1)
}
if (ruleParts[0] === 'MATCH') {
return {
type: 'MATCH',
payload: '',
proxy: ruleParts[1],
offset: offset > 0 ? offset : undefined
}
} else {
const additionalParams = ruleParts.slice(3).filter((param) => param.trim() !== '') || []
return {
type: ruleParts[0],
payload: ruleParts[1],
proxy: ruleParts[2],
additionalParams,
offset: offset > 0 ? offset : undefined
}
}
}
// 规则转字符串
const convertRuleToString = (rule: RuleItem): string => {
const parts = [rule.type]
if (rule.payload) parts.push(rule.payload)
if (rule.proxy) parts.push(rule.proxy)
if (rule.additionalParams && rule.additionalParams.length > 0) {
parts.push(...rule.additionalParams)
}
// 添加偏移量
if (rule.offset !== undefined && rule.offset > 0) {
parts.unshift(rule.offset.toString())
}
return parts.join(',')
}
// 处理前置规则位置
const processRulesWithPositions = (
rules: RuleItem[],
allRules: RuleItem[],
positionCalculator: (rule: RuleItem, currentRules: RuleItem[]) => number
): { updatedRules: RuleItem[]; ruleIndices: Set<number> } => {
const updatedRules = [...allRules]
const ruleIndices = new Set<number>()
// 按顺序处理规则
rules.forEach((rule) => {
const targetPosition = positionCalculator(rule, updatedRules)
const actualPosition = Math.min(targetPosition, updatedRules.length)
updatedRules.splice(actualPosition, 0, rule)
// 更新索引
const newRuleIndices = new Set<number>()
ruleIndices.forEach((idx) => {
if (idx >= actualPosition) {
newRuleIndices.add(idx + 1)
} else {
newRuleIndices.add(idx)
}
})
// 添加当前规则索引
newRuleIndices.add(actualPosition)
// 更新索引集合
ruleIndices.clear()
newRuleIndices.forEach((idx) => ruleIndices.add(idx))
})
return { updatedRules, ruleIndices }
}
// 处理后置规则位置
const processAppendRulesWithPositions = (
rules: RuleItem[],
allRules: RuleItem[],
positionCalculator: (rule: RuleItem, currentRules: RuleItem[]) => number
): { updatedRules: RuleItem[]; ruleIndices: Set<number> } => {
const updatedRules = [...allRules]
const ruleIndices = new Set<number>()
// 按顺序处理规则
rules.forEach((rule) => {
const targetPosition = positionCalculator(rule, updatedRules)
const actualPosition = Math.min(targetPosition, updatedRules.length)
updatedRules.splice(actualPosition, 0, rule)
// 更新索引
const newRuleIndices = new Set<number>()
ruleIndices.forEach((idx) => {
if (idx >= actualPosition) {
newRuleIndices.add(idx + 1)
} else {
newRuleIndices.add(idx)
}
})
// 添加当前规则索引
newRuleIndices.add(actualPosition)
// 更新索引集合
ruleIndices.clear()
newRuleIndices.forEach((idx) => ruleIndices.add(idx))
})
return { updatedRules, ruleIndices }
}
// 更新规则索引 // 更新规则索引
const updateRuleIndices = (prev: Set<number>, index1: number, index2: number): Set<number> => { const updateRuleIndices = (prev: Set<number>, index1: number, index2: number): Set<number> => {
const newSet = new Set<number>() const newSet = new Set<number>()
@ -1137,57 +1156,20 @@ const EditRulesModal: React.FC<Props> = (props) => {
return newSet return newSet
} }
// 计算插入位置的索引 // 规则转字符串
const getUpdatedIndexForInsertion = (index: number, insertPosition: number): number => { const convertRuleToString = (rule: RuleItem): string => {
if (index >= insertPosition) { const parts = [rule.type]
return index + 1 if (rule.payload) parts.push(rule.payload)
} else { if (rule.proxy) parts.push(rule.proxy)
return index if (rule.additionalParams && rule.additionalParams.length > 0) {
} parts.push(...rule.additionalParams)
}
// 插入规则后更新所有索引
const updateAllRuleIndicesAfterInsertion = (
prependRules: Set<number>,
appendRules: Set<number>,
deletedRules: Set<number>,
insertPosition: number,
isNewPrependRule: boolean = false,
isNewAppendRule: boolean = false
): {
newPrependRules: Set<number>
newAppendRules: Set<number>
newDeletedRules: Set<number>
} => {
const newPrependRules = new Set<number>()
const newAppendRules = new Set<number>()
const newDeletedRules = new Set<number>()
// 更新前置规则索引
prependRules.forEach((idx) => {
newPrependRules.add(getUpdatedIndexForInsertion(idx, insertPosition))
})
// 更新后置规则索引
appendRules.forEach((idx) => {
newAppendRules.add(getUpdatedIndexForInsertion(idx, insertPosition))
})
// 更新删除规则索引
deletedRules.forEach((idx) => {
newDeletedRules.add(getUpdatedIndexForInsertion(idx, insertPosition))
})
// 标记新规则
if (isNewPrependRule) {
newPrependRules.add(insertPosition)
} }
if (isNewAppendRule) { if (rule.offset !== undefined && rule.offset > 0) {
newAppendRules.add(insertPosition) parts.unshift(rule.offset.toString())
} }
return { newPrependRules, newAppendRules, newDeletedRules } return parts.join(',')
} }
return ( return (

View File

@ -1,5 +1,5 @@
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from '@heroui/react' import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from '@heroui/react'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState, useMemo } from 'react'
import { getFileStr, setFileStr, convertMrsRuleset, getRuntimeConfig } from '@renderer/utils/ipc' import { getFileStr, setFileStr, convertMrsRuleset, getRuntimeConfig } from '@renderer/utils/ipc'
import yaml from 'js-yaml' import yaml from 'js-yaml'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -20,63 +20,66 @@ const Viewer: React.FC<Props> = (props) => {
const { type, path, title, format, privderType, behavior, onClose } = props const { type, path, title, format, privderType, behavior, onClose } = props
const [currData, setCurrData] = useState('') const [currData, setCurrData] = useState('')
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
let language: Language = !format || format === 'YamlRule' ? 'yaml' : 'text'
const getContent = async (): Promise<void> => { const language: Language = useMemo(() => {
setIsLoading(true) if (format === 'MrsRule') return 'text'
try { if (type === 'Inline') return 'yaml'
let fileContent: React.SetStateAction<string> if (!format || format === 'YamlRule') return 'yaml'
return 'text'
if (format === 'MrsRule') { }, [format, type])
language = 'text'
let ruleBehavior: string = behavior || 'domain'
if (!behavior) {
try {
const runtimeConfig = await getRuntimeConfig()
const provider = runtimeConfig['rule-providers']?.[title]
ruleBehavior = provider?.behavior || 'domain'
} catch {
ruleBehavior = 'domain'
}
}
fileContent = await convertMrsRuleset(path, ruleBehavior)
setCurrData(fileContent)
return
}
if (type === 'Inline') {
fileContent = await getFileStr('config.yaml')
language = 'yaml'
} else {
fileContent = await getFileStr(path)
}
try {
const parsedYaml = yaml.load(fileContent)
if (privderType === 'proxy-providers') {
setCurrData(
yaml.dump({
proxies: parsedYaml[privderType][title].payload
})
)
} else {
setCurrData(
yaml.dump({
rules: parsedYaml[privderType][title].payload
})
)
}
} catch {
setCurrData(fileContent)
}
} finally {
setIsLoading(false)
}
}
useEffect(() => { useEffect(() => {
getContent() const loadContent = async (): Promise<void> => {
}, []) setIsLoading(true)
try {
let fileContent: React.SetStateAction<string>
if (format === 'MrsRule') {
let ruleBehavior: string = behavior || 'domain'
if (!behavior) {
try {
const runtimeConfig = await getRuntimeConfig()
const provider = runtimeConfig['rule-providers']?.[title]
ruleBehavior = provider?.behavior || 'domain'
} catch {
ruleBehavior = 'domain'
}
}
fileContent = await convertMrsRuleset(path, ruleBehavior)
setCurrData(fileContent)
return
}
if (type === 'Inline') {
fileContent = await getFileStr('config.yaml')
} else {
fileContent = await getFileStr(path)
}
try {
const parsedYaml = yaml.load(fileContent)
if (privderType === 'proxy-providers') {
setCurrData(
yaml.dump({
proxies: parsedYaml[privderType][title].payload
})
)
} else {
setCurrData(
yaml.dump({
rules: parsedYaml[privderType][title].payload
})
)
}
} catch {
setCurrData(fileContent)
}
} finally {
setIsLoading(false)
}
}
loadContent()
}, [path, type, title, format, privderType, behavior])
return ( return (
<Modal <Modal

View File

@ -51,7 +51,7 @@ const MihomoCoreCard: React.FC<Props> = (props) => {
PubSub.unsubscribe(token) PubSub.unsubscribe(token)
window.electron.ipcRenderer.removeAllListeners('mihomoMemory') window.electron.ipcRenderer.removeAllListeners('mihomoMemory')
} }
}, []) }, [mutate])
if (iconOnly) { if (iconOnly) {
return ( return (

View File

@ -39,7 +39,7 @@ export const ControledMihomoConfigProvider: React.FC<{ children: ReactNode }> =
return (): void => { return (): void => {
window.electron.ipcRenderer.removeListener('controledMihomoConfigUpdated', handler) window.electron.ipcRenderer.removeListener('controledMihomoConfigUpdated', handler)
} }
}, []) }, [mutateControledMihomoConfig])
return ( return (
<ControledMihomoConfigContext.Provider <ControledMihomoConfigContext.Provider

View File

@ -27,7 +27,7 @@ export const GroupsProvider: React.FC<{ children: ReactNode }> = ({ children })
return (): void => { return (): void => {
window.electron.ipcRenderer.removeListener('groupsUpdated', handler) window.electron.ipcRenderer.removeListener('groupsUpdated', handler)
} }
}, []) }, [mutate])
return <GroupsContext.Provider value={{ groups, mutate }}>{children}</GroupsContext.Provider> return <GroupsContext.Provider value={{ groups, mutate }}>{children}</GroupsContext.Provider>
} }

View File

@ -23,7 +23,7 @@ export const RulesProvider: React.FC<{ children: ReactNode }> = ({ children }) =
return (): void => { return (): void => {
window.electron.ipcRenderer.removeListener('rulesUpdated', handler) window.electron.ipcRenderer.removeListener('rulesUpdated', handler)
} }
}, []) }, [mutate])
return <RulesContext.Provider value={{ rules, mutate }}>{children}</RulesContext.Provider> return <RulesContext.Provider value={{ rules, mutate }}>{children}</RulesContext.Provider>
} }

View File

@ -52,6 +52,34 @@ const CoreMap = {
'mihomo-specific': 'mihomo.specificVersion' 'mihomo-specific': 'mihomo.specificVersion'
} }
interface WebUIPanel {
id: string
name: string
url: string
isDefault?: boolean
}
const defaultWebUIPanels: WebUIPanel[] = [
{
id: 'metacubexd',
name: 'MetaCubeXD',
url: 'https://metacubex.github.io/metacubexd/#/setup?http=true&hostname=%host&port=%port&secret=%secret',
isDefault: true
},
{
id: 'yacd',
name: 'YACD',
url: 'https://yacd.metacubex.one/?hostname=%host&port=%port&secret=%secret',
isDefault: true
},
{
id: 'zashboard',
name: 'Zashboard',
url: 'https://board.zash.run.place/#/setup?http=true&hostname=%host&port=%port&secret=%secret',
isDefault: true
}
]
const Mihomo: React.FC = () => { const Mihomo: React.FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { appConfig, patchAppConfig } = useAppConfig() const { appConfig, patchAppConfig } = useAppConfig()
@ -79,13 +107,6 @@ const Mihomo: React.FC = () => {
} = appConfig || {} } = appConfig || {}
const { controledMihomoConfig, patchControledMihomoConfig } = useControledMihomoConfig() const { controledMihomoConfig, patchControledMihomoConfig } = useControledMihomoConfig()
interface WebUIPanel {
id: string
name: string
url: string
isDefault?: boolean
}
const { const {
ipv6, ipv6,
'external-controller': externalController = '', 'external-controller': externalController = '',
@ -153,28 +174,6 @@ const Mihomo: React.FC = () => {
// 生成随机端口 (范围 1024-65535) // 生成随机端口 (范围 1024-65535)
const generateRandomPort = () => Math.floor(Math.random() * (65535 - 1024 + 1)) + 1024 const generateRandomPort = () => Math.floor(Math.random() * (65535 - 1024 + 1)) + 1024
// 默认 WebUI 面板选项
const defaultWebUIPanels: WebUIPanel[] = [
{
id: 'metacubexd',
name: 'MetaCubeXD',
url: 'https://metacubex.github.io/metacubexd/#/setup?http=true&hostname=%host&port=%port&secret=%secret',
isDefault: true
},
{
id: 'yacd',
name: 'YACD',
url: 'https://yacd.metacubex.one/?hostname=%host&port=%port&secret=%secret',
isDefault: true
},
{
id: 'zashboard',
name: 'Zashboard',
url: 'https://board.zash.run.place/#/setup?http=true&hostname=%host&port=%port&secret=%secret',
isDefault: true
}
]
// 初始化面板列表 // 初始化面板列表
useEffect(() => { useEffect(() => {
const savedPanels = localStorage.getItem('webui-panels') const savedPanels = localStorage.getItem('webui-panels')

View File

@ -126,7 +126,7 @@ const Profiles: React.FC = () => {
}) })
} }
return items return items
}, [subs, collections]) }, [subs, collections, t])
const handleImport = async (): Promise<void> => { const handleImport = async (): Promise<void> => {
setImporting(true) setImporting(true)
await addProfileItem({ await addProfileItem({

View File

@ -85,7 +85,7 @@ const useProxyState = (
return prev.slice(0, groups.length) return prev.slice(0, groups.length)
}) })
} }
}, [groups.length]) }, [groups.length, isOpen.length, setIsOpen])
// 保存展开状态 // 保存展开状态
useEffect(() => { useEffect(() => {
@ -133,7 +133,7 @@ const Proxies: React.FC = () => {
if (groups.length !== searchValue.length) { if (groups.length !== searchValue.length) {
setSearchValue(Array(groups.length).fill('')) setSearchValue(Array(groups.length).fill(''))
} }
}, [groups.length]) }, [groups.length, searchValue.length])
// 代理列表排序 // 代理列表排序
const sortProxies = useCallback((proxies: (IMihomoProxy | IMihomoGroup)[], order: string) => { const sortProxies = useCallback((proxies: (IMihomoProxy | IMihomoGroup)[], order: string) => {