diff --git a/eslint.config.cjs b/eslint.config.cjs index 06c0fc7..ebd28df 100644 --- a/eslint.config.cjs +++ b/eslint.config.cjs @@ -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' + } } ] diff --git a/src/main/config/profile.ts b/src/main/config/profile.ts index ca2c73f..dab40a8 100644 --- a/src/main/config/profile.ts +++ b/src/main/config/profile.ts @@ -41,7 +41,7 @@ export async function setProfileConfig(config: IProfileConfig): Promise { export async function updateProfileConfig( updater: (config: IProfileConfig) => IProfileConfig | Promise ): Promise { - 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 { diff --git a/src/main/core/manager.ts b/src/main/core/manager.ts index 91c2208..b50feef 100644 --- a/src/main/core/manager.ts +++ b/src/main/core/manager.ts @@ -41,8 +41,6 @@ import { getAxios } from './mihomoApi' import { generateProfile } from './factory' - -// 拆分模块 import { getSessionAdminStatus } from './permissions' import { cleanupSocketFile, diff --git a/src/main/core/mihomoApi.ts b/src/main/core/mihomoApi.ts index 0ae3bfe..3534daf 100644 --- a/src/main/core/mihomoApi.ts +++ b/src/main/core/mihomoApi.ts @@ -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 => { const dynamicIpcPath = getMihomoIpcPath() - // 如路径改变 强制重新创建实例 if (axiosIns && !force && currentIpcPath === dynamicIpcPath) { return axiosIns } diff --git a/src/main/index.ts b/src/main/index.ts index 0a84126..0431373 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -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() diff --git a/src/main/resolve/floatingWindow.ts b/src/main/resolve/floatingWindow.ts index 93844a7..2dfe915 100644 --- a/src/main/resolve/floatingWindow.ts +++ b/src/main/resolve/floatingWindow.ts @@ -49,7 +49,9 @@ async function createFloatingWindow(): Promise { 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 { }) floatingWindow.on('moved', () => { - floatingWindow && floatingWindowState.saveState(floatingWindow) + if (floatingWindow) { + floatingWindowState.saveState(floatingWindow) + } }) // IPC 监听器 diff --git a/src/main/utils/github.ts b/src/main/utils/github.ts index dc5736f..95a9621 100644 --- a/src/main/utils/github.ts +++ b/src/main/utils/github.ts @@ -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 } diff --git a/src/preload/index.ts b/src/preload/index.ts index 21b0a82..c553312 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -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) } }, diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index cfa5c1f..30a59e1 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -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 => { const { active, over } = event diff --git a/src/renderer/src/components/base/base-page.tsx b/src/renderer/src/components/base/base-page.tsx index bce32c4..631bfb8 100644 --- a/src/renderer/src/components/base/base-page.tsx +++ b/src/renderer/src/components/base/base-page.tsx @@ -36,7 +36,7 @@ const BasePage = forwardRef((props, ref) => { // ignore } } - }, []) + }, [useWindowFrame]) const contentRef = useRef(null) useImperativeHandle(ref, () => { diff --git a/src/renderer/src/components/override/edit-file-modal.tsx b/src/renderer/src/components/override/edit-file-modal.tsx index 725a757..a7237a6 100644 --- a/src/renderer/src/components/override/edit-file-modal.tsx +++ b/src/renderer/src/components/override/edit-file-modal.tsx @@ -15,13 +15,12 @@ const EditFileModal: React.FC = (props) => { const [currData, setCurrData] = useState('') const { t } = useTranslation() - const getContent = async (): Promise => { - setCurrData(await getOverride(id, language === 'javascript' ? 'js' : 'yaml')) - } - useEffect(() => { - getContent() - }, []) + const loadContent = async (): Promise => { + setCurrData(await getOverride(id, language === 'javascript' ? 'js' : 'yaml')) + } + loadContent() + }, [id, language]) return ( = (props) => { const [logs, setLogs] = useState([]) const { t } = useTranslation() - const getLog = async (): Promise => { - setLogs((await getOverride(id, 'log')).split('\n').filter(Boolean)) - } - useEffect(() => { - getLog() - }, []) + const loadLog = async (): Promise => { + setLogs((await getOverride(id, 'log')).split('\n').filter(Boolean)) + } + loadLog() + }, [id]) return ( = (props) => { const navigate = useNavigate() const { t } = useTranslation() - const getContent = async (): Promise => { - setCurrData(await getProfileStr(id)) - } - useEffect(() => { - getContent() - }, []) + const loadContent = async (): Promise => { + setCurrData(await getProfileStr(id)) + } + loadContent() + }, [id]) return ( = (props) => { const deferredFilteredRules = useDeferredValue(filteredRules) - const getContent = async (): Promise => { - 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 | 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 } => { + const updatedRules = [...allRules] + const ruleIndices = new Set() + + rulesToProcess.forEach((rule) => { + const targetPosition = positionCalculator(rule, updatedRules) + const actualPosition = Math.min(targetPosition, updatedRules.length) + updatedRules.splice(actualPosition, 0, rule) + + const newRuleIndices = new Set() + 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>) - .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>) - .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 } => { + const updatedRules = [...allRules] + const ruleIndices = new Set() - // 预置出站 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() + 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 => { + 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 | 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() - const newAppendRules = new Set() - const newDeletedRules = new Set() + 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>) + .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>) + .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() + const newAppendRules = new Set() + const newDeletedRules = new Set() + + 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) => { }) } + // 计算插入位置的索引 + const getUpdatedIndexForInsertion = (index: number, insertPosition: number): number => { + if (index >= insertPosition) { + return index + 1 + } else { + return index + } + } + + // 插入规则后更新所有索引 + const updateAllRuleIndicesAfterInsertion = useCallback( + ( + currentPrependRules: Set, + currentAppendRules: Set, + currentDeletedRules: Set, + insertPosition: number, + isNewPrependRule: boolean = false, + isNewAppendRule: boolean = false + ): { + newPrependRules: Set + newAppendRules: Set + newDeletedRules: Set + } => { + const newPrependRules = new Set() + const newAppendRules = new Set() + const newDeletedRules = new Set() + + 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) => { }) 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) => { [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 } => { - const updatedRules = [...allRules] - const ruleIndices = new Set() - - // 按顺序处理规则 - rules.forEach((rule) => { - const targetPosition = positionCalculator(rule, updatedRules) - const actualPosition = Math.min(targetPosition, updatedRules.length) - updatedRules.splice(actualPosition, 0, rule) - - // 更新索引 - const newRuleIndices = new Set() - 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 } => { - const updatedRules = [...allRules] - const ruleIndices = new Set() - - // 按顺序处理规则 - rules.forEach((rule) => { - const targetPosition = positionCalculator(rule, updatedRules) - const actualPosition = Math.min(targetPosition, updatedRules.length) - updatedRules.splice(actualPosition, 0, rule) - - // 更新索引 - const newRuleIndices = new Set() - 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, index1: number, index2: number): Set => { const newSet = new Set() @@ -1137,57 +1156,20 @@ const EditRulesModal: React.FC = (props) => { return newSet } - // 计算插入位置的索引 - const getUpdatedIndexForInsertion = (index: number, insertPosition: number): number => { - if (index >= insertPosition) { - return index + 1 - } else { - return index - } - } - - // 插入规则后更新所有索引 - const updateAllRuleIndicesAfterInsertion = ( - prependRules: Set, - appendRules: Set, - deletedRules: Set, - insertPosition: number, - isNewPrependRule: boolean = false, - isNewAppendRule: boolean = false - ): { - newPrependRules: Set - newAppendRules: Set - newDeletedRules: Set - } => { - const newPrependRules = new Set() - const newAppendRules = new Set() - const newDeletedRules = new Set() - - // 更新前置规则索引 - 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 ( diff --git a/src/renderer/src/components/resources/viewer.tsx b/src/renderer/src/components/resources/viewer.tsx index 0474e99..b2140cb 100644 --- a/src/renderer/src/components/resources/viewer.tsx +++ b/src/renderer/src/components/resources/viewer.tsx @@ -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) => { 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 => { - setIsLoading(true) - try { - let fileContent: React.SetStateAction - - 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 => { + setIsLoading(true) + try { + let fileContent: React.SetStateAction + + 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 ( = (props) => { PubSub.unsubscribe(token) window.electron.ipcRenderer.removeAllListeners('mihomoMemory') } - }, []) + }, [mutate]) if (iconOnly) { return ( diff --git a/src/renderer/src/hooks/use-controled-mihomo-config.tsx b/src/renderer/src/hooks/use-controled-mihomo-config.tsx index abe7371..63aeb10 100644 --- a/src/renderer/src/hooks/use-controled-mihomo-config.tsx +++ b/src/renderer/src/hooks/use-controled-mihomo-config.tsx @@ -39,7 +39,7 @@ export const ControledMihomoConfigProvider: React.FC<{ children: ReactNode }> = return (): void => { window.electron.ipcRenderer.removeListener('controledMihomoConfigUpdated', handler) } - }, []) + }, [mutateControledMihomoConfig]) return ( = ({ children }) return (): void => { window.electron.ipcRenderer.removeListener('groupsUpdated', handler) } - }, []) + }, [mutate]) return {children} } diff --git a/src/renderer/src/hooks/use-rules.tsx b/src/renderer/src/hooks/use-rules.tsx index 411be1d..96191fa 100644 --- a/src/renderer/src/hooks/use-rules.tsx +++ b/src/renderer/src/hooks/use-rules.tsx @@ -23,7 +23,7 @@ export const RulesProvider: React.FC<{ children: ReactNode }> = ({ children }) = return (): void => { window.electron.ipcRenderer.removeListener('rulesUpdated', handler) } - }, []) + }, [mutate]) return {children} } diff --git a/src/renderer/src/pages/mihomo.tsx b/src/renderer/src/pages/mihomo.tsx index b75c75f..4e17dfc 100644 --- a/src/renderer/src/pages/mihomo.tsx +++ b/src/renderer/src/pages/mihomo.tsx @@ -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') diff --git a/src/renderer/src/pages/profiles.tsx b/src/renderer/src/pages/profiles.tsx index 6eb8896..de27fef 100644 --- a/src/renderer/src/pages/profiles.tsx +++ b/src/renderer/src/pages/profiles.tsx @@ -126,7 +126,7 @@ const Profiles: React.FC = () => { }) } return items - }, [subs, collections]) + }, [subs, collections, t]) const handleImport = async (): Promise => { setImporting(true) await addProfileItem({ diff --git a/src/renderer/src/pages/proxies.tsx b/src/renderer/src/pages/proxies.tsx index 240be10..cfaf644 100644 --- a/src/renderer/src/pages/proxies.tsx +++ b/src/renderer/src/pages/proxies.tsx @@ -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) => {