mirror of
https://gh.catmak.name/https://github.com/mihomo-party-org/mihomo-party
synced 2025-12-27 05:00:30 +08:00
chore: ensure ESLint passes and format code & update changelog.md
* chore: ensure ESLint passes and format code * chore: update changelog.md
This commit is contained in:
parent
8f5486064b
commit
6542be8490
@ -18,6 +18,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
### 本项目认证稳定机场推荐:“[狗狗加速](https://party.dginv.click/#/register?code=ARdo0mXx)”
|
### 本项目认证稳定机场推荐:“[狗狗加速](https://party.dginv.click/#/register?code=ARdo0mXx)”
|
||||||
|
|
||||||
##### [狗狗加速 —— 技术流机场 Doggygo VPN](https://party.dginv.click/#/register?code=ARdo0mXx)
|
##### [狗狗加速 —— 技术流机场 Doggygo VPN](https://party.dginv.click/#/register?code=ARdo0mXx)
|
||||||
|
|
||||||
- 高性能海外机场,稳定首选,海外团队,无跑路风险
|
- 高性能海外机场,稳定首选,海外团队,无跑路风险
|
||||||
|
|||||||
36
changelog.md
36
changelog.md
@ -1,6 +1,7 @@
|
|||||||
## 1.8.9
|
# 1.8.9
|
||||||
|
|
||||||
|
## 新功能 (Feat)
|
||||||
|
|
||||||
### 新功能 (Feat)
|
|
||||||
- 升级内核版本
|
- 升级内核版本
|
||||||
- 可视化规则编辑
|
- 可视化规则编辑
|
||||||
- 连接页面支持暂停
|
- 连接页面支持暂停
|
||||||
@ -10,18 +11,21 @@
|
|||||||
- 在菜单中显示当前代理
|
- 在菜单中显示当前代理
|
||||||
- 支持修改 数据收集文件大小
|
- 支持修改 数据收集文件大小
|
||||||
|
|
||||||
### 修复 (Fix)
|
## 修复 (Fix)
|
||||||
|
|
||||||
- 更安全的内核提权检查
|
- 更安全的内核提权检查
|
||||||
- Tun 模式无法在 linux 中正常工作
|
- Tun 模式无法在 linux 中正常工作
|
||||||
- 配置导致的程序崩溃问题
|
- 配置导致的程序崩溃问题
|
||||||
|
|
||||||
# 其他 (chore)
|
### 其他 (chore)
|
||||||
|
|
||||||
- 添加缺失的多国语言翻译
|
- 添加缺失的多国语言翻译
|
||||||
- 更新依赖
|
- 更新依赖
|
||||||
|
|
||||||
## 1.8.8
|
# 1.8.8
|
||||||
|
|
||||||
|
## 新功能 (Feat)
|
||||||
|
|
||||||
### 新功能 (Feat)
|
|
||||||
- 升级内核版本
|
- 升级内核版本
|
||||||
- 增加内核版本选择
|
- 增加内核版本选择
|
||||||
- 记住日志页面的筛选关键字
|
- 记住日志页面的筛选关键字
|
||||||
@ -30,23 +34,27 @@
|
|||||||
- 支持修改点击任务栏的窗口触发行为
|
- 支持修改点击任务栏的窗口触发行为
|
||||||
- 内核设置下增加 WebUI 快捷打开方式
|
- 内核设置下增加 WebUI 快捷打开方式
|
||||||
|
|
||||||
### 修复 (Fix)
|
## 修复 (Fix)
|
||||||
|
|
||||||
- MacOS 首次启动时的 ENOENT: no such file or directory(config.yaml)
|
- MacOS 首次启动时的 ENOENT: no such file or directory(config.yaml)
|
||||||
- 自动更新获取老的文件名称
|
- 自动更新获取老的文件名称
|
||||||
- 修复 mihomo.yaml 文件缺失的问题
|
- 修复 mihomo.yaml 文件缺失的问题
|
||||||
- Smart 配置文件验证出错的问题
|
- Smart 配置文件验证出错的问题
|
||||||
- 开发环境的 electron 问题
|
- 开发环境的 electron 问题
|
||||||
|
|
||||||
### 优化 (Optimize)
|
## 优化 (Optimize)
|
||||||
|
|
||||||
- 加快以管理员模式重启速度
|
- 加快以管理员模式重启速度
|
||||||
- 优化仅用户滚动滚轮时触发自动滚动
|
- 优化仅用户滚动滚轮时触发自动滚动
|
||||||
- 改进俄语翻译
|
- 改进俄语翻译
|
||||||
- 使用重载替换不必要的重启
|
- 使用重载替换不必要的重启
|
||||||
|
|
||||||
# 其他 (chore)
|
## 样式调整 (Sytle)
|
||||||
- 更新依赖
|
|
||||||
|
|
||||||
### 样式调整 (Sytle)
|
- 改进 logo 设计
|
||||||
- 改进 logo 设计
|
- 卡片尺寸
|
||||||
- 卡片尺寸
|
- 设置页可展开项增加指示图标
|
||||||
- 设置页可展开项增加指示图标
|
|
||||||
|
### 其他 (chore)
|
||||||
|
|
||||||
|
- 更新依赖
|
||||||
|
|||||||
@ -52,7 +52,7 @@ if (copiedCount > 0) {
|
|||||||
console.log('📋 现在 dist 目录包含以下文件:')
|
console.log('📋 现在 dist 目录包含以下文件:')
|
||||||
|
|
||||||
const finalFiles = readdirSync(distDir).sort()
|
const finalFiles = readdirSync(distDir).sort()
|
||||||
finalFiles.forEach(file => {
|
finalFiles.forEach((file) => {
|
||||||
if (file.includes('clash-party') || file.includes('mihomo-party')) {
|
if (file.includes('clash-party') || file.includes('mihomo-party')) {
|
||||||
const isLegacy = file.includes('mihomo-party')
|
const isLegacy = file.includes('mihomo-party')
|
||||||
console.log(` ${isLegacy ? '🔄' : '📦'} ${file}`)
|
console.log(` ${isLegacy ? '🔄' : '📦'} ${file}`)
|
||||||
|
|||||||
@ -1,6 +1,12 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { readFileSync } from 'fs'
|
import { readFileSync } from 'fs'
|
||||||
import { getProcessedVersion, isDevBuild, getDownloadUrl, generateDownloadLinksMarkdown, getGitCommitHash } from './version-utils.mjs'
|
import {
|
||||||
|
getProcessedVersion,
|
||||||
|
isDevBuild,
|
||||||
|
getDownloadUrl,
|
||||||
|
generateDownloadLinksMarkdown,
|
||||||
|
getGitCommitHash
|
||||||
|
} from './version-utils.mjs'
|
||||||
|
|
||||||
const chat_id = '@MihomoPartyChannel'
|
const chat_id = '@MihomoPartyChannel'
|
||||||
const pkg = readFileSync('package.json', 'utf-8')
|
const pkg = readFileSync('package.json', 'utf-8')
|
||||||
@ -14,35 +20,29 @@ const isDevRelease = releaseType === 'dev' || isDevBuild()
|
|||||||
|
|
||||||
function convertMarkdownToTelegramHTML(content) {
|
function convertMarkdownToTelegramHTML(content) {
|
||||||
return content
|
return content
|
||||||
.split("\n")
|
.split('\n')
|
||||||
.map((line) => {
|
.map((line) => {
|
||||||
if (line.trim().length === 0) {
|
if (line.trim().length === 0) {
|
||||||
return "";
|
return ''
|
||||||
} else if (line.startsWith("## ")) {
|
} else if (line.startsWith('## ')) {
|
||||||
return `<b>${line.replace("## ", "")}</b>`;
|
return `<b>${line.replace('## ', '')}</b>`
|
||||||
} else if (line.startsWith("### ")) {
|
} else if (line.startsWith('### ')) {
|
||||||
return `<b>${line.replace("### ", "")}</b>`;
|
return `<b>${line.replace('### ', '')}</b>`
|
||||||
} else if (line.startsWith("#### ")) {
|
} else if (line.startsWith('#### ')) {
|
||||||
return `<b>${line.replace("#### ", "")}</b>`;
|
return `<b>${line.replace('#### ', '')}</b>`
|
||||||
} else {
|
} else {
|
||||||
let processedLine = line.replace(
|
let processedLine = line.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, text, url) => {
|
||||||
/\[([^\]]+)\]\(([^)]+)\)/g,
|
const encodedUrl = encodeURI(url)
|
||||||
(match, text, url) => {
|
return `<a href="${encodedUrl}">${text}</a>`
|
||||||
const encodedUrl = encodeURI(url);
|
})
|
||||||
return `<a href="${encodedUrl}">${text}</a>`;
|
processedLine = processedLine.replace(/\*\*([^*]+)\*\*/g, '<b>$1</b>')
|
||||||
},
|
return processedLine
|
||||||
);
|
|
||||||
processedLine = processedLine.replace(
|
|
||||||
/\*\*([^*]+)\*\*/g,
|
|
||||||
"<b>$1</b>",
|
|
||||||
);
|
|
||||||
return processedLine;
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.join("\n");
|
.join('\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
let content = '';
|
let content = ''
|
||||||
|
|
||||||
if (isDevRelease) {
|
if (isDevRelease) {
|
||||||
// 版本号中提取commit hash
|
// 版本号中提取commit hash
|
||||||
|
|||||||
@ -20,7 +20,6 @@ function updatePackageVersion() {
|
|||||||
writeFileSync(packagePath, JSON.stringify(packageData, null, 2) + '\n')
|
writeFileSync(packagePath, JSON.stringify(packageData, null, 2) + '\n')
|
||||||
|
|
||||||
console.log(`✅ package.json版本号已更新为: ${newVersion}`)
|
console.log(`✅ package.json版本号已更新为: ${newVersion}`)
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ 更新package.json版本号失败:', error.message)
|
console.error('❌ 更新package.json版本号失败:', error.message)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
|
|||||||
@ -1,6 +1,11 @@
|
|||||||
import yaml from 'yaml'
|
import yaml from 'yaml'
|
||||||
import { readFileSync, writeFileSync } from 'fs'
|
import { readFileSync, writeFileSync } from 'fs'
|
||||||
import { getProcessedVersion, isDevBuild, getDownloadUrl, generateDownloadLinksMarkdown } from './version-utils.mjs'
|
import {
|
||||||
|
getProcessedVersion,
|
||||||
|
isDevBuild,
|
||||||
|
getDownloadUrl,
|
||||||
|
generateDownloadLinksMarkdown
|
||||||
|
} from './version-utils.mjs'
|
||||||
|
|
||||||
const pkg = readFileSync('package.json', 'utf-8')
|
const pkg = readFileSync('package.json', 'utf-8')
|
||||||
let changelog = readFileSync('changelog.md', 'utf-8')
|
let changelog = readFileSync('changelog.md', 'utf-8')
|
||||||
@ -19,7 +24,8 @@ const latest = {
|
|||||||
// 使用统一的下载链接生成函数
|
// 使用统一的下载链接生成函数
|
||||||
changelog += generateDownloadLinksMarkdown(downloadUrl, version)
|
changelog += generateDownloadLinksMarkdown(downloadUrl, version)
|
||||||
|
|
||||||
changelog += '\n\n### 机场推荐:\n- 高性能海外机场,稳定首选:[https://狗狗加速.com](https://party.dginv.click/#/register?code=ARdo0mXx)'
|
changelog +=
|
||||||
|
'\n\n### 机场推荐:\n- 高性能海外机场,稳定首选:[https://狗狗加速.com](https://party.dginv.click/#/register?code=ARdo0mXx)'
|
||||||
|
|
||||||
writeFileSync('latest.yml', yaml.stringify(latest))
|
writeFileSync('latest.yml', yaml.stringify(latest))
|
||||||
writeFileSync('changelog.md', changelog)
|
writeFileSync('changelog.md', changelog)
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { execSync } from 'child_process'
|
import { execSync } from 'child_process'
|
||||||
import { readFileSync } from 'fs'
|
import { readFileSync } from 'fs'
|
||||||
|
|
||||||
// 获取Git commit hash
|
// 获取Git commit hash
|
||||||
export function getGitCommitHash(short = true) {
|
export function getGitCommitHash(short = true) {
|
||||||
try {
|
try {
|
||||||
const command = short ? 'git rev-parse --short HEAD' : 'git rev-parse HEAD'
|
const command = short ? 'git rev-parse --short HEAD' : 'git rev-parse HEAD'
|
||||||
@ -12,7 +12,7 @@ export function getGitCommitHash(short = true) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取当前月份日期
|
// 获取当前月份日期
|
||||||
export function getCurrentMonthDate() {
|
export function getCurrentMonthDate() {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const month = String(now.getMonth() + 1).padStart(2, '0')
|
const month = String(now.getMonth() + 1).padStart(2, '0')
|
||||||
@ -20,7 +20,7 @@ export function getCurrentMonthDate() {
|
|||||||
return `${month}${day}`
|
return `${month}${day}`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从package.json读取基础版本号
|
// 从package.json读取基础版本号
|
||||||
export function getBaseVersion() {
|
export function getBaseVersion() {
|
||||||
try {
|
try {
|
||||||
const pkg = readFileSync('package.json', 'utf-8')
|
const pkg = readFileSync('package.json', 'utf-8')
|
||||||
@ -33,7 +33,7 @@ export function getBaseVersion() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成dev版本号
|
// 生成dev版本号
|
||||||
export function getDevVersion() {
|
export function getDevVersion() {
|
||||||
const baseVersion = getBaseVersion()
|
const baseVersion = getBaseVersion()
|
||||||
const monthDate = getCurrentMonthDate()
|
const monthDate = getCurrentMonthDate()
|
||||||
@ -42,14 +42,16 @@ export function getDevVersion() {
|
|||||||
return `${baseVersion}-d${monthDate}.${commitHash}`
|
return `${baseVersion}-d${monthDate}.${commitHash}`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查当前环境是否为dev构建
|
// 检查当前环境是否为dev构建
|
||||||
export function isDevBuild() {
|
export function isDevBuild() {
|
||||||
return process.env.NODE_ENV === 'development' ||
|
return (
|
||||||
|
process.env.NODE_ENV === 'development' ||
|
||||||
process.argv.includes('--dev') ||
|
process.argv.includes('--dev') ||
|
||||||
process.env.GITHUB_EVENT_NAME === 'workflow_dispatch'
|
process.env.GITHUB_EVENT_NAME === 'workflow_dispatch'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取处理后的版本号
|
// 获取处理后的版本号
|
||||||
export function getProcessedVersion() {
|
export function getProcessedVersion() {
|
||||||
if (isDevBuild()) {
|
if (isDevBuild()) {
|
||||||
return getDevVersion()
|
return getDevVersion()
|
||||||
@ -58,7 +60,7 @@ export function getProcessedVersion() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成下载URL
|
// 生成下载URL
|
||||||
export function getDownloadUrl(isDev, version) {
|
export function getDownloadUrl(isDev, version) {
|
||||||
if (isDev) {
|
if (isDev) {
|
||||||
return 'https://github.com/mihomo-party-org/clash-party/releases/download/dev'
|
return 'https://github.com/mihomo-party-org/clash-party/releases/download/dev'
|
||||||
|
|||||||
@ -17,7 +17,11 @@ export async function getControledMihomoConfig(force = false): Promise<Partial<I
|
|||||||
} else {
|
} else {
|
||||||
controledMihomoConfig = defaultControledMihomoConfig
|
controledMihomoConfig = defaultControledMihomoConfig
|
||||||
try {
|
try {
|
||||||
await writeFile(controledMihomoConfigPath(), stringify(defaultControledMihomoConfig), 'utf-8')
|
await writeFile(
|
||||||
|
controledMihomoConfigPath(),
|
||||||
|
stringify(defaultControledMihomoConfig),
|
||||||
|
'utf-8'
|
||||||
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to create mihomo.yaml file:', error)
|
console.error('Failed to create mihomo.yaml file:', error)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,12 @@ const SMART_OVERRIDE_ID = 'smart-core-override'
|
|||||||
/**
|
/**
|
||||||
* Smart 内核的覆写配置模板
|
* Smart 内核的覆写配置模板
|
||||||
*/
|
*/
|
||||||
function generateSmartOverrideTemplate(useLightGBM: boolean, collectData: boolean, strategy: string, collectorSize: number): string {
|
function generateSmartOverrideTemplate(
|
||||||
|
useLightGBM: boolean,
|
||||||
|
collectData: boolean,
|
||||||
|
strategy: string,
|
||||||
|
collectorSize: number
|
||||||
|
): string {
|
||||||
return `
|
return `
|
||||||
// 配置会在启用 Smart 内核时自动应用
|
// 配置会在启用 Smart 内核时自动应用
|
||||||
|
|
||||||
|
|||||||
@ -30,9 +30,10 @@ function processRulesWithOffset(ruleStrings: string[], currentRules: string[], i
|
|||||||
const normalRules: string[] = []
|
const normalRules: string[] = []
|
||||||
let rules = [...currentRules]
|
let rules = [...currentRules]
|
||||||
|
|
||||||
ruleStrings.forEach(ruleStr => {
|
ruleStrings.forEach((ruleStr) => {
|
||||||
const parts = ruleStr.split(',')
|
const parts = ruleStr.split(',')
|
||||||
const firstPartIsNumber = !isNaN(Number(parts[0])) && parts[0].trim() !== '' && parts.length >= 3
|
const firstPartIsNumber =
|
||||||
|
!isNaN(Number(parts[0])) && parts[0].trim() !== '' && parts.length >= 3
|
||||||
|
|
||||||
if (firstPartIsNumber) {
|
if (firstPartIsNumber) {
|
||||||
const offset = parseInt(parts[0])
|
const offset = parseInt(parts[0])
|
||||||
@ -58,7 +59,12 @@ function processRulesWithOffset(ruleStrings: string[], currentRules: string[], i
|
|||||||
export async function generateProfile(): Promise<void> {
|
export async function generateProfile(): Promise<void> {
|
||||||
// 读取最新的配置
|
// 读取最新的配置
|
||||||
const { current } = await getProfileConfig(true)
|
const { current } = await getProfileConfig(true)
|
||||||
const { diffWorkDir = false, controlDns = true, controlSniff = true, useNameserverPolicy } = await getAppConfig()
|
const {
|
||||||
|
diffWorkDir = false,
|
||||||
|
controlDns = true,
|
||||||
|
controlSniff = true,
|
||||||
|
useNameserverPolicy
|
||||||
|
} = await getAppConfig()
|
||||||
let currentProfile = await overrideProfile(current, await getProfile(current))
|
let currentProfile = await overrideProfile(current, await getProfile(current))
|
||||||
let controledMihomoConfig = await getControledMihomoConfig()
|
let controledMihomoConfig = await getControledMihomoConfig()
|
||||||
|
|
||||||
@ -80,7 +86,11 @@ export async function generateProfile(): Promise<void> {
|
|||||||
const ruleFilePath = rulePath(current || 'default')
|
const ruleFilePath = rulePath(current || 'default')
|
||||||
if (existsSync(ruleFilePath)) {
|
if (existsSync(ruleFilePath)) {
|
||||||
const ruleFileContent = await readFile(ruleFilePath, 'utf-8')
|
const ruleFileContent = await readFile(ruleFilePath, 'utf-8')
|
||||||
const ruleData = parse(ruleFileContent) as { prepend?: string[], append?: string[], delete?: string[] } | null
|
const ruleData = parse(ruleFileContent) as {
|
||||||
|
prepend?: string[]
|
||||||
|
append?: string[]
|
||||||
|
delete?: string[]
|
||||||
|
} | null
|
||||||
|
|
||||||
if (ruleData && typeof ruleData === 'object') {
|
if (ruleData && typeof ruleData === 'object') {
|
||||||
// 确保 rules 数组存在
|
// 确保 rules 数组存在
|
||||||
@ -92,20 +102,27 @@ export async function generateProfile(): Promise<void> {
|
|||||||
|
|
||||||
// 处理前置规则
|
// 处理前置规则
|
||||||
if (ruleData.prepend?.length) {
|
if (ruleData.prepend?.length) {
|
||||||
const { normalRules: prependRules, insertRules } = processRulesWithOffset(ruleData.prepend, rules)
|
const { normalRules: prependRules, insertRules } = processRulesWithOffset(
|
||||||
|
ruleData.prepend,
|
||||||
|
rules
|
||||||
|
)
|
||||||
rules = [...prependRules, ...insertRules]
|
rules = [...prependRules, ...insertRules]
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理后置规则
|
// 处理后置规则
|
||||||
if (ruleData.append?.length) {
|
if (ruleData.append?.length) {
|
||||||
const { normalRules: appendRules, insertRules } = processRulesWithOffset(ruleData.append, rules, true)
|
const { normalRules: appendRules, insertRules } = processRulesWithOffset(
|
||||||
|
ruleData.append,
|
||||||
|
rules,
|
||||||
|
true
|
||||||
|
)
|
||||||
rules = [...insertRules, ...appendRules]
|
rules = [...insertRules, ...appendRules]
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理删除规则
|
// 处理删除规则
|
||||||
if (ruleData.delete?.length) {
|
if (ruleData.delete?.length) {
|
||||||
const deleteSet = new Set(ruleData.delete)
|
const deleteSet = new Set(ruleData.delete)
|
||||||
rules = rules.filter(rule => {
|
rules = rules.filter((rule) => {
|
||||||
const ruleStr = Array.isArray(rule) ? rule.join(',') : rule
|
const ruleStr = Array.isArray(rule) ? rule.join(',') : rule
|
||||||
return !deleteSet.has(ruleStr)
|
return !deleteSet.has(ruleStr)
|
||||||
})
|
})
|
||||||
|
|||||||
@ -46,15 +46,14 @@ import { managerLogger } from '../utils/logger'
|
|||||||
|
|
||||||
// 内核名称白名单
|
// 内核名称白名单
|
||||||
const ALLOWED_CORES = ['mihomo', 'mihomo-alpha', 'mihomo-smart'] as const
|
const ALLOWED_CORES = ['mihomo', 'mihomo-alpha', 'mihomo-smart'] as const
|
||||||
type AllowedCore = typeof ALLOWED_CORES[number]
|
type AllowedCore = (typeof ALLOWED_CORES)[number]
|
||||||
|
|
||||||
function isValidCoreName(core: string): core is AllowedCore {
|
function isValidCoreName(core: string): core is AllowedCore {
|
||||||
return ALLOWED_CORES.includes(core as AllowedCore)
|
return ALLOWED_CORES.includes(core as AllowedCore)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 路径检查
|
// 路径检查
|
||||||
function validateCorePath(corePath: string): void {
|
function validateCorePath(corePath: string): void {
|
||||||
|
|
||||||
if (corePath.includes('..')) {
|
if (corePath.includes('..')) {
|
||||||
throw new Error('Invalid core path: directory traversal detected')
|
throw new Error('Invalid core path: directory traversal detected')
|
||||||
}
|
}
|
||||||
@ -176,7 +175,9 @@ export async function startCore(detached = false): Promise<Promise<void>[]> {
|
|||||||
os.setPriority(child.pid, os.constants.priority[mihomoCpuPriority])
|
os.setPriority(child.pid, os.constants.priority[mihomoCpuPriority])
|
||||||
}
|
}
|
||||||
if (detached) {
|
if (detached) {
|
||||||
await managerLogger.info(`Core process detached successfully on ${process.platform}, PID: ${child.pid}`)
|
await managerLogger.info(
|
||||||
|
`Core process detached successfully on ${process.platform}, PID: ${child.pid}`
|
||||||
|
)
|
||||||
child.unref()
|
child.unref()
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
resolve([new Promise(() => {})])
|
resolve([new Promise(() => {})])
|
||||||
@ -210,7 +211,8 @@ export async function startCore(detached = false): Promise<Promise<void>[]> {
|
|||||||
reject(i18next.t('tun.error.tunPermissionDenied'))
|
reject(i18next.t('tun.error.tunPermissionDenied'))
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((process.platform !== 'win32' && str.includes('External controller unix listen error')) ||
|
if (
|
||||||
|
(process.platform !== 'win32' && str.includes('External controller unix listen error')) ||
|
||||||
(process.platform === 'win32' && str.includes('External controller pipe listen error'))
|
(process.platform === 'win32' && str.includes('External controller pipe listen error'))
|
||||||
) {
|
) {
|
||||||
await managerLogger.error('External controller listen error detected:', str)
|
await managerLogger.error('External controller listen error detected:', str)
|
||||||
@ -219,7 +221,7 @@ export async function startCore(detached = false): Promise<Promise<void>[]> {
|
|||||||
await managerLogger.info('Attempting Windows pipe cleanup and retry...')
|
await managerLogger.info('Attempting Windows pipe cleanup and retry...')
|
||||||
try {
|
try {
|
||||||
await cleanupWindowsNamedPipes()
|
await cleanupWindowsNamedPipes()
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
await new Promise((resolve) => setTimeout(resolve, 2000))
|
||||||
} catch (cleanupError) {
|
} catch (cleanupError) {
|
||||||
await managerLogger.error('Pipe cleanup failed:', cleanupError)
|
await managerLogger.error('Pipe cleanup failed:', cleanupError)
|
||||||
}
|
}
|
||||||
@ -235,7 +237,9 @@ export async function startCore(detached = false): Promise<Promise<void>[]> {
|
|||||||
resolve([
|
resolve([
|
||||||
new Promise((resolve) => {
|
new Promise((resolve) => {
|
||||||
child.stdout?.on('data', async (data) => {
|
child.stdout?.on('data', async (data) => {
|
||||||
if (data.toString().toLowerCase().includes('start initial compatible provider default')) {
|
if (
|
||||||
|
data.toString().toLowerCase().includes('start initial compatible provider default')
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
mainWindow?.webContents.send('groupsUpdated')
|
mainWindow?.webContents.send('groupsUpdated')
|
||||||
mainWindow?.webContents.send('rulesUpdated')
|
mainWindow?.webContents.send('rulesUpdated')
|
||||||
@ -337,7 +341,7 @@ async function cleanupWindowsNamedPipes(): Promise<void> {
|
|||||||
await managerLogger.warn('Failed to parse process list JSON:', parseError)
|
await managerLogger.warn('Failed to parse process list JSON:', parseError)
|
||||||
|
|
||||||
// 回退到文本解析
|
// 回退到文本解析
|
||||||
const lines = stdout.split('\n').filter(line => line.includes('mihomo'))
|
const lines = stdout.split('\n').filter((line) => line.includes('mihomo'))
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const match = line.match(/(\d+)/)
|
const match = line.match(/(\d+)/)
|
||||||
if (match) {
|
if (match) {
|
||||||
@ -361,8 +365,7 @@ async function cleanupWindowsNamedPipes(): Promise<void> {
|
|||||||
await managerLogger.warn('Failed to check mihomo processes:', error)
|
await managerLogger.warn('Failed to check mihomo processes:', error)
|
||||||
}
|
}
|
||||||
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await managerLogger.error('Windows named pipe cleanup failed:', error)
|
await managerLogger.error('Windows named pipe cleanup failed:', error)
|
||||||
}
|
}
|
||||||
@ -451,10 +454,7 @@ export async function quitWithoutCore(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function checkProfile(): Promise<void> {
|
async function checkProfile(): Promise<void> {
|
||||||
const {
|
const { core = 'mihomo', diffWorkDir = false } = await getAppConfig()
|
||||||
core = 'mihomo',
|
|
||||||
diffWorkDir = false
|
|
||||||
} = await getAppConfig()
|
|
||||||
const { current } = await getProfileConfig()
|
const { current } = await getProfileConfig()
|
||||||
const corePath = mihomoCorePath(core)
|
const corePath = mihomoCorePath(core)
|
||||||
const execFilePromise = promisify(execFile)
|
const execFilePromise = promisify(execFile)
|
||||||
@ -484,13 +484,15 @@ async function checkProfile(): Promise<void> {
|
|||||||
}
|
}
|
||||||
return line.trim()
|
return line.trim()
|
||||||
})
|
})
|
||||||
.filter(line => line.length > 0)
|
.filter((line) => line.length > 0)
|
||||||
|
|
||||||
if (errorLines.length === 0) {
|
if (errorLines.length === 0) {
|
||||||
const allLines = stdout.split('\n').filter(line => line.trim().length > 0)
|
const allLines = stdout.split('\n').filter((line) => line.trim().length > 0)
|
||||||
throw new Error(`${i18next.t('mihomo.error.profileCheckFailed')}:\n${allLines.join('\n')}`)
|
throw new Error(`${i18next.t('mihomo.error.profileCheckFailed')}:\n${allLines.join('\n')}`)
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`${i18next.t('mihomo.error.profileCheckFailed')}:\n${errorLines.join('\n')}`)
|
throw new Error(
|
||||||
|
`${i18next.t('mihomo.error.profileCheckFailed')}:\n${errorLines.join('\n')}`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`${i18next.t('mihomo.error.profileCheckFailed')}: ${error}`)
|
throw new Error(`${i18next.t('mihomo.error.profileCheckFailed')}: ${error}`)
|
||||||
@ -570,7 +572,7 @@ async function waitForCoreReady(): Promise<void> {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await new Promise(resolve => setTimeout(resolve, retryInterval))
|
await new Promise((resolve) => setTimeout(resolve, retryInterval))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -598,7 +600,9 @@ export async function checkAdminPrivileges(): Promise<boolean> {
|
|||||||
return true
|
return true
|
||||||
} catch (netSessionError: any) {
|
} catch (netSessionError: any) {
|
||||||
const netErrorCode = netSessionError?.code || 0
|
const netErrorCode = netSessionError?.code || 0
|
||||||
await managerLogger.debug(`Both fltmc and net session failed, no admin privileges. Error codes: fltmc=${errorCode}, net=${netErrorCode}`)
|
await managerLogger.debug(
|
||||||
|
`Both fltmc and net session failed, no admin privileges. Error codes: fltmc=${errorCode}, net=${netErrorCode}`
|
||||||
|
)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -613,11 +617,14 @@ export async function showTunPermissionDialog(): Promise<boolean> {
|
|||||||
await managerLogger.info(`i18next available: ${typeof i18next.t === 'function'}`)
|
await managerLogger.info(`i18next available: ${typeof i18next.t === 'function'}`)
|
||||||
|
|
||||||
const title = i18next.t('tun.permissions.title') || '需要管理员权限'
|
const title = i18next.t('tun.permissions.title') || '需要管理员权限'
|
||||||
const message = i18next.t('tun.permissions.message') || '启用TUN模式需要管理员权限,是否现在重启应用获取权限?'
|
const message =
|
||||||
|
i18next.t('tun.permissions.message') || '启用TUN模式需要管理员权限,是否现在重启应用获取权限?'
|
||||||
const confirmText = i18next.t('common.confirm') || '确认'
|
const confirmText = i18next.t('common.confirm') || '确认'
|
||||||
const cancelText = i18next.t('common.cancel') || '取消'
|
const cancelText = i18next.t('common.cancel') || '取消'
|
||||||
|
|
||||||
await managerLogger.info(`Dialog texts - Title: "${title}", Message: "${message}", Confirm: "${confirmText}", Cancel: "${cancelText}"`)
|
await managerLogger.info(
|
||||||
|
`Dialog texts - Title: "${title}", Message: "${message}", Confirm: "${confirmText}", Cancel: "${cancelText}"`
|
||||||
|
)
|
||||||
|
|
||||||
const choice = dialog.showMessageBoxSync({
|
const choice = dialog.showMessageBoxSync({
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
@ -759,8 +766,11 @@ async function checkHighPrivilegeMihomoProcess(): Promise<boolean> {
|
|||||||
|
|
||||||
for (const executable of mihomoExecutables) {
|
for (const executable of mihomoExecutables) {
|
||||||
try {
|
try {
|
||||||
const { stdout } = await execPromise(`chcp 65001 >nul 2>&1 && tasklist /FI "IMAGENAME eq ${executable}" /FO CSV`, { encoding: 'utf8' })
|
const { stdout } = await execPromise(
|
||||||
const lines = stdout.split('\n').filter(line => line.includes(executable))
|
`chcp 65001 >nul 2>&1 && tasklist /FI "IMAGENAME eq ${executable}" /FO CSV`,
|
||||||
|
{ encoding: 'utf8' }
|
||||||
|
)
|
||||||
|
const lines = stdout.split('\n').filter((line) => line.includes(executable))
|
||||||
|
|
||||||
if (lines.length > 0) {
|
if (lines.length > 0) {
|
||||||
await managerLogger.info(`Found ${lines.length} ${executable} processes running`)
|
await managerLogger.info(`Found ${lines.length} ${executable} processes running`)
|
||||||
@ -781,7 +791,9 @@ async function checkHighPrivilegeMihomoProcess(): Promise<boolean> {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await managerLogger.info(`Cannot get info for process ${pid}, might be high privilege`)
|
await managerLogger.info(
|
||||||
|
`Cannot get info for process ${pid}, might be high privilege`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -802,7 +814,9 @@ async function checkHighPrivilegeMihomoProcess(): Promise<boolean> {
|
|||||||
for (const executable of mihomoExecutables) {
|
for (const executable of mihomoExecutables) {
|
||||||
try {
|
try {
|
||||||
const { stdout } = await execPromise(`ps aux | grep ${executable} | grep -v grep`)
|
const { stdout } = await execPromise(`ps aux | grep ${executable} | grep -v grep`)
|
||||||
const lines = stdout.split('\n').filter(line => line.trim() && line.includes(executable))
|
const lines = stdout
|
||||||
|
.split('\n')
|
||||||
|
.filter((line) => line.trim() && line.includes(executable))
|
||||||
|
|
||||||
if (lines.length > 0) {
|
if (lines.length > 0) {
|
||||||
foundProcesses = true
|
foundProcesses = true
|
||||||
@ -820,8 +834,7 @@ async function checkHighPrivilegeMihomoProcess(): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!foundProcesses) {
|
if (!foundProcesses) {
|
||||||
|
|||||||
@ -195,7 +195,7 @@ export const mihomoUpgradeConfig = async (): Promise<void> => {
|
|||||||
const instance = await getAxios()
|
const instance = await getAxios()
|
||||||
console.log('[mihomoApi] axios instance obtained')
|
console.log('[mihomoApi] axios instance obtained')
|
||||||
const { diffWorkDir = false } = await getAppConfig()
|
const { diffWorkDir = false } = await getAppConfig()
|
||||||
const { current } = await import('../config').then(mod => mod.getProfileConfig(true))
|
const { current } = await import('../config').then((mod) => mod.getProfileConfig(true))
|
||||||
const { mihomoWorkConfigPath } = await import('../utils/dirs')
|
const { mihomoWorkConfigPath } = await import('../utils/dirs')
|
||||||
const configPath = diffWorkDir ? mihomoWorkConfigPath(current) : mihomoWorkConfigPath('work')
|
const configPath = diffWorkDir ? mihomoWorkConfigPath(current) : mihomoWorkConfigPath('work')
|
||||||
console.log('[mihomoApi] config path:', configPath)
|
console.log('[mihomoApi] config path:', configPath)
|
||||||
@ -441,7 +441,10 @@ export const TunStatus = async (): Promise<boolean> => {
|
|||||||
return config?.tun?.enable === true
|
return config?.tun?.enable === true
|
||||||
}
|
}
|
||||||
|
|
||||||
export function calculateTrayIconStatus(sysProxyEnabled: boolean, tunEnabled: boolean): 'white' | 'blue' | 'green' | 'red' {
|
export function calculateTrayIconStatus(
|
||||||
|
sysProxyEnabled: boolean,
|
||||||
|
tunEnabled: boolean
|
||||||
|
): 'white' | 'blue' | 'green' | 'red' {
|
||||||
if (sysProxyEnabled && tunEnabled) {
|
if (sysProxyEnabled && tunEnabled) {
|
||||||
return 'red' // 系统代理 + TUN 同时启用(警告状态)
|
return 'red' // 系统代理 + TUN 同时启用(警告状态)
|
||||||
} else if (sysProxyEnabled) {
|
} else if (sysProxyEnabled) {
|
||||||
|
|||||||
@ -85,7 +85,7 @@ export async function addProfileUpdater(item: IProfileItem): Promise<void> {
|
|||||||
if (item.type === 'remote' && item.autoUpdate && item.interval) {
|
if (item.type === 'remote' && item.autoUpdate && item.interval) {
|
||||||
if (intervalPool[item.id]) {
|
if (intervalPool[item.id]) {
|
||||||
if (intervalPool[item.id] instanceof Cron) {
|
if (intervalPool[item.id] instanceof Cron) {
|
||||||
(intervalPool[item.id] as Cron).stop()
|
;(intervalPool[item.id] as Cron).stop()
|
||||||
} else {
|
} else {
|
||||||
clearInterval(intervalPool[item.id] as NodeJS.Timeout)
|
clearInterval(intervalPool[item.id] as NodeJS.Timeout)
|
||||||
}
|
}
|
||||||
@ -117,7 +117,7 @@ export async function addProfileUpdater(item: IProfileItem): Promise<void> {
|
|||||||
export async function removeProfileUpdater(id: string): Promise<void> {
|
export async function removeProfileUpdater(id: string): Promise<void> {
|
||||||
if (intervalPool[id]) {
|
if (intervalPool[id]) {
|
||||||
if (intervalPool[id] instanceof Cron) {
|
if (intervalPool[id] instanceof Cron) {
|
||||||
(intervalPool[id] as Cron).stop()
|
;(intervalPool[id] as Cron).stop()
|
||||||
} else {
|
} else {
|
||||||
clearInterval(intervalPool[id] as NodeJS.Timeout)
|
clearInterval(intervalPool[id] as NodeJS.Timeout)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,15 @@ import { registerIpcMainHandlers } from './utils/ipc'
|
|||||||
import windowStateKeeper from 'electron-window-state'
|
import windowStateKeeper from 'electron-window-state'
|
||||||
import { app, shell, BrowserWindow, Menu, dialog, Notification, powerMonitor } from 'electron'
|
import { app, shell, BrowserWindow, Menu, dialog, Notification, powerMonitor } from 'electron'
|
||||||
import { addProfileItem, getAppConfig, patchAppConfig } from './config'
|
import { addProfileItem, getAppConfig, patchAppConfig } from './config'
|
||||||
import { quitWithoutCore, startCore, stopCore, checkAdminRestartForTun, checkHighPrivilegeCore, restartAsAdmin, initAdminStatus } from './core/manager'
|
import {
|
||||||
|
quitWithoutCore,
|
||||||
|
startCore,
|
||||||
|
stopCore,
|
||||||
|
checkAdminRestartForTun,
|
||||||
|
checkHighPrivilegeCore,
|
||||||
|
restartAsAdmin,
|
||||||
|
initAdminStatus
|
||||||
|
} from './core/manager'
|
||||||
import { triggerSysProxy } from './sys/sysproxy'
|
import { triggerSysProxy } from './sys/sysproxy'
|
||||||
import icon from '../../resources/icon.png?asset'
|
import icon from '../../resources/icon.png?asset'
|
||||||
import { createTray, hideDockIcon, showDockIcon } from './resolve/tray'
|
import { createTray, hideDockIcon, showDockIcon } from './resolve/tray'
|
||||||
@ -23,7 +31,6 @@ import i18next from 'i18next'
|
|||||||
import { logger } from './utils/logger'
|
import { logger } from './utils/logger'
|
||||||
import { initWebdavBackupScheduler } from './resolve/backup'
|
import { initWebdavBackupScheduler } from './resolve/backup'
|
||||||
|
|
||||||
|
|
||||||
async function fixUserDataPermissions(): Promise<void> {
|
async function fixUserDataPermissions(): Promise<void> {
|
||||||
if (process.platform !== 'darwin') return
|
if (process.platform !== 'darwin') return
|
||||||
|
|
||||||
@ -60,11 +67,10 @@ async function initApp(): Promise<void> {
|
|||||||
await fixUserDataPermissions()
|
await fixUserDataPermissions()
|
||||||
}
|
}
|
||||||
|
|
||||||
initApp()
|
initApp().catch((e) => {
|
||||||
.catch((e) => {
|
|
||||||
safeShowErrorBox('common.error.initFailed', `${e}`)
|
safeShowErrorBox('common.error.initFailed', `${e}`)
|
||||||
app.quit()
|
app.quit()
|
||||||
})
|
})
|
||||||
|
|
||||||
export function customRelaunch(): void {
|
export function customRelaunch(): void {
|
||||||
const script = `while kill -0 ${process.pid} 2>/dev/null; do
|
const script = `while kill -0 ${process.pid} 2>/dev/null; do
|
||||||
@ -111,7 +117,8 @@ async function checkHighPrivilegeCoreEarly(): Promise<void> {
|
|||||||
if (hasHighPrivilegeCore) {
|
if (hasHighPrivilegeCore) {
|
||||||
try {
|
try {
|
||||||
const appConfig = await getAppConfig()
|
const appConfig = await getAppConfig()
|
||||||
const language = appConfig.language || (app.getLocale().startsWith('zh') ? 'zh-CN' : 'en-US')
|
const language =
|
||||||
|
appConfig.language || (app.getLocale().startsWith('zh') ? 'zh-CN' : 'en-US')
|
||||||
await initI18n({ lng: language })
|
await initI18n({ lng: language })
|
||||||
} catch {
|
} catch {
|
||||||
await initI18n({ lng: 'zh-CN' })
|
await initI18n({ lng: 'zh-CN' })
|
||||||
|
|||||||
@ -131,9 +131,13 @@ export async function downloadAndInstallUpdate(version: string): Promise<void> {
|
|||||||
await appLogger.info('Opened installer with shell.openPath as fallback')
|
await appLogger.info('Opened installer with shell.openPath as fallback')
|
||||||
} catch (fallbackError) {
|
} catch (fallbackError) {
|
||||||
await appLogger.error('Fallback method also failed', fallbackError)
|
await appLogger.error('Fallback method also failed', fallbackError)
|
||||||
const installerErrorMessage = installerError instanceof Error ? installerError.message : String(installerError)
|
const installerErrorMessage =
|
||||||
const fallbackErrorMessage = fallbackError instanceof Error ? fallbackError.message : String(fallbackError)
|
installerError instanceof Error ? installerError.message : String(installerError)
|
||||||
throw new Error(`Failed to execute installer: ${installerErrorMessage}. Fallback also failed: ${fallbackErrorMessage}`)
|
const fallbackErrorMessage =
|
||||||
|
fallbackError instanceof Error ? fallbackError.message : String(fallbackError)
|
||||||
|
throw new Error(
|
||||||
|
`Failed to execute installer: ${installerErrorMessage}. Fallback also failed: ${fallbackErrorMessage}`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,9 +19,8 @@ async function createFloatingWindow(): Promise<void> {
|
|||||||
const { customTheme = 'default.css', floatingWindowCompatMode = true } = await getAppConfig()
|
const { customTheme = 'default.css', floatingWindowCompatMode = true } = await getAppConfig()
|
||||||
|
|
||||||
const safeMode = process.env.FLOATING_SAFE_MODE === 'true'
|
const safeMode = process.env.FLOATING_SAFE_MODE === 'true'
|
||||||
const useCompatMode = floatingWindowCompatMode ||
|
const useCompatMode =
|
||||||
process.env.FLOATING_COMPAT_MODE === 'true' ||
|
floatingWindowCompatMode || process.env.FLOATING_COMPAT_MODE === 'true' || safeMode
|
||||||
safeMode
|
|
||||||
|
|
||||||
const windowOptions: Electron.BrowserWindowConstructorOptions = {
|
const windowOptions: Electron.BrowserWindowConstructorOptions = {
|
||||||
width: 120,
|
width: 120,
|
||||||
@ -38,7 +37,7 @@ async function createFloatingWindow(): Promise<void> {
|
|||||||
maximizable: safeMode,
|
maximizable: safeMode,
|
||||||
fullscreenable: false,
|
fullscreenable: false,
|
||||||
closable: safeMode,
|
closable: safeMode,
|
||||||
backgroundColor: safeMode ? '#ffffff' : (useCompatMode ? '#f0f0f0' : '#00000000'),
|
backgroundColor: safeMode ? '#ffffff' : useCompatMode ? '#f0f0f0' : '#00000000',
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: join(__dirname, '../preload/index.js'),
|
preload: join(__dirname, '../preload/index.js'),
|
||||||
spellcheck: false,
|
spellcheck: false,
|
||||||
@ -81,7 +80,8 @@ async function createFloatingWindow(): Promise<void> {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 加载页面
|
// 加载页面
|
||||||
const url = is.dev && process.env['ELECTRON_RENDERER_URL']
|
const url =
|
||||||
|
is.dev && process.env['ELECTRON_RENDERER_URL']
|
||||||
? `${process.env['ELECTRON_RENDERER_URL']}/floating.html`
|
? `${process.env['ELECTRON_RENDERER_URL']}/floating.html`
|
||||||
: join(__dirname, '../renderer/floating.html')
|
: join(__dirname, '../renderer/floating.html')
|
||||||
|
|
||||||
|
|||||||
@ -83,9 +83,7 @@ export async function getGistUrl(): Promise<string> {
|
|||||||
} else {
|
} else {
|
||||||
await uploadRuntimeConfig()
|
await uploadRuntimeConfig()
|
||||||
const gists = await listGists(githubToken)
|
const gists = await listGists(githubToken)
|
||||||
const gist = gists.find(
|
const gist = gists.find((gist) => gist.description === 'Auto Synced Clash Party Runtime Config')
|
||||||
(gist) => gist.description === 'Auto Synced Clash Party Runtime Config'
|
|
||||||
)
|
|
||||||
if (!gist) throw new Error('Gist not found')
|
if (!gist) throw new Error('Gist not found')
|
||||||
return gist.html_url
|
return gist.html_url
|
||||||
}
|
}
|
||||||
|
|||||||
@ -44,7 +44,11 @@ export async function registerShortcut(
|
|||||||
await triggerSysProxy(!enable)
|
await triggerSysProxy(!enable)
|
||||||
await patchAppConfig({ sysProxy: { enable: !enable } })
|
await patchAppConfig({ sysProxy: { enable: !enable } })
|
||||||
new Notification({
|
new Notification({
|
||||||
title: i18next.t(!enable ? 'common.notification.systemProxyEnabled' : 'common.notification.systemProxyDisabled')
|
title: i18next.t(
|
||||||
|
!enable
|
||||||
|
? 'common.notification.systemProxyEnabled'
|
||||||
|
: 'common.notification.systemProxyDisabled'
|
||||||
|
)
|
||||||
}).show()
|
}).show()
|
||||||
mainWindow?.webContents.send('appConfigUpdated')
|
mainWindow?.webContents.send('appConfigUpdated')
|
||||||
floatingWindow?.webContents.send('appConfigUpdated')
|
floatingWindow?.webContents.send('appConfigUpdated')
|
||||||
@ -68,7 +72,9 @@ export async function registerShortcut(
|
|||||||
}
|
}
|
||||||
await restartCore()
|
await restartCore()
|
||||||
new Notification({
|
new Notification({
|
||||||
title: i18next.t(!enable ? 'common.notification.tunEnabled' : 'common.notification.tunDisabled')
|
title: i18next.t(
|
||||||
|
!enable ? 'common.notification.tunEnabled' : 'common.notification.tunDisabled'
|
||||||
|
)
|
||||||
}).show()
|
}).show()
|
||||||
mainWindow?.webContents.send('controledMihomoConfigUpdated')
|
mainWindow?.webContents.send('controledMihomoConfigUpdated')
|
||||||
floatingWindow?.webContents.send('appConfigUpdated')
|
floatingWindow?.webContents.send('appConfigUpdated')
|
||||||
|
|||||||
@ -15,12 +15,25 @@ import pngIconBlue from '../../../resources/icon_blue.png?asset'
|
|||||||
import pngIconRed from '../../../resources/icon_red.png?asset'
|
import pngIconRed from '../../../resources/icon_red.png?asset'
|
||||||
import pngIconGreen from '../../../resources/icon_green.png?asset'
|
import pngIconGreen from '../../../resources/icon_green.png?asset'
|
||||||
import templateIcon from '../../../resources/iconTemplate.png?asset'
|
import templateIcon from '../../../resources/iconTemplate.png?asset'
|
||||||
import { mihomoChangeProxy, mihomoCloseAllConnections, mihomoGroups, patchMihomoConfig, getTrayIconStatus, calculateTrayIconStatus } from '../core/mihomoApi'
|
import {
|
||||||
|
mihomoChangeProxy,
|
||||||
|
mihomoCloseAllConnections,
|
||||||
|
mihomoGroups,
|
||||||
|
patchMihomoConfig,
|
||||||
|
getTrayIconStatus,
|
||||||
|
calculateTrayIconStatus
|
||||||
|
} from '../core/mihomoApi'
|
||||||
import { mainWindow, showMainWindow, triggerMainWindow } from '..'
|
import { mainWindow, showMainWindow, triggerMainWindow } from '..'
|
||||||
import { app, clipboard, ipcMain, Menu, nativeImage, shell, Tray } from 'electron'
|
import { app, clipboard, ipcMain, Menu, nativeImage, shell, Tray } from 'electron'
|
||||||
import { dataDir, logDir, mihomoCoreDir, mihomoWorkDir } from '../utils/dirs'
|
import { dataDir, logDir, mihomoCoreDir, mihomoWorkDir } from '../utils/dirs'
|
||||||
import { triggerSysProxy } from '../sys/sysproxy'
|
import { triggerSysProxy } from '../sys/sysproxy'
|
||||||
import { quitWithoutCore, restartCore, checkMihomoCorePermissions, requestTunPermissions, restartAsAdmin } from '../core/manager'
|
import {
|
||||||
|
quitWithoutCore,
|
||||||
|
restartCore,
|
||||||
|
checkMihomoCorePermissions,
|
||||||
|
requestTunPermissions,
|
||||||
|
restartAsAdmin
|
||||||
|
} from '../core/manager'
|
||||||
import { floatingWindow, triggerFloatingWindow } from './floatingWindow'
|
import { floatingWindow, triggerFloatingWindow } from './floatingWindow'
|
||||||
import { t } from 'i18next'
|
import { t } from 'i18next'
|
||||||
import { trayLogger } from '../utils/logger'
|
import { trayLogger } from '../utils/logger'
|
||||||
@ -30,8 +43,14 @@ export let tray: Tray | null = null
|
|||||||
export const buildContextMenu = async (): Promise<Menu> => {
|
export const buildContextMenu = async (): Promise<Menu> => {
|
||||||
// 添加调试日志
|
// 添加调试日志
|
||||||
await trayLogger.debug('Current translation for tray.showWindow', t('tray.showWindow'))
|
await trayLogger.debug('Current translation for tray.showWindow', t('tray.showWindow'))
|
||||||
await trayLogger.debug('Current translation for tray.hideFloatingWindow', t('tray.hideFloatingWindow'))
|
await trayLogger.debug(
|
||||||
await trayLogger.debug('Current translation for tray.showFloatingWindow', t('tray.showFloatingWindow'))
|
'Current translation for tray.hideFloatingWindow',
|
||||||
|
t('tray.hideFloatingWindow')
|
||||||
|
)
|
||||||
|
await trayLogger.debug(
|
||||||
|
'Current translation for tray.showFloatingWindow',
|
||||||
|
t('tray.showFloatingWindow')
|
||||||
|
)
|
||||||
|
|
||||||
const { mode, tun } = await getControledMihomoConfig()
|
const { mode, tun } = await getControledMihomoConfig()
|
||||||
const {
|
const {
|
||||||
@ -55,7 +74,7 @@ export const buildContextMenu = async (): Promise<Menu> => {
|
|||||||
try {
|
try {
|
||||||
const groups = await mihomoGroups()
|
const groups = await mihomoGroups()
|
||||||
groupsMenu = groups.map((group) => {
|
groupsMenu = groups.map((group) => {
|
||||||
const groupLabel = showCurrentProxyInTray ? `${group.name} | ${group.now}` : group.name;
|
const groupLabel = showCurrentProxyInTray ? `${group.name} | ${group.now}` : group.name
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: group.name,
|
id: group.name,
|
||||||
@ -106,7 +125,9 @@ export const buildContextMenu = async (): Promise<Menu> => {
|
|||||||
{
|
{
|
||||||
id: 'show-floating',
|
id: 'show-floating',
|
||||||
accelerator: showFloatingWindowShortcut,
|
accelerator: showFloatingWindowShortcut,
|
||||||
label: floatingWindow?.isVisible() ? t('tray.hideFloatingWindow') : t('tray.showFloatingWindow'),
|
label: floatingWindow?.isVisible()
|
||||||
|
? t('tray.hideFloatingWindow')
|
||||||
|
: t('tray.showFloatingWindow'),
|
||||||
type: 'normal',
|
type: 'normal',
|
||||||
click: async (): Promise<void> => {
|
click: async (): Promise<void> => {
|
||||||
await triggerFloatingWindow()
|
await triggerFloatingWindow()
|
||||||
|
|||||||
@ -96,8 +96,7 @@ export async function enableAutoRun(): Promise<void> {
|
|||||||
await execPromise(
|
await execPromise(
|
||||||
`powershell -NoProfile -Command "Start-Process schtasks -Verb RunAs -ArgumentList '/create', '/tn', '${appName}', '/xml', '${taskFilePath}', '/f' -WindowStyle Hidden"`
|
`powershell -NoProfile -Command "Start-Process schtasks -Verb RunAs -ArgumentList '/create', '/tn', '${appName}', '/xml', '${taskFilePath}', '/f' -WindowStyle Hidden"`
|
||||||
)
|
)
|
||||||
}
|
} catch (e) {
|
||||||
catch (e) {
|
|
||||||
await managerLogger.info('Maybe the user rejected the UAC dialog?')
|
await managerLogger.info('Maybe the user rejected the UAC dialog?')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -77,8 +77,6 @@ export function setNativeTheme(theme: 'system' | 'light' | 'dark'): void {
|
|||||||
nativeTheme.themeSource = theme
|
nativeTheme.themeSource = theme
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export function resetAppConfig(): void {
|
export function resetAppConfig(): void {
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
spawn(
|
spawn(
|
||||||
|
|||||||
@ -174,7 +174,7 @@ async function requestSocketRecreation(): Promise<void> {
|
|||||||
await execPromise(`osascript -e '${command}'`)
|
await execPromise(`osascript -e '${command}'`)
|
||||||
|
|
||||||
// Wait a bit for socket recreation
|
// Wait a bit for socket recreation
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await proxyLogger.error('Failed to send signal to helper', error)
|
await proxyLogger.error('Failed to send signal to helper', error)
|
||||||
throw error
|
throw error
|
||||||
@ -192,13 +192,16 @@ async function helperRequest(requestFn: () => Promise<unknown>, maxRetries = 1):
|
|||||||
lastError = error as Error
|
lastError = error as Error
|
||||||
|
|
||||||
// Check if it's a connection error and socket file doesn't exist
|
// Check if it's a connection error and socket file doesn't exist
|
||||||
if (attempt < maxRetries &&
|
if (
|
||||||
|
attempt < maxRetries &&
|
||||||
((error as NodeJS.ErrnoException).code === 'ECONNREFUSED' ||
|
((error as NodeJS.ErrnoException).code === 'ECONNREFUSED' ||
|
||||||
(error as NodeJS.ErrnoException).code === 'ENOENT' ||
|
(error as NodeJS.ErrnoException).code === 'ENOENT' ||
|
||||||
(error as Error).message?.includes('connect ECONNREFUSED') ||
|
(error as Error).message?.includes('connect ECONNREFUSED') ||
|
||||||
(error as Error).message?.includes('ENOENT'))) {
|
(error as Error).message?.includes('ENOENT'))
|
||||||
|
) {
|
||||||
await proxyLogger.info(`Helper request failed (attempt ${attempt + 1}), checking socket file...`)
|
await proxyLogger.info(
|
||||||
|
`Helper request failed (attempt ${attempt + 1}), checking socket file...`
|
||||||
|
)
|
||||||
|
|
||||||
if (!isSocketFileExists()) {
|
if (!isSocketFileExists()) {
|
||||||
await proxyLogger.info('Socket file missing, requesting recreation...')
|
await proxyLogger.info('Socket file missing, requesting recreation...')
|
||||||
|
|||||||
@ -4,11 +4,13 @@ export interface RequestOptions {
|
|||||||
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
|
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
|
||||||
headers?: Record<string, string>
|
headers?: Record<string, string>
|
||||||
body?: string | Buffer
|
body?: string | Buffer
|
||||||
proxy?: {
|
proxy?:
|
||||||
|
| {
|
||||||
protocol: 'http' | 'https' | 'socks5'
|
protocol: 'http' | 'https' | 'socks5'
|
||||||
host: string
|
host: string
|
||||||
port: number
|
port: number
|
||||||
} | false
|
}
|
||||||
|
| false
|
||||||
timeout?: number
|
timeout?: number
|
||||||
responseType?: 'text' | 'json' | 'arraybuffer'
|
responseType?: 'text' | 'json' | 'arraybuffer'
|
||||||
followRedirect?: boolean
|
followRedirect?: boolean
|
||||||
|
|||||||
@ -48,7 +48,11 @@ const versionCache = new Map<string, VersionCache>()
|
|||||||
* @param forceRefresh 是否强制刷新缓存
|
* @param forceRefresh 是否强制刷新缓存
|
||||||
* @returns 标签列表
|
* @returns 标签列表
|
||||||
*/
|
*/
|
||||||
export async function getGitHubTags(owner: string, repo: string, forceRefresh = false): Promise<GitHubTag[]> {
|
export async function getGitHubTags(
|
||||||
|
owner: string,
|
||||||
|
repo: string,
|
||||||
|
forceRefresh = false
|
||||||
|
): Promise<GitHubTag[]> {
|
||||||
const cacheKey = `${owner}/${repo}`
|
const cacheKey = `${owner}/${repo}`
|
||||||
|
|
||||||
// 检查缓存
|
// 检查缓存
|
||||||
@ -172,7 +176,7 @@ export async function installMihomoCore(version: string): Promise<void> {
|
|||||||
console.log(`[GitHub] Extracting ZIP file ${tempZip}`)
|
console.log(`[GitHub] Extracting ZIP file ${tempZip}`)
|
||||||
const zip = new AdmZip(tempZip)
|
const zip = new AdmZip(tempZip)
|
||||||
const entries = zip.getEntries()
|
const entries = zip.getEntries()
|
||||||
const entry = entries.find(e => e.entryName.includes(exeFile))
|
const entry = entries.find((e) => e.entryName.includes(exeFile))
|
||||||
|
|
||||||
if (entry) {
|
if (entry) {
|
||||||
zip.extractEntryTo(entry, coreDir, false, true, false, targetFile)
|
zip.extractEntryTo(entry, coreDir, false, true, false, targetFile)
|
||||||
@ -216,6 +220,8 @@ export async function installMihomoCore(version: string): Promise<void> {
|
|||||||
console.log(`[GitHub] Successfully installed mihomo core version ${version}`)
|
console.log(`[GitHub] Successfully installed mihomo core version ${version}`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[GitHub] Failed to install mihomo core:', error)
|
console.error('[GitHub] Failed to install mihomo core:', error)
|
||||||
throw new Error(`Failed to install core: ${error instanceof Error ? error.message : String(error)}`)
|
throw new Error(
|
||||||
|
`Failed to install core: ${error instanceof Error ? error.message : String(error)}`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -84,7 +84,7 @@ async function fixDataDirPermissions(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 比较修改geodata文件修改时间
|
// 比较修改geodata文件修改时间
|
||||||
async function isSourceNewer(sourcePath: string, targetPath: string): Promise<boolean> {
|
async function isSourceNewer(sourcePath: string, targetPath: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const sourceStats = await stat(sourcePath)
|
const sourceStats = await stat(sourcePath)
|
||||||
@ -130,7 +130,11 @@ async function initConfig(): Promise<void> {
|
|||||||
{ path: profileConfigPath(), content: defaultProfileConfig, name: 'profile config' },
|
{ path: profileConfigPath(), content: defaultProfileConfig, name: 'profile config' },
|
||||||
{ path: overrideConfigPath(), content: defaultOverrideConfig, name: 'override config' },
|
{ path: overrideConfigPath(), content: defaultOverrideConfig, name: 'override config' },
|
||||||
{ path: profilePath('default'), content: defaultProfile, name: 'default profile' },
|
{ path: profilePath('default'), content: defaultProfile, name: 'default profile' },
|
||||||
{ path: controledMihomoConfigPath(), content: defaultControledMihomoConfig, name: 'mihomo config' }
|
{
|
||||||
|
path: controledMihomoConfigPath(),
|
||||||
|
content: defaultControledMihomoConfig,
|
||||||
|
name: 'mihomo config'
|
||||||
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
for (const config of configs) {
|
for (const config of configs) {
|
||||||
@ -154,13 +158,15 @@ async function initFiles(): Promise<void> {
|
|||||||
try {
|
try {
|
||||||
// 检查是否需要复制
|
// 检查是否需要复制
|
||||||
if (existsSync(sourcePath)) {
|
if (existsSync(sourcePath)) {
|
||||||
const shouldCopyToWork = !existsSync(targetPath) || await isSourceNewer(sourcePath, targetPath)
|
const shouldCopyToWork =
|
||||||
|
!existsSync(targetPath) || (await isSourceNewer(sourcePath, targetPath))
|
||||||
if (shouldCopyToWork) {
|
if (shouldCopyToWork) {
|
||||||
await cp(sourcePath, targetPath, { recursive: true })
|
await cp(sourcePath, targetPath, { recursive: true })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (existsSync(sourcePath)) {
|
if (existsSync(sourcePath)) {
|
||||||
const shouldCopyToTest = !existsSync(testTargetPath) || await isSourceNewer(sourcePath, testTargetPath)
|
const shouldCopyToTest =
|
||||||
|
!existsSync(testTargetPath) || (await isSourceNewer(sourcePath, testTargetPath))
|
||||||
if (shouldCopyToTest) {
|
if (shouldCopyToTest) {
|
||||||
await cp(sourcePath, testTargetPath, { recursive: true })
|
await cp(sourcePath, testTargetPath, { recursive: true })
|
||||||
}
|
}
|
||||||
@ -276,7 +282,7 @@ async function migration(): Promise<void> {
|
|||||||
if (!skipAuthPrefixes) {
|
if (!skipAuthPrefixes) {
|
||||||
await patchControledMihomoConfig({ 'skip-auth-prefixes': ['127.0.0.1/32', '::1/128'] })
|
await patchControledMihomoConfig({ 'skip-auth-prefixes': ['127.0.0.1/32', '::1/128'] })
|
||||||
} else if (skipAuthPrefixes.length >= 1 && skipAuthPrefixes[0] === '127.0.0.1/32') {
|
} else if (skipAuthPrefixes.length >= 1 && skipAuthPrefixes[0] === '127.0.0.1/32') {
|
||||||
const filteredPrefixes = skipAuthPrefixes.filter(ip => ip !== '::1/128')
|
const filteredPrefixes = skipAuthPrefixes.filter((ip) => ip !== '::1/128')
|
||||||
const newPrefixes = [filteredPrefixes[0], '::1/128', ...filteredPrefixes.slice(1)]
|
const newPrefixes = [filteredPrefixes[0], '::1/128', ...filteredPrefixes.slice(1)]
|
||||||
if (JSON.stringify(newPrefixes) !== JSON.stringify(skipAuthPrefixes)) {
|
if (JSON.stringify(newPrefixes) !== JSON.stringify(skipAuthPrefixes)) {
|
||||||
await patchControledMihomoConfig({ 'skip-auth-prefixes': newPrefixes })
|
await patchControledMihomoConfig({ 'skip-auth-prefixes': newPrefixes })
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { app, dialog, ipcMain } from 'electron'
|
import { app, ipcMain } from 'electron'
|
||||||
import {
|
import {
|
||||||
mihomoChangeProxy,
|
mihomoChangeProxy,
|
||||||
mihomoCloseAllConnections,
|
mihomoCloseAllConnections,
|
||||||
@ -86,9 +86,22 @@ import {
|
|||||||
setupFirewall
|
setupFirewall
|
||||||
} from '../sys/misc'
|
} from '../sys/misc'
|
||||||
import { getRuntimeConfig, getRuntimeConfigStr } from '../core/factory'
|
import { getRuntimeConfig, getRuntimeConfigStr } from '../core/factory'
|
||||||
import { listWebdavBackups, webdavBackup, webdavDelete, webdavRestore, exportLocalBackup, importLocalBackup } from '../resolve/backup'
|
import {
|
||||||
|
listWebdavBackups,
|
||||||
|
webdavBackup,
|
||||||
|
webdavDelete,
|
||||||
|
webdavRestore,
|
||||||
|
exportLocalBackup,
|
||||||
|
importLocalBackup
|
||||||
|
} from '../resolve/backup'
|
||||||
import { getInterfaces } from '../sys/interface'
|
import { getInterfaces } from '../sys/interface'
|
||||||
import { closeTrayIcon, copyEnv, showTrayIcon, updateTrayIcon, updateTrayIconImmediate } from '../resolve/tray'
|
import {
|
||||||
|
closeTrayIcon,
|
||||||
|
copyEnv,
|
||||||
|
showTrayIcon,
|
||||||
|
updateTrayIcon,
|
||||||
|
updateTrayIconImmediate
|
||||||
|
} from '../resolve/tray'
|
||||||
import { registerShortcut } from '../resolve/shortcut'
|
import { registerShortcut } from '../resolve/shortcut'
|
||||||
import { closeMainWindow, mainWindow, showMainWindow, triggerMainWindow } from '..'
|
import { closeMainWindow, mainWindow, showMainWindow, triggerMainWindow } from '..'
|
||||||
import {
|
import {
|
||||||
@ -135,7 +148,9 @@ function ipcErrorWrapper<T>( // eslint-disable-next-line @typescript-eslint/no-e
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GitHub版本管理相关IPC处理程序
|
// GitHub版本管理相关IPC处理程序
|
||||||
export async function fetchMihomoTags(forceRefresh = false): Promise<{name: string, zipball_url: string, tarball_url: string}[]> {
|
export async function fetchMihomoTags(
|
||||||
|
forceRefresh = false
|
||||||
|
): Promise<{ name: string; zipball_url: string; tarball_url: string }[]> {
|
||||||
return await getGitHubTags('MetaCubeX', 'mihomo', forceRefresh)
|
return await getGitHubTags('MetaCubeX', 'mihomo', forceRefresh)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -216,7 +231,9 @@ export function registerIpcMainHandlers(): void {
|
|||||||
ipcMain.handle('getProfileStr', (_e, id) => ipcErrorWrapper(getProfileStr)(id))
|
ipcMain.handle('getProfileStr', (_e, id) => ipcErrorWrapper(getProfileStr)(id))
|
||||||
ipcMain.handle('getFileStr', (_e, path) => ipcErrorWrapper(getFileStr)(path))
|
ipcMain.handle('getFileStr', (_e, path) => ipcErrorWrapper(getFileStr)(path))
|
||||||
ipcMain.handle('setFileStr', (_e, path, str) => ipcErrorWrapper(setFileStr)(path, str))
|
ipcMain.handle('setFileStr', (_e, path, str) => ipcErrorWrapper(setFileStr)(path, str))
|
||||||
ipcMain.handle('convertMrsRuleset', (_e, path, behavior) => ipcErrorWrapper(convertMrsRuleset)(path, behavior))
|
ipcMain.handle('convertMrsRuleset', (_e, path, behavior) =>
|
||||||
|
ipcErrorWrapper(convertMrsRuleset)(path, behavior)
|
||||||
|
)
|
||||||
ipcMain.handle('setProfileStr', (_e, id, str) => ipcErrorWrapper(setProfileStr)(id, str))
|
ipcMain.handle('setProfileStr', (_e, id, str) => ipcErrorWrapper(setProfileStr)(id, str))
|
||||||
ipcMain.handle('updateProfileItem', (_e, item) => ipcErrorWrapper(updateProfileItem)(item))
|
ipcMain.handle('updateProfileItem', (_e, item) => ipcErrorWrapper(updateProfileItem)(item))
|
||||||
ipcMain.handle('changeCurrentProfile', (_e, id) => ipcErrorWrapper(changeCurrentProfile)(id))
|
ipcMain.handle('changeCurrentProfile', (_e, id) => ipcErrorWrapper(changeCurrentProfile)(id))
|
||||||
@ -242,7 +259,9 @@ export function registerIpcMainHandlers(): void {
|
|||||||
ipcMain.handle('requestTunPermissions', () => ipcErrorWrapper(requestTunPermissions)())
|
ipcMain.handle('requestTunPermissions', () => ipcErrorWrapper(requestTunPermissions)())
|
||||||
ipcMain.handle('checkHighPrivilegeCore', () => ipcErrorWrapper(checkHighPrivilegeCore)())
|
ipcMain.handle('checkHighPrivilegeCore', () => ipcErrorWrapper(checkHighPrivilegeCore)())
|
||||||
ipcMain.handle('showTunPermissionDialog', () => ipcErrorWrapper(showTunPermissionDialog)())
|
ipcMain.handle('showTunPermissionDialog', () => ipcErrorWrapper(showTunPermissionDialog)())
|
||||||
ipcMain.handle('showErrorDialog', (_, title: string, message: string) => ipcErrorWrapper(showErrorDialog)(title, message))
|
ipcMain.handle('showErrorDialog', (_, title: string, message: string) =>
|
||||||
|
ipcErrorWrapper(showErrorDialog)(title, message)
|
||||||
|
)
|
||||||
|
|
||||||
ipcMain.handle('checkTunPermissions', () => ipcErrorWrapper(checkTunPermissions)())
|
ipcMain.handle('checkTunPermissions', () => ipcErrorWrapper(checkTunPermissions)())
|
||||||
ipcMain.handle('grantTunPermissions', () => ipcErrorWrapper(grantTunPermissions)())
|
ipcMain.handle('grantTunPermissions', () => ipcErrorWrapper(grantTunPermissions)())
|
||||||
@ -349,10 +368,14 @@ export function registerIpcMainHandlers(): void {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 注册获取Mihomo标签的IPC处理程序
|
// 注册获取Mihomo标签的IPC处理程序
|
||||||
ipcMain.handle('fetchMihomoTags', (_e, forceRefresh) => ipcErrorWrapper(fetchMihomoTags)(forceRefresh))
|
ipcMain.handle('fetchMihomoTags', (_e, forceRefresh) =>
|
||||||
|
ipcErrorWrapper(fetchMihomoTags)(forceRefresh)
|
||||||
|
)
|
||||||
|
|
||||||
// 注册安装特定版本Mihomo核心的IPC处理程序
|
// 注册安装特定版本Mihomo核心的IPC处理程序
|
||||||
ipcMain.handle('installSpecificMihomoCore', (_e, version) => ipcErrorWrapper(installSpecificMihomoCore)(version))
|
ipcMain.handle('installSpecificMihomoCore', (_e, version) =>
|
||||||
|
ipcErrorWrapper(installSpecificMihomoCore)(version)
|
||||||
|
)
|
||||||
|
|
||||||
// 注册清除版本缓存的IPC处理程序
|
// 注册清除版本缓存的IPC处理程序
|
||||||
ipcMain.handle('clearMihomoVersionCache', () => ipcErrorWrapper(clearMihomoVersionCache)())
|
ipcMain.handle('clearMihomoVersionCache', () => ipcErrorWrapper(clearMihomoVersionCache)())
|
||||||
|
|||||||
@ -28,7 +28,10 @@ class Logger {
|
|||||||
} catch (logError) {
|
} catch (logError) {
|
||||||
// 如果写入日志文件失败,仍然输出到控制台
|
// 如果写入日志文件失败,仍然输出到控制台
|
||||||
console.error(`[Logger] Failed to write to log file:`, logError)
|
console.error(`[Logger] Failed to write to log file:`, logError)
|
||||||
console.error(`[Logger] Original message: [${level.toUpperCase()}] [${this.moduleName}] ${message}`, error)
|
console.error(
|
||||||
|
`[Logger] Original message: [${level.toUpperCase()}] [${this.moduleName}] ${message}`,
|
||||||
|
error
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,9 @@
|
|||||||
@source '../../../../node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}';
|
@source '../../../../node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}';
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
--default-font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
--default-font-family:
|
||||||
|
system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell,
|
||||||
|
'Open Sans', 'Helvetica Neue', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
|
|||||||
@ -141,7 +141,7 @@ export const BaseEditor: React.FC<Props> = (props) => {
|
|||||||
}}
|
}}
|
||||||
editorWillMount={editorWillMount}
|
editorWillMount={editorWillMount}
|
||||||
editorDidMount={editorDidMount}
|
editorDidMount={editorDidMount}
|
||||||
editorWillUnmount={(): void => { }}
|
editorWillUnmount={(): void => {}}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -8,9 +8,7 @@ const ErrorFallback = ({ error }: FallbackProps): React.ReactElement => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<h2 className="my-2 text-lg font-bold">
|
<h2 className="my-2 text-lg font-bold">{t('common.error.appCrash')}</h2>
|
||||||
{t('common.error.appCrash')}
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@ -50,7 +50,8 @@ export const toast = {
|
|||||||
error: (message: string, title?: string): void => addToast('error', message, title, 1800),
|
error: (message: string, title?: string): void => addToast('error', message, title, 1800),
|
||||||
warning: (message: string, title?: string): void => addToast('warning', message, title),
|
warning: (message: string, title?: string): void => addToast('warning', message, title),
|
||||||
info: (message: string, title?: string): void => addToast('info', message, title),
|
info: (message: string, title?: string): void => addToast('info', message, title),
|
||||||
detailedError: (message: string, title?: string): void => addDetailedToast('error', message, title)
|
detailedError: (message: string, title?: string): void =>
|
||||||
|
addDetailedToast('error', message, title)
|
||||||
}
|
}
|
||||||
|
|
||||||
const ToastItem: React.FC<{
|
const ToastItem: React.FC<{
|
||||||
@ -119,7 +120,9 @@ const ToastItem: React.FC<{
|
|||||||
>
|
>
|
||||||
<div className="flex items-center justify-between overflow-visible">
|
<div className="flex items-center justify-between overflow-visible">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className={`flex-shrink-0 w-8 h-8 ${iconBg} rounded-full flex items-center justify-center`}>
|
<div
|
||||||
|
className={`flex-shrink-0 w-8 h-8 ${iconBg} rounded-full flex items-center justify-center`}
|
||||||
|
>
|
||||||
{icon}
|
{icon}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-base font-semibold text-foreground">{data.title || '错误'}</p>
|
<p className="text-base font-semibold text-foreground">{data.title || '错误'}</p>
|
||||||
@ -130,11 +133,18 @@ const ToastItem: React.FC<{
|
|||||||
className="p-2 rounded-lg hover:bg-default-200 transition-colors"
|
className="p-2 rounded-lg hover:bg-default-200 transition-colors"
|
||||||
>
|
>
|
||||||
<div className="relative w-4 h-4">
|
<div className="relative w-4 h-4">
|
||||||
<IoCopy className={`absolute inset-0 text-base text-foreground-500 transition-all duration-200 ${copied ? 'opacity-0 scale-50' : 'opacity-100 scale-100'}`} />
|
<IoCopy
|
||||||
<IoCheckmark className={`absolute inset-0 text-base text-success transition-all duration-200 ${copied ? 'opacity-100 scale-100' : 'opacity-0 scale-50'}`} />
|
className={`absolute inset-0 text-base text-foreground-500 transition-all duration-200 ${copied ? 'opacity-0 scale-50' : 'opacity-100 scale-100'}`}
|
||||||
|
/>
|
||||||
|
<IoCheckmark
|
||||||
|
className={`absolute inset-0 text-base text-success transition-all duration-200 ${copied ? 'opacity-100 scale-100' : 'opacity-0 scale-50'}`}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<div className={`absolute top-full mt-1 left-1/2 -translate-x-1/2 px-2 py-1 text-xs text-foreground bg-content2 border border-default-200 rounded shadow-md whitespace-nowrap transition-all duration-200 ${copied ? 'opacity-100 translate-y-0' : 'opacity-0 -translate-y-1 pointer-events-none'}`} style={{ zIndex: 99999 }}>
|
<div
|
||||||
|
className={`absolute top-full mt-1 left-1/2 -translate-x-1/2 px-2 py-1 text-xs text-foreground bg-content2 border border-default-200 rounded shadow-md whitespace-nowrap transition-all duration-200 ${copied ? 'opacity-100 translate-y-0' : 'opacity-0 -translate-y-1 pointer-events-none'}`}
|
||||||
|
style={{ zIndex: 99999 }}
|
||||||
|
>
|
||||||
已复制
|
已复制
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -166,16 +176,14 @@ const ToastItem: React.FC<{
|
|||||||
`}
|
`}
|
||||||
style={{ width: 340 }}
|
style={{ width: 340 }}
|
||||||
>
|
>
|
||||||
<div className={`flex-shrink-0 w-7 h-7 ${iconBg} rounded-full flex items-center justify-center`}>
|
<div
|
||||||
|
className={`flex-shrink-0 w-7 h-7 ${iconBg} rounded-full flex items-center justify-center`}
|
||||||
|
>
|
||||||
{icon}
|
{icon}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
{data.title && (
|
{data.title && <p className="text-sm font-medium text-foreground">{data.title}</p>}
|
||||||
<p className="text-sm font-medium text-foreground">{data.title}</p>
|
<p className="text-sm text-foreground-500 break-words select-text">{data.message}</p>
|
||||||
)}
|
|
||||||
<p className="text-sm text-foreground-500 break-words select-text">
|
|
||||||
{data.message}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={handleClose}
|
onClick={handleClose}
|
||||||
|
|||||||
@ -41,7 +41,8 @@ const CopyableSettingItem: React.FC<{
|
|||||||
const menuItems = [
|
const menuItems = [
|
||||||
{ key: 'raw', text: displayName || (Array.isArray(value) ? value.join(', ') : value) },
|
{ key: 'raw', text: displayName || (Array.isArray(value) ? value.join(', ') : value) },
|
||||||
...(Array.isArray(value)
|
...(Array.isArray(value)
|
||||||
? value.map((v, i) => {
|
? value
|
||||||
|
.map((v, i) => {
|
||||||
const p = prefix[i]
|
const p = prefix[i]
|
||||||
if (!p || !v) return null
|
if (!p || !v) return null
|
||||||
|
|
||||||
@ -59,13 +60,17 @@ const CopyableSettingItem: React.FC<{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const suffix = (p === 'IP-CIDR' || p === 'SRC-IP-CIDR') ? (isIPv6(v) ? '/128' : '/32') : ''
|
const suffix =
|
||||||
|
p === 'IP-CIDR' || p === 'SRC-IP-CIDR' ? (isIPv6(v) ? '/128' : '/32') : ''
|
||||||
return {
|
return {
|
||||||
key: `${p},${v}${suffix}`,
|
key: `${p},${v}${suffix}`,
|
||||||
text: `${p},${v}${suffix}`
|
text: `${p},${v}${suffix}`
|
||||||
}
|
}
|
||||||
}).filter(Boolean).flat()
|
})
|
||||||
: prefix.map(p => {
|
.filter(Boolean)
|
||||||
|
.flat()
|
||||||
|
: prefix
|
||||||
|
.map((p) => {
|
||||||
const v = value as string
|
const v = value as string
|
||||||
if (p === 'DOMAIN-SUFFIX') {
|
if (p === 'DOMAIN-SUFFIX') {
|
||||||
return getSubDomains(v).map((subV) => ({
|
return getSubDomains(v).map((subV) => ({
|
||||||
@ -81,12 +86,14 @@ const CopyableSettingItem: React.FC<{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const suffix = (p === 'IP-CIDR' || p === 'SRC-IP-CIDR') ? (isIPv6(v) ? '/128' : '/32') : ''
|
const suffix =
|
||||||
|
p === 'IP-CIDR' || p === 'SRC-IP-CIDR' ? (isIPv6(v) ? '/128' : '/32') : ''
|
||||||
return {
|
return {
|
||||||
key: `${p},${v}${suffix}`,
|
key: `${p},${v}${suffix}`,
|
||||||
text: `${p},${v}${suffix}`
|
text: `${p},${v}${suffix}`
|
||||||
}
|
}
|
||||||
}).flat())
|
})
|
||||||
|
.flat())
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -137,16 +144,28 @@ const ConnectionDetailModal: React.FC<Props> = (props) => {
|
|||||||
<ModalContent className="flag-emoji break-all">
|
<ModalContent className="flag-emoji break-all">
|
||||||
<ModalHeader className="flex app-drag">{t('connections.detail.title')}</ModalHeader>
|
<ModalHeader className="flex app-drag">{t('connections.detail.title')}</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<SettingItem title={t('connections.detail.establishTime')}>{dayjs(connection.start).fromNow()}</SettingItem>
|
<SettingItem title={t('connections.detail.establishTime')}>
|
||||||
|
{dayjs(connection.start).fromNow()}
|
||||||
|
</SettingItem>
|
||||||
<SettingItem title={t('connections.detail.rule')}>
|
<SettingItem title={t('connections.detail.rule')}>
|
||||||
{connection.rule}
|
{connection.rule}
|
||||||
{connection.rulePayload ? `(${connection.rulePayload})` : ''}
|
{connection.rulePayload ? `(${connection.rulePayload})` : ''}
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<SettingItem title={t('connections.detail.proxyChain')}>{[...connection.chains].reverse().join('>>')}</SettingItem>
|
<SettingItem title={t('connections.detail.proxyChain')}>
|
||||||
<SettingItem title={t('connections.uploadSpeed')}>{calcTraffic(connection.uploadSpeed || 0)}/s</SettingItem>
|
{[...connection.chains].reverse().join('>>')}
|
||||||
<SettingItem title={t('connections.downloadSpeed')}>{calcTraffic(connection.downloadSpeed || 0)}/s</SettingItem>
|
</SettingItem>
|
||||||
<SettingItem title={t('connections.uploadAmount')}>{calcTraffic(connection.upload)}</SettingItem>
|
<SettingItem title={t('connections.uploadSpeed')}>
|
||||||
<SettingItem title={t('connections.downloadAmount')}>{calcTraffic(connection.download)}</SettingItem>
|
{calcTraffic(connection.uploadSpeed || 0)}/s
|
||||||
|
</SettingItem>
|
||||||
|
<SettingItem title={t('connections.downloadSpeed')}>
|
||||||
|
{calcTraffic(connection.downloadSpeed || 0)}/s
|
||||||
|
</SettingItem>
|
||||||
|
<SettingItem title={t('connections.uploadAmount')}>
|
||||||
|
{calcTraffic(connection.upload)}
|
||||||
|
</SettingItem>
|
||||||
|
<SettingItem title={t('connections.downloadAmount')}>
|
||||||
|
{calcTraffic(connection.download)}
|
||||||
|
</SettingItem>
|
||||||
<CopyableSettingItem
|
<CopyableSettingItem
|
||||||
title={t('connections.detail.connectionType')}
|
title={t('connections.detail.connectionType')}
|
||||||
value={[connection.metadata.type, connection.metadata.network]}
|
value={[connection.metadata.type, connection.metadata.network]}
|
||||||
@ -278,16 +297,24 @@ const ConnectionDetailModal: React.FC<Props> = (props) => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{connection.metadata.remoteDestination && (
|
{connection.metadata.remoteDestination && (
|
||||||
<SettingItem title={t('connections.detail.remoteDestination')}>{connection.metadata.remoteDestination}</SettingItem>
|
<SettingItem title={t('connections.detail.remoteDestination')}>
|
||||||
|
{connection.metadata.remoteDestination}
|
||||||
|
</SettingItem>
|
||||||
)}
|
)}
|
||||||
{connection.metadata.dnsMode && (
|
{connection.metadata.dnsMode && (
|
||||||
<SettingItem title={t('connections.detail.dnsMode')}>{connection.metadata.dnsMode}</SettingItem>
|
<SettingItem title={t('connections.detail.dnsMode')}>
|
||||||
|
{connection.metadata.dnsMode}
|
||||||
|
</SettingItem>
|
||||||
)}
|
)}
|
||||||
{connection.metadata.specialProxy && (
|
{connection.metadata.specialProxy && (
|
||||||
<SettingItem title={t('connections.detail.specialProxy')}>{connection.metadata.specialProxy}</SettingItem>
|
<SettingItem title={t('connections.detail.specialProxy')}>
|
||||||
|
{connection.metadata.specialProxy}
|
||||||
|
</SettingItem>
|
||||||
)}
|
)}
|
||||||
{connection.metadata.specialRules && (
|
{connection.metadata.specialRules && (
|
||||||
<SettingItem title={t('connections.detail.specialRules')}>{connection.metadata.specialRules}</SettingItem>
|
<SettingItem title={t('connections.detail.specialRules')}>
|
||||||
|
{connection.metadata.specialRules}
|
||||||
|
</SettingItem>
|
||||||
)}
|
)}
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
|
|||||||
@ -68,7 +68,8 @@ const ConnectionItem: React.FC<Props> = (props) => {
|
|||||||
</Chip>
|
</Chip>
|
||||||
{info.uploadSpeed !== 0 || info.downloadSpeed !== 0 ? (
|
{info.uploadSpeed !== 0 || info.downloadSpeed !== 0 ? (
|
||||||
<Chip color="primary" size="sm" radius="sm" variant="bordered">
|
<Chip color="primary" size="sm" radius="sm" variant="bordered">
|
||||||
↑ {calcTraffic(info.uploadSpeed || 0)}/s ↓ {calcTraffic(info.downloadSpeed || 0)}
|
↑ {calcTraffic(info.uploadSpeed || 0)}/s ↓{' '}
|
||||||
|
{calcTraffic(info.downloadSpeed || 0)}
|
||||||
/s
|
/s
|
||||||
</Chip>
|
</Chip>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@ -35,8 +35,8 @@ const DEFAULT_COLUMNS: Omit<ColumnConfig, 'label'>[] = [
|
|||||||
width: 80,
|
width: 80,
|
||||||
minWidth: 60,
|
minWidth: 60,
|
||||||
visible: true,
|
visible: true,
|
||||||
getValue: (conn) => conn.isActive ? 'active' : 'closed',
|
getValue: (conn) => (conn.isActive ? 'active' : 'closed'),
|
||||||
sortValue: (conn) => conn.isActive ? 1 : 0
|
sortValue: (conn) => (conn.isActive ? 1 : 0)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'establishTime',
|
key: 'establishTime',
|
||||||
@ -53,7 +53,9 @@ const DEFAULT_COLUMNS: Omit<ColumnConfig, 'label'>[] = [
|
|||||||
visible: true,
|
visible: true,
|
||||||
getValue: (conn) => `${conn.metadata.type}(${conn.metadata.network})`,
|
getValue: (conn) => `${conn.metadata.type}(${conn.metadata.network})`,
|
||||||
render: (conn) => (
|
render: (conn) => (
|
||||||
<span className="text-xs">{conn.metadata.type}({conn.metadata.network.toUpperCase()})</span>
|
<span className="text-xs">
|
||||||
|
{conn.metadata.type}({conn.metadata.network.toUpperCase()})
|
||||||
|
</span>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -210,24 +212,28 @@ const ConnectionTable: React.FC<Props> = ({
|
|||||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>(initialSortDirection || 'asc')
|
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>(initialSortDirection || 'asc')
|
||||||
|
|
||||||
// 状态列渲染函数
|
// 状态列渲染函数
|
||||||
const renderStatus = useCallback((conn: IMihomoConnectionDetail) => (
|
const renderStatus = useCallback(
|
||||||
<Chip
|
(conn: IMihomoConnectionDetail) => (
|
||||||
color={conn.isActive ? 'primary' : 'danger'}
|
<Chip color={conn.isActive ? 'primary' : 'danger'} size="sm" radius="sm" variant="dot">
|
||||||
size="sm"
|
|
||||||
radius="sm"
|
|
||||||
variant="dot"
|
|
||||||
>
|
|
||||||
{conn.isActive ? t('connections.active') : t('connections.closed')}
|
{conn.isActive ? t('connections.active') : t('connections.closed')}
|
||||||
</Chip>
|
</Chip>
|
||||||
), [t])
|
),
|
||||||
|
[t]
|
||||||
|
)
|
||||||
|
|
||||||
// 连接类型渲染函数
|
// 连接类型渲染函数
|
||||||
const renderType = useCallback((conn: IMihomoConnectionDetail) => (
|
const renderType = useCallback(
|
||||||
<span className="text-xs">{conn.metadata.type}({conn.metadata.network.toUpperCase()})</span>
|
(conn: IMihomoConnectionDetail) => (
|
||||||
), [])
|
<span className="text-xs">
|
||||||
|
{conn.metadata.type}({conn.metadata.network.toUpperCase()})
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
// 翻译标签映射
|
// 翻译标签映射
|
||||||
const getLabelForColumn = useCallback((key: string): string => {
|
const getLabelForColumn = useCallback(
|
||||||
|
(key: string): string => {
|
||||||
const translationMap: Record<string, string> = {
|
const translationMap: Record<string, string> = {
|
||||||
status: t('connections.detail.status'),
|
status: t('connections.detail.status'),
|
||||||
establishTime: t('connections.detail.establishTime'),
|
establishTime: t('connections.detail.establishTime'),
|
||||||
@ -252,38 +258,43 @@ const ConnectionTable: React.FC<Props> = ({
|
|||||||
dnsMode: t('connections.detail.dnsMode')
|
dnsMode: t('connections.detail.dnsMode')
|
||||||
}
|
}
|
||||||
return translationMap[key] || key
|
return translationMap[key] || key
|
||||||
}, [t])
|
},
|
||||||
|
[t]
|
||||||
|
)
|
||||||
|
|
||||||
// 初始化列配置(保留宽度状态)
|
// 初始化列配置(保留宽度状态)
|
||||||
const [columnWidths, setColumnWidths] = useState<Record<string, number>>(() => {
|
const [columnWidths, setColumnWidths] = useState<Record<string, number>>(() => {
|
||||||
const widths: Record<string, number> = {}
|
const widths: Record<string, number> = {}
|
||||||
DEFAULT_COLUMNS.forEach(col => {
|
DEFAULT_COLUMNS.forEach((col) => {
|
||||||
widths[col.key] = initialColumnWidths?.[col.key] || col.width
|
widths[col.key] = initialColumnWidths?.[col.key] || col.width
|
||||||
})
|
})
|
||||||
return widths
|
return widths
|
||||||
})
|
})
|
||||||
|
|
||||||
// 更新列标签和可见性
|
// 更新列标签和可见性
|
||||||
const columnsWithLabels = useMemo(() =>
|
const columnsWithLabels = useMemo(
|
||||||
DEFAULT_COLUMNS.map(col => ({
|
() =>
|
||||||
|
DEFAULT_COLUMNS.map((col) => ({
|
||||||
...col,
|
...col,
|
||||||
label: getLabelForColumn(col.key),
|
label: getLabelForColumn(col.key),
|
||||||
visible: visibleColumns.has(col.key),
|
visible: visibleColumns.has(col.key),
|
||||||
width: columnWidths[col.key] || col.width
|
width: columnWidths[col.key] || col.width
|
||||||
}))
|
})),
|
||||||
, [getLabelForColumn, visibleColumns, columnWidths])
|
[getLabelForColumn, visibleColumns, columnWidths]
|
||||||
|
)
|
||||||
|
|
||||||
// 处理列宽度调整
|
// 处理列宽度调整
|
||||||
const handleMouseDown = useCallback((e: React.MouseEvent, columnKey: string) => {
|
const handleMouseDown = useCallback(
|
||||||
|
(e: React.MouseEvent, columnKey: string) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setResizingColumn(columnKey)
|
setResizingColumn(columnKey)
|
||||||
|
|
||||||
const startX = e.clientX
|
const startX = e.clientX
|
||||||
const column = DEFAULT_COLUMNS.find(c => c.key === columnKey)
|
const column = DEFAULT_COLUMNS.find((c) => c.key === columnKey)
|
||||||
if (!column) return
|
if (!column) return
|
||||||
|
|
||||||
let currentWidth = column.width
|
let currentWidth = column.width
|
||||||
setColumnWidths(prev => {
|
setColumnWidths((prev) => {
|
||||||
currentWidth = prev[columnKey] || column.width
|
currentWidth = prev[columnKey] || column.width
|
||||||
return prev
|
return prev
|
||||||
})
|
})
|
||||||
@ -292,7 +303,7 @@ const ConnectionTable: React.FC<Props> = ({
|
|||||||
const diff = e.clientX - startX
|
const diff = e.clientX - startX
|
||||||
const newWidth = Math.max(column.minWidth, currentWidth + diff)
|
const newWidth = Math.max(column.minWidth, currentWidth + diff)
|
||||||
|
|
||||||
setColumnWidths(prev => ({
|
setColumnWidths((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[columnKey]: newWidth
|
[columnKey]: newWidth
|
||||||
}))
|
}))
|
||||||
@ -304,7 +315,7 @@ const ConnectionTable: React.FC<Props> = ({
|
|||||||
document.removeEventListener('mouseup', handleMouseUp)
|
document.removeEventListener('mouseup', handleMouseUp)
|
||||||
// 保存列宽度
|
// 保存列宽度
|
||||||
if (onColumnWidthChange) {
|
if (onColumnWidthChange) {
|
||||||
setColumnWidths(currentWidths => {
|
setColumnWidths((currentWidths) => {
|
||||||
onColumnWidthChange(currentWidths)
|
onColumnWidthChange(currentWidths)
|
||||||
return currentWidths
|
return currentWidths
|
||||||
})
|
})
|
||||||
@ -313,10 +324,13 @@ const ConnectionTable: React.FC<Props> = ({
|
|||||||
|
|
||||||
document.addEventListener('mousemove', handleMouseMove)
|
document.addEventListener('mousemove', handleMouseMove)
|
||||||
document.addEventListener('mouseup', handleMouseUp)
|
document.addEventListener('mouseup', handleMouseUp)
|
||||||
}, [onColumnWidthChange])
|
},
|
||||||
|
[onColumnWidthChange]
|
||||||
|
)
|
||||||
|
|
||||||
// 处理排序
|
// 处理排序
|
||||||
const handleSort = useCallback((columnKey: string) => {
|
const handleSort = useCallback(
|
||||||
|
(columnKey: string) => {
|
||||||
let newDirection: 'asc' | 'desc' = 'asc'
|
let newDirection: 'asc' | 'desc' = 'asc'
|
||||||
let newColumn = columnKey
|
let newColumn = columnKey
|
||||||
|
|
||||||
@ -332,13 +346,15 @@ const ConnectionTable: React.FC<Props> = ({
|
|||||||
if (onSortChange) {
|
if (onSortChange) {
|
||||||
onSortChange(newColumn, newDirection)
|
onSortChange(newColumn, newDirection)
|
||||||
}
|
}
|
||||||
}, [sortColumn, sortDirection, onSortChange])
|
},
|
||||||
|
[sortColumn, sortDirection, onSortChange]
|
||||||
|
)
|
||||||
|
|
||||||
// 排序连接
|
// 排序连接
|
||||||
const sortedConnections = useMemo(() => {
|
const sortedConnections = useMemo(() => {
|
||||||
if (!sortColumn) return connections
|
if (!sortColumn) return connections
|
||||||
|
|
||||||
const column = columnsWithLabels.find(c => c.key === sortColumn)
|
const column = columnsWithLabels.find((c) => c.key === sortColumn)
|
||||||
if (!column) return connections
|
if (!column) return connections
|
||||||
|
|
||||||
return [...connections].sort((a, b) => {
|
return [...connections].sort((a, b) => {
|
||||||
@ -357,7 +373,7 @@ const ConnectionTable: React.FC<Props> = ({
|
|||||||
})
|
})
|
||||||
}, [connections, sortColumn, sortDirection, columnsWithLabels])
|
}, [connections, sortColumn, sortDirection, columnsWithLabels])
|
||||||
|
|
||||||
const visibleColumnsFiltered = columnsWithLabels.filter(col => col.visible)
|
const visibleColumnsFiltered = columnsWithLabels.filter((col) => col.visible)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col">
|
<div className="h-full flex flex-col">
|
||||||
@ -366,7 +382,7 @@ const ConnectionTable: React.FC<Props> = ({
|
|||||||
<table className="w-full border-collapse">
|
<table className="w-full border-collapse">
|
||||||
<thead className="sticky top-0 z-10 bg-content2">
|
<thead className="sticky top-0 z-10 bg-content2">
|
||||||
<tr>
|
<tr>
|
||||||
{visibleColumnsFiltered.map(col => (
|
{visibleColumnsFiltered.map((col) => (
|
||||||
<th
|
<th
|
||||||
key={col.key}
|
key={col.key}
|
||||||
className="relative border-b border-divider text-left text-xs font-semibold text-foreground-600 px-3 h-10"
|
className="relative border-b border-divider text-left text-xs font-semibold text-foreground-600 px-3 h-10"
|
||||||
@ -379,9 +395,7 @@ const ConnectionTable: React.FC<Props> = ({
|
|||||||
>
|
>
|
||||||
{col.label}
|
{col.label}
|
||||||
{sortColumn === col.key && (
|
{sortColumn === col.key && (
|
||||||
<span className="ml-1">
|
<span className="ml-1">{sortDirection === 'asc' ? '↑' : '↓'}</span>
|
||||||
{sortDirection === 'asc' ? '↑' : '↓'}
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
<div
|
<div
|
||||||
@ -411,7 +425,7 @@ const ConnectionTable: React.FC<Props> = ({
|
|||||||
setIsDetailModalOpen(true)
|
setIsDetailModalOpen(true)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{visibleColumnsFiltered.map(col => {
|
{visibleColumnsFiltered.map((col) => {
|
||||||
let content: React.ReactNode
|
let content: React.ReactNode
|
||||||
// 根据列类型选择渲染方式
|
// 根据列类型选择渲染方式
|
||||||
if (col.key === 'status') {
|
if (col.key === 'status') {
|
||||||
@ -429,7 +443,11 @@ const ConnectionTable: React.FC<Props> = ({
|
|||||||
key={col.key}
|
key={col.key}
|
||||||
className="px-3 text-sm text-foreground truncate"
|
className="px-3 text-sm text-foreground truncate"
|
||||||
style={{ maxWidth: col.width }}
|
style={{ maxWidth: col.width }}
|
||||||
title={typeof col.getValue(connection) === 'string' ? col.getValue(connection) as string : ''}
|
title={
|
||||||
|
typeof col.getValue(connection) === 'string'
|
||||||
|
? (col.getValue(connection) as string)
|
||||||
|
: ''
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
</td>
|
</td>
|
||||||
@ -445,7 +463,11 @@ const ConnectionTable: React.FC<Props> = ({
|
|||||||
close(connection.id)
|
close(connection.id)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{connection.isActive ? <CgClose className="text-lg" /> : <CgTrash className="text-lg" />}
|
{connection.isActive ? (
|
||||||
|
<CgClose className="text-lg" />
|
||||||
|
) : (
|
||||||
|
<CgTrash className="text-lg" />
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@ -36,7 +36,10 @@ const EditFileModal: React.FC<Props> = (props) => {
|
|||||||
<ModalContent className="h-full w-[calc(100%-100px)]">
|
<ModalContent className="h-full w-[calc(100%-100px)]">
|
||||||
<ModalHeader className="flex pb-0 app-drag">
|
<ModalHeader className="flex pb-0 app-drag">
|
||||||
{t('override.editFile.title', {
|
{t('override.editFile.title', {
|
||||||
type: language === 'javascript' ? t('override.editFile.script') : t('override.editFile.config')
|
type:
|
||||||
|
language === 'javascript'
|
||||||
|
? t('override.editFile.script')
|
||||||
|
: t('override.editFile.config')
|
||||||
})}
|
})}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<ModalBody className="h-full">
|
<ModalBody className="h-full">
|
||||||
|
|||||||
@ -125,8 +125,6 @@ const OverrideItem: React.FC<Props> = (props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const handleContextMenu = (e: React.MouseEvent) => {
|
const handleContextMenu = (e: React.MouseEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
@ -170,12 +168,7 @@ const OverrideItem: React.FC<Props> = (props) => {
|
|||||||
setOpenFileEditor(true)
|
setOpenFileEditor(true)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div ref={setNodeRef} {...attributes} {...listeners} className="h-full w-full">
|
||||||
ref={setNodeRef}
|
|
||||||
{...attributes}
|
|
||||||
{...listeners}
|
|
||||||
className="h-full w-full"
|
|
||||||
>
|
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<div className="flex justify-between h-[32px]">
|
<div className="flex justify-between h-[32px]">
|
||||||
<h3
|
<h3
|
||||||
@ -211,10 +204,7 @@ const OverrideItem: React.FC<Props> = (props) => {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Dropdown
|
<Dropdown isOpen={dropdownOpen} onOpenChange={setDropdownOpen}>
|
||||||
isOpen={dropdownOpen}
|
|
||||||
onOpenChange={setDropdownOpen}
|
|
||||||
>
|
|
||||||
<DropdownTrigger>
|
<DropdownTrigger>
|
||||||
<Button isIconOnly size="sm" variant="light" color="default">
|
<Button isIconOnly size="sm" variant="light" color="default">
|
||||||
<IoMdMore color="default" className={`text-[24px]`} />
|
<IoMdMore color="default" className={`text-[24px]`} />
|
||||||
|
|||||||
@ -21,7 +21,7 @@ import { restartCore, addProfileUpdater } from '@renderer/utils/ipc'
|
|||||||
import { MdDeleteForever } from 'react-icons/md'
|
import { MdDeleteForever } from 'react-icons/md'
|
||||||
import { FaPlus } from 'react-icons/fa6'
|
import { FaPlus } from 'react-icons/fa6'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { isValidCron } from 'cron-validator';
|
import { isValidCron } from 'cron-validator'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
item: IProfileItem
|
item: IProfileItem
|
||||||
@ -44,7 +44,7 @@ const EditInfoModal: React.FC<Props> = (props) => {
|
|||||||
(i) =>
|
(i) =>
|
||||||
overrideItems.find((t) => t.id === i) && !overrideItems.find((t) => t.id === i)?.global
|
overrideItems.find((t) => t.id === i) && !overrideItems.find((t) => t.id === i)?.global
|
||||||
)
|
)
|
||||||
};
|
}
|
||||||
await updateProfileItem(updatedItem)
|
await updateProfileItem(updatedItem)
|
||||||
await addProfileUpdater(updatedItem)
|
await addProfileUpdater(updatedItem)
|
||||||
await restartCore()
|
await restartCore()
|
||||||
@ -143,12 +143,12 @@ const EditInfoModal: React.FC<Props> = (props) => {
|
|||||||
if (/^[\d\s*\-,\/]*$/.test(v)) {
|
if (/^[\d\s*\-,\/]*$/.test(v)) {
|
||||||
// 纯数字
|
// 纯数字
|
||||||
if (/^\d+$/.test(v)) {
|
if (/^\d+$/.test(v)) {
|
||||||
setValues({ ...values, interval: parseInt(v, 10) || 0 });
|
setValues({ ...values, interval: parseInt(v, 10) || 0 })
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
// 非纯数字
|
// 非纯数字
|
||||||
try {
|
try {
|
||||||
setValues({ ...values, interval: v });
|
setValues({ ...values, interval: v })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
@ -158,22 +158,24 @@ const EditInfoModal: React.FC<Props> = (props) => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 动态提示信息 */}
|
{/* 动态提示信息 */}
|
||||||
<div className="text-xs" style={{
|
<div
|
||||||
color: typeof values.interval === 'string' &&
|
className="text-xs"
|
||||||
|
style={{
|
||||||
|
color:
|
||||||
|
typeof values.interval === 'string' &&
|
||||||
!/^\d+$/.test(values.interval) &&
|
!/^\d+$/.test(values.interval) &&
|
||||||
!isValidCron(values.interval, { seconds: false })
|
!isValidCron(values.interval, { seconds: false })
|
||||||
? '#ef4444'
|
? '#ef4444'
|
||||||
: '#6b7280'
|
: '#6b7280'
|
||||||
}}>
|
}}
|
||||||
{typeof values.interval === 'number' ? (
|
>
|
||||||
t('profiles.editInfo.intervalMinutes')
|
{typeof values.interval === 'number'
|
||||||
) : /^\d+$/.test(values.interval?.toString() || '') ? (
|
? t('profiles.editInfo.intervalMinutes')
|
||||||
t('profiles.editInfo.intervalMinutes')
|
: /^\d+$/.test(values.interval?.toString() || '')
|
||||||
) : isValidCron(values.interval?.toString() || '', { seconds: false }) ? (
|
? t('profiles.editInfo.intervalMinutes')
|
||||||
t('profiles.editInfo.intervalCron')
|
: isValidCron(values.interval?.toString() || '', { seconds: false })
|
||||||
) : (
|
? t('profiles.editInfo.intervalCron')
|
||||||
t('profiles.editInfo.intervalHint')
|
: t('profiles.editInfo.intervalHint')}
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -270,17 +270,9 @@ const ProfileItem: React.FC<Props> = (props) => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Dropdown
|
<Dropdown isOpen={dropdownOpen} onOpenChange={setDropdownOpen}>
|
||||||
isOpen={dropdownOpen}
|
|
||||||
onOpenChange={setDropdownOpen}
|
|
||||||
>
|
|
||||||
<DropdownTrigger>
|
<DropdownTrigger>
|
||||||
<Button
|
<Button isIconOnly size="sm" variant="light" color="default">
|
||||||
isIconOnly
|
|
||||||
size="sm"
|
|
||||||
variant="light"
|
|
||||||
color="default"
|
|
||||||
>
|
|
||||||
<IoMdMore
|
<IoMdMore
|
||||||
color="default"
|
color="default"
|
||||||
className={`text-[24px] ${isCurrent ? 'text-primary-foreground' : 'text-foreground'}`}
|
className={`text-[24px] ${isCurrent ? 'text-primary-foreground' : 'text-foreground'}`}
|
||||||
@ -316,7 +308,9 @@ const ProfileItem: React.FC<Props> = (props) => {
|
|||||||
await patchAppConfig({ profileDisplayDate: 'update' })
|
await patchAppConfig({ profileDisplayDate: 'update' })
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{extra.expire ? dayjs.unix(extra.expire).format('YYYY-MM-DD') : t('profiles.neverExpire')}
|
{extra.expire
|
||||||
|
? dayjs.unix(extra.expire).format('YYYY-MM-DD')
|
||||||
|
: t('profiles.neverExpire')}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@ -22,9 +22,19 @@ function delayColor(delay: number): 'primary' | 'success' | 'warning' | 'danger'
|
|||||||
return 'warning'
|
return 'warning'
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProxyItem: React.FC<Props> = React.memo((props) => {
|
const ProxyItem: React.FC<Props> = React.memo(
|
||||||
|
(props) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { mutateProxies, proxyDisplayMode, group, proxy, selected, onSelect, onProxyDelay, isGroupTesting = false } = props
|
const {
|
||||||
|
mutateProxies,
|
||||||
|
proxyDisplayMode,
|
||||||
|
group,
|
||||||
|
proxy,
|
||||||
|
selected,
|
||||||
|
onSelect,
|
||||||
|
onProxyDelay,
|
||||||
|
isGroupTesting = false
|
||||||
|
} = props
|
||||||
|
|
||||||
const delay = useMemo(() => {
|
const delay = useMemo(() => {
|
||||||
if (proxy.history.length > 0) {
|
if (proxy.history.length > 0) {
|
||||||
@ -51,7 +61,10 @@ const ProxyItem: React.FC<Props> = React.memo((props) => {
|
|||||||
})
|
})
|
||||||
}, [proxy.name, group.testUrl, onProxyDelay, mutateProxies])
|
}, [proxy.name, group.testUrl, onProxyDelay, mutateProxies])
|
||||||
|
|
||||||
const fixed = useMemo(() => group.fixed && group.fixed === proxy.name, [group.fixed, proxy.name])
|
const fixed = useMemo(
|
||||||
|
() => group.fixed && group.fixed === proxy.name,
|
||||||
|
[group.fixed, proxy.name]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
@ -99,9 +112,13 @@ const ProxyItem: React.FC<Props> = React.memo((props) => {
|
|||||||
<div className="text-foreground-400 text-xs bg-default-100 px-1 rounded-md">
|
<div className="text-foreground-400 text-xs bg-default-100 px-1 rounded-md">
|
||||||
{proxy.type}
|
{proxy.type}
|
||||||
</div>
|
</div>
|
||||||
{['tfo', 'udp', 'xudp', 'mptcp', 'smux'].map(protocol =>
|
{['tfo', 'udp', 'xudp', 'mptcp', 'smux'].map(
|
||||||
|
(protocol) =>
|
||||||
proxy[protocol as keyof IMihomoProxy] && (
|
proxy[protocol as keyof IMihomoProxy] && (
|
||||||
<div key={protocol} className="text-foreground-400 text-xs bg-default-100 px-1 rounded-md">
|
<div
|
||||||
|
key={protocol}
|
||||||
|
className="text-foreground-400 text-xs bg-default-100 px-1 rounded-md"
|
||||||
|
>
|
||||||
{protocol}
|
{protocol}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -116,9 +133,7 @@ const ProxyItem: React.FC<Props> = React.memo((props) => {
|
|||||||
variant="light"
|
variant="light"
|
||||||
className="h-full text-sm ml-auto -mt-0.5 px-2 relative w-min whitespace-nowrap"
|
className="h-full text-sm ml-auto -mt-0.5 px-2 relative w-min whitespace-nowrap"
|
||||||
>
|
>
|
||||||
<div className="w-full h-full flex items-center justify-end">
|
<div className="w-full h-full flex items-center justify-end">{delayText}</div>
|
||||||
{delayText}
|
|
||||||
</div>
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -154,9 +169,7 @@ const ProxyItem: React.FC<Props> = React.memo((props) => {
|
|||||||
variant="light"
|
variant="light"
|
||||||
className="h-full text-sm px-2 relative w-min whitespace-nowrap"
|
className="h-full text-sm px-2 relative w-min whitespace-nowrap"
|
||||||
>
|
>
|
||||||
<div className="w-full h-full flex items-center justify-end">
|
<div className="w-full h-full flex items-center justify-end">{delayText}</div>
|
||||||
{delayText}
|
|
||||||
</div>
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -164,7 +177,8 @@ const ProxyItem: React.FC<Props> = React.memo((props) => {
|
|||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
}, (prevProps, nextProps) => {
|
},
|
||||||
|
(prevProps, nextProps) => {
|
||||||
// 必要时重新渲染
|
// 必要时重新渲染
|
||||||
return (
|
return (
|
||||||
prevProps.proxy.name === nextProps.proxy.name &&
|
prevProps.proxy.name === nextProps.proxy.name &&
|
||||||
@ -174,7 +188,8 @@ const ProxyItem: React.FC<Props> = React.memo((props) => {
|
|||||||
prevProps.group.fixed === nextProps.group.fixed &&
|
prevProps.group.fixed === nextProps.group.fixed &&
|
||||||
prevProps.isGroupTesting === nextProps.isGroupTesting
|
prevProps.isGroupTesting === nextProps.isGroupTesting
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
ProxyItem.displayName = 'ProxyItem'
|
ProxyItem.displayName = 'ProxyItem'
|
||||||
|
|
||||||
|
|||||||
@ -30,7 +30,7 @@ const ProxyProvider: React.FC = () => {
|
|||||||
if (showDetails.title) {
|
if (showDetails.title) {
|
||||||
const fetchProviderPath = async (name: string): Promise<void> => {
|
const fetchProviderPath = async (name: string): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const providers= await getRuntimeConfig()
|
const providers = await getRuntimeConfig()
|
||||||
const provider = providers['proxy-providers'][name]
|
const provider = providers['proxy-providers'][name]
|
||||||
if (provider) {
|
if (provider) {
|
||||||
setShowDetails((prev) => ({
|
setShowDetails((prev) => ({
|
||||||
@ -89,7 +89,9 @@ const ProxyProvider: React.FC = () => {
|
|||||||
type={showDetails.type}
|
type={showDetails.type}
|
||||||
title={showDetails.title}
|
title={showDetails.title}
|
||||||
privderType={showDetails.privderType}
|
privderType={showDetails.privderType}
|
||||||
onClose={() => setShowDetails({ show: false, path: '', type: '', title: '', privderType: '' })}
|
onClose={() =>
|
||||||
|
setShowDetails({ show: false, path: '', type: '', title: '', privderType: '' })
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<SettingItem title={t('resources.proxyProviders.title')} divider>
|
<SettingItem title={t('resources.proxyProviders.title')} divider>
|
||||||
@ -123,7 +125,9 @@ const ProxyProvider: React.FC = () => {
|
|||||||
</Button> */}
|
</Button> */}
|
||||||
<Button
|
<Button
|
||||||
isIconOnly
|
isIconOnly
|
||||||
title={provider.vehicleType == 'File' ? t('common.editor.edit') : t('common.viewer.view')}
|
title={
|
||||||
|
provider.vehicleType == 'File' ? t('common.editor.edit') : t('common.viewer.view')
|
||||||
|
}
|
||||||
className="ml-2"
|
className="ml-2"
|
||||||
size="sm"
|
size="sm"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
|
|||||||
@ -32,7 +32,7 @@ const RuleProvider: React.FC = () => {
|
|||||||
if (showDetails.title) {
|
if (showDetails.title) {
|
||||||
const fetchProviderPath = async (name: string): Promise<void> => {
|
const fetchProviderPath = async (name: string): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const providers= await getRuntimeConfig()
|
const providers = await getRuntimeConfig()
|
||||||
const provider = providers['rule-providers'][name]
|
const provider = providers['rule-providers'][name]
|
||||||
if (provider) {
|
if (provider) {
|
||||||
setShowDetails((prev) => ({
|
setShowDetails((prev) => ({
|
||||||
@ -97,7 +97,17 @@ const RuleProvider: React.FC = () => {
|
|||||||
format={showDetails.format}
|
format={showDetails.format}
|
||||||
privderType={showDetails.privderType}
|
privderType={showDetails.privderType}
|
||||||
behavior={showDetails.behavior}
|
behavior={showDetails.behavior}
|
||||||
onClose={() => setShowDetails({ show: false, path: '', type: '', title: '', format: '', privderType: '', behavior: '' })}
|
onClose={() =>
|
||||||
|
setShowDetails({
|
||||||
|
show: false,
|
||||||
|
path: '',
|
||||||
|
type: '',
|
||||||
|
title: '',
|
||||||
|
format: '',
|
||||||
|
privderType: '',
|
||||||
|
behavior: ''
|
||||||
|
})
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<SettingItem title={t('resources.ruleProviders.title')} divider>
|
<SettingItem title={t('resources.ruleProviders.title')} divider>
|
||||||
@ -127,7 +137,9 @@ const RuleProvider: React.FC = () => {
|
|||||||
<div>{dayjs(provider.updatedAt).fromNow()}</div>
|
<div>{dayjs(provider.updatedAt).fromNow()}</div>
|
||||||
<Button
|
<Button
|
||||||
isIconOnly
|
isIconOnly
|
||||||
title={provider.vehicleType == 'File' ? t('common.editor.edit') : t('common.viewer.view')}
|
title={
|
||||||
|
provider.vehicleType == 'File' ? t('common.editor.edit') : t('common.viewer.view')
|
||||||
|
}
|
||||||
className="ml-2"
|
className="ml-2"
|
||||||
size="sm"
|
size="sm"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
|
|||||||
@ -54,13 +54,17 @@ const Viewer: React.FC<Props> = (props) => {
|
|||||||
try {
|
try {
|
||||||
const parsedYaml = yaml.load(fileContent)
|
const parsedYaml = yaml.load(fileContent)
|
||||||
if (privderType === 'proxy-providers') {
|
if (privderType === 'proxy-providers') {
|
||||||
setCurrData(yaml.dump({
|
setCurrData(
|
||||||
'proxies': parsedYaml[privderType][title].payload
|
yaml.dump({
|
||||||
}))
|
proxies: parsedYaml[privderType][title].payload
|
||||||
|
})
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
setCurrData(yaml.dump({
|
setCurrData(
|
||||||
'rules': parsedYaml[privderType][title].payload
|
yaml.dump({
|
||||||
}))
|
rules: parsedYaml[privderType][title].payload
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setCurrData(fileContent)
|
setCurrData(fileContent)
|
||||||
|
|||||||
@ -140,7 +140,8 @@ const GeneralConfig: React.FC = () => {
|
|||||||
onValueChange={async (v) => {
|
onValueChange={async (v) => {
|
||||||
try {
|
try {
|
||||||
// 检查管理员权限
|
// 检查管理员权限
|
||||||
const hasAdminPrivileges = await window.electron.ipcRenderer.invoke('checkAdminPrivileges')
|
const hasAdminPrivileges =
|
||||||
|
await window.electron.ipcRenderer.invoke('checkAdminPrivileges')
|
||||||
|
|
||||||
if (!hasAdminPrivileges) {
|
if (!hasAdminPrivileges) {
|
||||||
const notification = new Notification(t('settings.autoStart.permissions'))
|
const notification = new Notification(t('settings.autoStart.permissions'))
|
||||||
@ -286,10 +287,7 @@ const GeneralConfig: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<SettingItem
|
<SettingItem title={t('settings.floatingWindowCompatMode')} divider>
|
||||||
title={t('settings.floatingWindowCompatMode')}
|
|
||||||
divider
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Switch
|
<Switch
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@ -57,20 +57,12 @@ const LocalBackupConfig: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
<SettingCard title={t('localBackup.title')}>
|
<SettingCard title={t('localBackup.title')}>
|
||||||
<SettingItem title={t('localBackup.export.title')} divider>
|
<SettingItem title={t('localBackup.export.title')} divider>
|
||||||
<Button
|
<Button isLoading={exporting} size="sm" onPress={handleExport}>
|
||||||
isLoading={exporting}
|
|
||||||
size="sm"
|
|
||||||
onPress={handleExport}
|
|
||||||
>
|
|
||||||
{t('localBackup.export.button')}
|
{t('localBackup.export.button')}
|
||||||
</Button>
|
</Button>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<SettingItem title={t('localBackup.import.title')}>
|
<SettingItem title={t('localBackup.import.title')}>
|
||||||
<Button
|
<Button isLoading={importing} size="sm" onPress={onOpen}>
|
||||||
isLoading={importing}
|
|
||||||
size="sm"
|
|
||||||
onPress={onOpen}
|
|
||||||
>
|
|
||||||
{t('localBackup.import.button')}
|
{t('localBackup.import.button')}
|
||||||
</Button>
|
</Button>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
|
|||||||
@ -183,9 +183,13 @@ const MihomoConfig: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<SelectItem key="PRIORITY_HIGHEST">{t('mihomo.cpuPriority.realtime')}</SelectItem>
|
<SelectItem key="PRIORITY_HIGHEST">{t('mihomo.cpuPriority.realtime')}</SelectItem>
|
||||||
<SelectItem key="PRIORITY_HIGH">{t('mihomo.cpuPriority.high')}</SelectItem>
|
<SelectItem key="PRIORITY_HIGH">{t('mihomo.cpuPriority.high')}</SelectItem>
|
||||||
<SelectItem key="PRIORITY_ABOVE_NORMAL">{t('mihomo.cpuPriority.aboveNormal')}</SelectItem>
|
<SelectItem key="PRIORITY_ABOVE_NORMAL">
|
||||||
|
{t('mihomo.cpuPriority.aboveNormal')}
|
||||||
|
</SelectItem>
|
||||||
<SelectItem key="PRIORITY_NORMAL">{t('mihomo.cpuPriority.normal')}</SelectItem>
|
<SelectItem key="PRIORITY_NORMAL">{t('mihomo.cpuPriority.normal')}</SelectItem>
|
||||||
<SelectItem key="PRIORITY_BELOW_NORMAL">{t('mihomo.cpuPriority.belowNormal')}</SelectItem>
|
<SelectItem key="PRIORITY_BELOW_NORMAL">
|
||||||
|
{t('mihomo.cpuPriority.belowNormal')}
|
||||||
|
</SelectItem>
|
||||||
<SelectItem key="PRIORITY_LOW">{t('mihomo.cpuPriority.low')}</SelectItem>
|
<SelectItem key="PRIORITY_LOW">{t('mihomo.cpuPriority.low')}</SelectItem>
|
||||||
</Select>
|
</Select>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
@ -215,7 +219,6 @@ const MihomoConfig: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
|
|
||||||
|
|
||||||
<SettingItem title={t('mihomo.autoCloseConnection')} divider>
|
<SettingItem title={t('mihomo.autoCloseConnection')} divider>
|
||||||
<Switch
|
<Switch
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@ -35,8 +35,22 @@ const WebdavConfig: React.FC = () => {
|
|||||||
webdavBackupCron
|
webdavBackupCron
|
||||||
})
|
})
|
||||||
const setWebdavDebounce = debounce(
|
const setWebdavDebounce = debounce(
|
||||||
({ webdavUrl, webdavUsername, webdavPassword, webdavDir, webdavMaxBackups, webdavBackupCron }) => {
|
({
|
||||||
patchAppConfig({ webdavUrl, webdavUsername, webdavPassword, webdavDir, webdavMaxBackups, webdavBackupCron })
|
webdavUrl,
|
||||||
|
webdavUsername,
|
||||||
|
webdavPassword,
|
||||||
|
webdavDir,
|
||||||
|
webdavMaxBackups,
|
||||||
|
webdavBackupCron
|
||||||
|
}) => {
|
||||||
|
patchAppConfig({
|
||||||
|
webdavUrl,
|
||||||
|
webdavUsername,
|
||||||
|
webdavPassword,
|
||||||
|
webdavDir,
|
||||||
|
webdavMaxBackups,
|
||||||
|
webdavBackupCron
|
||||||
|
})
|
||||||
},
|
},
|
||||||
500
|
500
|
||||||
)
|
)
|
||||||
@ -172,7 +186,6 @@ const WebdavConfig: React.FC = () => {
|
|||||||
setWebdav({ ...webdav, webdavBackupCron: v })
|
setWebdav({ ...webdav, webdavBackupCron: v })
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<div className="flex justify0between">
|
<div className="flex justify0between">
|
||||||
|
|||||||
@ -31,7 +31,10 @@ const WebdavRestoreModal: React.FC<Props> = (props) => {
|
|||||||
{filenames.length === 0 ? (
|
{filenames.length === 0 ? (
|
||||||
<div className="flex justify-center">{t('webdav.restore.noBackups')}</div>
|
<div className="flex justify-center">{t('webdav.restore.noBackups')}</div>
|
||||||
) : (
|
) : (
|
||||||
filenames.sort().reverse().map((filename) => (
|
filenames
|
||||||
|
.sort()
|
||||||
|
.reverse()
|
||||||
|
.map((filename) => (
|
||||||
<div className="flex" key={filename}>
|
<div className="flex" key={filename}>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@ -36,7 +36,12 @@ interface Props {
|
|||||||
const ConnCard: React.FC<Props> = (props) => {
|
const ConnCard: React.FC<Props> = (props) => {
|
||||||
const { iconOnly } = props
|
const { iconOnly } = props
|
||||||
const { appConfig } = useAppConfig()
|
const { appConfig } = useAppConfig()
|
||||||
const { showTraffic = false, connectionCardStatus = 'col-span-2', disableAnimations = false, hideConnectionCardWave = false } = appConfig || {}
|
const {
|
||||||
|
showTraffic = false,
|
||||||
|
connectionCardStatus = 'col-span-2',
|
||||||
|
disableAnimations = false,
|
||||||
|
hideConnectionCardWave = false
|
||||||
|
} = appConfig || {}
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const match = location.pathname.includes('/connections')
|
const match = location.pathname.includes('/connections')
|
||||||
|
|||||||
@ -18,7 +18,11 @@ const DNSCard: React.FC<Props> = (props) => {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { appConfig, patchAppConfig } = useAppConfig()
|
const { appConfig, patchAppConfig } = useAppConfig()
|
||||||
const { iconOnly } = props
|
const { iconOnly } = props
|
||||||
const { dnsCardStatus = 'col-span-1', controlDns = true, disableAnimations = false } = appConfig || {}
|
const {
|
||||||
|
dnsCardStatus = 'col-span-1',
|
||||||
|
controlDns = true,
|
||||||
|
disableAnimations = false
|
||||||
|
} = appConfig || {}
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const match = location.pathname.includes('/dns')
|
const match = location.pathname.includes('/dns')
|
||||||
|
|||||||
@ -34,9 +34,21 @@ const OutboundModeSwitcher: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
onSelectionChange={(key: Key) => onChangeMode(key as OutboundMode)}
|
onSelectionChange={(key: Key) => onChangeMode(key as OutboundMode)}
|
||||||
>
|
>
|
||||||
<Tab className={`${mode === 'rule' ? 'font-bold' : ''}`} key="rule" title={t('sider.cards.outbound.rule')} />
|
<Tab
|
||||||
<Tab className={`${mode === 'global' ? 'font-bold' : ''}`} key="global" title={t('sider.cards.outbound.global')} />
|
className={`${mode === 'rule' ? 'font-bold' : ''}`}
|
||||||
<Tab className={`${mode === 'direct' ? 'font-bold' : ''}`} key="direct" title={t('sider.cards.outbound.direct')} />
|
key="rule"
|
||||||
|
title={t('sider.cards.outbound.rule')}
|
||||||
|
/>
|
||||||
|
<Tab
|
||||||
|
className={`${mode === 'global' ? 'font-bold' : ''}`}
|
||||||
|
key="global"
|
||||||
|
title={t('sider.cards.outbound.global')}
|
||||||
|
/>
|
||||||
|
<Tab
|
||||||
|
className={`${mode === 'direct' ? 'font-bold' : ''}`}
|
||||||
|
key="direct"
|
||||||
|
title={t('sider.cards.outbound.direct')}
|
||||||
|
/>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,7 +26,11 @@ const ProfileCard: React.FC<Props> = (props) => {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { appConfig, patchAppConfig } = useAppConfig()
|
const { appConfig, patchAppConfig } = useAppConfig()
|
||||||
const { iconOnly } = props
|
const { iconOnly } = props
|
||||||
const { profileCardStatus = 'col-span-2', profileDisplayDate = 'expire', disableAnimations = false } = appConfig || {}
|
const {
|
||||||
|
profileCardStatus = 'col-span-2',
|
||||||
|
profileDisplayDate = 'expire',
|
||||||
|
disableAnimations = false
|
||||||
|
} = appConfig || {}
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const match = location.pathname.includes('/profiles')
|
const match = location.pathname.includes('/profiles')
|
||||||
@ -158,7 +162,9 @@ const ProfileCard: React.FC<Props> = (props) => {
|
|||||||
await patchAppConfig({ profileDisplayDate: 'update' })
|
await patchAppConfig({ profileDisplayDate: 'update' })
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{extra.expire ? dayjs.unix(extra.expire).format('YYYY-MM-DD') : t('sider.cards.neverExpire')}
|
{extra.expire
|
||||||
|
? dayjs.unix(extra.expire).format('YYYY-MM-DD')
|
||||||
|
: t('sider.cards.neverExpire')}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@ -18,7 +18,11 @@ const SniffCard: React.FC<Props> = (props) => {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { appConfig, patchAppConfig } = useAppConfig()
|
const { appConfig, patchAppConfig } = useAppConfig()
|
||||||
const { iconOnly } = props
|
const { iconOnly } = props
|
||||||
const { sniffCardStatus = 'col-span-1', controlSniff = true, disableAnimations = false } = appConfig || {}
|
const {
|
||||||
|
sniffCardStatus = 'col-span-1',
|
||||||
|
controlSniff = true,
|
||||||
|
disableAnimations = false
|
||||||
|
} = appConfig || {}
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const match = location.pathname.includes('/sniffer')
|
const match = location.pathname.includes('/sniffer')
|
||||||
|
|||||||
@ -15,7 +15,11 @@ const SubStoreCard: React.FC<Props> = (props) => {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { appConfig } = useAppConfig()
|
const { appConfig } = useAppConfig()
|
||||||
const { iconOnly } = props
|
const { iconOnly } = props
|
||||||
const { substoreCardStatus = 'col-span-1', useSubStore = true, disableAnimations = false } = appConfig || {}
|
const {
|
||||||
|
substoreCardStatus = 'col-span-1',
|
||||||
|
useSubStore = true,
|
||||||
|
disableAnimations = false
|
||||||
|
} = appConfig || {}
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const match = location.pathname.includes('/substore')
|
const match = location.pathname.includes('/substore')
|
||||||
|
|||||||
@ -42,7 +42,9 @@ const TunSwitcher: React.FC<Props> = (props) => {
|
|||||||
if (enable) {
|
if (enable) {
|
||||||
try {
|
try {
|
||||||
// 检查内核权限
|
// 检查内核权限
|
||||||
const hasPermissions = await window.electron.ipcRenderer.invoke('checkMihomoCorePermissions')
|
const hasPermissions = await window.electron.ipcRenderer.invoke(
|
||||||
|
'checkMihomoCorePermissions'
|
||||||
|
)
|
||||||
|
|
||||||
if (!hasPermissions) {
|
if (!hasPermissions) {
|
||||||
if (window.electron.process.platform === 'win32') {
|
if (window.electron.process.platform === 'win32') {
|
||||||
@ -55,7 +57,11 @@ const TunSwitcher: React.FC<Props> = (props) => {
|
|||||||
return
|
return
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to restart as admin:', error)
|
console.error('Failed to restart as admin:', error)
|
||||||
await window.electron.ipcRenderer.invoke('showErrorDialog', t('tun.permissions.failed'), String(error))
|
await window.electron.ipcRenderer.invoke(
|
||||||
|
'showErrorDialog',
|
||||||
|
t('tun.permissions.failed'),
|
||||||
|
String(error)
|
||||||
|
)
|
||||||
updateTrayIconImmediate(sysProxyEnabled, false)
|
updateTrayIconImmediate(sysProxyEnabled, false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -69,7 +75,11 @@ const TunSwitcher: React.FC<Props> = (props) => {
|
|||||||
await window.electron.ipcRenderer.invoke('requestTunPermissions')
|
await window.electron.ipcRenderer.invoke('requestTunPermissions')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Permission grant failed:', error)
|
console.warn('Permission grant failed:', error)
|
||||||
await window.electron.ipcRenderer.invoke('showErrorDialog', t('tun.permissions.failed'), String(error))
|
await window.electron.ipcRenderer.invoke(
|
||||||
|
'showErrorDialog',
|
||||||
|
t('tun.permissions.failed'),
|
||||||
|
String(error)
|
||||||
|
)
|
||||||
updateTrayIconImmediate(sysProxyEnabled, false)
|
updateTrayIconImmediate(sysProxyEnabled, false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,12 @@
|
|||||||
import { Button, Code, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from '@heroui/react'
|
import {
|
||||||
|
Button,
|
||||||
|
Code,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalContent,
|
||||||
|
ModalFooter,
|
||||||
|
ModalHeader
|
||||||
|
} from '@heroui/react'
|
||||||
import { toast } from '@renderer/components/base/toast'
|
import { toast } from '@renderer/components/base/toast'
|
||||||
import ReactMarkdown from 'react-markdown'
|
import ReactMarkdown from 'react-markdown'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
|
|||||||
@ -30,8 +30,18 @@ const Connections: React.FC = () => {
|
|||||||
connectionOrderBy = 'time',
|
connectionOrderBy = 'time',
|
||||||
connectionViewMode = 'list',
|
connectionViewMode = 'list',
|
||||||
connectionTableColumns = [
|
connectionTableColumns = [
|
||||||
'status', 'establishTime', 'type', 'host', 'process', 'rule',
|
'status',
|
||||||
'proxyChain', 'remoteDestination', 'uploadSpeed', 'downloadSpeed', 'upload', 'download'
|
'establishTime',
|
||||||
|
'type',
|
||||||
|
'host',
|
||||||
|
'process',
|
||||||
|
'rule',
|
||||||
|
'proxyChain',
|
||||||
|
'remoteDestination',
|
||||||
|
'uploadSpeed',
|
||||||
|
'downloadSpeed',
|
||||||
|
'upload',
|
||||||
|
'download'
|
||||||
],
|
],
|
||||||
connectionTableColumnWidths,
|
connectionTableColumnWidths,
|
||||||
connectionTableSortColumn,
|
connectionTableSortColumn,
|
||||||
@ -64,21 +74,28 @@ const Connections: React.FC = () => {
|
|||||||
)
|
)
|
||||||
}, [selected, activeConnections, closedConnections])
|
}, [selected, activeConnections, closedConnections])
|
||||||
|
|
||||||
const handleColumnWidthChange = useCallback(async (widths: Record<string, number>) => {
|
const handleColumnWidthChange = useCallback(
|
||||||
|
async (widths: Record<string, number>) => {
|
||||||
await patchAppConfig({ connectionTableColumnWidths: widths })
|
await patchAppConfig({ connectionTableColumnWidths: widths })
|
||||||
}, [patchAppConfig])
|
},
|
||||||
|
[patchAppConfig]
|
||||||
|
)
|
||||||
|
|
||||||
const handleSortChange = useCallback(async (column: string | null, direction: 'asc' | 'desc') => {
|
const handleSortChange = useCallback(
|
||||||
|
async (column: string | null, direction: 'asc' | 'desc') => {
|
||||||
await patchAppConfig({
|
await patchAppConfig({
|
||||||
connectionTableSortColumn: column || undefined,
|
connectionTableSortColumn: column || undefined,
|
||||||
connectionTableSortDirection: direction
|
connectionTableSortDirection: direction
|
||||||
})
|
})
|
||||||
}, [patchAppConfig])
|
},
|
||||||
|
[patchAppConfig]
|
||||||
|
)
|
||||||
|
|
||||||
const filteredConnections = useMemo(() => {
|
const filteredConnections = useMemo(() => {
|
||||||
const connections = tab === 'active' ? activeConnections : closedConnections
|
const connections = tab === 'active' ? activeConnections : closedConnections
|
||||||
|
|
||||||
const filtered = filter === ''
|
const filtered =
|
||||||
|
filter === ''
|
||||||
? connections
|
? connections
|
||||||
: connections.filter((connection) => {
|
: connections.filter((connection) => {
|
||||||
const raw = JSON.stringify(connection)
|
const raw = JSON.stringify(connection)
|
||||||
@ -110,15 +127,26 @@ const Connections: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return filtered
|
return filtered
|
||||||
}, [activeConnections, closedConnections, tab, filter, connectionDirection, connectionOrderBy, viewMode])
|
}, [
|
||||||
|
activeConnections,
|
||||||
|
closedConnections,
|
||||||
|
tab,
|
||||||
|
filter,
|
||||||
|
connectionDirection,
|
||||||
|
connectionOrderBy,
|
||||||
|
viewMode
|
||||||
|
])
|
||||||
|
|
||||||
const closeAllConnections = useCallback((): void => {
|
const closeAllConnections = useCallback((): void => {
|
||||||
tab === 'active' ? mihomoCloseAllConnections() : trashAllClosedConnection()
|
tab === 'active' ? mihomoCloseAllConnections() : trashAllClosedConnection()
|
||||||
}, [tab])
|
}, [tab])
|
||||||
|
|
||||||
const closeConnection = useCallback((id: string): void => {
|
const closeConnection = useCallback(
|
||||||
|
(id: string): void => {
|
||||||
tab === 'active' ? mihomoCloseConnection(id) : trashClosedConnection(id)
|
tab === 'active' ? mihomoCloseConnection(id) : trashClosedConnection(id)
|
||||||
}, [tab])
|
},
|
||||||
|
[tab]
|
||||||
|
)
|
||||||
|
|
||||||
const trashAllClosedConnection = (): void => {
|
const trashAllClosedConnection = (): void => {
|
||||||
setClosedConnections((closedConns) => {
|
setClosedConnections((closedConns) => {
|
||||||
@ -211,7 +239,11 @@ const Connections: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
className="app-nodrag ml-1"
|
className="app-nodrag ml-1"
|
||||||
title={viewMode === 'list' ? t('connections.table.switchToTable') : t('connections.table.switchToList')}
|
title={
|
||||||
|
viewMode === 'list'
|
||||||
|
? t('connections.table.switchToTable')
|
||||||
|
: t('connections.table.switchToList')
|
||||||
|
}
|
||||||
isIconOnly
|
isIconOnly
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="light"
|
variant="light"
|
||||||
@ -221,7 +253,11 @@ const Connections: React.FC = () => {
|
|||||||
await patchAppConfig({ connectionViewMode: newMode })
|
await patchAppConfig({ connectionViewMode: newMode })
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{viewMode === 'list' ? <MdTableChart className="text-lg" /> : <MdViewList className="text-lg" />}
|
{viewMode === 'list' ? (
|
||||||
|
<MdTableChart className="text-lg" />
|
||||||
|
) : (
|
||||||
|
<MdViewList className="text-lg" />
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className="app-nodrag ml-1"
|
className="app-nodrag ml-1"
|
||||||
@ -256,7 +292,10 @@ const Connections: React.FC = () => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{isDetailModalOpen && selectedConnection && (
|
{isDetailModalOpen && selectedConnection && (
|
||||||
<ConnectionDetailModal onClose={() => setIsDetailModalOpen(false)} connection={selectedConnection} />
|
<ConnectionDetailModal
|
||||||
|
onClose={() => setIsDetailModalOpen(false)}
|
||||||
|
connection={selectedConnection}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="overflow-x-auto sticky top-0 z-40">
|
<div className="overflow-x-auto sticky top-0 z-40">
|
||||||
<div className="flex p-2 gap-2">
|
<div className="flex p-2 gap-2">
|
||||||
@ -333,7 +372,9 @@ const Connections: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DropdownItem key="status">{t('connections.detail.status')}</DropdownItem>
|
<DropdownItem key="status">{t('connections.detail.status')}</DropdownItem>
|
||||||
<DropdownItem key="establishTime">{t('connections.detail.establishTime')}</DropdownItem>
|
<DropdownItem key="establishTime">
|
||||||
|
{t('connections.detail.establishTime')}
|
||||||
|
</DropdownItem>
|
||||||
<DropdownItem key="type">{t('connections.detail.connectionType')}</DropdownItem>
|
<DropdownItem key="type">{t('connections.detail.connectionType')}</DropdownItem>
|
||||||
<DropdownItem key="host">{t('connections.detail.host')}</DropdownItem>
|
<DropdownItem key="host">{t('connections.detail.host')}</DropdownItem>
|
||||||
<DropdownItem key="sniffHost">{t('connections.detail.sniffHost')}</DropdownItem>
|
<DropdownItem key="sniffHost">{t('connections.detail.sniffHost')}</DropdownItem>
|
||||||
@ -343,7 +384,9 @@ const Connections: React.FC = () => {
|
|||||||
<DropdownItem key="proxyChain">{t('connections.detail.proxyChain')}</DropdownItem>
|
<DropdownItem key="proxyChain">{t('connections.detail.proxyChain')}</DropdownItem>
|
||||||
<DropdownItem key="sourceIP">{t('connections.detail.sourceIP')}</DropdownItem>
|
<DropdownItem key="sourceIP">{t('connections.detail.sourceIP')}</DropdownItem>
|
||||||
<DropdownItem key="sourcePort">{t('connections.detail.sourcePort')}</DropdownItem>
|
<DropdownItem key="sourcePort">{t('connections.detail.sourcePort')}</DropdownItem>
|
||||||
<DropdownItem key="destinationPort">{t('connections.detail.destinationPort')}</DropdownItem>
|
<DropdownItem key="destinationPort">
|
||||||
|
{t('connections.detail.destinationPort')}
|
||||||
|
</DropdownItem>
|
||||||
<DropdownItem key="inboundIP">{t('connections.detail.inboundIP')}</DropdownItem>
|
<DropdownItem key="inboundIP">{t('connections.detail.inboundIP')}</DropdownItem>
|
||||||
<DropdownItem key="inboundPort">{t('connections.detail.inboundPort')}</DropdownItem>
|
<DropdownItem key="inboundPort">{t('connections.detail.inboundPort')}</DropdownItem>
|
||||||
<DropdownItem key="uploadSpeed">{t('connections.uploadSpeed')}</DropdownItem>
|
<DropdownItem key="uploadSpeed">{t('connections.uploadSpeed')}</DropdownItem>
|
||||||
@ -351,7 +394,9 @@ const Connections: React.FC = () => {
|
|||||||
<DropdownItem key="upload">{t('connections.uploadAmount')}</DropdownItem>
|
<DropdownItem key="upload">{t('connections.uploadAmount')}</DropdownItem>
|
||||||
<DropdownItem key="download">{t('connections.downloadAmount')}</DropdownItem>
|
<DropdownItem key="download">{t('connections.downloadAmount')}</DropdownItem>
|
||||||
<DropdownItem key="dscp">{t('connections.detail.dscp')}</DropdownItem>
|
<DropdownItem key="dscp">{t('connections.detail.dscp')}</DropdownItem>
|
||||||
<DropdownItem key="remoteDestination">{t('connections.detail.remoteDestination')}</DropdownItem>
|
<DropdownItem key="remoteDestination">
|
||||||
|
{t('connections.detail.remoteDestination')}
|
||||||
|
</DropdownItem>
|
||||||
<DropdownItem key="dnsMode">{t('connections.detail.dnsMode')}</DropdownItem>
|
<DropdownItem key="dnsMode">{t('connections.detail.dnsMode')}</DropdownItem>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|||||||
@ -122,12 +122,7 @@ const Logs: React.FC = () => {
|
|||||||
ref={virtuosoRef}
|
ref={virtuosoRef}
|
||||||
data={filteredLogs}
|
data={filteredLogs}
|
||||||
itemContent={(i, log) => (
|
itemContent={(i, log) => (
|
||||||
<LogItem
|
<LogItem index={i} time={log.time} type={log.type} payload={log.payload} />
|
||||||
index={i}
|
|
||||||
time={log.time}
|
|
||||||
type={log.type}
|
|
||||||
payload={log.payload}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,4 +1,20 @@
|
|||||||
import { Button, Divider, Input, Select, SelectItem, Switch, Tooltip, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, useDisclosure, Spinner, Chip } from '@heroui/react'
|
import {
|
||||||
|
Button,
|
||||||
|
Divider,
|
||||||
|
Input,
|
||||||
|
Select,
|
||||||
|
SelectItem,
|
||||||
|
Switch,
|
||||||
|
Tooltip,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
ModalHeader,
|
||||||
|
ModalBody,
|
||||||
|
ModalFooter,
|
||||||
|
useDisclosure,
|
||||||
|
Spinner,
|
||||||
|
Chip
|
||||||
|
} from '@heroui/react'
|
||||||
import BasePage from '@renderer/components/base/base-page'
|
import BasePage from '@renderer/components/base/base-page'
|
||||||
import { toast } from '@renderer/components/base/toast'
|
import { toast } from '@renderer/components/base/toast'
|
||||||
import { showError } from '@renderer/utils/error-display'
|
import { showError } from '@renderer/utils/error-display'
|
||||||
@ -8,7 +24,12 @@ import { useAppConfig } from '@renderer/hooks/use-app-config'
|
|||||||
import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config'
|
import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config'
|
||||||
import { platform } from '@renderer/utils/init'
|
import { platform } from '@renderer/utils/init'
|
||||||
import { FaNetworkWired } from 'react-icons/fa'
|
import { FaNetworkWired } from 'react-icons/fa'
|
||||||
import { IoMdCloudDownload, IoMdInformationCircleOutline, IoMdRefresh, IoMdShuffle } from 'react-icons/io'
|
import {
|
||||||
|
IoMdCloudDownload,
|
||||||
|
IoMdInformationCircleOutline,
|
||||||
|
IoMdRefresh,
|
||||||
|
IoMdShuffle
|
||||||
|
} from 'react-icons/io'
|
||||||
import PubSub from 'pubsub-js'
|
import PubSub from 'pubsub-js'
|
||||||
import {
|
import {
|
||||||
mihomoUpgrade,
|
mihomoUpgrade,
|
||||||
@ -102,7 +123,7 @@ const Mihomo: React.FC = () => {
|
|||||||
const [upgrading, setUpgrading] = useState(false)
|
const [upgrading, setUpgrading] = useState(false)
|
||||||
const [lanOpen, setLanOpen] = useState(false)
|
const [lanOpen, setLanOpen] = useState(false)
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||||
const [tags, setTags] = useState<{name: string, zipball_url: string, tarball_url: string}[]>([])
|
const [tags, setTags] = useState<{ name: string; zipball_url: string; tarball_url: string }[]>([])
|
||||||
const [loadingTags, setLoadingTags] = useState(false)
|
const [loadingTags, setLoadingTags] = useState(false)
|
||||||
const [selectedTag, setSelectedTag] = useState(specificVersion || '')
|
const [selectedTag, setSelectedTag] = useState(specificVersion || '')
|
||||||
const [installing, setInstalling] = useState(false)
|
const [installing, setInstalling] = useState(false)
|
||||||
@ -197,10 +218,7 @@ const Mihomo: React.FC = () => {
|
|||||||
|
|
||||||
// 打开WebUI面板
|
// 打开WebUI面板
|
||||||
const openWebUI = (panel: WebUIPanel) => {
|
const openWebUI = (panel: WebUIPanel) => {
|
||||||
const url = panel.url
|
const url = panel.url.replace('%host', host).replace('%port', port).replace('%secret', secret)
|
||||||
.replace('%host', host)
|
|
||||||
.replace('%port', port)
|
|
||||||
.replace('%secret', secret)
|
|
||||||
window.open(url, '_blank')
|
window.open(url, '_blank')
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -222,10 +240,8 @@ const Mihomo: React.FC = () => {
|
|||||||
// 更新面板
|
// 更新面板
|
||||||
const updatePanel = () => {
|
const updatePanel = () => {
|
||||||
if (editingPanel && newPanelName && newPanelUrl) {
|
if (editingPanel && newPanelName && newPanelUrl) {
|
||||||
const updatedPanels = allPanels.map(panel =>
|
const updatedPanels = allPanels.map((panel) =>
|
||||||
panel.id === editingPanel.id
|
panel.id === editingPanel.id ? { ...panel, name: newPanelName, url: newPanelUrl } : panel
|
||||||
? { ...panel, name: newPanelName, url: newPanelUrl }
|
|
||||||
: panel
|
|
||||||
)
|
)
|
||||||
setAllPanels(updatedPanels)
|
setAllPanels(updatedPanels)
|
||||||
setEditingPanel(null)
|
setEditingPanel(null)
|
||||||
@ -236,7 +252,7 @@ const Mihomo: React.FC = () => {
|
|||||||
|
|
||||||
// 删除面板
|
// 删除面板
|
||||||
const deletePanel = (id: string) => {
|
const deletePanel = (id: string) => {
|
||||||
setAllPanels(allPanels.filter(panel => panel.id !== id))
|
setAllPanels(allPanels.filter((panel) => panel.id !== id))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 开始编辑面板
|
// 开始编辑面板
|
||||||
@ -280,7 +296,7 @@ const Mihomo: React.FC = () => {
|
|||||||
|
|
||||||
// 可点击的变量标签组件
|
// 可点击的变量标签组件
|
||||||
const ClickableVariableTag: React.FC<{
|
const ClickableVariableTag: React.FC<{
|
||||||
variable: string;
|
variable: string
|
||||||
onClick: (variable: string) => void
|
onClick: (variable: string) => void
|
||||||
}> = ({ variable, onClick }) => {
|
}> = ({ variable, onClick }) => {
|
||||||
return (
|
return (
|
||||||
@ -385,7 +401,7 @@ const Mihomo: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 过滤标签
|
// 过滤标签
|
||||||
const filteredTags = tags.filter(tag =>
|
const filteredTags = tags.filter((tag) =>
|
||||||
tag.name.toLowerCase().includes(searchTerm.toLowerCase())
|
tag.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -402,15 +418,14 @@ const Mihomo: React.FC = () => {
|
|||||||
<BasePage title={t('mihomo.title')}>
|
<BasePage title={t('mihomo.title')}>
|
||||||
{/* Smart 内核设置 */}
|
{/* Smart 内核设置 */}
|
||||||
<SettingCard>
|
<SettingCard>
|
||||||
<div className={`rounded-md border p-2 transition-all duration-200 ${
|
<div
|
||||||
|
className={`rounded-md border p-2 transition-all duration-200 ${
|
||||||
enableSmartCore
|
enableSmartCore
|
||||||
? 'border-blue-300 bg-blue-50/30 dark:border-blue-700 dark:bg-blue-950/20'
|
? 'border-blue-300 bg-blue-50/30 dark:border-blue-700 dark:bg-blue-950/20'
|
||||||
: 'border-gray-300 bg-gray-50/30 dark:border-gray-600 dark:bg-gray-800/20'
|
: 'border-gray-300 bg-gray-50/30 dark:border-gray-600 dark:bg-gray-800/20'
|
||||||
}`}>
|
}`}
|
||||||
<SettingItem
|
|
||||||
title={t('mihomo.enableSmartCore')}
|
|
||||||
divider
|
|
||||||
>
|
>
|
||||||
|
<SettingItem title={t('mihomo.enableSmartCore')} divider>
|
||||||
<Switch
|
<Switch
|
||||||
size="sm"
|
size="sm"
|
||||||
isSelected={enableSmartCore}
|
isSelected={enableSmartCore}
|
||||||
@ -499,11 +514,7 @@ const Mihomo: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<IoMdCloudDownload className="text-lg" />
|
<IoMdCloudDownload className="text-lg" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button size="sm" variant="light" onPress={handleOpenModal}>
|
||||||
size="sm"
|
|
||||||
variant="light"
|
|
||||||
onPress={handleOpenModal}
|
|
||||||
>
|
|
||||||
{t('mihomo.selectSpecificVersion')}
|
{t('mihomo.selectSpecificVersion')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@ -519,12 +530,14 @@ const Mihomo: React.FC = () => {
|
|||||||
className="w-[150px]"
|
className="w-[150px]"
|
||||||
size="sm"
|
size="sm"
|
||||||
aria-label={t('mihomo.selectCoreVersion')}
|
aria-label={t('mihomo.selectCoreVersion')}
|
||||||
selectedKeys={new Set([
|
selectedKeys={new Set([core])}
|
||||||
core
|
|
||||||
])}
|
|
||||||
disallowEmptySelection={true}
|
disallowEmptySelection={true}
|
||||||
onSelectionChange={async (v) => {
|
onSelectionChange={async (v) => {
|
||||||
const selectedCore = v.currentKey as 'mihomo' | 'mihomo-alpha' | 'mihomo-smart' | 'mihomo-specific'
|
const selectedCore = v.currentKey as
|
||||||
|
| 'mihomo'
|
||||||
|
| 'mihomo-alpha'
|
||||||
|
| 'mihomo-smart'
|
||||||
|
| 'mihomo-specific'
|
||||||
// 如果切换到特定版本但没有设置specificVersion,则打开选择模态框
|
// 如果切换到特定版本但没有设置specificVersion,则打开选择模态框
|
||||||
if (selectedCore === 'mihomo-specific' && !specificVersion) {
|
if (selectedCore === 'mihomo-specific' && !specificVersion) {
|
||||||
handleOpenModal()
|
handleOpenModal()
|
||||||
@ -597,7 +610,6 @@ const Mihomo: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
|
|
||||||
|
|
||||||
<SettingItem
|
<SettingItem
|
||||||
title={
|
title={
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@ -635,11 +647,11 @@ const Mihomo: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
|
|
||||||
<SettingItem
|
<SettingItem title={t('mihomo.smartCoreStrategy')}>
|
||||||
title={t('mihomo.smartCoreStrategy')}
|
|
||||||
>
|
|
||||||
<Select
|
<Select
|
||||||
classNames={{ trigger: 'data-[hover=true]:bg-blue-100 dark:data-[hover=true]:bg-blue-900/50' }}
|
classNames={{
|
||||||
|
trigger: 'data-[hover=true]:bg-blue-100 dark:data-[hover=true]:bg-blue-900/50'
|
||||||
|
}}
|
||||||
className="w-[150px]"
|
className="w-[150px]"
|
||||||
size="sm"
|
size="sm"
|
||||||
aria-label={t('mihomo.smartCoreStrategy')}
|
aria-label={t('mihomo.smartCoreStrategy')}
|
||||||
@ -651,8 +663,12 @@ const Mihomo: React.FC = () => {
|
|||||||
await restartCore()
|
await restartCore()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectItem key="sticky-sessions">{t('mihomo.smartCoreStrategyStickySession')}</SelectItem>
|
<SelectItem key="sticky-sessions">
|
||||||
<SelectItem key="round-robin">{t('mihomo.smartCoreStrategyRoundRobin')}</SelectItem>
|
{t('mihomo.smartCoreStrategyStickySession')}
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem key="round-robin">
|
||||||
|
{t('mihomo.smartCoreStrategyRoundRobin')}
|
||||||
|
</SelectItem>
|
||||||
</Select>
|
</Select>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
</>
|
</>
|
||||||
@ -1364,7 +1380,7 @@ const Mihomo: React.FC = () => {
|
|||||||
<SelectItem key="debug">{t('mihomo.debug')}</SelectItem>
|
<SelectItem key="debug">{t('mihomo.debug')}</SelectItem>
|
||||||
</Select>
|
</Select>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
<SettingItem title={t('mihomo.findProcess')} >
|
<SettingItem title={t('mihomo.findProcess')}>
|
||||||
<Select
|
<Select
|
||||||
classNames={{ trigger: 'data-[hover=true]:bg-default-200' }}
|
classNames={{ trigger: 'data-[hover=true]:bg-default-200' }}
|
||||||
className="w-[100px]"
|
className="w-[100px]"
|
||||||
@ -1395,9 +1411,7 @@ const Mihomo: React.FC = () => {
|
|||||||
hideCloseButton
|
hideCloseButton
|
||||||
>
|
>
|
||||||
<ModalContent className="h-full w-[calc(100%-100px)]">
|
<ModalContent className="h-full w-[calc(100%-100px)]">
|
||||||
<ModalHeader className="flex pb-0 app-drag">
|
<ModalHeader className="flex pb-0 app-drag">{t('settings.webui.manage')}</ModalHeader>
|
||||||
{t('settings.webui.manage')}
|
|
||||||
</ModalHeader>
|
|
||||||
<ModalBody className="flex flex-col h-full">
|
<ModalBody className="flex flex-col h-full">
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
{/* 添加/编辑面板表单 */}
|
{/* 添加/编辑面板表单 */}
|
||||||
@ -1432,12 +1446,7 @@ const Mihomo: React.FC = () => {
|
|||||||
>
|
>
|
||||||
{t('common.save')}
|
{t('common.save')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button size="sm" color="default" variant="bordered" onPress={cancelEditing}>
|
||||||
size="sm"
|
|
||||||
color="default"
|
|
||||||
variant="bordered"
|
|
||||||
onPress={cancelEditing}
|
|
||||||
>
|
|
||||||
{t('common.cancel')}
|
{t('common.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
@ -1465,19 +1474,17 @@ const Mihomo: React.FC = () => {
|
|||||||
{/* 面板列表 */}
|
{/* 面板列表 */}
|
||||||
<div className="flex flex-col gap-2 mt-2 overflow-y-auto flex-grow">
|
<div className="flex flex-col gap-2 mt-2 overflow-y-auto flex-grow">
|
||||||
<h3 className="text-lg font-semibold">{t('settings.webui.panels')}</h3>
|
<h3 className="text-lg font-semibold">{t('settings.webui.panels')}</h3>
|
||||||
{allPanels.map(panel => (
|
{allPanels.map((panel) => (
|
||||||
<div key={panel.id} className="flex items-start justify-between p-3 bg-default-50 rounded-lg flex-shrink-0">
|
<div
|
||||||
|
key={panel.id}
|
||||||
|
className="flex items-start justify-between p-3 bg-default-50 rounded-lg flex-shrink-0"
|
||||||
|
>
|
||||||
<div className="flex-1 mr-2">
|
<div className="flex-1 mr-2">
|
||||||
<p className="font-medium">{panel.name}</p>
|
<p className="font-medium">{panel.name}</p>
|
||||||
<HighlightedUrl url={panel.url} />
|
<HighlightedUrl url={panel.url} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button isIconOnly size="sm" color="primary" onPress={() => openWebUI(panel)}>
|
||||||
isIconOnly
|
|
||||||
size="sm"
|
|
||||||
color="primary"
|
|
||||||
onPress={() => openWebUI(panel)}
|
|
||||||
>
|
|
||||||
<MdOpenInNew />
|
<MdOpenInNew />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@ -1503,10 +1510,7 @@ const Mihomo: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter className="pt-0">
|
<ModalFooter className="pt-0">
|
||||||
<Button
|
<Button color="primary" onPress={() => setIsWebUIModalOpen(false)}>
|
||||||
color="primary"
|
|
||||||
onPress={() => setIsWebUIModalOpen(false)}
|
|
||||||
>
|
|
||||||
{t('common.close')}
|
{t('common.close')}
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
|
|||||||
@ -375,7 +375,11 @@ const Profiles: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{subStoreMenuItems.map((item) => (
|
{subStoreMenuItems.map((item) => (
|
||||||
<DropdownItem startContent={item?.icon} key={item.key} showDivider={item.divider}>
|
<DropdownItem
|
||||||
|
startContent={item?.icon}
|
||||||
|
key={item.key}
|
||||||
|
showDivider={item.divider}
|
||||||
|
>
|
||||||
{item.children}
|
{item.children}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -26,12 +26,14 @@ const GROUP_EXPAND_STATE_KEY = 'proxy_group_expand_state'
|
|||||||
const SCROLL_POSITION_KEY = 'proxy_scroll_position'
|
const SCROLL_POSITION_KEY = 'proxy_scroll_position'
|
||||||
|
|
||||||
// 自定义 hook 用于管理展开状态
|
// 自定义 hook 用于管理展开状态
|
||||||
const useProxyState = (groups: IMihomoMixedGroup[]): {
|
const useProxyState = (
|
||||||
virtuosoRef: React.RefObject<GroupedVirtuosoHandle | null>;
|
groups: IMihomoMixedGroup[]
|
||||||
isOpen: boolean[];
|
): {
|
||||||
setIsOpen: React.Dispatch<React.SetStateAction<boolean[]>>;
|
virtuosoRef: React.RefObject<GroupedVirtuosoHandle | null>
|
||||||
initialTopMostItemIndex: number;
|
isOpen: boolean[]
|
||||||
handleRangeChanged: (range: { startIndex: number }) => void;
|
setIsOpen: React.Dispatch<React.SetStateAction<boolean[]>>
|
||||||
|
initialTopMostItemIndex: number
|
||||||
|
handleRangeChanged: (range: { startIndex: number }) => void
|
||||||
} => {
|
} => {
|
||||||
const virtuosoRef = useRef<GroupedVirtuosoHandle | null>(null)
|
const virtuosoRef = useRef<GroupedVirtuosoHandle | null>(null)
|
||||||
|
|
||||||
@ -101,7 +103,8 @@ const Proxies: React.FC = () => {
|
|||||||
} = appConfig || {}
|
} = appConfig || {}
|
||||||
|
|
||||||
const [cols, setCols] = useState(1)
|
const [cols, setCols] = useState(1)
|
||||||
const { virtuosoRef, isOpen, setIsOpen, initialTopMostItemIndex, handleRangeChanged } = useProxyState(groups)
|
const { virtuosoRef, isOpen, setIsOpen, initialTopMostItemIndex, handleRangeChanged } =
|
||||||
|
useProxyState(groups)
|
||||||
const [delaying, setDelaying] = useState(Array(groups.length).fill(false))
|
const [delaying, setDelaying] = useState(Array(groups.length).fill(false))
|
||||||
const [proxyDelaying, setProxyDelaying] = useState<Set<string>>(new Set())
|
const [proxyDelaying, setProxyDelaying] = useState<Set<string>>(new Set())
|
||||||
const [searchValue, setSearchValue] = useState(Array(groups.length).fill(''))
|
const [searchValue, setSearchValue] = useState(Array(groups.length).fill(''))
|
||||||
@ -153,19 +156,23 @@ const Proxies: React.FC = () => {
|
|||||||
return { groupCounts, allProxies }
|
return { groupCounts, allProxies }
|
||||||
}, [groups, isOpen, proxyDisplayOrder, cols, searchValue, sortProxies])
|
}, [groups, isOpen, proxyDisplayOrder, cols, searchValue, sortProxies])
|
||||||
|
|
||||||
const onChangeProxy = useCallback(async (group: string, proxy: string): Promise<void> => {
|
const onChangeProxy = useCallback(
|
||||||
|
async (group: string, proxy: string): Promise<void> => {
|
||||||
await mihomoChangeProxy(group, proxy)
|
await mihomoChangeProxy(group, proxy)
|
||||||
if (autoCloseConnection) {
|
if (autoCloseConnection) {
|
||||||
await mihomoCloseAllConnections()
|
await mihomoCloseAllConnections()
|
||||||
}
|
}
|
||||||
mutate()
|
mutate()
|
||||||
}, [autoCloseConnection, mutate])
|
},
|
||||||
|
[autoCloseConnection, mutate]
|
||||||
|
)
|
||||||
|
|
||||||
const onProxyDelay = useCallback(async (proxy: string, url?: string): Promise<IMihomoDelay> => {
|
const onProxyDelay = useCallback(async (proxy: string, url?: string): Promise<IMihomoDelay> => {
|
||||||
return await mihomoProxyDelay(proxy, url)
|
return await mihomoProxyDelay(proxy, url)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const onGroupDelay = useCallback(async (index: number): Promise<void> => {
|
const onGroupDelay = useCallback(
|
||||||
|
async (index: number): Promise<void> => {
|
||||||
if (allProxies[index].length === 0) {
|
if (allProxies[index].length === 0) {
|
||||||
setIsOpen((prev) => {
|
setIsOpen((prev) => {
|
||||||
const newOpen = [...prev]
|
const newOpen = [...prev]
|
||||||
@ -183,7 +190,7 @@ const Proxies: React.FC = () => {
|
|||||||
const groupProxies = allProxies[index]
|
const groupProxies = allProxies[index]
|
||||||
setProxyDelaying((prev) => {
|
setProxyDelaying((prev) => {
|
||||||
const newSet = new Set(prev)
|
const newSet = new Set(prev)
|
||||||
groupProxies.forEach(proxy => newSet.add(proxy.name))
|
groupProxies.forEach((proxy) => newSet.add(proxy.name))
|
||||||
return newSet
|
return newSet
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -226,11 +233,13 @@ const Proxies: React.FC = () => {
|
|||||||
// 状态清理
|
// 状态清理
|
||||||
setProxyDelaying((prev) => {
|
setProxyDelaying((prev) => {
|
||||||
const newSet = new Set(prev)
|
const newSet = new Set(prev)
|
||||||
groupProxies.forEach(proxy => newSet.delete(proxy.name))
|
groupProxies.forEach((proxy) => newSet.delete(proxy.name))
|
||||||
return newSet
|
return newSet
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [allProxies, groups, delayTestConcurrency, mutate, setIsOpen])
|
},
|
||||||
|
[allProxies, groups, delayTestConcurrency, mutate, setIsOpen]
|
||||||
|
)
|
||||||
|
|
||||||
const calcCols = useCallback((): number => {
|
const calcCols = useCallback((): number => {
|
||||||
if (proxyCols !== 'auto') {
|
if (proxyCols !== 'auto') {
|
||||||
@ -281,7 +290,8 @@ const Proxies: React.FC = () => {
|
|||||||
loadImages()
|
loadImages()
|
||||||
}, [groups, mutate])
|
}, [groups, mutate])
|
||||||
|
|
||||||
const renderGroupContent = useCallback((index: number) => {
|
const renderGroupContent = useCallback(
|
||||||
|
(index: number) => {
|
||||||
return groups[index] ? (
|
return groups[index] ? (
|
||||||
<div
|
<div
|
||||||
className={`w-full pt-2 ${index === groupCounts.length - 1 && !isOpen[index] ? 'pb-2' : ''} px-2`}
|
className={`w-full pt-2 ${index === groupCounts.length - 1 && !isOpen[index] ? 'pb-2' : ''} px-2`}
|
||||||
@ -370,9 +380,8 @@ const Proxies: React.FC = () => {
|
|||||||
i += groupCounts[j]
|
i += groupCounts[j]
|
||||||
}
|
}
|
||||||
i += Math.floor(
|
i += Math.floor(
|
||||||
allProxies[index].findIndex(
|
allProxies[index].findIndex((proxy) => proxy.name === groups[index].now) /
|
||||||
(proxy) => proxy.name === groups[index].now
|
cols
|
||||||
) / cols
|
|
||||||
)
|
)
|
||||||
virtuosoRef.current?.scrollToIndex({
|
virtuosoRef.current?.scrollToIndex({
|
||||||
index: Math.floor(i),
|
index: Math.floor(i),
|
||||||
@ -405,9 +414,25 @@ const Proxies: React.FC = () => {
|
|||||||
) : (
|
) : (
|
||||||
<div>Never See This</div>
|
<div>Never See This</div>
|
||||||
)
|
)
|
||||||
}, [groups, groupCounts, isOpen, proxyDisplayMode, searchValue, delaying, cols, allProxies, virtuosoRef, t, setIsOpen, onGroupDelay])
|
},
|
||||||
|
[
|
||||||
|
groups,
|
||||||
|
groupCounts,
|
||||||
|
isOpen,
|
||||||
|
proxyDisplayMode,
|
||||||
|
searchValue,
|
||||||
|
delaying,
|
||||||
|
cols,
|
||||||
|
allProxies,
|
||||||
|
virtuosoRef,
|
||||||
|
t,
|
||||||
|
setIsOpen,
|
||||||
|
onGroupDelay
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
const renderItemContent = useCallback((index: number, groupIndex: number) => {
|
const renderItemContent = useCallback(
|
||||||
|
(index: number, groupIndex: number) => {
|
||||||
let innerIndex = index
|
let innerIndex = index
|
||||||
groupCounts.slice(0, groupIndex).forEach((count) => {
|
groupCounts.slice(0, groupIndex).forEach((count) => {
|
||||||
innerIndex -= count
|
innerIndex -= count
|
||||||
@ -433,10 +458,11 @@ const Proxies: React.FC = () => {
|
|||||||
group={groups[groupIndex]}
|
group={groups[groupIndex]}
|
||||||
proxyDisplayMode={proxyDisplayMode}
|
proxyDisplayMode={proxyDisplayMode}
|
||||||
selected={
|
selected={
|
||||||
allProxies[groupIndex][innerIndex * cols + i]?.name ===
|
allProxies[groupIndex][innerIndex * cols + i]?.name === groups[groupIndex].now
|
||||||
groups[groupIndex].now
|
|
||||||
}
|
}
|
||||||
isGroupTesting={proxyDelaying.has(allProxies[groupIndex][innerIndex * cols + i].name)}
|
isGroupTesting={proxyDelaying.has(
|
||||||
|
allProxies[groupIndex][innerIndex * cols + i].name
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
@ -444,7 +470,20 @@ const Proxies: React.FC = () => {
|
|||||||
) : (
|
) : (
|
||||||
<div>Never See This</div>
|
<div>Never See This</div>
|
||||||
)
|
)
|
||||||
}, [groupCounts, allProxies, proxyCols, cols, groups, proxyDisplayMode, proxyDelaying, mutate, onProxyDelay, onChangeProxy])
|
},
|
||||||
|
[
|
||||||
|
groupCounts,
|
||||||
|
allProxies,
|
||||||
|
proxyCols,
|
||||||
|
cols,
|
||||||
|
groups,
|
||||||
|
proxyDisplayMode,
|
||||||
|
proxyDelaying,
|
||||||
|
mutate,
|
||||||
|
onProxyDelay,
|
||||||
|
onChangeProxy
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BasePage
|
<BasePage
|
||||||
|
|||||||
@ -10,12 +10,16 @@ function ipcErrorWrapper(response: any): any {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GitHub版本管理相关IPC调用
|
// GitHub版本管理相关IPC调用
|
||||||
export async function fetchMihomoTags(forceRefresh = false): Promise<{name: string, zipball_url: string, tarball_url: string}[]> {
|
export async function fetchMihomoTags(
|
||||||
|
forceRefresh = false
|
||||||
|
): Promise<{ name: string; zipball_url: string; tarball_url: string }[]> {
|
||||||
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('fetchMihomoTags', forceRefresh))
|
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('fetchMihomoTags', forceRefresh))
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function installSpecificMihomoCore(version: string): Promise<void> {
|
export async function installSpecificMihomoCore(version: string): Promise<void> {
|
||||||
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('installSpecificMihomoCore', version))
|
return ipcErrorWrapper(
|
||||||
|
await window.electron.ipcRenderer.invoke('installSpecificMihomoCore', version)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function clearMihomoVersionCache(): Promise<void> {
|
export async function clearMihomoVersionCache(): Promise<void> {
|
||||||
@ -101,11 +105,15 @@ export async function mihomoGroupDelay(group: string, url?: string): Promise<IMi
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function mihomoSmartGroupWeights(groupName: string): Promise<Record<string, number>> {
|
export async function mihomoSmartGroupWeights(groupName: string): Promise<Record<string, number>> {
|
||||||
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('mihomoSmartGroupWeights', groupName))
|
return ipcErrorWrapper(
|
||||||
|
await window.electron.ipcRenderer.invoke('mihomoSmartGroupWeights', groupName)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function mihomoSmartFlushCache(configName?: string): Promise<void> {
|
export async function mihomoSmartFlushCache(configName?: string): Promise<void> {
|
||||||
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('mihomoSmartFlushCache', configName))
|
return ipcErrorWrapper(
|
||||||
|
await window.electron.ipcRenderer.invoke('mihomoSmartFlushCache', configName)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSmartOverrideContent(): Promise<string | null> {
|
export async function getSmartOverrideContent(): Promise<string | null> {
|
||||||
@ -205,7 +213,9 @@ export async function setProfileStr(id: string, str: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function convertMrsRuleset(path: string, behavior: string): Promise<string> {
|
export async function convertMrsRuleset(path: string, behavior: string): Promise<string> {
|
||||||
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('convertMrsRuleset', path, behavior))
|
return ipcErrorWrapper(
|
||||||
|
await window.electron.ipcRenderer.invoke('convertMrsRuleset', path, behavior)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getOverrideConfig(force = false): Promise<IOverrideConfig> {
|
export async function getOverrideConfig(force = false): Promise<IOverrideConfig> {
|
||||||
@ -281,7 +291,9 @@ export async function showTunPermissionDialog(): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function showErrorDialog(title: string, message: string): Promise<void> {
|
export async function showErrorDialog(title: string, message: string): Promise<void> {
|
||||||
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('showErrorDialog', title, message))
|
return ipcErrorWrapper(
|
||||||
|
await window.electron.ipcRenderer.invoke('showErrorDialog', title, message)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getFilePath(ext: string[]): Promise<string[] | undefined> {
|
export async function getFilePath(ext: string[]): Promise<string[] | undefined> {
|
||||||
@ -348,9 +360,7 @@ export async function webdavDelete(filename: string): Promise<void> {
|
|||||||
|
|
||||||
// WebDAV 备份调度器相关 IPC 调用
|
// WebDAV 备份调度器相关 IPC 调用
|
||||||
export async function reinitWebdavBackupScheduler(): Promise<void> {
|
export async function reinitWebdavBackupScheduler(): Promise<void> {
|
||||||
return ipcErrorWrapper(
|
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('reinitWebdavBackupScheduler'))
|
||||||
await window.electron.ipcRenderer.invoke('reinitWebdavBackupScheduler')
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 本地备份相关 IPC 调用
|
// 本地备份相关 IPC 调用
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user