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,41 +20,35 @@ 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
|
||||||
const shortCommitSha = getGitCommitHash(true)
|
const shortCommitSha = getGitCommitHash(true)
|
||||||
const commitSha = getGitCommitHash(false)
|
const commitSha = getGitCommitHash(false)
|
||||||
|
|
||||||
content = `<b>🚧 <a href="https://github.com/mihomo-party-org/clash-party/releases/tag/dev">Clash Party Dev Build</a> 开发版本发布</b>\n\n`
|
content = `<b>🚧 <a href="https://github.com/mihomo-party-org/clash-party/releases/tag/dev">Clash Party Dev Build</a> 开发版本发布</b>\n\n`
|
||||||
content += `<b>基于版本:</b> ${version}\n`
|
content += `<b>基于版本:</b> ${version}\n`
|
||||||
content += `<b>提交哈希:</b> <a href="https://github.com/mihomo-party-org/clash-party/commit/${commitSha}">${shortCommitSha}</a>\n\n`
|
content += `<b>提交哈希:</b> <a href="https://github.com/mihomo-party-org/clash-party/commit/${commitSha}">${shortCommitSha}</a>\n\n`
|
||||||
@ -78,4 +78,4 @@ await axios.post(`https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}/
|
|||||||
parse_mode: 'HTML'
|
parse_mode: 'HTML'
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log(`${isDevRelease ? '开发版本' : '正式版本'}Telegram 通知发送成功`)
|
console.log(`${isDevRelease ? '开发版本' : '正式版本'}Telegram 通知发送成功`)
|
||||||
|
|||||||
@ -10,7 +10,7 @@ function updatePackageVersion() {
|
|||||||
|
|
||||||
// 获取处理后的版本号
|
// 获取处理后的版本号
|
||||||
const newVersion = getProcessedVersion()
|
const newVersion = getProcessedVersion()
|
||||||
|
|
||||||
console.log(`当前版本: ${packageData.version}`)
|
console.log(`当前版本: ${packageData.version}`)
|
||||||
console.log(`${isDevBuild() ? 'Dev构建' : '正式构建'} - 新版本: ${newVersion}`)
|
console.log(`${isDevBuild() ? 'Dev构建' : '正式构建'} - 新版本: ${newVersion}`)
|
||||||
|
|
||||||
@ -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)
|
||||||
@ -29,4 +28,4 @@ function updatePackageVersion() {
|
|||||||
|
|
||||||
updatePackageVersion()
|
updatePackageVersion()
|
||||||
|
|
||||||
export { updatePackageVersion }
|
export { updatePackageVersion }
|
||||||
|
|||||||
@ -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.argv.includes('--dev') ||
|
process.env.NODE_ENV === 'development' ||
|
||||||
process.env.GITHUB_EVENT_NAME === 'workflow_dispatch'
|
process.argv.includes('--dev') ||
|
||||||
|
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'
|
||||||
@ -81,6 +83,6 @@ export function generateDownloadLinksMarkdown(downloadUrl, version) {
|
|||||||
links += '\n#### Linux:\n\n'
|
links += '\n#### Linux:\n\n'
|
||||||
links += `- DEB:[64位](${downloadUrl}/clash-party-linux-${version}-amd64.deb) | [ARM64](${downloadUrl}/clash-party-linux-${version}-arm64.deb)\n\n`
|
links += `- DEB:[64位](${downloadUrl}/clash-party-linux-${version}-amd64.deb) | [ARM64](${downloadUrl}/clash-party-linux-${version}-arm64.deb)\n\n`
|
||||||
links += `- RPM:[64位](${downloadUrl}/clash-party-linux-${version}-x86_64.rpm) | [ARM64](${downloadUrl}/clash-party-linux-${version}-aarch64.rpm)`
|
links += `- RPM:[64位](${downloadUrl}/clash-party-linux-${version}-x86_64.rpm) | [ARM64](${downloadUrl}/clash-party-linux-${version}-aarch64.rpm)`
|
||||||
|
|
||||||
return links
|
return links
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,12 +17,16 @@ 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确保配置包含所有必要的默认字段,处理升级场景
|
// 确保配置包含所有必要的默认字段,处理升级场景
|
||||||
controledMihomoConfig = deepMerge(defaultControledMihomoConfig, controledMihomoConfig)
|
controledMihomoConfig = deepMerge(defaultControledMihomoConfig, controledMihomoConfig)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 内核时自动应用
|
||||||
|
|
||||||
|
|||||||
@ -29,15 +29,16 @@ let runtimeConfig: IMihomoConfig
|
|||||||
function processRulesWithOffset(ruleStrings: string[], currentRules: string[], isAppend = false) {
|
function processRulesWithOffset(ruleStrings: string[], currentRules: string[], isAppend = false) {
|
||||||
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])
|
||||||
const rule = parts.slice(1).join(',')
|
const rule = parts.slice(1).join(',')
|
||||||
|
|
||||||
if (isAppend) {
|
if (isAppend) {
|
||||||
// 后置规则的插入位置计算
|
// 后置规则的插入位置计算
|
||||||
const insertPosition = Math.max(0, rules.length - Math.min(offset, rules.length))
|
const insertPosition = Math.max(0, rules.length - Math.min(offset, rules.length))
|
||||||
@ -51,14 +52,19 @@ function processRulesWithOffset(ruleStrings: string[], currentRules: string[], i
|
|||||||
normalRules.push(ruleStr)
|
normalRules.push(ruleStr)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return { normalRules, insertRules: rules }
|
return { normalRules, insertRules: rules }
|
||||||
}
|
}
|
||||||
|
|
||||||
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,37 +86,48 @@ 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 数组存在
|
||||||
if (!currentProfile.rules) {
|
if (!currentProfile.rules) {
|
||||||
currentProfile.rules = [] as unknown as []
|
currentProfile.rules = [] as unknown as []
|
||||||
}
|
}
|
||||||
|
|
||||||
let rules = [...currentProfile.rules] as unknown as string[]
|
let rules = [...currentProfile.rules] as unknown as string[]
|
||||||
|
|
||||||
// 处理前置规则
|
// 处理前置规则
|
||||||
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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
currentProfile.rules = rules as unknown as []
|
currentProfile.rules = rules as unknown as []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -190,12 +190,12 @@ export const mihomoUpgradeUI = async (): Promise<void> => {
|
|||||||
|
|
||||||
export const mihomoUpgradeConfig = async (): Promise<void> => {
|
export const mihomoUpgradeConfig = async (): Promise<void> => {
|
||||||
console.log('[mihomoApi] mihomoUpgradeConfig called')
|
console.log('[mihomoApi] mihomoUpgradeConfig called')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
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) {
|
||||||
@ -456,4 +459,4 @@ export function calculateTrayIconStatus(sysProxyEnabled: boolean, tunEnabled: bo
|
|||||||
export async function getTrayIconStatus(): Promise<'white' | 'blue' | 'green' | 'red'> {
|
export async function getTrayIconStatus(): Promise<'white' | 'blue' | 'green' | 'red'> {
|
||||||
const [sysProxyEnabled, tunEnabled] = await Promise.all([SysProxyStatus(), TunStatus()])
|
const [sysProxyEnabled, tunEnabled] = await Promise.all([SysProxyStatus(), TunStatus()])
|
||||||
return calculateTrayIconStatus(sysProxyEnabled, tunEnabled)
|
return calculateTrayIconStatus(sysProxyEnabled, tunEnabled)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,7 @@ const intervalPool: Record<string, Cron | NodeJS.Timeout> = {}
|
|||||||
export async function initProfileUpdater(): Promise<void> {
|
export async function initProfileUpdater(): Promise<void> {
|
||||||
const { items, current } = await getProfileConfig()
|
const { items, current } = await getProfileConfig()
|
||||||
const currentItem = await getCurrentProfileItem()
|
const currentItem = await getCurrentProfileItem()
|
||||||
|
|
||||||
for (const item of items.filter((i) => i.id !== current)) {
|
for (const item of items.filter((i) => i.id !== current)) {
|
||||||
if (item.type === 'remote' && item.autoUpdate && item.interval) {
|
if (item.type === 'remote' && item.autoUpdate && item.interval) {
|
||||||
if (typeof item.interval === 'number') {
|
if (typeof item.interval === 'number') {
|
||||||
@ -31,7 +31,7 @@ export async function initProfileUpdater(): Promise<void> {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await addProfileItem(item)
|
await addProfileItem(item)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -52,7 +52,7 @@ export async function initProfileUpdater(): Promise<void> {
|
|||||||
},
|
},
|
||||||
currentItem.interval * 60 * 1000
|
currentItem.interval * 60 * 1000
|
||||||
)
|
)
|
||||||
|
|
||||||
setTimeout(
|
setTimeout(
|
||||||
async () => {
|
async () => {
|
||||||
try {
|
try {
|
||||||
@ -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,10 +117,10 @@ 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)
|
||||||
}
|
}
|
||||||
delete intervalPool[id]
|
delete intervalPool[id]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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' })
|
||||||
@ -223,8 +230,8 @@ app.whenReady().then(async () => {
|
|||||||
try {
|
try {
|
||||||
const [startPromise] = await startCore()
|
const [startPromise] = await startCore()
|
||||||
startPromise.then(async () => {
|
startPromise.then(async () => {
|
||||||
await initProfileUpdater()
|
await initProfileUpdater()
|
||||||
await initWebdavBackupScheduler() // 初始化WebDAV定时备份任务
|
await initWebdavBackupScheduler() // 初始化WebDAV定时备份任务
|
||||||
// 上次是否为了开启 TUN 而重启
|
// 上次是否为了开启 TUN 而重启
|
||||||
await checkAdminRestartForTun()
|
await checkAdminRestartForTun()
|
||||||
})
|
})
|
||||||
@ -412,18 +419,18 @@ export async function createWindow(): Promise<void> {
|
|||||||
export function triggerMainWindow(force?: boolean): void {
|
export function triggerMainWindow(force?: boolean): void {
|
||||||
if (mainWindow) {
|
if (mainWindow) {
|
||||||
getAppConfig()
|
getAppConfig()
|
||||||
.then(({ triggerMainWindowBehavior = 'toggle' }) => {
|
.then(({ triggerMainWindowBehavior = 'toggle' }) => {
|
||||||
if (force === true || triggerMainWindowBehavior === 'toggle') {
|
if (force === true || triggerMainWindowBehavior === 'toggle') {
|
||||||
if (mainWindow?.isVisible()) {
|
if (mainWindow?.isVisible()) {
|
||||||
closeMainWindow()
|
closeMainWindow()
|
||||||
|
} else {
|
||||||
|
showMainWindow()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
showMainWindow()
|
showMainWindow()
|
||||||
}
|
}
|
||||||
} else {
|
})
|
||||||
showMainWindow()
|
.catch(showMainWindow)
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(showMainWindow)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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}`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -138,7 +138,7 @@ export async function initWebdavBackupScheduler(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { webdavBackupCron } = await getAppConfig()
|
const { webdavBackupCron } = await getAppConfig()
|
||||||
|
|
||||||
// 如果配置了Cron表达式,则启动定时任务
|
// 如果配置了Cron表达式,则启动定时任务
|
||||||
if (webdavBackupCron) {
|
if (webdavBackupCron) {
|
||||||
backupCronJob = new Cron(webdavBackupCron, async () => {
|
backupCronJob = new Cron(webdavBackupCron, async () => {
|
||||||
@ -149,7 +149,7 @@ export async function initWebdavBackupScheduler(): Promise<void> {
|
|||||||
await systemLogger.error('Failed to execute WebDAV backup via cron job', error)
|
await systemLogger.error('Failed to execute WebDAV backup via cron job', error)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
await systemLogger.info(`WebDAV backup scheduler initialized with cron: ${webdavBackupCron}`)
|
await systemLogger.info(`WebDAV backup scheduler initialized with cron: ${webdavBackupCron}`)
|
||||||
await systemLogger.info(`WebDAV backup scheduler nextRun: ${backupCronJob.nextRun()}`)
|
await systemLogger.info(`WebDAV backup scheduler nextRun: ${backupCronJob.nextRun()}`)
|
||||||
} else {
|
} else {
|
||||||
@ -214,7 +214,7 @@ export async function exportLocalBackup(): Promise<boolean> {
|
|||||||
if (existsSync(rulesDir())) {
|
if (existsSync(rulesDir())) {
|
||||||
zip.addLocalFolder(rulesDir(), 'rules')
|
zip.addLocalFolder(rulesDir(), 'rules')
|
||||||
}
|
}
|
||||||
|
|
||||||
const date = new Date()
|
const date = new Date()
|
||||||
const zipFileName = `clash-party-backup-${dayjs(date).format('YYYY-MM-DD_HH-mm-ss')}.zip`
|
const zipFileName = `clash-party-backup-${dayjs(date).format('YYYY-MM-DD_HH-mm-ss')}.zip`
|
||||||
const result = await dialog.showSaveDialog({
|
const result = await dialog.showSaveDialog({
|
||||||
@ -225,7 +225,7 @@ export async function exportLocalBackup(): Promise<boolean> {
|
|||||||
{ name: 'All Files', extensions: ['*'] }
|
{ name: 'All Files', extensions: ['*'] }
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!result.canceled && result.filePath) {
|
if (!result.canceled && result.filePath) {
|
||||||
zip.writeZip(result.filePath)
|
zip.writeZip(result.filePath)
|
||||||
await systemLogger.info(`Local backup exported to: ${result.filePath}`)
|
await systemLogger.info(`Local backup exported to: ${result.filePath}`)
|
||||||
@ -246,7 +246,7 @@ export async function importLocalBackup(): Promise<boolean> {
|
|||||||
],
|
],
|
||||||
properties: ['openFile']
|
properties: ['openFile']
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!result.canceled && result.filePaths.length > 0) {
|
if (!result.canceled && result.filePaths.length > 0) {
|
||||||
const filePath = result.filePaths[0]
|
const filePath = result.filePaths[0]
|
||||||
const zip = new AdmZip(filePath)
|
const zip = new AdmZip(filePath)
|
||||||
|
|||||||
@ -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,9 +80,10 @@ async function createFloatingWindow(): Promise<void> {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 加载页面
|
// 加载页面
|
||||||
const url = is.dev && process.env['ELECTRON_RENDERER_URL']
|
const url =
|
||||||
? `${process.env['ELECTRON_RENDERER_URL']}/floating.html`
|
is.dev && process.env['ELECTRON_RENDERER_URL']
|
||||||
: join(__dirname, '../renderer/floating.html')
|
? `${process.env['ELECTRON_RENDERER_URL']}/floating.html`
|
||||||
|
: join(__dirname, '../renderer/floating.html')
|
||||||
|
|
||||||
is.dev ? await floatingWindow.loadURL(url) : await floatingWindow.loadFile(url)
|
is.dev ? await floatingWindow.loadURL(url) : await floatingWindow.loadFile(url)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -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,8 +74,8 @@ 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,
|
||||||
label: groupLabel,
|
label: groupLabel,
|
||||||
@ -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()
|
||||||
@ -487,7 +508,7 @@ export function updateTrayIconImmediate(sysProxyEnabled: boolean, tunEnabled: bo
|
|||||||
|
|
||||||
const status = calculateTrayIconStatus(sysProxyEnabled, tunEnabled)
|
const status = calculateTrayIconStatus(sysProxyEnabled, tunEnabled)
|
||||||
const iconPaths = getIconPaths()
|
const iconPaths = getIconPaths()
|
||||||
|
|
||||||
getAppConfig().then(({ disableTrayIconColor = false }) => {
|
getAppConfig().then(({ disableTrayIconColor = false }) => {
|
||||||
if (!tray) return
|
if (!tray) return
|
||||||
const iconPath = disableTrayIconColor ? iconPaths.white : iconPaths[status]
|
const iconPath = disableTrayIconColor ? iconPaths.white : iconPaths[status]
|
||||||
@ -526,4 +547,4 @@ export async function updateTrayIcon(): Promise<void> {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('更新托盘图标失败:', error)
|
console.error('更新托盘图标失败:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -84,7 +84,7 @@ async function enableSysProxy(): Promise<void> {
|
|||||||
triggerAutoProxy(true, `http://${host || '127.0.0.1'}:${pacPort}/pac`)
|
triggerAutoProxy(true, `http://${host || '127.0.0.1'}:${pacPort}/pac`)
|
||||||
}
|
}
|
||||||
} else if (process.platform === 'darwin') {
|
} else if (process.platform === 'darwin') {
|
||||||
await helperRequest(() =>
|
await helperRequest(() =>
|
||||||
axios.post(
|
axios.post(
|
||||||
'http://localhost/pac',
|
'http://localhost/pac',
|
||||||
{ url: `http://${host || '127.0.0.1'}:${pacPort}/pac` },
|
{ url: `http://${host || '127.0.0.1'}:${pacPort}/pac` },
|
||||||
@ -167,14 +167,14 @@ async function requestSocketRecreation(): Promise<void> {
|
|||||||
const { exec } = require('child_process')
|
const { exec } = require('child_process')
|
||||||
const { promisify } = require('util')
|
const { promisify } = require('util')
|
||||||
const execPromise = promisify(exec)
|
const execPromise = promisify(exec)
|
||||||
|
|
||||||
// Use osascript with administrator privileges (same pattern as grantTunPermissions)
|
// Use osascript with administrator privileges (same pattern as grantTunPermissions)
|
||||||
const shell = `pkill -USR1 -f party.mihomo.helper`
|
const shell = `pkill -USR1 -f party.mihomo.helper`
|
||||||
const command = `do shell script "${shell}" with administrator privileges`
|
const command = `do shell script "${shell}" with administrator privileges`
|
||||||
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
|
||||||
@ -184,21 +184,24 @@ async function requestSocketRecreation(): Promise<void> {
|
|||||||
// Wrapper function for helper requests with auto-retry on socket issues
|
// Wrapper function for helper requests with auto-retry on socket issues
|
||||||
async function helperRequest(requestFn: () => Promise<unknown>, maxRetries = 1): Promise<unknown> {
|
async function helperRequest(requestFn: () => Promise<unknown>, maxRetries = 1): Promise<unknown> {
|
||||||
let lastError: Error | null = null
|
let lastError: Error | null = null
|
||||||
|
|
||||||
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||||
try {
|
try {
|
||||||
return await requestFn()
|
return await requestFn()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
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 (
|
||||||
((error as NodeJS.ErrnoException).code === 'ECONNREFUSED' ||
|
attempt < maxRetries &&
|
||||||
(error as NodeJS.ErrnoException).code === 'ENOENT' ||
|
((error as NodeJS.ErrnoException).code === 'ECONNREFUSED' ||
|
||||||
(error as Error).message?.includes('connect ECONNREFUSED') ||
|
(error as NodeJS.ErrnoException).code === 'ENOENT' ||
|
||||||
(error as Error).message?.includes('ENOENT'))) {
|
(error as Error).message?.includes('connect ECONNREFUSED') ||
|
||||||
|
(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...')
|
||||||
@ -211,13 +214,13 @@ async function helperRequest(requestFn: () => Promise<unknown>, maxRetries = 1):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If not a connection error or we've exhausted retries, throw the error
|
// If not a connection error or we've exhausted retries, throw the error
|
||||||
if (attempt === maxRetries) {
|
if (attempt === maxRetries) {
|
||||||
throw lastError
|
throw lastError
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw lastError
|
throw lastError
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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'
|
| {
|
||||||
host: string
|
protocol: 'http' | 'https' | 'socks5'
|
||||||
port: number
|
host: string
|
||||||
} | false
|
port: number
|
||||||
|
}
|
||||||
|
| false
|
||||||
timeout?: number
|
timeout?: number
|
||||||
responseType?: 'text' | 'json' | 'arraybuffer'
|
responseType?: 'text' | 'json' | 'arraybuffer'
|
||||||
followRedirect?: boolean
|
followRedirect?: boolean
|
||||||
|
|||||||
@ -23,7 +23,7 @@ export function taskDir(): string {
|
|||||||
if (!existsSync(userDataDir)) {
|
if (!existsSync(userDataDir)) {
|
||||||
mkdirSync(userDataDir, { recursive: true })
|
mkdirSync(userDataDir, { recursive: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
const dir = path.join(userDataDir, 'tasks')
|
const dir = path.join(userDataDir, 'tasks')
|
||||||
if (!existsSync(dir)) {
|
if (!existsSync(dir)) {
|
||||||
mkdirSync(dir, { recursive: true })
|
mkdirSync(dir, { recursive: true })
|
||||||
|
|||||||
@ -48,9 +48,13 @@ 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}`
|
||||||
|
|
||||||
// 检查缓存
|
// 检查缓存
|
||||||
if (!forceRefresh && versionCache.has(cacheKey)) {
|
if (!forceRefresh && versionCache.has(cacheKey)) {
|
||||||
const cache = versionCache.get(cacheKey)!
|
const cache = versionCache.get(cacheKey)!
|
||||||
@ -60,7 +64,7 @@ export async function getGitHubTags(owner: string, repo: string, forceRefresh =
|
|||||||
return cache.data
|
return cache.data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log(`[GitHub] Fetching tags for ${owner}/${repo}`)
|
console.log(`[GitHub] Fetching tags for ${owner}/${repo}`)
|
||||||
const response = await chromeRequest.get<GitHubTag[]>(
|
const response = await chromeRequest.get<GitHubTag[]>(
|
||||||
@ -135,45 +139,45 @@ async function downloadGitHubAsset(url: string, outputPath: string): Promise<voi
|
|||||||
export async function installMihomoCore(version: string): Promise<void> {
|
export async function installMihomoCore(version: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
console.log(`[GitHub] Installing mihomo core version ${version}`)
|
console.log(`[GitHub] Installing mihomo core version ${version}`)
|
||||||
|
|
||||||
const plat = platform()
|
const plat = platform()
|
||||||
let arch = process.arch
|
let arch = process.arch
|
||||||
|
|
||||||
// 映射平台和架构到GitHub Release文件名
|
// 映射平台和架构到GitHub Release文件名
|
||||||
const key = `${plat}-${arch}`
|
const key = `${plat}-${arch}`
|
||||||
const name = PLATFORM_MAP[key]
|
const name = PLATFORM_MAP[key]
|
||||||
|
|
||||||
if (!name) {
|
if (!name) {
|
||||||
throw new Error(`Unsupported platform "${plat}-${arch}"`)
|
throw new Error(`Unsupported platform "${plat}-${arch}"`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const isWin = plat === 'win32'
|
const isWin = plat === 'win32'
|
||||||
const urlExt = isWin ? 'zip' : 'gz'
|
const urlExt = isWin ? 'zip' : 'gz'
|
||||||
const downloadURL = `https://github.com/MetaCubeX/mihomo/releases/download/${version}/${name}-${version}.${urlExt}`
|
const downloadURL = `https://github.com/MetaCubeX/mihomo/releases/download/${version}/${name}-${version}.${urlExt}`
|
||||||
|
|
||||||
const coreDir = mihomoCoreDir()
|
const coreDir = mihomoCoreDir()
|
||||||
const tempZip = join(coreDir, `temp-core.${urlExt}`)
|
const tempZip = join(coreDir, `temp-core.${urlExt}`)
|
||||||
const exeFile = `${name}${isWin ? '.exe' : ''}`
|
const exeFile = `${name}${isWin ? '.exe' : ''}`
|
||||||
const targetFile = `mihomo-specific${isWin ? '.exe' : ''}`
|
const targetFile = `mihomo-specific${isWin ? '.exe' : ''}`
|
||||||
const targetPath = join(coreDir, targetFile)
|
const targetPath = join(coreDir, targetFile)
|
||||||
|
|
||||||
// 如果目标文件已存在,先停止核心
|
// 如果目标文件已存在,先停止核心
|
||||||
if (existsSync(targetPath)) {
|
if (existsSync(targetPath)) {
|
||||||
console.log('[GitHub] Stopping core before extracting new core file')
|
console.log('[GitHub] Stopping core before extracting new core file')
|
||||||
// 先停止核心
|
// 先停止核心
|
||||||
await stopCore(true)
|
await stopCore(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 下载文件
|
// 下载文件
|
||||||
await downloadGitHubAsset(downloadURL, tempZip)
|
await downloadGitHubAsset(downloadURL, tempZip)
|
||||||
|
|
||||||
// 解压文件
|
// 解压文件
|
||||||
if (urlExt === 'zip') {
|
if (urlExt === 'zip') {
|
||||||
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)
|
||||||
console.log(`[GitHub] Successfully extracted ${exeFile} to ${targetPath}`)
|
console.log(`[GitHub] Successfully extracted ${exeFile} to ${targetPath}`)
|
||||||
@ -185,13 +189,13 @@ export async function installMihomoCore(version: string): Promise<void> {
|
|||||||
console.log(`[GitHub] Extracting GZ file ${tempZip}`)
|
console.log(`[GitHub] Extracting GZ file ${tempZip}`)
|
||||||
const readStream = createReadStream(tempZip)
|
const readStream = createReadStream(tempZip)
|
||||||
const writeStream = createWriteStream(targetPath)
|
const writeStream = createWriteStream(targetPath)
|
||||||
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
const onError = (error: Error) => {
|
const onError = (error: Error) => {
|
||||||
console.error('[GitHub] Gzip decompression failed:', error.message)
|
console.error('[GitHub] Gzip decompression failed:', error.message)
|
||||||
reject(new Error(`Gzip decompression failed: ${error.message}`))
|
reject(new Error(`Gzip decompression failed: ${error.message}`))
|
||||||
}
|
}
|
||||||
|
|
||||||
readStream
|
readStream
|
||||||
.pipe(createGunzip().on('error', onError))
|
.pipe(createGunzip().on('error', onError))
|
||||||
.pipe(writeStream)
|
.pipe(writeStream)
|
||||||
@ -208,14 +212,16 @@ export async function installMihomoCore(version: string): Promise<void> {
|
|||||||
.on('error', onError)
|
.on('error', onError)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清理临时文件
|
// 清理临时文件
|
||||||
console.log(`[GitHub] Cleaning up temporary file ${tempZip}`)
|
console.log(`[GitHub] Cleaning up temporary file ${tempZip}`)
|
||||||
rmSync(tempZip)
|
rmSync(tempZip)
|
||||||
|
|
||||||
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 })
|
||||||
}
|
}
|
||||||
@ -223,7 +229,7 @@ async function cleanup(): Promise<void> {
|
|||||||
async function migrateSubStoreFiles(): Promise<void> {
|
async function migrateSubStoreFiles(): Promise<void> {
|
||||||
const oldJsPath = path.join(mihomoWorkDir(), 'sub-store.bundle.js')
|
const oldJsPath = path.join(mihomoWorkDir(), 'sub-store.bundle.js')
|
||||||
const newCjsPath = path.join(mihomoWorkDir(), 'sub-store.bundle.cjs')
|
const newCjsPath = path.join(mihomoWorkDir(), 'sub-store.bundle.cjs')
|
||||||
|
|
||||||
if (existsSync(oldJsPath) && !existsSync(newCjsPath)) {
|
if (existsSync(oldJsPath) && !existsSync(newCjsPath)) {
|
||||||
try {
|
try {
|
||||||
await rename(oldJsPath, newCjsPath)
|
await rename(oldJsPath, newCjsPath)
|
||||||
@ -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)())
|
||||||
@ -347,17 +366,21 @@ export function registerIpcMainHandlers(): void {
|
|||||||
// 触发托盘菜单更新
|
// 触发托盘菜单更新
|
||||||
ipcMain.emit('updateTrayMenu')
|
ipcMain.emit('updateTrayMenu')
|
||||||
})
|
})
|
||||||
|
|
||||||
// 注册获取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)())
|
||||||
|
|
||||||
// 规则相关IPC处理程序
|
// 规则相关IPC处理程序
|
||||||
ipcMain.handle('getRuleStr', (_e, id) => ipcErrorWrapper(getRuleStr)(id))
|
ipcMain.handle('getRuleStr', (_e, id) => ipcErrorWrapper(getRuleStr)(id))
|
||||||
ipcMain.handle('setRuleStr', (_e, id, str) => ipcErrorWrapper(setRuleStr)(id, str))
|
ipcMain.handle('setRuleStr', (_e, id, str) => ipcErrorWrapper(setRuleStr)(id, str))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,13 +28,16 @@ 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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private logToConsole(level: LogLevel, message: string, error?: any): void {
|
private logToConsole(level: LogLevel, message: string, error?: any): void {
|
||||||
const prefix = `[${this.moduleName}] ${message}`
|
const prefix = `[${this.moduleName}] ${message}`
|
||||||
|
|
||||||
switch (level) {
|
switch (level) {
|
||||||
case 'debug':
|
case 'debug':
|
||||||
console.debug(prefix, error || '')
|
console.debug(prefix, error || '')
|
||||||
|
|||||||
@ -10,4 +10,4 @@ export const parse = <T = unknown>(content: string): T => {
|
|||||||
|
|
||||||
export function stringify(content: unknown): string {
|
export function stringify(content: unknown): string {
|
||||||
return yaml.stringify(content)
|
return yaml.stringify(content)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 {
|
||||||
@ -192,4 +194,4 @@
|
|||||||
to {
|
to {
|
||||||
width: 0%;
|
width: 0%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -5,12 +5,10 @@ import { useTranslation } from 'react-i18next'
|
|||||||
|
|
||||||
const ErrorFallback = ({ error }: FallbackProps): React.ReactElement => {
|
const ErrorFallback = ({ error }: FallbackProps): React.ReactElement => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
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<{
|
||||||
@ -76,7 +77,7 @@ const ToastItem: React.FC<{
|
|||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
icon: <IoClose className="text-white text-sm" />,
|
icon: <IoClose className="text-white text-sm" />,
|
||||||
bg: 'bg-content1',
|
bg: 'bg-content1',
|
||||||
iconBg: 'bg-danger'
|
iconBg: 'bg-danger'
|
||||||
},
|
},
|
||||||
warning: {
|
warning: {
|
||||||
@ -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}
|
||||||
|
|||||||
@ -33,60 +33,67 @@ const CopyableSettingItem: React.FC<{
|
|||||||
domain.split('.').length <= 2
|
domain.split('.').length <= 2
|
||||||
? [domain]
|
? [domain]
|
||||||
: domain
|
: domain
|
||||||
.split('.')
|
.split('.')
|
||||||
.map((_, i, parts) => parts.slice(i).join('.'))
|
.map((_, i, parts) => parts.slice(i).join('.'))
|
||||||
.slice(0, -1)
|
.slice(0, -1)
|
||||||
const isIPv6 = (ip: string) => ip.includes(':')
|
const isIPv6 = (ip: string) => ip.includes(':')
|
||||||
|
|
||||||
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
|
||||||
const p = prefix[i]
|
.map((v, i) => {
|
||||||
if (!p || !v) return null
|
const p = prefix[i]
|
||||||
|
if (!p || !v) return null
|
||||||
if (p === 'DOMAIN-SUFFIX') {
|
|
||||||
return getSubDomains(v).map((subV) => ({
|
if (p === 'DOMAIN-SUFFIX') {
|
||||||
key: `${p},${subV}`,
|
return getSubDomains(v).map((subV) => ({
|
||||||
text: `${p},${subV}`
|
key: `${p},${subV}`,
|
||||||
}))
|
text: `${p},${subV}`
|
||||||
}
|
}))
|
||||||
|
|
||||||
if (p === 'IP-ASN' || p === 'SRC-IP-ASN') {
|
|
||||||
return {
|
|
||||||
key: `${p},${v.split(' ')[0]}`,
|
|
||||||
text: `${p},${v.split(' ')[0]}`
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
if (p === 'IP-ASN' || p === 'SRC-IP-ASN') {
|
||||||
const suffix = (p === 'IP-CIDR' || p === 'SRC-IP-CIDR') ? (isIPv6(v) ? '/128' : '/32') : ''
|
return {
|
||||||
return {
|
key: `${p},${v.split(' ')[0]}`,
|
||||||
key: `${p},${v}${suffix}`,
|
text: `${p},${v.split(' ')[0]}`
|
||||||
text: `${p},${v}${suffix}`
|
}
|
||||||
}
|
|
||||||
}).filter(Boolean).flat()
|
|
||||||
: prefix.map(p => {
|
|
||||||
const v = value as string
|
|
||||||
if (p === 'DOMAIN-SUFFIX') {
|
|
||||||
return getSubDomains(v).map((subV) => ({
|
|
||||||
key: `${p},${subV}`,
|
|
||||||
text: `${p},${subV}`
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (p === 'IP-ASN' || p === 'SRC-IP-ASN') {
|
|
||||||
return {
|
|
||||||
key: `${p},${v.split(' ')[0]}`,
|
|
||||||
text: `${p},${v.split(' ')[0]}`
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
const suffix =
|
||||||
const suffix = (p === 'IP-CIDR' || p === 'SRC-IP-CIDR') ? (isIPv6(v) ? '/128' : '/32') : ''
|
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())
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.flat()
|
||||||
|
: prefix
|
||||||
|
.map((p) => {
|
||||||
|
const v = value as string
|
||||||
|
if (p === 'DOMAIN-SUFFIX') {
|
||||||
|
return getSubDomains(v).map((subV) => ({
|
||||||
|
key: `${p},${subV}`,
|
||||||
|
text: `${p},${subV}`
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p === 'IP-ASN' || p === 'SRC-IP-ASN') {
|
||||||
|
return {
|
||||||
|
key: `${p},${v.split(' ')[0]}`,
|
||||||
|
text: `${p},${v.split(' ')[0]}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const suffix =
|
||||||
|
p === 'IP-CIDR' || p === 'SRC-IP-CIDR' ? (isIPv6(v) ? '/128' : '/32') : ''
|
||||||
|
return {
|
||||||
|
key: `${p},${v}${suffix}`,
|
||||||
|
text: `${p},${v}${suffix}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.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,135 +212,149 @@ 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"
|
{conn.isActive ? t('connections.active') : t('connections.closed')}
|
||||||
radius="sm"
|
</Chip>
|
||||||
variant="dot"
|
),
|
||||||
>
|
[t]
|
||||||
{conn.isActive ? t('connections.active') : t('connections.closed')}
|
)
|
||||||
</Chip>
|
|
||||||
), [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(
|
||||||
const translationMap: Record<string, string> = {
|
(key: string): string => {
|
||||||
status: t('connections.detail.status'),
|
const translationMap: Record<string, string> = {
|
||||||
establishTime: t('connections.detail.establishTime'),
|
status: t('connections.detail.status'),
|
||||||
type: t('connections.detail.connectionType'),
|
establishTime: t('connections.detail.establishTime'),
|
||||||
host: t('connections.detail.host'),
|
type: t('connections.detail.connectionType'),
|
||||||
sniffHost: t('connections.detail.sniffHost'),
|
host: t('connections.detail.host'),
|
||||||
process: t('connections.detail.processName'),
|
sniffHost: t('connections.detail.sniffHost'),
|
||||||
processPath: t('connections.detail.processPath'),
|
process: t('connections.detail.processName'),
|
||||||
rule: t('connections.detail.rule'),
|
processPath: t('connections.detail.processPath'),
|
||||||
proxyChain: t('connections.detail.proxyChain'),
|
rule: t('connections.detail.rule'),
|
||||||
sourceIP: t('connections.detail.sourceIP'),
|
proxyChain: t('connections.detail.proxyChain'),
|
||||||
sourcePort: t('connections.detail.sourcePort'),
|
sourceIP: t('connections.detail.sourceIP'),
|
||||||
destinationPort: t('connections.detail.destinationPort'),
|
sourcePort: t('connections.detail.sourcePort'),
|
||||||
inboundIP: t('connections.detail.inboundIP'),
|
destinationPort: t('connections.detail.destinationPort'),
|
||||||
inboundPort: t('connections.detail.inboundPort'),
|
inboundIP: t('connections.detail.inboundIP'),
|
||||||
uploadSpeed: t('connections.uploadSpeed'),
|
inboundPort: t('connections.detail.inboundPort'),
|
||||||
downloadSpeed: t('connections.downloadSpeed'),
|
uploadSpeed: t('connections.uploadSpeed'),
|
||||||
upload: t('connections.uploadAmount'),
|
downloadSpeed: t('connections.downloadSpeed'),
|
||||||
download: t('connections.downloadAmount'),
|
upload: t('connections.uploadAmount'),
|
||||||
dscp: t('connections.detail.dscp'),
|
download: t('connections.downloadAmount'),
|
||||||
remoteDestination: t('connections.detail.remoteDestination'),
|
dscp: t('connections.detail.dscp'),
|
||||||
dnsMode: t('connections.detail.dnsMode')
|
remoteDestination: t('connections.detail.remoteDestination'),
|
||||||
}
|
dnsMode: t('connections.detail.dnsMode')
|
||||||
return translationMap[key] || key
|
}
|
||||||
}, [t])
|
return translationMap[key] || key
|
||||||
|
},
|
||||||
|
[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 => ({
|
() =>
|
||||||
...col,
|
DEFAULT_COLUMNS.map((col) => ({
|
||||||
label: getLabelForColumn(col.key),
|
...col,
|
||||||
visible: visibleColumns.has(col.key),
|
label: getLabelForColumn(col.key),
|
||||||
width: columnWidths[col.key] || col.width
|
visible: visibleColumns.has(col.key),
|
||||||
}))
|
width: columnWidths[col.key] || col.width
|
||||||
, [getLabelForColumn, visibleColumns, columnWidths])
|
})),
|
||||||
|
[getLabelForColumn, visibleColumns, columnWidths]
|
||||||
|
)
|
||||||
|
|
||||||
// 处理列宽度调整
|
// 处理列宽度调整
|
||||||
const handleMouseDown = useCallback((e: React.MouseEvent, columnKey: string) => {
|
const handleMouseDown = useCallback(
|
||||||
e.preventDefault()
|
(e: React.MouseEvent, columnKey: string) => {
|
||||||
setResizingColumn(columnKey)
|
e.preventDefault()
|
||||||
|
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
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleMouseMove = (e: MouseEvent) => {
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
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
|
||||||
}))
|
}))
|
||||||
}
|
|
||||||
|
|
||||||
const handleMouseUp = () => {
|
|
||||||
setResizingColumn(null)
|
|
||||||
document.removeEventListener('mousemove', handleMouseMove)
|
|
||||||
document.removeEventListener('mouseup', handleMouseUp)
|
|
||||||
// 保存列宽度
|
|
||||||
if (onColumnWidthChange) {
|
|
||||||
setColumnWidths(currentWidths => {
|
|
||||||
onColumnWidthChange(currentWidths)
|
|
||||||
return currentWidths
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('mousemove', handleMouseMove)
|
const handleMouseUp = () => {
|
||||||
document.addEventListener('mouseup', handleMouseUp)
|
setResizingColumn(null)
|
||||||
}, [onColumnWidthChange])
|
document.removeEventListener('mousemove', handleMouseMove)
|
||||||
|
document.removeEventListener('mouseup', handleMouseUp)
|
||||||
|
// 保存列宽度
|
||||||
|
if (onColumnWidthChange) {
|
||||||
|
setColumnWidths((currentWidths) => {
|
||||||
|
onColumnWidthChange(currentWidths)
|
||||||
|
return currentWidths
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', handleMouseMove)
|
||||||
|
document.addEventListener('mouseup', handleMouseUp)
|
||||||
|
},
|
||||||
|
[onColumnWidthChange]
|
||||||
|
)
|
||||||
|
|
||||||
// 处理排序
|
// 处理排序
|
||||||
const handleSort = useCallback((columnKey: string) => {
|
const handleSort = useCallback(
|
||||||
let newDirection: 'asc' | 'desc' = 'asc'
|
(columnKey: string) => {
|
||||||
let newColumn = columnKey
|
let newDirection: 'asc' | 'desc' = 'asc'
|
||||||
|
let newColumn = columnKey
|
||||||
|
|
||||||
if (sortColumn === columnKey) {
|
if (sortColumn === columnKey) {
|
||||||
newDirection = sortDirection === 'asc' ? 'desc' : 'asc'
|
newDirection = sortDirection === 'asc' ? 'desc' : 'asc'
|
||||||
setSortDirection(newDirection)
|
setSortDirection(newDirection)
|
||||||
} else {
|
} else {
|
||||||
setSortColumn(columnKey)
|
setSortColumn(columnKey)
|
||||||
setSortDirection('asc')
|
setSortDirection('asc')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存排序状态
|
// 保存排序状态
|
||||||
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>
|
||||||
|
|||||||
@ -69,4 +69,4 @@ const InterfaceModal: React.FC<Props> = (props) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default InterfaceModal
|
export default InterfaceModal
|
||||||
|
|||||||
@ -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()
|
||||||
@ -132,10 +132,10 @@ const EditInfoModal: React.FC<Props> = (props) => {
|
|||||||
className={cn(
|
className={cn(
|
||||||
inputWidth,
|
inputWidth,
|
||||||
// 不合法
|
// 不合法
|
||||||
typeof values.interval === 'string' &&
|
typeof values.interval === 'string' &&
|
||||||
!/^\d+$/.test(values.interval) &&
|
!/^\d+$/.test(values.interval) &&
|
||||||
!isValidCron(values.interval, { seconds: false }) &&
|
!isValidCron(values.interval, { seconds: false }) &&
|
||||||
'border-red-500'
|
'border-red-500'
|
||||||
)}
|
)}
|
||||||
value={values.interval?.toString() ?? ''}
|
value={values.interval?.toString() ?? ''}
|
||||||
onValueChange={(v) => {
|
onValueChange={(v) => {
|
||||||
@ -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"
|
||||||
!/^\d+$/.test(values.interval) &&
|
style={{
|
||||||
!isValidCron(values.interval, { seconds: false })
|
color:
|
||||||
|
typeof values.interval === 'string' &&
|
||||||
|
!/^\d+$/.test(values.interval) &&
|
||||||
|
!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
@ -222,7 +222,7 @@ const ProfileItem: React.FC<Props> = (props) => {
|
|||||||
updateProfileItem={updateProfileItem}
|
updateProfileItem={updateProfileItem}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Card
|
<Card
|
||||||
as="div"
|
as="div"
|
||||||
fullWidth
|
fullWidth
|
||||||
@ -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
|
||||||
@ -343,7 +337,7 @@ const ProfileItem: React.FC<Props> = (props) => {
|
|||||||
variant="bordered"
|
variant="bordered"
|
||||||
className={`${isCurrent ? 'text-primary-foreground border-primary-foreground' : 'border-primary text-primary'}`}
|
className={`${isCurrent ? 'text-primary-foreground border-primary-foreground' : 'border-primary text-primary'}`}
|
||||||
>
|
>
|
||||||
{t('profiles.remote')}
|
{t('profiles.remote')}
|
||||||
</Chip>
|
</Chip>
|
||||||
<small>{dayjs(info.updated).fromNow()}</small>
|
<small>{dayjs(info.updated).fromNow()}</small>
|
||||||
</div>
|
</div>
|
||||||
@ -357,14 +351,14 @@ const ProfileItem: React.FC<Props> = (props) => {
|
|||||||
variant="bordered"
|
variant="bordered"
|
||||||
className={`${isCurrent ? 'text-primary-foreground border-primary-foreground' : 'border-primary text-primary'}`}
|
className={`${isCurrent ? 'text-primary-foreground border-primary-foreground' : 'border-primary text-primary'}`}
|
||||||
>
|
>
|
||||||
{t('profiles.local')}
|
{t('profiles.local')}
|
||||||
</Chip>
|
</Chip>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{extra && (
|
{extra && (
|
||||||
<Progress
|
<Progress
|
||||||
className="w-full"
|
className="w-full"
|
||||||
aria-label={t('profiles.trafficUsage')}
|
aria-label={t('profiles.trafficUsage')}
|
||||||
classNames={{
|
classNames={{
|
||||||
indicator: isCurrent ? 'bg-primary-foreground' : 'bg-foreground'
|
indicator: isCurrent ? 'bg-primary-foreground' : 'bg-foreground'
|
||||||
}}
|
}}
|
||||||
@ -378,4 +372,4 @@ const ProfileItem: React.FC<Props> = (props) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ProfileItem
|
export default ProfileItem
|
||||||
|
|||||||
@ -22,160 +22,175 @@ 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(
|
||||||
const { t } = useTranslation()
|
(props) => {
|
||||||
const { mutateProxies, proxyDisplayMode, group, proxy, selected, onSelect, onProxyDelay, isGroupTesting = false } = props
|
const { t } = useTranslation()
|
||||||
|
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) {
|
||||||
return proxy.history[proxy.history.length - 1].delay
|
return proxy.history[proxy.history.length - 1].delay
|
||||||
}
|
}
|
||||||
return -1
|
return -1
|
||||||
}, [proxy.history])
|
}, [proxy.history])
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
const isLoading = loading || isGroupTesting
|
const isLoading = loading || isGroupTesting
|
||||||
|
|
||||||
const delayText = useMemo(() => {
|
const delayText = useMemo(() => {
|
||||||
if (delay === -1) return t('proxies.delay.test')
|
if (delay === -1) return t('proxies.delay.test')
|
||||||
if (delay === 0) return t('proxies.delay.timeout')
|
if (delay === 0) return t('proxies.delay.timeout')
|
||||||
return delay.toString()
|
return delay.toString()
|
||||||
}, [delay, t])
|
}, [delay, t])
|
||||||
|
|
||||||
const onDelay = useCallback((): void => {
|
const onDelay = useCallback((): void => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
onProxyDelay(proxy.name, group.testUrl).finally(() => {
|
onProxyDelay(proxy.name, group.testUrl).finally(() => {
|
||||||
mutateProxies()
|
mutateProxies()
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
})
|
})
|
||||||
}, [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
|
||||||
as="div"
|
as="div"
|
||||||
onPress={() => onSelect(group.name, proxy.name)}
|
onPress={() => onSelect(group.name, proxy.name)}
|
||||||
isPressable
|
isPressable
|
||||||
fullWidth
|
fullWidth
|
||||||
shadow="sm"
|
shadow="sm"
|
||||||
className={`${
|
className={`${
|
||||||
fixed
|
fixed
|
||||||
? 'bg-secondary/30 border-r-2 border-r-secondary border-l-2 border-l-secondary'
|
? 'bg-secondary/30 border-r-2 border-r-secondary border-l-2 border-l-secondary'
|
||||||
: selected
|
: selected
|
||||||
? 'bg-primary/30 border-r-2 border-r-primary border-l-2 border-l-primary'
|
? 'bg-primary/30 border-r-2 border-r-primary border-l-2 border-l-primary'
|
||||||
: 'bg-content2'
|
: 'bg-content2'
|
||||||
}`}
|
}`}
|
||||||
radius="sm"
|
radius="sm"
|
||||||
>
|
>
|
||||||
<CardBody className="p-1">
|
<CardBody className="p-1">
|
||||||
{proxyDisplayMode === 'full' ? (
|
{proxyDisplayMode === 'full' ? (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
|
<div className="flex justify-between items-center pl-1">
|
||||||
|
<div className="text-ellipsis overflow-hidden whitespace-nowrap">
|
||||||
|
<div className="flag-emoji inline" title={proxy.name}>
|
||||||
|
{proxy.name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{fixed && (
|
||||||
|
<Button
|
||||||
|
isIconOnly
|
||||||
|
title={t('proxies.unpin')}
|
||||||
|
color="danger"
|
||||||
|
onPress={async () => {
|
||||||
|
await mihomoUnfixedProxy(group.name)
|
||||||
|
mutateProxies()
|
||||||
|
}}
|
||||||
|
variant="light"
|
||||||
|
className="h-[20px] p-0 text-sm"
|
||||||
|
>
|
||||||
|
<FaMapPin className="text-md le" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center pl-1">
|
||||||
|
<div className="flex gap-1 items-center">
|
||||||
|
<div className="text-foreground-400 text-xs bg-default-100 px-1 rounded-md">
|
||||||
|
{proxy.type}
|
||||||
|
</div>
|
||||||
|
{['tfo', 'udp', 'xudp', 'mptcp', 'smux'].map(
|
||||||
|
(protocol) =>
|
||||||
|
proxy[protocol as keyof IMihomoProxy] && (
|
||||||
|
<div
|
||||||
|
key={protocol}
|
||||||
|
className="text-foreground-400 text-xs bg-default-100 px-1 rounded-md"
|
||||||
|
>
|
||||||
|
{protocol}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
isIconOnly
|
||||||
|
title={proxy.type}
|
||||||
|
isLoading={isLoading}
|
||||||
|
color={delayColor(delay)}
|
||||||
|
onPress={onDelay}
|
||||||
|
variant="light"
|
||||||
|
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">{delayText}</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<div className="flex justify-between items-center pl-1">
|
<div className="flex justify-between items-center pl-1">
|
||||||
<div className="text-ellipsis overflow-hidden whitespace-nowrap">
|
<div className="text-ellipsis overflow-hidden whitespace-nowrap">
|
||||||
<div className="flag-emoji inline" title={proxy.name}>
|
<div className="flag-emoji inline" title={proxy.name}>
|
||||||
{proxy.name}
|
{proxy.name}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{fixed && (
|
<div className="flex justify-end">
|
||||||
|
{fixed && (
|
||||||
|
<Button
|
||||||
|
isIconOnly
|
||||||
|
title={t('proxies.unpin')}
|
||||||
|
color="danger"
|
||||||
|
onPress={async () => {
|
||||||
|
await mihomoUnfixedProxy(group.name)
|
||||||
|
mutateProxies()
|
||||||
|
}}
|
||||||
|
variant="light"
|
||||||
|
className="h-[20px] p-0 text-sm"
|
||||||
|
>
|
||||||
|
<FaMapPin className="text-md le" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
isIconOnly
|
isIconOnly
|
||||||
title={t('proxies.unpin')}
|
title={proxy.type}
|
||||||
color="danger"
|
isLoading={isLoading}
|
||||||
onPress={async () => {
|
color={delayColor(delay)}
|
||||||
await mihomoUnfixedProxy(group.name)
|
onPress={onDelay}
|
||||||
mutateProxies()
|
|
||||||
}}
|
|
||||||
variant="light"
|
variant="light"
|
||||||
className="h-[20px] p-0 text-sm"
|
className="h-full text-sm px-2 relative w-min whitespace-nowrap"
|
||||||
>
|
>
|
||||||
<FaMapPin className="text-md le" />
|
<div className="w-full h-full flex items-center justify-end">{delayText}</div>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between items-center pl-1">
|
|
||||||
<div className="flex gap-1 items-center">
|
|
||||||
<div className="text-foreground-400 text-xs bg-default-100 px-1 rounded-md">
|
|
||||||
{proxy.type}
|
|
||||||
</div>
|
|
||||||
{['tfo', 'udp', 'xudp', 'mptcp', 'smux'].map(protocol =>
|
|
||||||
proxy[protocol as keyof IMihomoProxy] && (
|
|
||||||
<div key={protocol} className="text-foreground-400 text-xs bg-default-100 px-1 rounded-md">
|
|
||||||
{protocol}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<Button
|
|
||||||
isIconOnly
|
|
||||||
title={proxy.type}
|
|
||||||
isLoading={isLoading}
|
|
||||||
color={delayColor(delay)}
|
|
||||||
onPress={onDelay}
|
|
||||||
variant="light"
|
|
||||||
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">
|
|
||||||
{delayText}
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
) : (
|
</CardBody>
|
||||||
<div className="flex justify-between items-center pl-1">
|
</Card>
|
||||||
<div className="text-ellipsis overflow-hidden whitespace-nowrap">
|
)
|
||||||
<div className="flag-emoji inline" title={proxy.name}>
|
},
|
||||||
{proxy.name}
|
(prevProps, nextProps) => {
|
||||||
</div>
|
// 必要时重新渲染
|
||||||
</div>
|
return (
|
||||||
<div className="flex justify-end">
|
prevProps.proxy.name === nextProps.proxy.name &&
|
||||||
{fixed && (
|
prevProps.proxy.history === nextProps.proxy.history &&
|
||||||
<Button
|
prevProps.selected === nextProps.selected &&
|
||||||
isIconOnly
|
prevProps.proxyDisplayMode === nextProps.proxyDisplayMode &&
|
||||||
title={t('proxies.unpin')}
|
prevProps.group.fixed === nextProps.group.fixed &&
|
||||||
color="danger"
|
prevProps.isGroupTesting === nextProps.isGroupTesting
|
||||||
onPress={async () => {
|
)
|
||||||
await mihomoUnfixedProxy(group.name)
|
}
|
||||||
mutateProxies()
|
)
|
||||||
}}
|
|
||||||
variant="light"
|
|
||||||
className="h-[20px] p-0 text-sm"
|
|
||||||
>
|
|
||||||
<FaMapPin className="text-md le" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
isIconOnly
|
|
||||||
title={proxy.type}
|
|
||||||
isLoading={isLoading}
|
|
||||||
color={delayColor(delay)}
|
|
||||||
onPress={onDelay}
|
|
||||||
variant="light"
|
|
||||||
className="h-full text-sm px-2 relative w-min whitespace-nowrap"
|
|
||||||
>
|
|
||||||
<div className="w-full h-full flex items-center justify-end">
|
|
||||||
{delayText}
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardBody>
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
}, (prevProps, nextProps) => {
|
|
||||||
// 必要时重新渲染
|
|
||||||
return (
|
|
||||||
prevProps.proxy.name === nextProps.proxy.name &&
|
|
||||||
prevProps.proxy.history === nextProps.proxy.history &&
|
|
||||||
prevProps.selected === nextProps.selected &&
|
|
||||||
prevProps.proxyDisplayMode === nextProps.proxyDisplayMode &&
|
|
||||||
prevProps.group.fixed === nextProps.group.fixed &&
|
|
||||||
prevProps.isGroupTesting === nextProps.isGroupTesting
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
ProxyItem.displayName = 'ProxyItem'
|
ProxyItem.displayName = 'ProxyItem'
|
||||||
|
|
||||||
export default ProxyItem
|
export default 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)
|
||||||
|
|||||||
@ -65,8 +65,8 @@ const Actions: React.FC = () => {
|
|||||||
setChangelog(version.changelog)
|
setChangelog(version.changelog)
|
||||||
setOpenUpdate(true)
|
setOpenUpdate(true)
|
||||||
} else {
|
} else {
|
||||||
new window.Notification(t('actions.update.upToDate.title'), {
|
new window.Notification(t('actions.update.upToDate.title'), {
|
||||||
body: t('actions.update.upToDate.body')
|
body: t('actions.update.upToDate.body')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@ -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"
|
||||||
@ -309,47 +307,47 @@ const GeneralConfig: React.FC = () => {
|
|||||||
</SettingItem>
|
</SettingItem>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<SettingItem title={t('settings.disableTray')} divider>
|
<SettingItem title={t('settings.disableTray')} divider>
|
||||||
<Switch
|
<Switch
|
||||||
size="sm"
|
size="sm"
|
||||||
isSelected={disableTray}
|
isSelected={disableTray}
|
||||||
onValueChange={async (v) => {
|
onValueChange={async (v) => {
|
||||||
await patchAppConfig({ disableTray: v })
|
await patchAppConfig({ disableTray: v })
|
||||||
if (v) {
|
if (v) {
|
||||||
|
closeTrayIcon()
|
||||||
|
} else {
|
||||||
|
showTrayIcon()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SettingItem>
|
||||||
|
{!disableTray && (
|
||||||
|
<>
|
||||||
|
<SettingItem title={t('settings.swapTrayClick')} divider>
|
||||||
|
<Switch
|
||||||
|
size="sm"
|
||||||
|
isSelected={swapTrayClick}
|
||||||
|
onValueChange={async (v) => {
|
||||||
|
await patchAppConfig({ swapTrayClick: v })
|
||||||
closeTrayIcon()
|
closeTrayIcon()
|
||||||
} else {
|
setTimeout(() => {
|
||||||
showTrayIcon()
|
showTrayIcon()
|
||||||
}
|
}, 100)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
{!disableTray && (
|
<SettingItem title={t('settings.disableTrayIconColor')} divider>
|
||||||
<>
|
<Switch
|
||||||
<SettingItem title={t('settings.swapTrayClick')} divider>
|
size="sm"
|
||||||
<Switch
|
isSelected={disableTrayIconColor}
|
||||||
size="sm"
|
onValueChange={async (v) => {
|
||||||
isSelected={swapTrayClick}
|
await patchAppConfig({ disableTrayIconColor: v })
|
||||||
onValueChange={async (v) => {
|
await updateTrayIcon()
|
||||||
await patchAppConfig({ swapTrayClick: v })
|
}}
|
||||||
closeTrayIcon()
|
/>
|
||||||
setTimeout(() => {
|
</SettingItem>
|
||||||
showTrayIcon()
|
</>
|
||||||
}, 100)
|
)}
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</SettingItem>
|
|
||||||
<SettingItem title={t('settings.disableTrayIconColor')} divider>
|
|
||||||
<Switch
|
|
||||||
size="sm"
|
|
||||||
isSelected={disableTrayIconColor}
|
|
||||||
onValueChange={async (v) => {
|
|
||||||
await patchAppConfig({ disableTrayIconColor: v })
|
|
||||||
await updateTrayIcon()
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</SettingItem>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{platform !== 'linux' && (
|
{platform !== 'linux' && (
|
||||||
<>
|
<>
|
||||||
<SettingItem title={t('settings.proxyInTray')} divider>
|
<SettingItem title={t('settings.proxyInTray')} divider>
|
||||||
|
|||||||
@ -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>
|
||||||
@ -79,4 +71,4 @@ const LocalBackupConfig: React.FC = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default LocalBackupConfig
|
export default LocalBackupConfig
|
||||||
|
|||||||
@ -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
|
||||||
)
|
)
|
||||||
@ -142,7 +156,7 @@ const WebdavConfig: React.FC = () => {
|
|||||||
<SettingItem title={t('webdav.backup.cron.title')} divider>
|
<SettingItem title={t('webdav.backup.cron.title')} divider>
|
||||||
<div className="flex w-[60%] gap-2">
|
<div className="flex w-[60%] gap-2">
|
||||||
{webdavBackupCron !== webdav.webdavBackupCron && (
|
{webdavBackupCron !== webdav.webdavBackupCron && (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
color="primary"
|
color="primary"
|
||||||
onPress={async () => {
|
onPress={async () => {
|
||||||
@ -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,45 +31,48 @@ 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
|
||||||
<div className="flex" key={filename}>
|
.sort()
|
||||||
<Button
|
.reverse()
|
||||||
size="sm"
|
.map((filename) => (
|
||||||
fullWidth
|
<div className="flex" key={filename}>
|
||||||
isLoading={restoring}
|
<Button
|
||||||
variant="flat"
|
size="sm"
|
||||||
onPress={async () => {
|
fullWidth
|
||||||
setRestoring(true)
|
isLoading={restoring}
|
||||||
try {
|
variant="flat"
|
||||||
await webdavRestore(filename)
|
onPress={async () => {
|
||||||
await relaunchApp()
|
setRestoring(true)
|
||||||
} catch (e) {
|
try {
|
||||||
toast.error(t('common.error.restoreFailed', { error: e }))
|
await webdavRestore(filename)
|
||||||
} finally {
|
await relaunchApp()
|
||||||
setRestoring(false)
|
} catch (e) {
|
||||||
}
|
toast.error(t('common.error.restoreFailed', { error: e }))
|
||||||
}}
|
} finally {
|
||||||
>
|
setRestoring(false)
|
||||||
{filename}
|
}
|
||||||
</Button>
|
}}
|
||||||
<Button
|
>
|
||||||
size="sm"
|
{filename}
|
||||||
color="warning"
|
</Button>
|
||||||
variant="flat"
|
<Button
|
||||||
className="ml-2"
|
size="sm"
|
||||||
onPress={async () => {
|
color="warning"
|
||||||
try {
|
variant="flat"
|
||||||
await webdavDelete(filename)
|
className="ml-2"
|
||||||
setFilenames(filenames.filter((name) => name !== filename))
|
onPress={async () => {
|
||||||
} catch (e) {
|
try {
|
||||||
toast.error(t('common.error.deleteFailed', { error: e }))
|
await webdavDelete(filename)
|
||||||
}
|
setFilenames(filenames.filter((name) => name !== filename))
|
||||||
}}
|
} catch (e) {
|
||||||
>
|
toast.error(t('common.error.deleteFailed', { error: e }))
|
||||||
<MdDeleteForever className="text-lg" />
|
}
|
||||||
</Button>
|
}}
|
||||||
</div>
|
>
|
||||||
))
|
<MdDeleteForever className="text-lg" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
)}
|
)}
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
|
|||||||
@ -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')
|
||||||
|
|||||||
@ -40,14 +40,14 @@ const SysproxySwitcher: React.FC<Props> = (props) => {
|
|||||||
const onChange = async (enable: boolean): Promise<void> => {
|
const onChange = async (enable: boolean): Promise<void> => {
|
||||||
const previousState = !enable
|
const previousState = !enable
|
||||||
const tunEnabled = tun?.enable ?? false
|
const tunEnabled = tun?.enable ?? false
|
||||||
|
|
||||||
// 立即更新图标
|
// 立即更新图标
|
||||||
updateTrayIconImmediate(enable, tunEnabled)
|
updateTrayIconImmediate(enable, tunEnabled)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await patchAppConfig({ sysProxy: { enable } })
|
await patchAppConfig({ sysProxy: { enable } })
|
||||||
await triggerSysProxy(enable)
|
await triggerSysProxy(enable)
|
||||||
|
|
||||||
window.electron.ipcRenderer.send('updateFloatingWindow')
|
window.electron.ipcRenderer.send('updateFloatingWindow')
|
||||||
window.electron.ipcRenderer.send('updateTrayMenu')
|
window.electron.ipcRenderer.send('updateTrayMenu')
|
||||||
await updateTrayIcon()
|
await updateTrayIcon()
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -15,4 +15,4 @@ i18n.on('languageChanged', (lng) => {
|
|||||||
window.electron.ipcRenderer.invoke('changeLanguage', lng)
|
window.electron.ipcRenderer.invoke('changeLanguage', lng)
|
||||||
})
|
})
|
||||||
|
|
||||||
export default i18n
|
export default i18n
|
||||||
|
|||||||
@ -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,26 +74,33 @@ const Connections: React.FC = () => {
|
|||||||
)
|
)
|
||||||
}, [selected, activeConnections, closedConnections])
|
}, [selected, activeConnections, closedConnections])
|
||||||
|
|
||||||
const handleColumnWidthChange = useCallback(async (widths: Record<string, number>) => {
|
const handleColumnWidthChange = useCallback(
|
||||||
await patchAppConfig({ connectionTableColumnWidths: widths })
|
async (widths: Record<string, number>) => {
|
||||||
}, [patchAppConfig])
|
await patchAppConfig({ connectionTableColumnWidths: widths })
|
||||||
|
},
|
||||||
|
[patchAppConfig]
|
||||||
|
)
|
||||||
|
|
||||||
const handleSortChange = useCallback(async (column: string | null, direction: 'asc' | 'desc') => {
|
const handleSortChange = useCallback(
|
||||||
await patchAppConfig({
|
async (column: string | null, direction: 'asc' | 'desc') => {
|
||||||
connectionTableSortColumn: column || undefined,
|
await patchAppConfig({
|
||||||
connectionTableSortDirection: direction
|
connectionTableSortColumn: column || undefined,
|
||||||
})
|
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 =
|
||||||
? connections
|
filter === ''
|
||||||
: connections.filter((connection) => {
|
? connections
|
||||||
const raw = JSON.stringify(connection)
|
: connections.filter((connection) => {
|
||||||
return includesIgnoreCase(raw, filter)
|
const raw = JSON.stringify(connection)
|
||||||
})
|
return includesIgnoreCase(raw, filter)
|
||||||
|
})
|
||||||
|
|
||||||
if (viewMode === 'list' && connectionOrderBy) {
|
if (viewMode === 'list' && connectionOrderBy) {
|
||||||
return [...filtered].sort((a, b) => {
|
return [...filtered].sort((a, b) => {
|
||||||
@ -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(
|
||||||
tab === 'active' ? mihomoCloseConnection(id) : trashClosedConnection(id)
|
(id: string): void => {
|
||||||
}, [tab])
|
tab === 'active' ? mihomoCloseConnection(id) : trashClosedConnection(id)
|
||||||
|
},
|
||||||
|
[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,22 +123,22 @@ 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)
|
||||||
const [searchTerm, setSearchTerm] = useState('')
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
const [refreshing, setRefreshing] = useState(false)
|
const [refreshing, setRefreshing] = useState(false)
|
||||||
|
|
||||||
// WebUI管理状态
|
// WebUI管理状态
|
||||||
const [isWebUIModalOpen, setIsWebUIModalOpen] = useState(false)
|
const [isWebUIModalOpen, setIsWebUIModalOpen] = useState(false)
|
||||||
const [allPanels, setAllPanels] = useState<WebUIPanel[]>([])
|
const [allPanels, setAllPanels] = useState<WebUIPanel[]>([])
|
||||||
const [editingPanel, setEditingPanel] = useState<WebUIPanel | null>(null)
|
const [editingPanel, setEditingPanel] = useState<WebUIPanel | null>(null)
|
||||||
const [newPanelName, setNewPanelName] = useState('')
|
const [newPanelName, setNewPanelName] = useState('')
|
||||||
const [newPanelUrl, setNewPanelUrl] = useState('')
|
const [newPanelUrl, setNewPanelUrl] = useState('')
|
||||||
|
|
||||||
const urlInputRef = useRef<HTMLInputElement>(null)
|
const urlInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
// 解析主机和端口
|
// 解析主机和端口
|
||||||
const parseController = () => {
|
const parseController = () => {
|
||||||
if (externalController) {
|
if (externalController) {
|
||||||
@ -126,12 +147,12 @@ const Mihomo: React.FC = () => {
|
|||||||
}
|
}
|
||||||
return { host: '127.0.0.1', port: '9090' }
|
return { host: '127.0.0.1', port: '9090' }
|
||||||
}
|
}
|
||||||
|
|
||||||
const { host, port } = parseController()
|
const { host, port } = parseController()
|
||||||
|
|
||||||
// 生成随机端口(范围1024-65535)
|
// 生成随机端口(范围1024-65535)
|
||||||
const generateRandomPort = () => Math.floor(Math.random() * (65535 - 1024 + 1)) + 1024
|
const generateRandomPort = () => Math.floor(Math.random() * (65535 - 1024 + 1)) + 1024
|
||||||
|
|
||||||
// 默认WebUI面板选项
|
// 默认WebUI面板选项
|
||||||
const defaultWebUIPanels: WebUIPanel[] = [
|
const defaultWebUIPanels: WebUIPanel[] = [
|
||||||
{
|
{
|
||||||
@ -153,7 +174,7 @@ const Mihomo: React.FC = () => {
|
|||||||
isDefault: true
|
isDefault: true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
// 初始化面板列表
|
// 初始化面板列表
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const savedPanels = localStorage.getItem('webui-panels')
|
const savedPanels = localStorage.getItem('webui-panels')
|
||||||
@ -163,28 +184,28 @@ const Mihomo: React.FC = () => {
|
|||||||
setAllPanels(defaultWebUIPanels)
|
setAllPanels(defaultWebUIPanels)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// 保存面板列表到localStorage
|
// 保存面板列表到localStorage
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (allPanels.length > 0) {
|
if (allPanels.length > 0) {
|
||||||
localStorage.setItem('webui-panels', JSON.stringify(allPanels))
|
localStorage.setItem('webui-panels', JSON.stringify(allPanels))
|
||||||
}
|
}
|
||||||
}, [allPanels])
|
}, [allPanels])
|
||||||
|
|
||||||
// 在URL输入框光标处插入或替换变量
|
// 在URL输入框光标处插入或替换变量
|
||||||
const insertVariableAtCursor = (variable: string) => {
|
const insertVariableAtCursor = (variable: string) => {
|
||||||
if (!urlInputRef.current) return
|
if (!urlInputRef.current) return
|
||||||
|
|
||||||
const input = urlInputRef.current
|
const input = urlInputRef.current
|
||||||
const start = input.selectionStart || 0
|
const start = input.selectionStart || 0
|
||||||
const end = input.selectionEnd || 0
|
const end = input.selectionEnd || 0
|
||||||
const currentValue = newPanelUrl || ''
|
const currentValue = newPanelUrl || ''
|
||||||
|
|
||||||
// 如果有选中文本,则替换选中的文本
|
// 如果有选中文本,则替换选中的文本
|
||||||
const newValue = currentValue.substring(0, start) + variable + currentValue.substring(end)
|
const newValue = currentValue.substring(0, start) + variable + currentValue.substring(end)
|
||||||
|
|
||||||
setNewPanelUrl(newValue)
|
setNewPanelUrl(newValue)
|
||||||
|
|
||||||
// 设置光标位置到插入变量之后
|
// 设置光标位置到插入变量之后
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (urlInputRef.current) {
|
if (urlInputRef.current) {
|
||||||
@ -194,16 +215,13 @@ const Mihomo: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}, 0)
|
}, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 打开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')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加新面板
|
// 添加新面板
|
||||||
const addNewPanel = () => {
|
const addNewPanel = () => {
|
||||||
if (newPanelName && newPanelUrl) {
|
if (newPanelName && newPanelUrl) {
|
||||||
@ -218,14 +236,12 @@ const Mihomo: React.FC = () => {
|
|||||||
setEditingPanel(null)
|
setEditingPanel(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新面板
|
// 更新面板
|
||||||
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)
|
||||||
@ -233,35 +249,35 @@ const Mihomo: React.FC = () => {
|
|||||||
setNewPanelUrl('')
|
setNewPanelUrl('')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除面板
|
// 删除面板
|
||||||
const deletePanel = (id: string) => {
|
const deletePanel = (id: string) => {
|
||||||
setAllPanels(allPanels.filter(panel => panel.id !== id))
|
setAllPanels(allPanels.filter((panel) => panel.id !== id))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 开始编辑面板
|
// 开始编辑面板
|
||||||
const startEditing = (panel: WebUIPanel) => {
|
const startEditing = (panel: WebUIPanel) => {
|
||||||
setEditingPanel(panel)
|
setEditingPanel(panel)
|
||||||
setNewPanelName(panel.name)
|
setNewPanelName(panel.name)
|
||||||
setNewPanelUrl(panel.url)
|
setNewPanelUrl(panel.url)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 取消编辑
|
// 取消编辑
|
||||||
const cancelEditing = () => {
|
const cancelEditing = () => {
|
||||||
setEditingPanel(null)
|
setEditingPanel(null)
|
||||||
setNewPanelName('')
|
setNewPanelName('')
|
||||||
setNewPanelUrl('')
|
setNewPanelUrl('')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 恢复默认面板
|
// 恢复默认面板
|
||||||
const restoreDefaultPanels = () => {
|
const restoreDefaultPanels = () => {
|
||||||
setAllPanels(defaultWebUIPanels)
|
setAllPanels(defaultWebUIPanels)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 用于高亮显示URL中的变量
|
// 用于高亮显示URL中的变量
|
||||||
const HighlightedUrl: React.FC<{ url: string }> = ({ url }) => {
|
const HighlightedUrl: React.FC<{ url: string }> = ({ url }) => {
|
||||||
const parts = url.split(/(%host|%port|%secret)/g)
|
const parts = url.split(/(%host|%port|%secret)/g)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<p className="text-sm text-default-500 break-all">
|
<p className="text-sm text-default-500 break-all">
|
||||||
{parts.map((part, index) => {
|
{parts.map((part, index) => {
|
||||||
@ -277,14 +293,14 @@ const Mihomo: React.FC = () => {
|
|||||||
</p>
|
</p>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 可点击的变量标签组件
|
// 可点击的变量标签组件
|
||||||
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 (
|
||||||
<span
|
<span
|
||||||
className="bg-warning-200 text-warning-800 px-1 rounded ml-1 cursor-pointer hover:bg-warning-300"
|
className="bg-warning-200 text-warning-800 px-1 rounded ml-1 cursor-pointer hover:bg-warning-300"
|
||||||
onClick={() => onClick(variable)}
|
onClick={() => onClick(variable)}
|
||||||
>
|
>
|
||||||
@ -292,7 +308,7 @@ const Mihomo: React.FC = () => {
|
|||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onChangeNeedRestart = async (patch: Partial<IMihomoConfig>): Promise<void> => {
|
const onChangeNeedRestart = async (patch: Partial<IMihomoConfig>): Promise<void> => {
|
||||||
await patchControledMihomoConfig(patch)
|
await patchControledMihomoConfig(patch)
|
||||||
await restartCore()
|
await restartCore()
|
||||||
@ -311,7 +327,7 @@ const Mihomo: React.FC = () => {
|
|||||||
PubSub.publish('mihomo-core-changed')
|
PubSub.publish('mihomo-core-changed')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取GitHub标签列表(带缓存)
|
// 获取GitHub标签列表(带缓存)
|
||||||
const fetchTags = async (forceRefresh = false) => {
|
const fetchTags = async (forceRefresh = false) => {
|
||||||
setLoadingTags(true)
|
setLoadingTags(true)
|
||||||
@ -326,28 +342,28 @@ const Mihomo: React.FC = () => {
|
|||||||
setLoadingTags(false)
|
setLoadingTags(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 安装特定版本的核心
|
// 安装特定版本的核心
|
||||||
const installSpecificCore = async () => {
|
const installSpecificCore = async () => {
|
||||||
if (!selectedTag) return
|
if (!selectedTag) return
|
||||||
|
|
||||||
setInstalling(true)
|
setInstalling(true)
|
||||||
try {
|
try {
|
||||||
// 下载并安装特定版本的核心
|
// 下载并安装特定版本的核心
|
||||||
await installSpecificMihomoCore(selectedTag)
|
await installSpecificMihomoCore(selectedTag)
|
||||||
|
|
||||||
// 更新应用配置
|
// 更新应用配置
|
||||||
await patchAppConfig({
|
await patchAppConfig({
|
||||||
core: 'mihomo-specific',
|
core: 'mihomo-specific',
|
||||||
specificVersion: selectedTag
|
specificVersion: selectedTag
|
||||||
})
|
})
|
||||||
|
|
||||||
// 重启核心
|
// 重启核心
|
||||||
await restartCore()
|
await restartCore()
|
||||||
|
|
||||||
// 关闭模态框
|
// 关闭模态框
|
||||||
onClose()
|
onClose()
|
||||||
|
|
||||||
// 通知用户
|
// 通知用户
|
||||||
new Notification(t('mihomo.coreUpgradeSuccess'))
|
new Notification(t('mihomo.coreUpgradeSuccess'))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -357,7 +373,7 @@ const Mihomo: React.FC = () => {
|
|||||||
setInstalling(false)
|
setInstalling(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 刷新标签列表
|
// 刷新标签列表
|
||||||
const refreshTags = async () => {
|
const refreshTags = async () => {
|
||||||
setRefreshing(true)
|
setRefreshing(true)
|
||||||
@ -369,7 +385,7 @@ const Mihomo: React.FC = () => {
|
|||||||
setRefreshing(false)
|
setRefreshing(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 打开模态框时获取标签
|
// 打开模态框时获取标签
|
||||||
const handleOpenModal = async () => {
|
const handleOpenModal = async () => {
|
||||||
onOpen()
|
onOpen()
|
||||||
@ -377,40 +393,39 @@ const Mihomo: React.FC = () => {
|
|||||||
if (tags.length === 0) {
|
if (tags.length === 0) {
|
||||||
await fetchTags(false) // 使用缓存
|
await fetchTags(false) // 使用缓存
|
||||||
}
|
}
|
||||||
|
|
||||||
// 在后台检查更新
|
// 在后台检查更新
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
fetchTags(true) // 强制刷新
|
fetchTags(true) // 强制刷新
|
||||||
}, 100)
|
}, 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 过滤标签
|
// 过滤标签
|
||||||
const filteredTags = tags.filter(tag =>
|
const filteredTags = tags.filter((tag) =>
|
||||||
tag.name.toLowerCase().includes(searchTerm.toLowerCase())
|
tag.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
)
|
)
|
||||||
|
|
||||||
// 当模态框打开时,确保选中当前版本
|
// 当模态框打开时,确保选中当前版本
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen && specificVersion) {
|
if (isOpen && specificVersion) {
|
||||||
setSelectedTag(specificVersion)
|
setSelectedTag(specificVersion)
|
||||||
}
|
}
|
||||||
}, [isOpen, specificVersion])
|
}, [isOpen, specificVersion])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{lanOpen && <InterfaceModal onClose={() => setLanOpen(false)} />}
|
{lanOpen && <InterfaceModal onClose={() => setLanOpen(false)} />}
|
||||||
<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
|
||||||
enableSmartCore
|
className={`rounded-md border p-2 transition-all duration-200 ${
|
||||||
? 'border-blue-300 bg-blue-50/30 dark:border-blue-700 dark:bg-blue-950/20'
|
enableSmartCore
|
||||||
: 'border-gray-300 bg-gray-50/30 dark:border-gray-600 dark:bg-gray-800/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'
|
||||||
<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>
|
||||||
</>
|
</>
|
||||||
@ -1020,9 +1036,9 @@ const Mihomo: React.FC = () => {
|
|||||||
</SettingItem>
|
</SettingItem>
|
||||||
<SettingItem title={t('settings.webui.title')} divider>
|
<SettingItem title={t('settings.webui.title')} divider>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
color="primary"
|
color="primary"
|
||||||
isDisabled={!externalController || externalController.trim() === ''}
|
isDisabled={!externalController || externalController.trim() === ''}
|
||||||
onPress={() => setIsWebUIModalOpen(true)}
|
onPress={() => setIsWebUIModalOpen(true)}
|
||||||
>
|
>
|
||||||
@ -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]"
|
||||||
@ -1383,10 +1399,10 @@ const Mihomo: React.FC = () => {
|
|||||||
</SettingItem>
|
</SettingItem>
|
||||||
</SettingCard>
|
</SettingCard>
|
||||||
</BasePage>
|
</BasePage>
|
||||||
|
|
||||||
{/* WebUI 管理模态框 */}
|
{/* WebUI 管理模态框 */}
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={isWebUIModalOpen}
|
isOpen={isWebUIModalOpen}
|
||||||
onOpenChange={setIsWebUIModalOpen}
|
onOpenChange={setIsWebUIModalOpen}
|
||||||
size="5xl"
|
size="5xl"
|
||||||
scrollBehavior="inside"
|
scrollBehavior="inside"
|
||||||
@ -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">
|
||||||
{/* 添加/编辑面板表单 */}
|
{/* 添加/编辑面板表单 */}
|
||||||
@ -1424,26 +1438,21 @@ const Mihomo: React.FC = () => {
|
|||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{editingPanel ? (
|
{editingPanel ? (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
color="primary"
|
color="primary"
|
||||||
onPress={updatePanel}
|
onPress={updatePanel}
|
||||||
isDisabled={!newPanelName || !newPanelUrl}
|
isDisabled={!newPanelName || !newPanelUrl}
|
||||||
>
|
>
|
||||||
{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>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
color="primary"
|
color="primary"
|
||||||
onPress={addNewPanel}
|
onPress={addNewPanel}
|
||||||
isDisabled={!newPanelName || !newPanelUrl}
|
isDisabled={!newPanelName || !newPanelUrl}
|
||||||
@ -1451,8 +1460,8 @@ const Mihomo: React.FC = () => {
|
|||||||
{t('settings.webui.addPanel')}
|
{t('settings.webui.addPanel')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
color="warning"
|
color="warning"
|
||||||
variant="bordered"
|
variant="bordered"
|
||||||
onPress={restoreDefaultPanels}
|
onPress={restoreDefaultPanels}
|
||||||
@ -1461,36 +1470,34 @@ const Mihomo: React.FC = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 面板列表 */}
|
{/* 面板列表 */}
|
||||||
<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
|
||||||
isIconOnly
|
isIconOnly
|
||||||
size="sm"
|
size="sm"
|
||||||
color="warning"
|
color="warning"
|
||||||
onPress={() => startEditing(panel)}
|
onPress={() => startEditing(panel)}
|
||||||
>
|
>
|
||||||
<MdEdit />
|
<MdEdit />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
isIconOnly
|
isIconOnly
|
||||||
size="sm"
|
size="sm"
|
||||||
color="danger"
|
color="danger"
|
||||||
onPress={() => deletePanel(panel.id)}
|
onPress={() => deletePanel(panel.id)}
|
||||||
>
|
>
|
||||||
@ -1503,20 +1510,17 @@ 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>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
{/* 自定义版本选择模态框 */}
|
{/* 自定义版本选择模态框 */}
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
size="5xl"
|
size="5xl"
|
||||||
backdrop="blur"
|
backdrop="blur"
|
||||||
classNames={{ backdrop: 'top-[48px]' }}
|
classNames={{ backdrop: 'top-[48px]' }}
|
||||||
|
|||||||
@ -325,61 +325,65 @@ const Profiles: React.FC = () => {
|
|||||||
<SubStoreIcon className="text-lg" />
|
<SubStoreIcon className="text-lg" />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownTrigger>
|
</DropdownTrigger>
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
className="max-h-[calc(100vh-200px)] overflow-y-auto"
|
className="max-h-[calc(100vh-200px)] overflow-y-auto"
|
||||||
onAction={async (key) => {
|
onAction={async (key) => {
|
||||||
if (key === 'open-substore') {
|
if (key === 'open-substore') {
|
||||||
navigate('/substore')
|
navigate('/substore')
|
||||||
} else if (key.toString().startsWith('sub-')) {
|
} else if (key.toString().startsWith('sub-')) {
|
||||||
setSubStoreImporting(true)
|
setSubStoreImporting(true)
|
||||||
try {
|
try {
|
||||||
const sub = subs.find(
|
const sub = subs.find(
|
||||||
(sub) => sub.name === key.toString().replace('sub-', '')
|
(sub) => sub.name === key.toString().replace('sub-', '')
|
||||||
)
|
)
|
||||||
await addProfileItem({
|
await addProfileItem({
|
||||||
name: sub?.displayName || sub?.name || '',
|
name: sub?.displayName || sub?.name || '',
|
||||||
substore: !useCustomSubStore,
|
substore: !useCustomSubStore,
|
||||||
type: 'remote',
|
type: 'remote',
|
||||||
url: useCustomSubStore
|
url: useCustomSubStore
|
||||||
? `${customSubStoreUrl}/download/${key.toString().replace('sub-', '')}?target=ClashMeta`
|
? `${customSubStoreUrl}/download/${key.toString().replace('sub-', '')}?target=ClashMeta`
|
||||||
: `/download/${key.toString().replace('sub-', '')}`,
|
: `/download/${key.toString().replace('sub-', '')}`,
|
||||||
useProxy
|
useProxy
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error(String(e))
|
toast.error(String(e))
|
||||||
} finally {
|
} finally {
|
||||||
setSubStoreImporting(false)
|
setSubStoreImporting(false)
|
||||||
|
}
|
||||||
|
} else if (key.toString().startsWith('collection-')) {
|
||||||
|
setSubStoreImporting(true)
|
||||||
|
try {
|
||||||
|
const collection = collections.find(
|
||||||
|
(collection) =>
|
||||||
|
collection.name === key.toString().replace('collection-', '')
|
||||||
|
)
|
||||||
|
await addProfileItem({
|
||||||
|
name: collection?.displayName || collection?.name || '',
|
||||||
|
type: 'remote',
|
||||||
|
substore: !useCustomSubStore,
|
||||||
|
url: useCustomSubStore
|
||||||
|
? `${customSubStoreUrl}/download/collection/${key.toString().replace('collection-', '')}?target=ClashMeta`
|
||||||
|
: `/download/collection/${key.toString().replace('collection-', '')}`,
|
||||||
|
useProxy
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
toast.error(String(e))
|
||||||
|
} finally {
|
||||||
|
setSubStoreImporting(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (key.toString().startsWith('collection-')) {
|
}}
|
||||||
setSubStoreImporting(true)
|
>
|
||||||
try {
|
{subStoreMenuItems.map((item) => (
|
||||||
const collection = collections.find(
|
<DropdownItem
|
||||||
(collection) =>
|
startContent={item?.icon}
|
||||||
collection.name === key.toString().replace('collection-', '')
|
key={item.key}
|
||||||
)
|
showDivider={item.divider}
|
||||||
await addProfileItem({
|
>
|
||||||
name: collection?.displayName || collection?.name || '',
|
{item.children}
|
||||||
type: 'remote',
|
</DropdownItem>
|
||||||
substore: !useCustomSubStore,
|
))}
|
||||||
url: useCustomSubStore
|
</DropdownMenu>
|
||||||
? `${customSubStoreUrl}/download/collection/${key.toString().replace('collection-', '')}?target=ClashMeta`
|
|
||||||
: `/download/collection/${key.toString().replace('collection-', '')}`,
|
|
||||||
useProxy
|
|
||||||
})
|
|
||||||
} catch (e) {
|
|
||||||
toast.error(String(e))
|
|
||||||
} finally {
|
|
||||||
setSubStoreImporting(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{subStoreMenuItems.map((item) => (
|
|
||||||
<DropdownItem startContent={item?.icon} key={item.key} showDivider={item.divider}>
|
|
||||||
{item.children}
|
|
||||||
</DropdownItem>
|
|
||||||
))}
|
|
||||||
</DropdownMenu>
|
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
)}
|
)}
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
@ -56,7 +58,7 @@ const useProxyState = (groups: IMihomoMixedGroup[]): {
|
|||||||
console.error('Failed to save scroll position:', error)
|
console.error('Failed to save scroll position:', error)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// 初始化展开状态
|
// 初始化展开状态
|
||||||
const [isOpen, setIsOpen] = useState<boolean[]>(() => {
|
const [isOpen, setIsOpen] = useState<boolean[]>(() => {
|
||||||
try {
|
try {
|
||||||
@ -99,9 +101,10 @@ const Proxies: React.FC = () => {
|
|||||||
proxyCols = 'auto',
|
proxyCols = 'auto',
|
||||||
delayTestConcurrency = 50
|
delayTestConcurrency = 50
|
||||||
} = 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,84 +156,90 @@ 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(
|
||||||
await mihomoChangeProxy(group, proxy)
|
async (group: string, proxy: string): Promise<void> => {
|
||||||
if (autoCloseConnection) {
|
await mihomoChangeProxy(group, proxy)
|
||||||
await mihomoCloseAllConnections()
|
if (autoCloseConnection) {
|
||||||
}
|
await mihomoCloseAllConnections()
|
||||||
mutate()
|
}
|
||||||
}, [autoCloseConnection, mutate])
|
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(
|
||||||
if (allProxies[index].length === 0) {
|
async (index: number): Promise<void> => {
|
||||||
setIsOpen((prev) => {
|
if (allProxies[index].length === 0) {
|
||||||
const newOpen = [...prev]
|
setIsOpen((prev) => {
|
||||||
newOpen[index] = true
|
const newOpen = [...prev]
|
||||||
return newOpen
|
newOpen[index] = true
|
||||||
})
|
return newOpen
|
||||||
}
|
|
||||||
setDelaying((prev) => {
|
|
||||||
const newDelaying = [...prev]
|
|
||||||
newDelaying[index] = true
|
|
||||||
return newDelaying
|
|
||||||
})
|
|
||||||
|
|
||||||
// 管理测试状态
|
|
||||||
const groupProxies = allProxies[index]
|
|
||||||
setProxyDelaying((prev) => {
|
|
||||||
const newSet = new Set(prev)
|
|
||||||
groupProxies.forEach(proxy => newSet.add(proxy.name))
|
|
||||||
return newSet
|
|
||||||
})
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 限制并发数量
|
|
||||||
const result: Promise<void>[] = []
|
|
||||||
const runningList: Promise<void>[] = []
|
|
||||||
for (const proxy of allProxies[index]) {
|
|
||||||
const promise = Promise.resolve().then(async () => {
|
|
||||||
try {
|
|
||||||
await mihomoProxyDelay(proxy.name, groups[index].testUrl)
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
} finally {
|
|
||||||
// 更新状态
|
|
||||||
setProxyDelaying((prev) => {
|
|
||||||
const newSet = new Set(prev)
|
|
||||||
newSet.delete(proxy.name)
|
|
||||||
return newSet
|
|
||||||
})
|
|
||||||
mutate()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
result.push(promise)
|
|
||||||
const running = promise.then(() => {
|
|
||||||
runningList.splice(runningList.indexOf(running), 1)
|
|
||||||
})
|
|
||||||
runningList.push(running)
|
|
||||||
if (runningList.length >= (delayTestConcurrency || 50)) {
|
|
||||||
await Promise.race(runningList)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
await Promise.all(result)
|
|
||||||
} finally {
|
|
||||||
setDelaying((prev) => {
|
setDelaying((prev) => {
|
||||||
const newDelaying = [...prev]
|
const newDelaying = [...prev]
|
||||||
newDelaying[index] = false
|
newDelaying[index] = true
|
||||||
return newDelaying
|
return newDelaying
|
||||||
})
|
})
|
||||||
// 状态清理
|
|
||||||
|
// 管理测试状态
|
||||||
|
const groupProxies = allProxies[index]
|
||||||
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.add(proxy.name))
|
||||||
return newSet
|
return newSet
|
||||||
})
|
})
|
||||||
}
|
|
||||||
}, [allProxies, groups, delayTestConcurrency, mutate, setIsOpen])
|
try {
|
||||||
|
// 限制并发数量
|
||||||
|
const result: Promise<void>[] = []
|
||||||
|
const runningList: Promise<void>[] = []
|
||||||
|
for (const proxy of allProxies[index]) {
|
||||||
|
const promise = Promise.resolve().then(async () => {
|
||||||
|
try {
|
||||||
|
await mihomoProxyDelay(proxy.name, groups[index].testUrl)
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
} finally {
|
||||||
|
// 更新状态
|
||||||
|
setProxyDelaying((prev) => {
|
||||||
|
const newSet = new Set(prev)
|
||||||
|
newSet.delete(proxy.name)
|
||||||
|
return newSet
|
||||||
|
})
|
||||||
|
mutate()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
result.push(promise)
|
||||||
|
const running = promise.then(() => {
|
||||||
|
runningList.splice(runningList.indexOf(running), 1)
|
||||||
|
})
|
||||||
|
runningList.push(running)
|
||||||
|
if (runningList.length >= (delayTestConcurrency || 50)) {
|
||||||
|
await Promise.race(runningList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await Promise.all(result)
|
||||||
|
} finally {
|
||||||
|
setDelaying((prev) => {
|
||||||
|
const newDelaying = [...prev]
|
||||||
|
newDelaying[index] = false
|
||||||
|
return newDelaying
|
||||||
|
})
|
||||||
|
// 状态清理
|
||||||
|
setProxyDelaying((prev) => {
|
||||||
|
const newSet = new Set(prev)
|
||||||
|
groupProxies.forEach((proxy) => newSet.delete(proxy.name))
|
||||||
|
return newSet
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[allProxies, groups, delayTestConcurrency, mutate, setIsOpen]
|
||||||
|
)
|
||||||
|
|
||||||
const calcCols = useCallback((): number => {
|
const calcCols = useCallback((): number => {
|
||||||
if (proxyCols !== 'auto') {
|
if (proxyCols !== 'auto') {
|
||||||
@ -281,170 +290,200 @@ const Proxies: React.FC = () => {
|
|||||||
loadImages()
|
loadImages()
|
||||||
}, [groups, mutate])
|
}, [groups, mutate])
|
||||||
|
|
||||||
const renderGroupContent = useCallback((index: number) => {
|
const renderGroupContent = useCallback(
|
||||||
return groups[index] ? (
|
(index: number) => {
|
||||||
<div
|
return groups[index] ? (
|
||||||
className={`w-full pt-2 ${index === groupCounts.length - 1 && !isOpen[index] ? 'pb-2' : ''} px-2`}
|
<div
|
||||||
>
|
className={`w-full pt-2 ${index === groupCounts.length - 1 && !isOpen[index] ? 'pb-2' : ''} px-2`}
|
||||||
<Card
|
|
||||||
as="div"
|
|
||||||
isPressable
|
|
||||||
fullWidth
|
|
||||||
onPress={() => {
|
|
||||||
setIsOpen((prev) => {
|
|
||||||
const newOpen = [...prev]
|
|
||||||
newOpen[index] = !prev[index]
|
|
||||||
return newOpen
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<CardBody className="w-full">
|
<Card
|
||||||
<div className="flex justify-between">
|
as="div"
|
||||||
<div className="flex text-ellipsis overflow-hidden whitespace-nowrap">
|
isPressable
|
||||||
{groups[index].icon ? (
|
fullWidth
|
||||||
<Avatar
|
onPress={() => {
|
||||||
className="bg-transparent mr-2"
|
setIsOpen((prev) => {
|
||||||
size="sm"
|
const newOpen = [...prev]
|
||||||
radius="sm"
|
newOpen[index] = !prev[index]
|
||||||
src={
|
return newOpen
|
||||||
groups[index].icon.startsWith('<svg')
|
})
|
||||||
? `data:image/svg+xml;utf8,${groups[index].icon}`
|
}}
|
||||||
: localStorage.getItem(groups[index].icon) || groups[index].icon
|
>
|
||||||
}
|
<CardBody className="w-full">
|
||||||
/>
|
<div className="flex justify-between">
|
||||||
) : null}
|
<div className="flex text-ellipsis overflow-hidden whitespace-nowrap">
|
||||||
<div className="text-ellipsis overflow-hidden whitespace-nowrap">
|
{groups[index].icon ? (
|
||||||
<div
|
<Avatar
|
||||||
title={groups[index].name}
|
className="bg-transparent mr-2"
|
||||||
className="inline flag-emoji h-[32px] text-md leading-[32px]"
|
size="sm"
|
||||||
>
|
radius="sm"
|
||||||
{groups[index].name}
|
src={
|
||||||
</div>
|
groups[index].icon.startsWith('<svg')
|
||||||
{proxyDisplayMode === 'full' && (
|
? `data:image/svg+xml;utf8,${groups[index].icon}`
|
||||||
|
: localStorage.getItem(groups[index].icon) || groups[index].icon
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<div className="text-ellipsis overflow-hidden whitespace-nowrap">
|
||||||
<div
|
<div
|
||||||
title={groups[index].type}
|
title={groups[index].name}
|
||||||
className="inline ml-2 text-sm text-foreground-500"
|
className="inline flag-emoji h-[32px] text-md leading-[32px]"
|
||||||
>
|
>
|
||||||
{groups[index].type}
|
{groups[index].name}
|
||||||
</div>
|
</div>
|
||||||
)}
|
{proxyDisplayMode === 'full' && (
|
||||||
|
<div
|
||||||
|
title={groups[index].type}
|
||||||
|
className="inline ml-2 text-sm text-foreground-500"
|
||||||
|
>
|
||||||
|
{groups[index].type}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{proxyDisplayMode === 'full' && (
|
||||||
|
<div className="inline flag-emoji ml-2 text-sm text-foreground-500">
|
||||||
|
{groups[index].now}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
{proxyDisplayMode === 'full' && (
|
{proxyDisplayMode === 'full' && (
|
||||||
<div className="inline flag-emoji ml-2 text-sm text-foreground-500">
|
<Chip size="sm" className="my-1 mr-2">
|
||||||
{groups[index].now}
|
{groups[index].all.length}
|
||||||
</div>
|
</Chip>
|
||||||
)}
|
)}
|
||||||
|
<CollapseInput
|
||||||
|
title={t('proxies.search.placeholder')}
|
||||||
|
value={searchValue[index]}
|
||||||
|
onValueChange={(v) => {
|
||||||
|
setSearchValue((prev) => {
|
||||||
|
const newSearchValue = [...prev]
|
||||||
|
newSearchValue[index] = v
|
||||||
|
return newSearchValue
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
title={t('proxies.locate')}
|
||||||
|
variant="light"
|
||||||
|
size="sm"
|
||||||
|
isIconOnly
|
||||||
|
onPress={() => {
|
||||||
|
if (!isOpen[index]) {
|
||||||
|
setIsOpen((prev) => {
|
||||||
|
const newOpen = [...prev]
|
||||||
|
newOpen[index] = true
|
||||||
|
return newOpen
|
||||||
|
})
|
||||||
|
}
|
||||||
|
let i = 0
|
||||||
|
for (let j = 0; j < index; j++) {
|
||||||
|
i += groupCounts[j]
|
||||||
|
}
|
||||||
|
i += Math.floor(
|
||||||
|
allProxies[index].findIndex((proxy) => proxy.name === groups[index].now) /
|
||||||
|
cols
|
||||||
|
)
|
||||||
|
virtuosoRef.current?.scrollToIndex({
|
||||||
|
index: Math.floor(i),
|
||||||
|
align: 'start'
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FaLocationCrosshairs className="text-lg text-foreground-500" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
title={t('proxies.delay.test')}
|
||||||
|
variant="light"
|
||||||
|
isLoading={delaying[index]}
|
||||||
|
size="sm"
|
||||||
|
isIconOnly
|
||||||
|
onPress={() => {
|
||||||
|
onGroupDelay(index)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MdOutlineSpeed className="text-lg text-foreground-500" />
|
||||||
|
</Button>
|
||||||
|
<IoIosArrowBack
|
||||||
|
className={`transition duration-200 ml-2 h-[32px] text-lg text-foreground-500 ${isOpen[index] ? '-rotate-90' : ''}`}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex">
|
</CardBody>
|
||||||
{proxyDisplayMode === 'full' && (
|
</Card>
|
||||||
<Chip size="sm" className="my-1 mr-2">
|
</div>
|
||||||
{groups[index].all.length}
|
) : (
|
||||||
</Chip>
|
<div>Never See This</div>
|
||||||
)}
|
)
|
||||||
<CollapseInput
|
},
|
||||||
title={t('proxies.search.placeholder')}
|
[
|
||||||
value={searchValue[index]}
|
groups,
|
||||||
onValueChange={(v) => {
|
groupCounts,
|
||||||
setSearchValue((prev) => {
|
isOpen,
|
||||||
const newSearchValue = [...prev]
|
proxyDisplayMode,
|
||||||
newSearchValue[index] = v
|
searchValue,
|
||||||
return newSearchValue
|
delaying,
|
||||||
})
|
cols,
|
||||||
}}
|
allProxies,
|
||||||
/>
|
virtuosoRef,
|
||||||
<Button
|
t,
|
||||||
title={t('proxies.locate')}
|
setIsOpen,
|
||||||
variant="light"
|
onGroupDelay
|
||||||
size="sm"
|
]
|
||||||
isIconOnly
|
)
|
||||||
onPress={() => {
|
|
||||||
if (!isOpen[index]) {
|
|
||||||
setIsOpen((prev) => {
|
|
||||||
const newOpen = [...prev]
|
|
||||||
newOpen[index] = true
|
|
||||||
return newOpen
|
|
||||||
})
|
|
||||||
}
|
|
||||||
let i = 0
|
|
||||||
for (let j = 0; j < index; j++) {
|
|
||||||
i += groupCounts[j]
|
|
||||||
}
|
|
||||||
i += Math.floor(
|
|
||||||
allProxies[index].findIndex(
|
|
||||||
(proxy) => proxy.name === groups[index].now
|
|
||||||
) / cols
|
|
||||||
)
|
|
||||||
virtuosoRef.current?.scrollToIndex({
|
|
||||||
index: Math.floor(i),
|
|
||||||
align: 'start'
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FaLocationCrosshairs className="text-lg text-foreground-500" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
title={t('proxies.delay.test')}
|
|
||||||
variant="light"
|
|
||||||
isLoading={delaying[index]}
|
|
||||||
size="sm"
|
|
||||||
isIconOnly
|
|
||||||
onPress={() => {
|
|
||||||
onGroupDelay(index)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MdOutlineSpeed className="text-lg text-foreground-500" />
|
|
||||||
</Button>
|
|
||||||
<IoIosArrowBack
|
|
||||||
className={`transition duration-200 ml-2 h-[32px] text-lg text-foreground-500 ${isOpen[index] ? '-rotate-90' : ''}`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardBody>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div>Never See This</div>
|
|
||||||
)
|
|
||||||
}, [groups, groupCounts, isOpen, proxyDisplayMode, searchValue, delaying, cols, allProxies, virtuosoRef, t, setIsOpen, onGroupDelay])
|
|
||||||
|
|
||||||
const renderItemContent = useCallback((index: number, groupIndex: number) => {
|
const renderItemContent = useCallback(
|
||||||
let innerIndex = index
|
(index: number, groupIndex: number) => {
|
||||||
groupCounts.slice(0, groupIndex).forEach((count) => {
|
let innerIndex = index
|
||||||
innerIndex -= count
|
groupCounts.slice(0, groupIndex).forEach((count) => {
|
||||||
})
|
innerIndex -= count
|
||||||
return allProxies[groupIndex] ? (
|
})
|
||||||
<div
|
return allProxies[groupIndex] ? (
|
||||||
style={
|
<div
|
||||||
proxyCols !== 'auto'
|
style={
|
||||||
? { gridTemplateColumns: `repeat(${proxyCols}, minmax(0, 1fr))` }
|
proxyCols !== 'auto'
|
||||||
: {}
|
? { gridTemplateColumns: `repeat(${proxyCols}, minmax(0, 1fr))` }
|
||||||
}
|
: {}
|
||||||
className={`grid ${proxyCols === 'auto' ? 'sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5' : ''} ${groupIndex === groupCounts.length - 1 && innerIndex === groupCounts[groupIndex] - 1 ? 'pb-2' : ''} gap-2 pt-2 mx-2`}
|
}
|
||||||
>
|
className={`grid ${proxyCols === 'auto' ? 'sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5' : ''} ${groupIndex === groupCounts.length - 1 && innerIndex === groupCounts[groupIndex] - 1 ? 'pb-2' : ''} gap-2 pt-2 mx-2`}
|
||||||
{Array.from({ length: cols }).map((_, i) => {
|
>
|
||||||
if (!allProxies[groupIndex][innerIndex * cols + i]) return null
|
{Array.from({ length: cols }).map((_, i) => {
|
||||||
return (
|
if (!allProxies[groupIndex][innerIndex * cols + i]) return null
|
||||||
<ProxyItem
|
return (
|
||||||
key={allProxies[groupIndex][innerIndex * cols + i].name}
|
<ProxyItem
|
||||||
mutateProxies={mutate}
|
key={allProxies[groupIndex][innerIndex * cols + i].name}
|
||||||
onProxyDelay={onProxyDelay}
|
mutateProxies={mutate}
|
||||||
onSelect={onChangeProxy}
|
onProxyDelay={onProxyDelay}
|
||||||
proxy={allProxies[groupIndex][innerIndex * cols + i]}
|
onSelect={onChangeProxy}
|
||||||
group={groups[groupIndex]}
|
proxy={allProxies[groupIndex][innerIndex * cols + i]}
|
||||||
proxyDisplayMode={proxyDisplayMode}
|
group={groups[groupIndex]}
|
||||||
selected={
|
proxyDisplayMode={proxyDisplayMode}
|
||||||
allProxies[groupIndex][innerIndex * cols + i]?.name ===
|
selected={
|
||||||
groups[groupIndex].now
|
allProxies[groupIndex][innerIndex * cols + i]?.name === groups[groupIndex].now
|
||||||
}
|
}
|
||||||
isGroupTesting={proxyDelaying.has(allProxies[groupIndex][innerIndex * cols + i].name)}
|
isGroupTesting={proxyDelaying.has(
|
||||||
/>
|
allProxies[groupIndex][innerIndex * cols + i].name
|
||||||
)
|
)}
|
||||||
})}
|
/>
|
||||||
</div>
|
)
|
||||||
) : (
|
})}
|
||||||
<div>Never See This</div>
|
</div>
|
||||||
)
|
) : (
|
||||||
}, [groupCounts, allProxies, proxyCols, cols, groups, proxyDisplayMode, proxyDelaying, mutate, onProxyDelay, onChangeProxy])
|
<div>Never See This</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
[
|
||||||
|
groupCounts,
|
||||||
|
allProxies,
|
||||||
|
proxyCols,
|
||||||
|
cols,
|
||||||
|
groups,
|
||||||
|
proxyDisplayMode,
|
||||||
|
proxyDelaying,
|
||||||
|
mutate,
|
||||||
|
onProxyDelay,
|
||||||
|
onChangeProxy
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BasePage
|
<BasePage
|
||||||
@ -522,4 +561,4 @@ const Proxies: React.FC = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Proxies
|
export default Proxies
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next'
|
|||||||
|
|
||||||
const Resources: React.FC = () => {
|
const Resources: React.FC = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BasePage title={t('sider.cards.resources')}>
|
<BasePage title={t('sider.cards.resources')}>
|
||||||
<GeoData />
|
<GeoData />
|
||||||
|
|||||||
@ -66,7 +66,7 @@ const Settings: React.FC = () => {
|
|||||||
<WebdavConfig />
|
<WebdavConfig />
|
||||||
<MihomoConfig />
|
<MihomoConfig />
|
||||||
<ShortcutConfig />
|
<ShortcutConfig />
|
||||||
<LocalBackupConfig />
|
<LocalBackupConfig />
|
||||||
<Actions />
|
<Actions />
|
||||||
</BasePage>
|
</BasePage>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -19,4 +19,4 @@ updateDayjsLocale()
|
|||||||
// 监听语言变化
|
// 监听语言变化
|
||||||
i18n.on('languageChanged', updateDayjsLocale)
|
i18n.on('languageChanged', updateDayjsLocale)
|
||||||
|
|
||||||
export default dayjs
|
export default dayjs
|
||||||
|
|||||||
@ -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