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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
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 yaml from 'js-yaml'
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 [currData, setCurrData] = useState('')
const [isLoading, setIsLoading] = useState(true)
let language: Language = !format || format === 'YamlRule' ? 'yaml' : 'text'
const getContent = async (): Promise<void> => {
setIsLoading(true)
try {
let fileContent: React.SetStateAction<string>
if (format === 'MrsRule') {
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)
}
}
const language: Language = useMemo(() => {
if (format === 'MrsRule') return 'text'
if (type === 'Inline') return 'yaml'
if (!format || format === 'YamlRule') return 'yaml'
return 'text'
}, [format, type])
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 (
<Modal

View File

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

View File

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

View File

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

View File

@ -52,6 +52,34 @@ const CoreMap = {
'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 { t } = useTranslation()
const { appConfig, patchAppConfig } = useAppConfig()
@ -79,13 +107,6 @@ const Mihomo: React.FC = () => {
} = appConfig || {}
const { controledMihomoConfig, patchControledMihomoConfig } = useControledMihomoConfig()
interface WebUIPanel {
id: string
name: string
url: string
isDefault?: boolean
}
const {
ipv6,
'external-controller': externalController = '',
@ -153,28 +174,6 @@ const Mihomo: React.FC = () => {
// 生成随机端口 (范围 1024-65535)
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(() => {
const savedPanels = localStorage.getItem('webui-panels')

View File

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

View File

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