Compare commits

...

49 Commits
v1.7.1 ... main

Author SHA1 Message Date
ezequielnick
364578f210 1.7.7 Released 2025-07-31 22:31:52 +08:00
Sabertaz
674cefcc29
feat: add webdav max backups configuration and cleanup logic (#862)
issue #648.
2025-07-29 16:55:05 +08:00
ezequielnick
827e744601 feat: add user data directory permission repair functionality 2025-07-29 16:47:49 +08:00
ezequielnick
151726fcce fix: 5: Input/output error when install macos helper 2025-07-28 20:03:38 +08:00
ezequielnick
6f3845151d chore: add missing trans 2025-06-22 10:21:10 +08:00
ezequielnick
a9b5887e15 1.7.6 Released 2025-06-17 22:19:59 +08:00
ezequielnick
727fd48684 chore: add missing trans 2025-06-13 22:04:28 +08:00
ezequielnick
e2ab88f4e2 i18n: add missing trans 2025-06-12 21:13:11 +08:00
Xia Wanxu
1a5c001dbd fix: synchronization issue with gist after mihomo core 1.19.8 update (#780)
Co-authored-by: 2045gemini <2045gemini@gmail.com>

感谢!

(cherry picked from commit f168adb68f917e0214064498efcac347c448672e)
2025-06-11 21:49:46 +08:00
ezequielnick
6744e14c66 fix: EACCES: permission denied on MacOS 2025-06-11 21:45:41 +08:00
ezequielnick
62a04cc5ad 1.7.5 Released 2025-06-08 10:14:43 +08:00
ezequielnick
555130001b feat: integrate with socket reconstruction mechanism 2025-06-07 22:45:38 +08:00
ezequielnick
78f9211ebe fix: builder scripts 2025-06-07 21:50:41 +08:00
ezequielnick
c6d0e05851 chore: change log 2025-06-07 21:50:41 +08:00
ezequielnick
caf962f921 1.7.4 Released
(cherry picked from commit 1e83bac48231a433206c966756d71d139f553f6f)
2025-06-07 21:50:41 +08:00
ezequielnick
0b8c77d200 feat: add animation during latency testing for better user feedback 2025-06-07 21:50:41 +08:00
ezequielnick
d6e456302e feat: enable right-click context menu on subscription cards 2025-06-06 22:42:41 +08:00
ezequielnick
daa8e7ba7e fix: electron-builder script path format 2025-05-30 12:39:43 +08:00
Xia Wanxu
27ab6d1b5c
fix: apply DNS hosts only if useHosts is true (#742)
Co-authored-by: 2045gemini <2045gemini@gmail.com>
2025-05-30 10:17:18 +08:00
Xia Wanxu
da5336ee36
fix: adds the functionality to update the update interval after saving a profile. (#671) 2025-05-30 10:17:08 +08:00
ezequielnick
bdb7fb6489 feat: improve installation mechanism 2025-05-30 10:11:13 +08:00
ezequielnick
1a1992c617 refactor: remove scroll position saving in proxy state management, may fix Maximum call stack size exceeded 2025-05-02 23:42:09 +08:00
miwu04
511eb0c7fa fix: disallow empty selection in Select component
(cherry picked from commit 48eef556770030d8a032fb6dd0e6888d1f0750a0)
2025-05-02 22:34:03 +08:00
miwu04
f54ffcf42b
Remove unused code 2025-04-27 23:11:11 +08:00
xishang0128
5a84d6485e
fix: proxy-provider 2025-04-27 20:55:25 +08:00
miwu04
afe93774b0
Revert "refactor: filter providers by file type instead of subscription info"
This reverts commit d28d33849c5758bb3e48a8337192096243423fce.
2025-04-27 14:30:25 +08:00
miwu04
00be605e6e
1.7.3 Released 2025-04-26 17:09:21 +08:00
miwu04
944475d791
refactor: move sub-store path to WorkDir 2025-04-26 12:05:01 +08:00
ezequielnick
08f15c7e01 feat: add warning prompt before resetting application 2025-04-25 22:33:16 +08:00
ezequielnick
0218f72c5f fix: #587 2025-04-22 23:19:41 +08:00
ezequielnick
c54ce3577d feat: change default UA and add version 2025-04-20 21:12:04 +08:00
miwu04
6cb432dea9 fix: sub-store temp dir 2025-04-18 22:04:44 +08:00
miwu04
a6a3afd3bb fix: update password prompt from 'root' to 'administrator' in multiple languages 2025-04-18 01:00:56 +08:00
YsielX
a2faf0fc8f
feat: add a fixed-interval button for profiles updating (#670) 2025-04-14 10:00:30 +08:00
miwu04
b15fc6ce3a refactor: replace password-based sudo with pkexec for improved security 2025-04-13 00:19:00 +08:00
ForestL
fcb323a17a
Errors occurred when reset firewall since no specified firewall rules existed (#650) 2025-04-10 19:53:03 +08:00
Amamiya Miu
69e65a3959
fix: Fix defaultBypass almost always be the default bypass of Windows (#602)
感谢PR!
2025-03-22 23:36:21 +08:00
Amamiya Miu
6ffcf544b8
fix: Fix missing window at startup due to mihomo core error (#601)
感谢PR!
2025-03-22 23:36:01 +08:00
Junjia
36746074da
feat: Add dynamic Dock icon visibility for macOS (#594)
Implemented `showDockIcon` and `hideDockIcon` to toggle the Dock icon visibility based on user preferences. Adjusted event handlers to respect the `useDockIcon` configuration setting, enhancing macOS-specific behavior and user experience.
2025-03-22 23:35:35 +08:00
ezequielnick
d763e93984 1.7.2 Released 2025-03-03 11:06:15 +08:00
ezequielnick
6ab920ddbd perf: 优化系统代理开关逻辑 2025-03-03 10:19:55 +08:00
ezequielnick
59bd7e8a08 perf: 优化代理页面性能和滚动体验 2025-03-03 09:48:32 +08:00
Alan Lin
f30596231b
fix: update sub-store on linux (#545)
thanks
2025-03-03 09:09:09 +08:00
ezequielnick
4b238d4dc2 refactor: Simplify launch daemon configuration for helper service 2025-02-14 11:53:43 +08:00
ezequielnick
81bb2c44e0 refactor: Improve postinstall script with enhanced error handling and path flexibility 2025-02-13 15:33:09 +08:00
ezequielnick
6cf1ae2c25 fix: button nesting hydration errors 2025-02-13 13:41:53 +08:00
Pompurin404
c906a10562 use privileged helper to set sysproxy 2025-02-13 13:40:03 +08:00
Sherry
eb12f13525
Add Iranian language support (#507)
* Add Iranian language support

* Add Iranian language support

* Add Iranian language support

* Add Iranian language support

* Update template.ts

* Update template.ts
2025-02-12 02:01:05 +08:00
Sherry
502c089f86
Add language Russian (#503)
* Create ru-RU.json

* Update i18n.ts

* Update general-config.tsx Add Russian option

* Add Russian option

* Add Russian option

* Update template.ts

* Update general-config.tsx fix
2025-02-11 12:40:03 +08:00
47 changed files with 1946 additions and 348 deletions

View File

@ -206,7 +206,7 @@ jobs:
CSC_LINK: ${{ secrets.CSC_LINK }} CSC_LINK: ${{ secrets.CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
run: | run: |
chmod +x build/pkg-scripts/postinstall chmod +x build/pkg-scripts/postinstall build/pkg-scripts/preinstall
pnpm build:mac --${{ matrix.arch }} pnpm build:mac --${{ matrix.arch }}
- name: Setup temporary installer signing keychain - name: Setup temporary installer signing keychain
uses: apple-actions/import-codesign-certs@v3 uses: apple-actions/import-codesign-certs@v3
@ -281,7 +281,7 @@ jobs:
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
run: | run: |
sed -i "" -e "s/macos/catalina/" electron-builder.yml sed -i "" -e "s/macos/catalina/" electron-builder.yml
chmod +x build/pkg-scripts/postinstall chmod +x build/pkg-scripts/postinstall build/pkg-scripts/preinstall
pnpm build:mac --${{ matrix.arch }} pnpm build:mac --${{ matrix.arch }}
- name: Setup temporary installer signing keychain - name: Setup temporary installer signing keychain
uses: apple-actions/import-codesign-certs@v3 uses: apple-actions/import-codesign-certs@v3

View File

@ -1,6 +1,173 @@
#!/bin/sh #!/bin/sh
chown root:admin $2/Mihomo\ Party.app/Contents/Resources/sidecar/mihomo set -e
chown root:admin $2/Mihomo\ Party.app/Contents/Resources/sidecar/mihomo-alpha
chmod +s $2/Mihomo\ Party.app/Contents/Resources/sidecar/mihomo # 设置日志文件
chmod +s $2/Mihomo\ Party.app/Contents/Resources/sidecar/mihomo-alpha LOG_FILE="/tmp/mihomo-party-install.log"
exec > "$LOG_FILE" 2>&1
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1"
}
# 检查 root 权限
if [ "$EUID" -ne 0 ]; then
log "Error: Please run as root"
exit 1
fi
# 判断 $2 是否以 .app 结尾
if [[ $2 == *".app" ]]; then
APP_PATH="$2"
else
APP_PATH="$2/Mihomo Party.app"
fi
HELPER_PATH="/Library/PrivilegedHelperTools/party.mihomo.helper"
LAUNCH_DAEMON="/Library/LaunchDaemons/party.mihomo.helper.plist"
log "Starting installation..."
# 创建目录并设置权限
log "Creating directories and setting permissions..."
mkdir -p "/Library/PrivilegedHelperTools"
chmod 755 "/Library/PrivilegedHelperTools"
chown root:wheel "/Library/PrivilegedHelperTools"
# 设置核心文件权限
log "Setting core file permissions..."
if [ -f "$APP_PATH/Contents/Resources/sidecar/mihomo" ]; then
chown root:admin "$APP_PATH/Contents/Resources/sidecar/mihomo"
chmod +s "$APP_PATH/Contents/Resources/sidecar/mihomo"
log "Set permissions for mihomo"
else
log "Warning: mihomo binary not found at $APP_PATH/Contents/Resources/sidecar/mihomo"
fi
if [ -f "$APP_PATH/Contents/Resources/sidecar/mihomo-alpha" ]; then
chown root:admin "$APP_PATH/Contents/Resources/sidecar/mihomo-alpha"
chmod +s "$APP_PATH/Contents/Resources/sidecar/mihomo-alpha"
log "Set permissions for mihomo-alpha"
else
log "Warning: mihomo-alpha binary not found at $APP_PATH/Contents/Resources/sidecar/mihomo-alpha"
fi
# 复制 helper 工具
log "Installing helper tool..."
if [ -f "$APP_PATH/Contents/Resources/files/party.mihomo.helper" ]; then
cp -f "$APP_PATH/Contents/Resources/files/party.mihomo.helper" "$HELPER_PATH"
chown root:wheel "$HELPER_PATH"
chmod 544 "$HELPER_PATH"
log "Helper tool installed successfully"
else
log "Error: Helper file not found at $APP_PATH/Contents/Resources/files/party.mihomo.helper"
exit 1
fi
# 创建并配置 LaunchDaemon
log "Configuring LaunchDaemon..."
mkdir -p "/Library/LaunchDaemons"
cat << EOF > "$LAUNCH_DAEMON"
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>party.mihomo.helper</string>
<key>AssociatedBundleIdentifiers</key>
<array>
<string>party.mihomo.app</string>
</array>
<key>KeepAlive</key>
<true/>
<key>Program</key>
<string>${HELPER_PATH}</string>
<key>StandardErrorPath</key>
<string>/tmp/party.mihomo.helper.err</string>
<key>StandardOutPath</key>
<string>/tmp/party.mihomo.helper.log</string>
</dict>
</plist>
EOF
chown root:wheel "$LAUNCH_DAEMON"
chmod 644 "$LAUNCH_DAEMON"
log "LaunchDaemon configured"
# 验证关键文件
log "Verifying installation..."
if [ ! -x "$HELPER_PATH" ]; then
log "Error: Helper tool is not executable: $HELPER_PATH"
exit 1
fi
# 检查二进制文件有效性
if ! file "$HELPER_PATH" | grep -q "Mach-O"; then
log "Error: Helper tool is not a valid Mach-O binary"
exit 1
fi
# 验证 plist 格式
if ! plutil -lint "$LAUNCH_DAEMON" >/dev/null 2>&1; then
log "Error: Invalid plist format"
exit 1
fi
# 获取 macOS 版本
macos_version=$(sw_vers -productVersion)
macos_major=$(echo "$macos_version" | cut -d. -f1)
log "macOS version: $macos_version"
# 清理现有服务
log "Cleaning up existing services..."
launchctl bootout system "$LAUNCH_DAEMON" 2>/dev/null || true
launchctl unload "$LAUNCH_DAEMON" 2>/dev/null || true
# 加载服务
log "Loading service..."
if [ "$macos_major" -ge 11 ]; then
# macOS Big Sur 及更新版本使用 bootstrap
if ! launchctl bootstrap system "$LAUNCH_DAEMON"; then
log "Bootstrap failed, trying legacy load..."
if ! launchctl load "$LAUNCH_DAEMON"; then
log "Error: Failed to load service with both methods"
exit 1
fi
fi
else
# 旧版本使用 load
if ! launchctl load "$LAUNCH_DAEMON"; then
log "Error: Failed to load service"
exit 1
fi
fi
# 验证服务状态
log "Verifying service status..."
sleep 2
if launchctl list | grep -q "party.mihomo.helper"; then
log "Service loaded successfully"
else
log "Warning: Service may not be running properly"
fi
log "Installation completed successfully"
# Fix user data directory permissions
log "Fixing user data directory permissions..."
for user_home in /Users/*; do
if [ -d "$user_home" ] && [ "$(basename "$user_home")" != "Shared" ] && [ "$(basename "$user_home")" != ".localized" ]; then
username=$(basename "$user_home")
user_data_dir="$user_home/Library/Application Support/mihomo-party"
if [ -d "$user_data_dir" ]; then
current_owner=$(stat -f "%Su" "$user_data_dir" 2>/dev/null || echo "unknown")
if [ "$current_owner" = "root" ]; then
log "Fixing ownership for user: $username"
chown -R "$username:staff" "$user_data_dir" 2>/dev/null || true
chmod -R u+rwX "$user_data_dir" 2>/dev/null || true
fi
fi
fi
done
exit 0 exit 0

View File

@ -0,0 +1,26 @@
#!/bin/sh
set -e
# 检查 root 权限
if [ "$EUID" -ne 0 ]; then
echo "Please run as root"
exit 1
fi
HELPER_PATH="/Library/PrivilegedHelperTools/party.mihomo.helper"
LAUNCH_DAEMON="/Library/LaunchDaemons/party.mihomo.helper.plist"
# 停止并卸载现有的 LaunchDaemon
if [ -f "$LAUNCH_DAEMON" ]; then
launchctl unload "$LAUNCH_DAEMON" 2>/dev/null || true
rm -f "$LAUNCH_DAEMON"
fi
# 移除 helper 工具
rm -f "$HELPER_PATH"
# 清理可能存在的旧版本文件
rm -rf "/Applications/Mihomo Party.app"
rm -rf "/Applications/Mihomo\\ Party.app"
exit 0

View File

@ -1,52 +1,72 @@
## 1.7.1 ## 1.7.7
**注意1主题失效请重新下载一次因为更新了UI组件老主题不兼容了**
**注意2如安装后为英文请在设置中反复选择几次不同语言以写入配置文件**
### 新功能 (Feat) ### 新功能 (Feat)
- 自动检测操作系统语言并设置app - Mihomo 内核升级 v1.19.12
- 新增 Webdav 最大备数设置和清理逻辑
### 修复 (Fix) ### 修复 (Fix)
- 修复详细模式下节点旗帜不显示的问题 - 修复 MacOS 下无法启动的问题(重置工作目录权限)
- 修复引导页显示问题 - 尝试修复不同版本 MacOS 下安装软件时候的报错Input/output error
- 修复缺失的hero-ui参数 - 部分遗漏的多国语言翻译
- 补充丢失的翻译
### 其他改进 (Chore) ## 1.7.6
- 美化延迟测试结果按钮的显示样式
- 默认开1-RTT延迟测试
- 替换默认延迟测试链接
## 1.7.0 **此版本修复了 1.7.5 中的几个严重 bug推荐所有人更新**
### 新功能 (Feat)
- 增加更多核心设置
- 添加内联支持和缺失的翻译
- 增加连接的时间排序功能
- 添加 i18n 支持,包含英文翻译
- 支持应用内更新和重启 Sub-Store
### 修复 (Fix) ### 修复 (Fix)
- 延迟按钮宽度的自动调整 - 修复了内核1.19.8更新后gist同步失效的问题(#780)
- 渲染 Card 为 div防止按钮嵌套导致的 Hydration 错误 - 部分遗漏的多国语言翻译
- 解决自动运行受控组件的警告 - MacOS 下启动Error: EACCES: permission denied
- 解决标题栏覆盖和受控组件的警告 - MacOS 系统代理 bypass 不生效
- 解决无法点击配置文件的问题 - MacOS 系统代理开启时 500 报错
- 解决组件层级中的无效按钮嵌套
- 在 NextUI 按钮组件中用 `onPress` 替换 `onClick`
- 添加 `aria-label` 以解决可访问性警告
- UI 中的延迟测试结果支持自动更新,无需点击
- 移除 NextUI Card 组件中的嵌套按钮元素
- 修复 `useWindowFrame` 切换时的重复重启问题 (#457)
### 重构 (Refactor) ## 1.7.5
- 按文件类型筛选提供者,而不是按订阅信息筛选
- 添加缺失的 `aria-label`,提升可访问性合规性
### 其他改进 (Chore) ### 新功能 (Feat)
- 格式化语言文件并删除未使用的文件 - 增加组延迟测试时的动画
- 添加缺失的 i18n 字符串和 UI 调整 - 订阅卡片可右键点击
- 更新依赖项 -
- 记住滚动位置和展开状态
- 增加节点详细信息
### 依赖更新 (Deps) ### 修复 (Fix)
- 更新依赖项,并将 UI 框架从 NextUI 迁移到 HeroUI - 1.7.4引入的内核启动错误
- 无法手动设置内核权限
- 完善 系统代理socket 重建和检测机制
## 1.7.4
### 新功能 (Feat)
- Mihomo 内核升级 v1.19.10
- 改进 socket创建机制防止 MacOS 下系统代理开启无法找到 socket 文件的问题
- mihomo-party-helper增加更多日志以方便调试
- 改进 MacOS 下签名和公正流程
- 增加 MacOS 下 plist 权限设置
- 改进安装流程
-
### 修复 (Fix)
- 修复mihomo-party-helper本地提权漏洞
- 修复 MacOS 下安装失败的问题
- 移除节点页面的滚动位置记忆,解决页面溢出的问题
- DNS hosts 设置在 useHosts 不为 true 时也会被错误应用的问题(#742)
- 当用户在 Profile 设置中修改了更新间隔并保存后,新的间隔时间不会立即生效(#671)
- 禁止选择器组件选择空值
- 修复proxy-provider
## 1.7.3
**注意:如安装后为英文,请在设置中反复选择几次不同语言以写入配置文件**
### 新功能 (Feat)
- Mihomo 内核升级 v1.19.5
- MacOS 下添加 Dock 图标动态展现方式 (#594)
- 更改默认 UA 并添加版本
- 添加固定间隔的配置文件更新按钮 (#670)
- 重构Linux上的手动授权内核方式
- 将sub-store迁移到工作目录下(#552)
- 重置软件增加警告提示
### 修复 (Fix)
- 修复代理节点页面因为重复刷新导致的溢出问题
- 修复由于 Mihomo 核心错误导致启动时窗口丢失 (#601)
- 修复macOS下的sub-store更新问题 (#552)
- 修复多语言翻译
- 修复 defaultBypass 几乎总是 Windows 默认绕过设置 (#602)
- 修复重置防火墙时发生的错误,因为没有指定防火墙规则 (#650)

View File

@ -1,6 +1,6 @@
{ {
"name": "mihomo-party", "name": "mihomo-party",
"version": "1.7.1", "version": "1.7.7",
"description": "Mihomo Party", "description": "Mihomo Party",
"main": "./out/main/index.js", "main": "./out/main/index.js",
"author": "mihomo-party-org", "author": "mihomo-party-org",

27
scripts/cleanup-mac.sh Executable file
View File

@ -0,0 +1,27 @@
#!/bin/bash
echo "=== Mihomo Party Cleanup Tool ==="
echo "This script will remove all Mihomo Party related files and services."
read -p "Are you sure you want to continue? (y/N) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
echo "Aborted."
exit 1
fi
# Stop and unload services
echo "Stopping services..."
sudo launchctl unload /Library/LaunchDaemons/party.mihomo.helper.plist 2>/dev/null || true
# Remove files
echo "Removing files..."
sudo rm -f /Library/LaunchDaemons/party.mihomo.helper.plist
sudo rm -f /Library/PrivilegedHelperTools/party.mihomo.helper
sudo rm -rf "/Applications/Mihomo Party.app"
sudo rm -rf "/Applications/Mihomo\\ Party.app"
sudo rm -rf ~/Library/Application\ Support/mihomo-party
sudo rm -rf ~/Library/Caches/mihomo-party
sudo rm -f ~/Library/Preferences/party.mihomo.app.helper.plist
sudo rm -f ~/Library/Preferences/party.mihomo.app.plist
echo "Cleanup complete. Please restart your computer to complete the process."

View File

@ -309,6 +309,11 @@ const resolveSubstore = () =>
downloadURL: downloadURL:
'https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store.bundle.js' 'https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store.bundle.js'
}) })
const resolveHelper = () =>
resolveResource({
file: 'party.mihomo.helper',
downloadURL: `https://github.com/mihomo-party-org/mihomo-party-helper/releases/download/${arch}/party.mihomo.helper`
})
const resolveSubstoreFrontend = async () => { const resolveSubstoreFrontend = async () => {
const tempDir = path.join(TEMP_DIR, 'substore-frontend') const tempDir = path.join(TEMP_DIR, 'substore-frontend')
const tempZip = path.join(tempDir, 'dist.zip') const tempZip = path.join(tempDir, 'dist.zip')
@ -404,6 +409,12 @@ const tasks = [
func: resolve7zip, func: resolve7zip,
retry: 5, retry: 5,
winOnly: true winOnly: true
},
{
name: 'helper',
func: resolveHelper,
retry: 5,
darwinOnly: true
} }
] ]
@ -413,6 +424,7 @@ async function runTask() {
if (task.winOnly && platform !== 'win32') return runTask() if (task.winOnly && platform !== 'win32') return runTask()
if (task.linuxOnly && platform !== 'linux') return runTask() if (task.linuxOnly && platform !== 'linux') return runTask()
if (task.unixOnly && platform === 'win32') return runTask() if (task.unixOnly && platform === 'win32') return runTask()
if (task.darwinOnly && platform !== 'darwin') return runTask()
for (let i = 0; i < task.retry; i++) { for (let i = 0; i < task.retry; i++) {
try { try {

View File

@ -10,6 +10,7 @@ import yaml from 'yaml'
import { defaultProfile } from '../utils/template' import { defaultProfile } from '../utils/template'
import { subStorePort } from '../resolve/server' import { subStorePort } from '../resolve/server'
import { join } from 'path' import { join } from 'path'
import { app } from 'electron'
let profileConfig: IProfileConfig // profile.yaml let profileConfig: IProfileConfig // profile.yaml
@ -114,6 +115,7 @@ export async function createProfile(item: Partial<IProfileItem>): Promise<IProfi
interval: item.interval || 0, interval: item.interval || 0,
override: item.override || [], override: item.override || [],
useProxy: item.useProxy || false, useProxy: item.useProxy || false,
allowFixedInterval: item.allowFixedInterval || false,
updated: new Date().getTime() updated: new Date().getTime()
} as IProfileItem } as IProfileItem
switch (newItem.type) { switch (newItem.type) {
@ -133,7 +135,7 @@ export async function createProfile(item: Partial<IProfileItem>): Promise<IProfi
} }
res = await axios.get(urlObj.toString(), { res = await axios.get(urlObj.toString(), {
headers: { headers: {
'User-Agent': userAgent || 'clash.meta' 'User-Agent': userAgent || `mihomo.party/v${app.getVersion()} (clash.meta)`
}, },
responseType: 'text' responseType: 'text'
}) })
@ -147,7 +149,7 @@ export async function createProfile(item: Partial<IProfileItem>): Promise<IProfi
} }
: false, : false,
headers: { headers: {
'User-Agent': userAgent || 'clash.meta' 'User-Agent': userAgent || `mihomo.party/v${app.getVersion()} (clash.meta)`
}, },
responseType: 'text' responseType: 'text'
}) })
@ -162,8 +164,10 @@ export async function createProfile(item: Partial<IProfileItem>): Promise<IProfi
newItem.home = headers['profile-web-page-url'] newItem.home = headers['profile-web-page-url']
} }
if (headers['profile-update-interval']) { if (headers['profile-update-interval']) {
if (!item.allowFixedInterval) {
newItem.interval = parseInt(headers['profile-update-interval']) * 60 newItem.interval = parseInt(headers['profile-update-interval']) * 60
} }
}
if (headers['subscription-userinfo']) { if (headers['subscription-userinfo']) {
newItem.extra = parseSubinfo(headers['subscription-userinfo']) newItem.extra = parseSubinfo(headers['subscription-userinfo'])
} }

View File

@ -145,6 +145,12 @@ 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')) ||
(process.platform === 'win32' && str.includes('External controller pipe listen error'))
) {
reject(i18next.t('mihomo.error.externalControllerListenError'))
}
if ( if (
(process.platform !== 'win32' && str.includes('RESTful API unix listening at')) || (process.platform !== 'win32' && str.includes('RESTful API unix listening at')) ||
(process.platform === 'win32' && str.includes('RESTful API pipe listening at')) (process.platform === 'win32' && str.includes('RESTful API pipe listening at'))
@ -152,7 +158,7 @@ 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().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')
@ -256,18 +262,22 @@ async function checkProfile(): Promise<void> {
} }
} }
export async function manualGrantCorePermition(password?: string): Promise<void> { export async function manualGrantCorePermition(): Promise<void> {
const { core = 'mihomo' } = await getAppConfig() const { core = 'mihomo' } = await getAppConfig()
const corePath = mihomoCorePath(core) const corePath = mihomoCorePath(core)
const execPromise = promisify(exec) const execPromise = promisify(exec)
const execFilePromise = promisify(execFile)
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
const shell = `chown root:admin ${corePath.replace(' ', '\\\\ ')}\nchmod +sx ${corePath.replace(' ', '\\\\ ')}` const shell = `chown root:admin ${corePath.replace(' ', '\\\\ ')}\nchmod +sx ${corePath.replace(' ', '\\\\ ')}`
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}'`)
} }
if (process.platform === 'linux') { if (process.platform === 'linux') {
await execPromise(`echo "${password}" | sudo -S chown root:root "${corePath}"`) await execFilePromise('pkexec', [
await execPromise(`echo "${password}" | sudo -S chmod +sx "${corePath}"`) 'bash',
'-c',
`chown root:root "${corePath}" && chmod +sx "${corePath}"`
])
} }
} }

View File

@ -6,12 +6,14 @@ import { addProfileItem, getAppConfig, patchAppConfig } from './config'
import { quitWithoutCore, startCore, stopCore } from './core/manager' import { quitWithoutCore, startCore, stopCore } 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 } from './resolve/tray' import { createTray, hideDockIcon, showDockIcon } from './resolve/tray'
import { init } from './utils/init' import { init } from './utils/init'
import { join } from 'path' import { join } from 'path'
import { initShortcut } from './resolve/shortcut' import { initShortcut } from './resolve/shortcut'
import { execSync, spawn } from 'child_process' import { execSync, spawn, exec } from 'child_process'
import { createElevateTask } from './sys/misc' import { createElevateTask } from './sys/misc'
import { promisify } from 'util'
import { stat } from 'fs/promises'
import { initProfileUpdater } from './core/profileUpdater' import { initProfileUpdater } from './core/profileUpdater'
import { existsSync, writeFileSync } from 'fs' import { existsSync, writeFileSync } from 'fs'
import { exePath, taskDir } from './utils/dirs' import { exePath, taskDir } from './utils/dirs'
@ -22,6 +24,29 @@ import iconv from 'iconv-lite'
import { initI18n } from '../shared/i18n' import { initI18n } from '../shared/i18n'
import i18next from 'i18next' import i18next from 'i18next'
async function fixUserDataPermissions(): Promise<void> {
if (process.platform !== 'darwin') return
const userDataPath = app.getPath('userData')
if (!existsSync(userDataPath)) return
try {
const stats = await stat(userDataPath)
const currentUid = process.getuid?.() || 0
if (stats.uid === 0 && currentUid !== 0) {
const execPromise = promisify(exec)
const username = process.env.USER || process.env.LOGNAME
if (username) {
await execPromise(`chown -R "${username}:staff" "${userDataPath}"`)
await execPromise(`chmod -R u+rwX "${userDataPath}"`)
}
}
} catch {
// ignore
}
}
let quitTimeout: NodeJS.Timeout | null = null let quitTimeout: NodeJS.Timeout | null = null
export let mainWindow: BrowserWindow | null = null export let mainWindow: BrowserWindow | null = null
@ -59,12 +84,27 @@ if (process.platform === 'win32' && !is.dev && !process.argv.includes('noadmin')
} }
} }
const gotTheLock = app.requestSingleInstanceLock() async function initApp(): Promise<void> {
await fixUserDataPermissions()
if (!gotTheLock) {
app.quit()
} }
initApp()
.then(() => {
const gotTheLock = app.requestSingleInstanceLock()
if (!gotTheLock) {
app.quit()
}
})
.catch(() => {
// ignore permission fix errors
const gotTheLock = app.requestSingleInstanceLock()
if (!gotTheLock) {
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
sleep 0.1 sleep 0.1
@ -269,10 +309,21 @@ export async function createWindow(): Promise<void> {
mainWindow?.webContents.reload() mainWindow?.webContents.reload()
}) })
mainWindow.on('show', () => {
showDockIcon()
})
mainWindow.on('close', async (event) => { mainWindow.on('close', async (event) => {
event.preventDefault() event.preventDefault()
mainWindow?.hide() mainWindow?.hide()
const { autoQuitWithoutCore = false, autoQuitWithoutCoreDelay = 60 } = await getAppConfig() const {
autoQuitWithoutCore = false,
autoQuitWithoutCoreDelay = 60,
useDockIcon = true
} = await getAppConfig()
if (!useDockIcon) {
hideDockIcon()
}
if (autoQuitWithoutCore) { if (autoQuitWithoutCore) {
if (quitTimeout) { if (quitTimeout) {
clearTimeout(quitTimeout) clearTimeout(quitTimeout)

View File

@ -19,7 +19,8 @@ export async function webdavBackup(): Promise<boolean> {
webdavUrl = '', webdavUrl = '',
webdavUsername = '', webdavUsername = '',
webdavPassword = '', webdavPassword = '',
webdavDir = 'mihomo-party' webdavDir = 'mihomo-party',
webdavMaxBackups = 0
} = await getAppConfig() } = await getAppConfig()
const zip = new AdmZip() const zip = new AdmZip()
@ -44,7 +45,41 @@ export async function webdavBackup(): Promise<boolean> {
// ignore // ignore
} }
return await client.putFileContents(`${webdavDir}/${zipFileName}`, zip.toBuffer()) const result = await client.putFileContents(`${webdavDir}/${zipFileName}`, zip.toBuffer())
if (webdavMaxBackups > 0) {
try {
const files = await client.getDirectoryContents(webdavDir, { glob: '*.zip' })
const fileList = Array.isArray(files) ? files : files.data
const currentPlatformFiles = fileList.filter((file) => {
return file.basename.startsWith(`${process.platform}_`)
})
currentPlatformFiles.sort((a, b) => {
const timeA = a.basename.match(/_(\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2})\.zip$/)?.[1] || ''
const timeB = b.basename.match(/_(\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2})\.zip$/)?.[1] || ''
return timeB.localeCompare(timeA)
})
if (currentPlatformFiles.length > webdavMaxBackups) {
const filesToDelete = currentPlatformFiles.slice(webdavMaxBackups)
for (let i = 0; i < filesToDelete.length; i++) {
const file = filesToDelete[i]
await client.deleteFile(`${webdavDir}/${file.basename}`)
if (i < filesToDelete.length - 1) {
await new Promise((resolve) => setTimeout(resolve, 500))
}
}
}
} catch (error) {
console.error('Failed to clean up old backup files:', error)
}
}
return result
} }
export async function webdavRestore(filename: string): Promise<void> { export async function webdavRestore(filename: string): Promise<void> {

View File

@ -1,6 +1,6 @@
import { getAppConfig, getControledMihomoConfig } from '../config' import { getAppConfig, getControledMihomoConfig } from '../config'
import { Worker } from 'worker_threads' import { Worker } from 'worker_threads'
import { mihomoWorkDir, resourcesFilesDir, subStoreDir, substoreLogPath } from '../utils/dirs' import { dataDir, mihomoWorkDir, subStoreDir, substoreLogPath } from '../utils/dirs'
import subStoreIcon from '../../../resources/subStoreIcon.png?asset' import subStoreIcon from '../../../resources/subStoreIcon.png?asset'
import { createWriteStream, existsSync, mkdirSync } from 'fs' import { createWriteStream, existsSync, mkdirSync } from 'fs'
import { writeFile, rm, cp } from 'fs/promises' import { writeFile, rm, cp } from 'fs/promises'
@ -77,7 +77,7 @@ export async function startSubStoreFrontendServer(): Promise<void> {
await stopSubStoreFrontendServer() await stopSubStoreFrontendServer()
subStoreFrontendPort = await findAvailablePort(14122) subStoreFrontendPort = await findAvailablePort(14122)
const app = express() const app = express()
app.use(express.static(path.join(resourcesFilesDir(), 'sub-store-frontend'))) app.use(express.static(path.join(mihomoWorkDir(), 'sub-store-frontend')))
subStoreFrontendServer = app.listen(subStoreFrontendPort, subStoreHost) subStoreFrontendServer = app.listen(subStoreFrontendPort, subStoreHost)
} }
@ -118,7 +118,7 @@ export async function startSubStoreBackendServer(): Promise<void> {
SUB_STORE_MMDB_COUNTRY_PATH: path.join(mihomoWorkDir(), 'country.mmdb'), SUB_STORE_MMDB_COUNTRY_PATH: path.join(mihomoWorkDir(), 'country.mmdb'),
SUB_STORE_MMDB_ASN_PATH: path.join(mihomoWorkDir(), 'ASN.mmdb') SUB_STORE_MMDB_ASN_PATH: path.join(mihomoWorkDir(), 'ASN.mmdb')
} }
subStoreBackendWorker = new Worker(path.join(resourcesFilesDir(), 'sub-store.bundle.js'), { subStoreBackendWorker = new Worker(path.join(mihomoWorkDir(), 'sub-store.bundle.js'), {
env: useProxyInSubStore env: useProxyInSubStore
? { ? {
...env, ...env,
@ -141,12 +141,19 @@ export async function stopSubStoreBackendServer(): Promise<void> {
export async function downloadSubStore(): Promise<void> { export async function downloadSubStore(): Promise<void> {
const { 'mixed-port': mixedPort = 7890 } = await getControledMihomoConfig() const { 'mixed-port': mixedPort = 7890 } = await getControledMihomoConfig()
const frontendDir = path.join(resourcesFilesDir(), 'sub-store-frontend') const frontendDir = path.join(mihomoWorkDir(), 'sub-store-frontend')
const backendPath = path.join(resourcesFilesDir(), 'sub-store.bundle.js') const backendPath = path.join(mihomoWorkDir(), 'sub-store.bundle.js')
const tempDir = path.join(resourcesFilesDir(), 'temp') const tempDir = path.join(dataDir(), 'temp')
try { try {
// 创建临时目录
if (existsSync(tempDir)) {
await rm(tempDir, { recursive: true })
}
mkdirSync(tempDir, { recursive: true })
// 下载后端文件 // 下载后端文件
const tempBackendPath = path.join(tempDir, 'sub-store.bundle.js')
const backendRes = await axios.get( const backendRes = await axios.get(
'https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store.bundle.js', 'https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store.bundle.js',
{ {
@ -159,9 +166,9 @@ export async function downloadSubStore(): Promise<void> {
} }
} }
) )
await writeFile(backendPath, Buffer.from(backendRes.data)) await writeFile(tempBackendPath, Buffer.from(backendRes.data))
// 下载前端文件 // 下载前端文件
const tempFrontendDir = path.join(tempDir, 'dist')
const frontendRes = await axios.get( const frontendRes = await axios.get(
'https://github.com/sub-store-org/Sub-Store-Front-End/releases/latest/download/dist.zip', 'https://github.com/sub-store-org/Sub-Store-Front-End/releases/latest/download/dist.zip',
{ {
@ -174,27 +181,15 @@ export async function downloadSubStore(): Promise<void> {
} }
} }
) )
// 创建临时目录
if (existsSync(tempDir)) {
await rm(tempDir, { recursive: true })
}
mkdirSync(tempDir, { recursive: true })
// 先解压到临时目录 // 先解压到临时目录
const zip = new AdmZip(Buffer.from(frontendRes.data)) const zip = new AdmZip(Buffer.from(frontendRes.data))
zip.extractAllTo(tempDir, true) zip.extractAllTo(tempDir, true)
await cp(tempBackendPath, backendPath)
// 确保目标目录存在并清空
if (existsSync(frontendDir)) { if (existsSync(frontendDir)) {
await rm(frontendDir, { recursive: true }) await rm(frontendDir, { recursive: true })
} }
mkdirSync(frontendDir, { recursive: true }) mkdirSync(frontendDir, { recursive: true })
// 将 dist 目录中的内容移动到目标目录
await cp(path.join(tempDir, 'dist'), frontendDir, { recursive: true }) await cp(path.join(tempDir, 'dist'), frontendDir, { recursive: true })
// 清理临时目录
await rm(tempDir, { recursive: true }) await rm(tempDir, { recursive: true })
} catch (error) { } catch (error) {
console.error('substore.downloadFailed:', error) console.error('substore.downloadFailed:', error)

View File

@ -309,7 +309,7 @@ export async function createTray(): Promise<void> {
tray?.setIgnoreDoubleClickEvents(true) tray?.setIgnoreDoubleClickEvents(true)
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
if (!useDockIcon) { if (!useDockIcon) {
app.dock.hide() hideDockIcon()
} }
ipcMain.on('trayIconUpdate', async (_, png: string) => { ipcMain.on('trayIconUpdate', async (_, png: string) => {
const image = nativeImage.createFromDataURL(png).resize({ height: 16 }) const image = nativeImage.createFromDataURL(png).resize({ height: 16 })
@ -387,3 +387,15 @@ export async function closeTrayIcon(): Promise<void> {
} }
tray = null tray = null
} }
export async function showDockIcon(): Promise<void> {
if (process.platform === 'darwin' && !app.dock.isVisible()) {
await app.dock.show()
}
}
export async function hideDockIcon(): Promise<void> {
if (process.platform === 'darwin' && app.dock.isVisible()) {
app.dock.hide()
}
}

View File

@ -7,7 +7,8 @@ import path from 'path'
const appName = 'mihomo-party' const appName = 'mihomo-party'
const taskXml = `<?xml version="1.0" encoding="UTF-16"?> function getTaskXml(): string {
return `<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task"> <Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<Triggers> <Triggers>
<LogonTrigger> <LogonTrigger>
@ -48,6 +49,7 @@ const taskXml = `<?xml version="1.0" encoding="UTF-16"?>
</Actions> </Actions>
</Task> </Task>
` `
}
export async function checkAutoRun(): Promise<boolean> { export async function checkAutoRun(): Promise<boolean> {
if (process.platform === 'win32') { if (process.platform === 'win32') {
@ -80,7 +82,7 @@ export async function enableAutoRun(): Promise<void> {
if (process.platform === 'win32') { if (process.platform === 'win32') {
const execPromise = promisify(exec) const execPromise = promisify(exec)
const taskFilePath = path.join(taskDir(), `${appName}.xml`) const taskFilePath = path.join(taskDir(), `${appName}.xml`)
await writeFile(taskFilePath, Buffer.from(`\ufeff${taskXml}`, 'utf-16le')) await writeFile(taskFilePath, Buffer.from(`\ufeff${getTaskXml()}`, 'utf-16le'))
await execPromise( await execPromise(
`%SystemRoot%\\System32\\schtasks.exe /create /tn "${appName}" /xml "${taskFilePath}" /f` `%SystemRoot%\\System32\\schtasks.exe /create /tn "${appName}" /xml "${taskFilePath}" /f`
) )

View File

@ -45,9 +45,12 @@ export async function openUWPTool(): Promise<void> {
export async function setupFirewall(): Promise<void> { export async function setupFirewall(): Promise<void> {
const execPromise = promisify(exec) const execPromise = promisify(exec)
const removeCommand = ` const removeCommand = `
Remove-NetFirewallRule -DisplayName "mihomo" -ErrorAction SilentlyContinue $rules = @("mihomo", "mihomo-alpha", "Mihomo Party")
Remove-NetFirewallRule -DisplayName "mihomo-alpha" -ErrorAction SilentlyContinue foreach ($rule in $rules) {
Remove-NetFirewallRule -DisplayName "Mihomo Party" -ErrorAction SilentlyContinue if (Get-NetFirewallRule -DisplayName $rule -ErrorAction SilentlyContinue) {
Remove-NetFirewallRule -DisplayName $rule -ErrorAction SilentlyContinue
}
}
` `
const createCommand = ` const createCommand = `
New-NetFirewallRule -DisplayName "mihomo" -Direction Inbound -Action Allow -Program "${mihomoCorePath('mihomo')}" -Enabled True -Profile Any -ErrorAction SilentlyContinue New-NetFirewallRule -DisplayName "mihomo" -Direction Inbound -Action Allow -Program "${mihomoCorePath('mihomo')}" -Enabled True -Profile Any -ErrorAction SilentlyContinue
@ -65,7 +68,8 @@ export function setNativeTheme(theme: 'system' | 'light' | 'dark'): void {
nativeTheme.themeSource = theme nativeTheme.themeSource = theme
} }
const elevateTaskXml = `<?xml version="1.0" encoding="UTF-16"?> function getElevateTaskXml(): string {
return `<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task"> <Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<Triggers /> <Triggers />
<Principals> <Principals>
@ -101,10 +105,11 @@ const elevateTaskXml = `<?xml version="1.0" encoding="UTF-16"?>
</Actions> </Actions>
</Task> </Task>
` `
}
export function createElevateTask(): void { export function createElevateTask(): void {
const taskFilePath = path.join(taskDir(), `mihomo-party-run.xml`) const taskFilePath = path.join(taskDir(), `mihomo-party-run.xml`)
writeFileSync(taskFilePath, Buffer.from(`\ufeff${elevateTaskXml}`, 'utf-16le')) writeFileSync(taskFilePath, Buffer.from(`\ufeff${getElevateTaskXml()}`, 'utf-16le'))
copyFileSync( copyFileSync(
path.join(resourcesFilesDir(), 'mihomo-party-run.exe'), path.join(resourcesFilesDir(), 'mihomo-party-run.exe'),
path.join(taskDir(), 'mihomo-party-run.exe') path.join(taskDir(), 'mihomo-party-run.exe')

View File

@ -6,9 +6,12 @@ import { execFile } from 'child_process'
import path from 'path' import path from 'path'
import { resourcesFilesDir } from '../utils/dirs' import { resourcesFilesDir } from '../utils/dirs'
import { net } from 'electron' import { net } from 'electron'
import axios from 'axios'
import fs from 'fs'
let defaultBypass: string[] let defaultBypass: string[]
let triggerSysProxyTimer: NodeJS.Timeout | null = null let triggerSysProxyTimer: NodeJS.Timeout | null = null
const helperSocketPath = '/tmp/mihomo-party-helper.sock'
if (process.platform === 'linux') if (process.platform === 'linux')
defaultBypass = ['localhost', '127.0.0.1', '192.168.0.0/16', '10.0.0.0/8', '172.16.0.0/12', '::1'] defaultBypass = ['localhost', '127.0.0.1', '192.168.0.0/16', '10.0.0.0/8', '172.16.0.0/12', '::1']
@ -79,6 +82,16 @@ async function enableSysProxy(): Promise<void> {
} catch { } catch {
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') {
await helperRequest(() =>
axios.post(
'http://localhost/pac',
{ url: `http://${host || '127.0.0.1'}:${pacPort}/pac` },
{
socketPath: helperSocketPath
}
)
)
} else { } else {
triggerAutoProxy(true, `http://${host || '127.0.0.1'}:${pacPort}/pac`) triggerAutoProxy(true, `http://${host || '127.0.0.1'}:${pacPort}/pac`)
} }
@ -97,6 +110,16 @@ async function enableSysProxy(): Promise<void> {
} catch { } catch {
triggerManualProxy(true, host || '127.0.0.1', port, bypass.join(',')) triggerManualProxy(true, host || '127.0.0.1', port, bypass.join(','))
} }
} else if (process.platform === 'darwin') {
await helperRequest(() =>
axios.post(
'http://localhost/global',
{ host: host || '127.0.0.1', port: port.toString(), bypass: bypass.join(',') },
{
socketPath: helperSocketPath
}
)
)
} else { } else {
triggerManualProxy(true, host || '127.0.0.1', port, bypass.join(',')) triggerManualProxy(true, host || '127.0.0.1', port, bypass.join(','))
} }
@ -115,8 +138,85 @@ async function disableSysProxy(): Promise<void> {
triggerAutoProxy(false, '') triggerAutoProxy(false, '')
triggerManualProxy(false, '', 0, '') triggerManualProxy(false, '', 0, '')
} }
} else if (process.platform === 'darwin') {
await helperRequest(() =>
axios.get('http://localhost/off', {
socketPath: helperSocketPath
})
)
} else { } else {
triggerAutoProxy(false, '') triggerAutoProxy(false, '')
triggerManualProxy(false, '', 0, '') triggerManualProxy(false, '', 0, '')
} }
} }
// Helper function to check if socket file exists
function isSocketFileExists(): boolean {
try {
return fs.existsSync(helperSocketPath)
} catch {
return false
}
}
// Helper function to send signal to recreate socket
async function requestSocketRecreation(): Promise<void> {
try {
// Send SIGUSR1 signal to helper process to recreate socket
const { exec } = require('child_process')
const { promisify } = require('util')
const execPromise = promisify(exec)
// Use osascript with administrator privileges (same pattern as manualGrantCorePermition)
const shell = `pkill -USR1 -f party.mihomo.helper`
const command = `do shell script "${shell}" with administrator privileges`
await execPromise(`osascript -e '${command}'`)
// Wait a bit for socket recreation
await new Promise(resolve => setTimeout(resolve, 1000))
} catch (error) {
console.log('Failed to send signal to helper:', error)
throw error
}
}
// Wrapper function for helper requests with auto-retry on socket issues
async function helperRequest(requestFn: () => Promise<unknown>, maxRetries = 1): Promise<unknown> {
let lastError: Error | null = null
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await requestFn()
} catch (error) {
lastError = error as Error
// Check if it's a connection error and socket file doesn't exist
if (attempt < maxRetries &&
((error as NodeJS.ErrnoException).code === 'ECONNREFUSED' ||
(error as NodeJS.ErrnoException).code === 'ENOENT' ||
(error as Error).message?.includes('connect ECONNREFUSED') ||
(error as Error).message?.includes('ENOENT'))) {
console.log(`Helper request failed (attempt ${attempt + 1}), checking socket file...`)
if (!isSocketFileExists()) {
console.log('Socket file missing, requesting recreation...')
try {
await requestSocketRecreation()
console.log('Socket recreation requested, retrying...')
continue
} catch (signalError) {
console.log('Failed to request socket recreation:', signalError)
}
}
}
// If not a connection error or we've exhausted retries, throw the error
if (attempt === maxRetries) {
throw lastError
}
}
}
throw lastError
}

View File

@ -18,7 +18,13 @@ export function dataDir(): string {
} }
export function taskDir(): string { export function taskDir(): string {
const dir = path.join(app.getPath('userData'), 'tasks') const userDataDir = app.getPath('userData')
// 确保 userData 目录存在
if (!existsSync(userDataDir)) {
mkdirSync(userDataDir, { recursive: true })
}
const dir = path.join(userDataDir, 'tasks')
if (!existsSync(dir)) { if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true }) mkdirSync(dir, { recursive: true })
} }

View File

@ -22,8 +22,10 @@ import {
defaultProfileConfig defaultProfileConfig
} from './template' } from './template'
import yaml from 'yaml' import yaml from 'yaml'
import { mkdir, writeFile, copyFile, rm, readdir } from 'fs/promises' import { mkdir, writeFile, rm, readdir, cp, stat } from 'fs/promises'
import { existsSync } from 'fs' import { existsSync } from 'fs'
import { exec } from 'child_process'
import { promisify } from 'util'
import path from 'path' import path from 'path'
import { import {
startPacServer, startPacServer,
@ -40,7 +42,32 @@ import {
import { app } from 'electron' import { app } from 'electron'
import { startSSIDCheck } from '../sys/ssid' import { startSSIDCheck } from '../sys/ssid'
async function fixDataDirPermissions(): Promise<void> {
if (process.platform !== 'darwin') return
const dataDirPath = dataDir()
if (!existsSync(dataDirPath)) return
try {
const stats = await stat(dataDirPath)
const currentUid = process.getuid?.() || 0
if (stats.uid === 0 && currentUid !== 0) {
const execPromise = promisify(exec)
const username = process.env.USER || process.env.LOGNAME
if (username) {
await execPromise(`chown -R "${username}:staff" "${dataDirPath}"`)
await execPromise(`chmod -R u+rwX "${dataDirPath}"`)
}
}
} catch {
// ignore
}
}
async function initDirs(): Promise<void> { async function initDirs(): Promise<void> {
await fixDataDirPermissions()
if (!existsSync(dataDir())) { if (!existsSync(dataDir())) {
await mkdir(dataDir()) await mkdir(dataDir())
} }
@ -88,13 +115,13 @@ async function initConfig(): Promise<void> {
async function initFiles(): Promise<void> { async function initFiles(): Promise<void> {
const copy = async (file: string): Promise<void> => { const copy = async (file: string): Promise<void> => {
const targetPath = path.join(mihomoWorkDir(), file) const targetPath = path.join(mihomoWorkDir(), file)
const testTargrtPath = path.join(mihomoTestDir(), file) const testTargetPath = path.join(mihomoTestDir(), file)
const sourcePath = path.join(resourcesFilesDir(), file) const sourcePath = path.join(resourcesFilesDir(), file)
if (!existsSync(targetPath) && existsSync(sourcePath)) { if (!existsSync(targetPath) && existsSync(sourcePath)) {
await copyFile(sourcePath, targetPath) await cp(sourcePath, targetPath, { recursive: true })
} }
if (!existsSync(testTargrtPath) && existsSync(sourcePath)) { if (!existsSync(testTargetPath) && existsSync(sourcePath)) {
await copyFile(sourcePath, testTargrtPath) await cp(sourcePath, testTargetPath, { recursive: true })
} }
} }
await Promise.all([ await Promise.all([
@ -102,7 +129,9 @@ async function initFiles(): Promise<void> {
copy('geoip.metadb'), copy('geoip.metadb'),
copy('geoip.dat'), copy('geoip.dat'),
copy('geosite.dat'), copy('geosite.dat'),
copy('ASN.mmdb') copy('ASN.mmdb'),
copy('sub-store.bundle.js'),
copy('sub-store-frontend')
]) ])
} }

View File

@ -89,6 +89,7 @@ import { getImageDataURL } from './image'
import { startMonitor } from '../resolve/trafficMonitor' import { startMonitor } from '../resolve/trafficMonitor'
import { closeFloatingWindow, showContextMenu, showFloatingWindow } from '../resolve/floatingWindow' import { closeFloatingWindow, showContextMenu, showFloatingWindow } from '../resolve/floatingWindow'
import i18next from 'i18next' import i18next from 'i18next'
import { addProfileUpdater } from '../core/profileUpdater'
function ipcErrorWrapper<T>( // eslint-disable-next-line @typescript-eslint/no-explicit-any function ipcErrorWrapper<T>( // eslint-disable-next-line @typescript-eslint/no-explicit-any
fn: (...args: any[]) => Promise<T> // eslint-disable-next-line @typescript-eslint/no-explicit-any fn: (...args: any[]) => Promise<T> // eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -163,6 +164,7 @@ export function registerIpcMainHandlers(): void {
ipcMain.handle('changeCurrentProfile', (_e, id) => ipcErrorWrapper(changeCurrentProfile)(id)) ipcMain.handle('changeCurrentProfile', (_e, id) => ipcErrorWrapper(changeCurrentProfile)(id))
ipcMain.handle('addProfileItem', (_e, item) => ipcErrorWrapper(addProfileItem)(item)) ipcMain.handle('addProfileItem', (_e, item) => ipcErrorWrapper(addProfileItem)(item))
ipcMain.handle('removeProfileItem', (_e, id) => ipcErrorWrapper(removeProfileItem)(id)) ipcMain.handle('removeProfileItem', (_e, id) => ipcErrorWrapper(removeProfileItem)(id))
ipcMain.handle('addProfileUpdater', (_e, item) => ipcErrorWrapper(addProfileUpdater)(item))
ipcMain.handle('getOverrideConfig', (_e, force) => ipcErrorWrapper(getOverrideConfig)(force)) ipcMain.handle('getOverrideConfig', (_e, force) => ipcErrorWrapper(getOverrideConfig)(force))
ipcMain.handle('setOverrideConfig', (_e, config) => ipcErrorWrapper(setOverrideConfig)(config)) ipcMain.handle('setOverrideConfig', (_e, config) => ipcErrorWrapper(setOverrideConfig)(config))
ipcMain.handle('getOverrideItem', (_e, id) => ipcErrorWrapper(getOverrideItem)(id)) ipcMain.handle('getOverrideItem', (_e, id) => ipcErrorWrapper(getOverrideItem)(id))
@ -174,9 +176,7 @@ export function registerIpcMainHandlers(): void {
ipcMain.handle('restartCore', ipcErrorWrapper(restartCore)) ipcMain.handle('restartCore', ipcErrorWrapper(restartCore))
ipcMain.handle('startMonitor', (_e, detached) => ipcErrorWrapper(startMonitor)(detached)) ipcMain.handle('startMonitor', (_e, detached) => ipcErrorWrapper(startMonitor)(detached))
ipcMain.handle('triggerSysProxy', (_e, enable) => ipcErrorWrapper(triggerSysProxy)(enable)) ipcMain.handle('triggerSysProxy', (_e, enable) => ipcErrorWrapper(triggerSysProxy)(enable))
ipcMain.handle('manualGrantCorePermition', (_e, password) => ipcMain.handle('manualGrantCorePermition', () => ipcErrorWrapper(manualGrantCorePermition)())
ipcErrorWrapper(manualGrantCorePermition)(password)
)
ipcMain.handle('getFilePath', (_e, ext) => getFilePath(ext)) ipcMain.handle('getFilePath', (_e, ext) => getFilePath(ext))
ipcMain.handle('readTextFile', (_e, filePath) => ipcErrorWrapper(readTextFile)(filePath)) ipcMain.handle('readTextFile', (_e, filePath) => ipcErrorWrapper(readTextFile)(filePath))
ipcMain.handle('getRuntimeConfigStr', ipcErrorWrapper(getRuntimeConfigStr)) ipcMain.handle('getRuntimeConfigStr', ipcErrorWrapper(getRuntimeConfigStr))
@ -204,7 +204,6 @@ export function registerIpcMainHandlers(): void {
ipcMain.handle('startSubStoreBackendServer', () => ipcErrorWrapper(startSubStoreBackendServer)()) ipcMain.handle('startSubStoreBackendServer', () => ipcErrorWrapper(startSubStoreBackendServer)())
ipcMain.handle('stopSubStoreBackendServer', () => ipcErrorWrapper(stopSubStoreBackendServer)()) ipcMain.handle('stopSubStoreBackendServer', () => ipcErrorWrapper(stopSubStoreBackendServer)())
ipcMain.handle('downloadSubStore', () => ipcErrorWrapper(downloadSubStore)()) ipcMain.handle('downloadSubStore', () => ipcErrorWrapper(downloadSubStore)())
ipcMain.handle('subStorePort', () => subStorePort) ipcMain.handle('subStorePort', () => subStorePort)
ipcMain.handle('subStoreFrontendPort', () => subStoreFrontendPort) ipcMain.handle('subStoreFrontendPort', () => subStoreFrontendPort)
ipcMain.handle('subStoreSubs', () => ipcErrorWrapper(subStoreSubs)()) ipcMain.handle('subStoreSubs', () => ipcErrorWrapper(subStoreSubs)())

View File

@ -0,0 +1,37 @@
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from '@heroui/react'
import React from 'react'
import { useTranslation } from 'react-i18next'
interface Props {
title: string
content: string
onCancel: () => void
onConfirm: () => void
isOpen: boolean
}
const BaseConfirmModal: React.FC<Props> = (props) => {
const { t } = useTranslation()
const { title, content, onCancel, onConfirm, isOpen } = props
return (
<Modal backdrop="blur" classNames={{ backdrop: 'top-[48px]' }} hideCloseButton isOpen={isOpen}>
<ModalContent>
<ModalHeader className="flex app-drag">{title}</ModalHeader>
<ModalBody>
<p>{content}</p>
</ModalBody>
<ModalFooter>
<Button size="sm" variant="light" onPress={onCancel}>
{t('common.cancel')}
</Button>
<Button size="sm" color="danger" onPress={onConfirm}>
{t('common.confirm')}
</Button>
</ModalFooter>
</ModalContent>
</Modal>
)
}
export default BaseConfirmModal

View File

@ -1,43 +0,0 @@
import {
Modal,
ModalContent,
ModalHeader,
ModalBody,
ModalFooter,
Button,
Input
} from '@heroui/react'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
interface Props {
onCancel: () => void
onConfirm: (script: string) => void
}
const BasePasswordModal: React.FC<Props> = (props) => {
const { t } = useTranslation()
const { onCancel, onConfirm } = props
const [password, setPassword] = useState('')
return (
<Modal backdrop="blur" classNames={{ backdrop: 'top-[48px]' }} hideCloseButton isOpen={true}>
<ModalContent>
<ModalHeader className="flex app-drag">{t('common.enterRootPassword')}</ModalHeader>
<ModalBody>
<Input fullWidth type="password" value={password} onValueChange={setPassword} />
</ModalBody>
<ModalFooter>
<Button size="sm" variant="light" onPress={onCancel}>
{t('common.cancel')}
</Button>
<Button size="sm" color="primary" onPress={() => onConfirm(password)}>
{t('common.confirm')}
</Button>
</ModalFooter>
</ModalContent>
</Modal>
)
}
export default BasePasswordModal

View File

@ -162,6 +162,7 @@ const OverrideItem: React.FC<Props> = (props) => {
)} )}
{openLog && <ExecLogModal id={info.id} onClose={() => setOpenLog(false)} />} {openLog && <ExecLogModal id={info.id} onClose={() => setOpenLog(false)} />}
<Card <Card
as="div"
fullWidth fullWidth
isPressable isPressable
onPress={() => { onPress={() => {

View File

@ -16,7 +16,7 @@ import {
import React, { useState } from 'react' import React, { useState } from 'react'
import SettingItem from '../base/base-setting-item' import SettingItem from '../base/base-setting-item'
import { useOverrideConfig } from '@renderer/hooks/use-override-config' import { useOverrideConfig } from '@renderer/hooks/use-override-config'
import { restartCore } from '@renderer/utils/ipc' 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'
@ -36,13 +36,15 @@ const EditInfoModal: React.FC<Props> = (props) => {
const onSave = async (): Promise<void> => { const onSave = async (): Promise<void> => {
try { try {
await updateProfileItem({ const updatedItem = {
...values, ...values,
override: values.override?.filter( override: values.override?.filter(
(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 addProfileUpdater(updatedItem)
await restartCore() await restartCore()
onClose() onClose()
} catch (e) { } catch (e) {
@ -108,6 +110,15 @@ const EditInfoModal: React.FC<Props> = (props) => {
}} }}
/> />
</SettingItem> </SettingItem>
<SettingItem title={t('profiles.editInfo.fixedInterval')}>
<Switch
size="sm"
isSelected={values.allowFixedInterval ?? false}
onValueChange={(v) => {
setValues({ ...values, allowFixedInterval: v })
}}
/>
</SettingItem>
</> </>
)} )}
<SettingItem title={t('profiles.editInfo.override.title')}> <SettingItem title={t('profiles.editInfo.override.title')}>

View File

@ -60,6 +60,7 @@ const ProfileItem: React.FC<Props> = (props) => {
const [selecting, setSelecting] = useState(false) const [selecting, setSelecting] = useState(false)
const [openInfoEditor, setOpenInfoEditor] = useState(false) const [openInfoEditor, setOpenInfoEditor] = useState(false)
const [openFileEditor, setOpenFileEditor] = useState(false) const [openFileEditor, setOpenFileEditor] = useState(false)
const [dropdownOpen, setDropdownOpen] = useState(false)
const { const {
attributes, attributes,
listeners, listeners,
@ -143,6 +144,12 @@ const ProfileItem: React.FC<Props> = (props) => {
} }
} }
const handleContextMenu = (e: React.MouseEvent) => {
e.preventDefault()
e.stopPropagation()
setDropdownOpen(true)
}
useEffect(() => { useEffect(() => {
if (isDragging) { if (isDragging) {
setTimeout(() => { setTimeout(() => {
@ -155,6 +162,8 @@ const ProfileItem: React.FC<Props> = (props) => {
} }
}, [isDragging]) }, [isDragging])
return ( return (
<div <div
className="grid col-span-1" className="grid col-span-1"
@ -173,6 +182,7 @@ const ProfileItem: React.FC<Props> = (props) => {
updateProfileItem={updateProfileItem} updateProfileItem={updateProfileItem}
/> />
)} )}
<Card <Card
as="div" as="div"
fullWidth fullWidth
@ -184,6 +194,7 @@ const ProfileItem: React.FC<Props> = (props) => {
setSelecting(false) setSelecting(false)
}) })
}} }}
onContextMenu={handleContextMenu}
className={`${isCurrent ? 'bg-primary' : ''} ${selecting ? 'blur-sm' : ''}`} className={`${isCurrent ? 'bg-primary' : ''} ${selecting ? 'blur-sm' : ''}`}
> >
<div ref={setNodeRef} {...attributes} {...listeners} className="w-full h-full"> <div ref={setNodeRef} {...attributes} {...listeners} className="w-full h-full">
@ -218,7 +229,10 @@ const ProfileItem: React.FC<Props> = (props) => {
</Tooltip> </Tooltip>
)} )}
<Dropdown> <Dropdown
isOpen={dropdownOpen}
onOpenChange={setDropdownOpen}
>
<DropdownTrigger> <DropdownTrigger>
<Button isIconOnly size="sm" variant="light" color="default"> <Button isIconOnly size="sm" variant="light" color="default">
<IoMdMore <IoMdMore

View File

@ -1,6 +1,7 @@
import { Button, Card, CardBody } from '@heroui/react' import { Button, Card, CardBody } from '@heroui/react'
import { mihomoUnfixedProxy } from '@renderer/utils/ipc' import { mihomoUnfixedProxy } from '@renderer/utils/ipc'
import React, { useMemo, useState } from 'react' import React from 'react'
import { useMemo, useState } from 'react'
import { FaMapPin } from 'react-icons/fa6' import { FaMapPin } from 'react-icons/fa6'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -12,11 +13,12 @@ interface Props {
group: IMihomoMixedGroup group: IMihomoMixedGroup
onSelect: (group: string, proxy: string) => void onSelect: (group: string, proxy: string) => void
selected: boolean selected: boolean
isGroupTesting?: boolean
} }
const ProxyItem: React.FC<Props> = (props) => { const ProxyItem: React.FC<Props> = (props) => {
const { t } = useTranslation() const { t } = useTranslation()
const { mutateProxies, proxyDisplayMode, group, proxy, selected, onSelect, onProxyDelay } = props const { mutateProxies, proxyDisplayMode, group, proxy, selected, onSelect, onProxyDelay, isGroupTesting = false } = props
const delay = useMemo(() => { const delay = useMemo(() => {
if (proxy.history.length > 0) { if (proxy.history.length > 0) {
@ -26,6 +28,9 @@ const ProxyItem: React.FC<Props> = (props) => {
}, [proxy]) }, [proxy])
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const isLoading = loading || isGroupTesting
function delayColor(delay: number): 'primary' | 'success' | 'warning' | 'danger' { function delayColor(delay: number): 'primary' | 'success' | 'warning' | 'danger' {
if (delay === -1) return 'primary' if (delay === -1) return 'primary'
if (delay === 0) return 'danger' if (delay === 0) return 'danger'
@ -106,7 +111,7 @@ const ProxyItem: React.FC<Props> = (props) => {
<Button <Button
isIconOnly isIconOnly
title={proxy.type} title={proxy.type}
isLoading={loading} isLoading={isLoading}
color={delayColor(delay)} color={delayColor(delay)}
onPress={onDelay} onPress={onDelay}
variant="light" variant="light"
@ -144,11 +149,11 @@ const ProxyItem: React.FC<Props> = (props) => {
<Button <Button
isIconOnly isIconOnly
title={proxy.type} title={proxy.type}
isLoading={loading} isLoading={isLoading}
color={delayColor(delay)} color={delayColor(delay)}
onPress={onDelay} onPress={onDelay}
variant="light" variant="light"
className="h-[24px] text-sm px-2 relative w-min whitespace-nowrap" className="h-full text-sm px-2 relative w-min whitespace-nowrap"
> >
<div className="w-full h-full flex items-center justify-end"> <div className="w-full h-full flex items-center justify-end">
{delayText(delay)} {delayText(delay)}

View File

@ -50,25 +50,10 @@ const ProxyProvider: React.FC = () => {
const providers = useMemo(() => { const providers = useMemo(() => {
if (!data) return [] if (!data) return []
return Object.values(data.providers) return Object.values(data.providers)
.map(provider => { .filter((provider) => provider.vehicleType !== 'Compatible')
if (provider.vehicleType === 'Inline' || provider.vehicleType === 'File') {
return {
...provider,
subscriptionInfo: null
}
}
return provider
})
.filter(provider => 'subscriptionInfo' in provider)
.sort((a, b) => { .sort((a, b) => {
if (a.vehicleType === 'File' && b.vehicleType !== 'File') { const order = { File: 1, Inline: 2, HTTP: 3 }
return -1 return (order[a.vehicleType] || 4) - (order[b.vehicleType] || 4)
}
if (a.vehicleType !== 'File' && b.vehicleType === 'File') {
return 1
}
return 0
}) })
}, [data]) }, [data])
const [updating, setUpdating] = useState(Array(providers.length).fill(false)) const [updating, setUpdating] = useState(Array(providers.length).fill(false))

View File

@ -14,6 +14,7 @@ import { version } from '@renderer/utils/init'
import { IoIosHelpCircle } from 'react-icons/io' import { IoIosHelpCircle } from 'react-icons/io'
import { getDriver } from '@renderer/App' import { getDriver } from '@renderer/App'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import BaseConfirmModal from '../base/base-confirm-modal'
const Actions: React.FC = () => { const Actions: React.FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
@ -21,6 +22,7 @@ const Actions: React.FC = () => {
const [changelog, setChangelog] = useState('') const [changelog, setChangelog] = useState('')
const [openUpdate, setOpenUpdate] = useState(false) const [openUpdate, setOpenUpdate] = useState(false)
const [checkingUpdate, setCheckingUpdate] = useState(false) const [checkingUpdate, setCheckingUpdate] = useState(false)
const [showResetConfirm, setShowResetConfirm] = useState(false)
return ( return (
<> <>
@ -31,6 +33,18 @@ const Actions: React.FC = () => {
changelog={changelog} changelog={changelog}
/> />
)} )}
{showResetConfirm && (
<BaseConfirmModal
isOpen={showResetConfirm}
title={t('actions.reset.confirm.title')}
content={t('actions.reset.confirm.content')}
onCancel={() => setShowResetConfirm(false)}
onConfirm={() => {
resetAppConfig()
setShowResetConfirm(false)
}}
/>
)}
<SettingCard> <SettingCard>
<SettingItem title={t('actions.guide.title')} divider> <SettingItem title={t('actions.guide.title')} divider>
<Button size="sm" onPress={() => getDriver()?.drive()}> <Button size="sm" onPress={() => getDriver()?.drive()}>
@ -75,7 +89,7 @@ const Actions: React.FC = () => {
} }
divider divider
> >
<Button size="sm" onPress={resetAppConfig}> <Button size="sm" onPress={() => setShowResetConfirm(true)}>
{t('actions.reset.button')} {t('actions.reset.button')}
</Button> </Button>
</SettingItem> </SettingItem>

View File

@ -86,13 +86,15 @@ const GeneralConfig: React.FC = () => {
selectedKeys={[language]} selectedKeys={[language]}
aria-label={t('settings.language')} aria-label={t('settings.language')}
onSelectionChange={async (v) => { onSelectionChange={async (v) => {
const newLang = Array.from(v)[0] as 'zh-CN' | 'en-US' const newLang = Array.from(v)[0] as 'zh-CN' | 'en-US' | 'ru-RU' | 'fa-IR'
await patchAppConfig({ language: newLang }) await patchAppConfig({ language: newLang })
i18n.changeLanguage(newLang) i18n.changeLanguage(newLang)
}} }}
> >
<SelectItem key="zh-CN"></SelectItem> <SelectItem key="zh-CN"></SelectItem>
<SelectItem key="en-US">English</SelectItem> <SelectItem key="en-US">English</SelectItem>
<SelectItem key="ru-RU">Русский</SelectItem>
<SelectItem key="fa-IR">فارسی</SelectItem>
</Select> </Select>
</SettingItem> </SettingItem>
<SettingItem title={t('settings.autoStart')} divider> <SettingItem title={t('settings.autoStart')} divider>
@ -191,6 +193,7 @@ const GeneralConfig: React.FC = () => {
selectionMode="multiple" selectionMode="multiple"
selectedKeys={new Set(envType)} selectedKeys={new Set(envType)}
aria-label={t('settings.envType')} aria-label={t('settings.envType')}
disallowEmptySelection={true}
onSelectionChange={async (v) => { onSelectionChange={async (v) => {
try { try {
await patchAppConfig({ await patchAppConfig({
@ -387,6 +390,7 @@ const GeneralConfig: React.FC = () => {
size="sm" size="sm"
selectedKeys={new Set([customTheme])} selectedKeys={new Set([customTheme])}
aria-label={t('settings.selectTheme')} aria-label={t('settings.selectTheme')}
disallowEmptySelection={true}
onSelectionChange={async (v) => { onSelectionChange={async (v) => {
try { try {
await patchAppConfig({ customTheme: v.currentKey as string }) await patchAppConfig({ customTheme: v.currentKey as string })

View File

@ -8,7 +8,7 @@ import { getGistUrl, patchControledMihomoConfig, restartCore } from '@renderer/u
import { MdDeleteForever } from 'react-icons/md' import { MdDeleteForever } from 'react-icons/md'
import { BiCopy } from 'react-icons/bi' import { BiCopy } from 'react-icons/bi'
import { IoIosHelpCircle } from 'react-icons/io' import { IoIosHelpCircle } from 'react-icons/io'
import { platform } from '@renderer/utils/init' import { platform, version } from '@renderer/utils/init'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
const MihomoConfig: React.FC = () => { const MihomoConfig: React.FC = () => {
@ -44,7 +44,7 @@ const MihomoConfig: React.FC = () => {
size="sm" size="sm"
className="w-[60%]" className="w-[60%]"
value={ua} value={ua}
placeholder={t('mihomo.userAgentPlaceholder')} placeholder={t('mihomo.userAgentPlaceholder', { version })}
onValueChange={(v) => { onValueChange={(v) => {
setUa(v) setUa(v)
setUaDebounce(v) setUaDebounce(v)
@ -129,6 +129,7 @@ const MihomoConfig: React.FC = () => {
size="sm" size="sm"
selectedKeys={new Set([proxyCols])} selectedKeys={new Set([proxyCols])}
aria-label={t('mihomo.proxyColumns.title')} aria-label={t('mihomo.proxyColumns.title')}
disallowEmptySelection={true}
onSelectionChange={async (v) => { onSelectionChange={async (v) => {
await patchAppConfig({ proxyCols: v.currentKey as 'auto' | '1' | '2' | '3' | '4' }) await patchAppConfig({ proxyCols: v.currentKey as 'auto' | '1' | '2' | '3' | '4' })
}} }}
@ -147,6 +148,7 @@ const MihomoConfig: React.FC = () => {
className="w-[150px]" className="w-[150px]"
size="sm" size="sm"
selectedKeys={new Set([mihomoCpuPriority])} selectedKeys={new Set([mihomoCpuPriority])}
disallowEmptySelection={true}
onSelectionChange={async (v) => { onSelectionChange={async (v) => {
try { try {
await patchAppConfig({ await patchAppConfig({

View File

@ -1,7 +1,7 @@
import React, { useState } from 'react' import React, { useState } from 'react'
import SettingCard from '../base/base-setting-card' import SettingCard from '../base/base-setting-card'
import SettingItem from '../base/base-setting-item' import SettingItem from '../base/base-setting-item'
import { Button, Input } from '@heroui/react' import { Button, Input, Select, SelectItem } from '@heroui/react'
import { listWebdavBackups, webdavBackup } from '@renderer/utils/ipc' import { listWebdavBackups, webdavBackup } from '@renderer/utils/ipc'
import WebdavRestoreModal from './webdav-restore-modal' import WebdavRestoreModal from './webdav-restore-modal'
import debounce from '@renderer/utils/debounce' import debounce from '@renderer/utils/debounce'
@ -11,16 +11,31 @@ import { useTranslation } from 'react-i18next'
const WebdavConfig: React.FC = () => { const WebdavConfig: React.FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { appConfig, patchAppConfig } = useAppConfig() const { appConfig, patchAppConfig } = useAppConfig()
const { webdavUrl, webdavUsername, webdavPassword, webdavDir = 'mihomo-party' } = appConfig || {} const {
webdavUrl,
webdavUsername,
webdavPassword,
webdavDir = 'mihomo-party',
webdavMaxBackups = 0
} = appConfig || {}
const [backuping, setBackuping] = useState(false) const [backuping, setBackuping] = useState(false)
const [restoring, setRestoring] = useState(false) const [restoring, setRestoring] = useState(false)
const [filenames, setFilenames] = useState<string[]>([]) const [filenames, setFilenames] = useState<string[]>([])
const [restoreOpen, setRestoreOpen] = useState(false) const [restoreOpen, setRestoreOpen] = useState(false)
const [webdav, setWebdav] = useState({ webdavUrl, webdavUsername, webdavPassword, webdavDir }) const [webdav, setWebdav] = useState({
const setWebdavDebounce = debounce(({ webdavUrl, webdavUsername, webdavPassword, webdavDir }) => { webdavUrl,
patchAppConfig({ webdavUrl, webdavUsername, webdavPassword, webdavDir }) webdavUsername,
}, 500) webdavPassword,
webdavDir,
webdavMaxBackups
})
const setWebdavDebounce = debounce(
({ webdavUrl, webdavUsername, webdavPassword, webdavDir, webdavMaxBackups }) => {
patchAppConfig({ webdavUrl, webdavUsername, webdavPassword, webdavDir, webdavMaxBackups })
},
500
)
const handleBackup = async (): Promise<void> => { const handleBackup = async (): Promise<void> => {
setBackuping(true) setBackuping(true)
try { try {
@ -98,6 +113,28 @@ const WebdavConfig: React.FC = () => {
}} }}
/> />
</SettingItem> </SettingItem>
<SettingItem title={t('webdav.maxBackups')} divider>
<Select
classNames={{ trigger: 'data-[hover=true]:bg-default-200' }}
className="w-[150px]"
size="sm"
selectedKeys={new Set([webdav.webdavMaxBackups.toString()])}
aria-label={t('webdav.maxBackups')}
onSelectionChange={(v) => {
const value = Number.parseInt(Array.from(v)[0] as string, 10)
setWebdav({ ...webdav, webdavMaxBackups: value })
setWebdavDebounce({ ...webdav, webdavMaxBackups: value })
}}
>
<SelectItem key="0">{t('webdav.noLimit')}</SelectItem>
<SelectItem key="1">1</SelectItem>
<SelectItem key="3">3</SelectItem>
<SelectItem key="5">5</SelectItem>
<SelectItem key="10">10</SelectItem>
<SelectItem key="15">15</SelectItem>
<SelectItem key="20">20</SelectItem>
</Select>
</SettingItem>
<div className="flex justify0between"> <div className="flex justify0between">
<Button isLoading={backuping} fullWidth size="sm" className="mr-1" onPress={handleBackup}> <Button isLoading={backuping} fullWidth size="sm" className="mr-1" onPress={handleBackup}>
{t('webdav.backup')} {t('webdav.backup')}

View File

@ -34,12 +34,17 @@ const SysproxySwitcher: React.FC<Props> = (props) => {
}) })
const transform = tf ? { x: tf.x, y: tf.y, scaleX: 1, scaleY: 1 } : null const transform = tf ? { x: tf.x, y: tf.y, scaleX: 1, scaleY: 1 } : null
const onChange = async (enable: boolean): Promise<void> => { const onChange = async (enable: boolean): Promise<void> => {
const previousState = !enable
// 立即更新UI
try { try {
await triggerSysProxy(enable)
await patchAppConfig({ sysProxy: { enable } }) await patchAppConfig({ sysProxy: { enable } })
await triggerSysProxy(enable)
window.electron.ipcRenderer.send('updateFloatingWindow') window.electron.ipcRenderer.send('updateFloatingWindow')
window.electron.ipcRenderer.send('updateTrayMenu') window.electron.ipcRenderer.send('updateTrayMenu')
} catch (e) { } catch (e) {
await patchAppConfig({ sysProxy: { enable: previousState } })
alert(e) alert(e)
} }
} }

View File

@ -15,6 +15,7 @@ export const GroupsProvider: React.FC<{ children: ReactNode }> = ({ children })
errorRetryCount: 10, errorRetryCount: 10,
refreshInterval: 2000, refreshInterval: 2000,
dedupingInterval: 1000, dedupingInterval: 1000,
keepPreviousData: true,
revalidateOnFocus: false revalidateOnFocus: false
}) })

View File

@ -15,11 +15,17 @@
"common.default": "Default", "common.default": "Default",
"common.close": "Close", "common.close": "Close",
"common.pinWindow": "Pin Window", "common.pinWindow": "Pin Window",
"common.enterRootPassword": "Please enter root password",
"common.next": "Next", "common.next": "Next",
"common.prev": "Previous", "common.prev": "Previous",
"common.done": "Done", "common.done": "Done",
"common.notification.restartRequired": "Restart required for changes to take effect", "common.notification.restartRequired": "Restart required for changes to take effect",
"common.notification.systemProxyEnabled": "System proxy enabled",
"common.notification.systemProxyDisabled": "System proxy disabled",
"common.notification.tunEnabled": "TUN enabled",
"common.notification.tunDisabled": "TUN disabled",
"common.notification.ruleMode": "Rule Mode",
"common.notification.globalMode": "Global Mode",
"common.notification.directMode": "Direct Mode",
"common.error.appCrash": "Application crashed :( Please submit the following information to the developer to troubleshoot", "common.error.appCrash": "Application crashed :( Please submit the following information to the developer to troubleshoot",
"common.error.copyErrorMessage": "Copy Error Message", "common.error.copyErrorMessage": "Copy Error Message",
"common.error.invalidCron": "Invalid Cron expression", "common.error.invalidCron": "Invalid Cron expression",
@ -67,7 +73,7 @@
"settings.links.telegram": "Telegram Group", "settings.links.telegram": "Telegram Group",
"settings.title": "Application Settings", "settings.title": "Application Settings",
"mihomo.userAgent": "Subscription User Agent", "mihomo.userAgent": "Subscription User Agent",
"mihomo.userAgentPlaceholder": "Default: clash.meta", "mihomo.userAgentPlaceholder": "Default: mihomo.party/v{{version}} (clash.meta)",
"mihomo.delayTest.url": "Delay Test URL", "mihomo.delayTest.url": "Delay Test URL",
"mihomo.delayTest.urlPlaceholder": "Default: http://www.gstatic.com/generate_204", "mihomo.delayTest.urlPlaceholder": "Default: http://www.gstatic.com/generate_204",
"mihomo.delayTest.concurrency": "Delay Test Concurrency", "mihomo.delayTest.concurrency": "Delay Test Concurrency",
@ -139,6 +145,7 @@
"mihomo.debug": "Debug", "mihomo.debug": "Debug",
"mihomo.error.coreStartFailed": "Core start failed", "mihomo.error.coreStartFailed": "Core start failed",
"mihomo.error.profileCheckFailed": "Profile Check Failed", "mihomo.error.profileCheckFailed": "Profile Check Failed",
"mihomo.error.externalControllerListenError": "External controller listen error",
"mihomo.findProcess": "Find Process", "mihomo.findProcess": "Find Process",
"mihomo.selectFindProcessMode": "Select Process Find Mode", "mihomo.selectFindProcessMode": "Select Process Find Mode",
"mihomo.strict": "Auto", "mihomo.strict": "Auto",
@ -172,6 +179,8 @@
"webdav.dir": "WebDAV Backup Directory", "webdav.dir": "WebDAV Backup Directory",
"webdav.username": "WebDAV Username", "webdav.username": "WebDAV Username",
"webdav.password": "WebDAV Password", "webdav.password": "WebDAV Password",
"webdav.maxBackups": "Max Backups",
"webdav.noLimit": "No Limit",
"webdav.backup": "Backup", "webdav.backup": "Backup",
"webdav.restore.title": "Restore Backup", "webdav.restore.title": "Restore Backup",
"webdav.restore.noBackups": "No backups available", "webdav.restore.noBackups": "No backups available",
@ -225,6 +234,8 @@
"actions.reset.title": "Reset App", "actions.reset.title": "Reset App",
"actions.reset.button": "Reset App", "actions.reset.button": "Reset App",
"actions.reset.tooltip": "Delete all configurations and restore the app to its initial state", "actions.reset.tooltip": "Delete all configurations and restore the app to its initial state",
"actions.reset.confirm.title": "Confirm Reset",
"actions.reset.confirm.content": "Are you sure you want to reset the app and delete all configurations? Only reset the app when it is not working properly. Your all subscriptions, overrides, and scripts will be lost!",
"actions.heapSnapshot.title": "Create Heap Snapshot", "actions.heapSnapshot.title": "Create Heap Snapshot",
"actions.heapSnapshot.button": "Create Heap Snapshot", "actions.heapSnapshot.button": "Create Heap Snapshot",
"actions.heapSnapshot.tooltip": "Create a heap snapshot of the main process for memory issue debugging", "actions.heapSnapshot.tooltip": "Create a heap snapshot of the main process for memory issue debugging",
@ -344,6 +355,7 @@
"profiles.editInfo.url": "Subscription URL", "profiles.editInfo.url": "Subscription URL",
"profiles.editInfo.useProxy": "Use Proxy to Update", "profiles.editInfo.useProxy": "Use Proxy to Update",
"profiles.editInfo.interval": "Upd. Interval (min)", "profiles.editInfo.interval": "Upd. Interval (min)",
"profiles.editInfo.fixedInterval": "Fixed Update Interval",
"profiles.editInfo.override.title": "Override", "profiles.editInfo.override.title": "Override",
"profiles.editInfo.override.global": "Global", "profiles.editInfo.override.global": "Global",
"profiles.editInfo.override.noAvailable": "No available overrides", "profiles.editInfo.override.noAvailable": "No available overrides",

View File

@ -0,0 +1,510 @@
{
"common.settings": "تنظیمات",
"common.profiles": "پروفایل‌ها",
"common.proxies": "پراکسی‌ها",
"common.connections": "اتصالات",
"common.dns": "DNS",
"common.tun": "TUN",
"common.save": "ذخیره",
"common.cancel": "لغو",
"common.edit": "ویرایش",
"common.delete": "حذف",
"common.seconds": "ثانیه",
"common.confirm": "تایید",
"common.auto": "خودکار",
"common.default": "پیش‌فرض",
"common.close": "بستن",
"common.pinWindow": "پین کردن پنجره",
"common.next": "بعدی",
"common.prev": "قبلی",
"common.done": "انجام شد",
"common.notification.restartRequired": "برای اعمال تغییرات نیاز به راه‌اندازی مجدد است",
"common.notification.systemProxyEnabled": "پراکسی سیستم فعال شد",
"common.notification.systemProxyDisabled": "پراکسی سیستم غیرفعال شد",
"common.notification.tunEnabled": "TUN فعال شد",
"common.notification.tunDisabled": "TUN غیرفعال شد",
"common.notification.ruleMode": "حالت قانون",
"common.notification.globalMode": "حالت جهانی",
"common.notification.directMode": "حالت مستقیم",
"common.error.appCrash": "برنامه دچار خطا شد :( لطفا اطلاعات زیر را برای رفع مشکل به توسعه‌دهنده ارسال کنید",
"common.error.copyErrorMessage": "کپی پیام خطا",
"common.error.invalidCron": "عبارت Cron نامعتبر است",
"common.error.getBackupListFailed": "دریافت لیست پشتیبان با خطا مواجه شد: {{error}}",
"common.error.restoreFailed": "بازیابی با خطا مواجه شد: {{error}}",
"common.error.deleteFailed": "حذف با خطا مواجه شد: {{error}}",
"common.error.shortcutRegistrationFailed": "ثبت میانبر با خطا مواجه شد",
"common.error.shortcutRegistrationFailedWithError": "ثبت میانبر با خطا مواجه شد: {{error}}",
"common.error.adminRequired": "لطفا برای اولین اجرا با دسترسی مدیر برنامه را اجرا کنید",
"common.error.initFailed": "راه‌اندازی برنامه با خطا مواجه شد",
"common.updater.versionReady": "نسخه v{{version}} آماده است",
"common.updater.goToDownload": "دانلود",
"common.updater.update": "به‌روزرسانی",
"settings.general": "تنظیمات عمومی",
"settings.mihomo": "تنظیمات Mihomo",
"settings.language": "زبان",
"settings.theme": "پوسته",
"settings.darkMode": "حالت تاریک",
"settings.lightMode": "حالت روشن",
"settings.autoStart": "اجرای خودکار در شروع سیستم",
"settings.autoCheckUpdate": "بررسی خودکار به‌روزرسانی",
"settings.silentStart": "اجرای بی‌صدا",
"settings.autoQuitWithoutCore": "ورود خودکار به حالت سبک",
"settings.autoQuitWithoutCoreTooltip": "ورود خودکار به حالت سبک پس از بستن پنجره برای زمان مشخص",
"settings.autoQuitWithoutCoreDelay": "تاخیر فعال‌سازی خودکار حالت سبک",
"settings.envType": "نوع متغیر محیطی",
"settings.showFloatingWindow": "نمایش پنجره شناور",
"settings.spinFloatingIcon": "چرخش آیکون شناور بر اساس سرعت شبکه",
"settings.disableTray": "غیرفعال کردن آیکون سیستم‌تری",
"settings.proxyInTray": "نمایش اطلاعات پراکسی در منوی سیستم‌تری",
"settings.showTraffic_windows": "نمایش سرعت شبکه در نوار وظیفه",
"settings.showTraffic_mac": "نمایش سرعت شبکه در نوار وضعیت",
"settings.showDockIcon": "نمایش آیکون Dock",
"settings.useWindowFrame": "استفاده از نوار عنوان سیستم",
"settings.backgroundColor": "رنگ پس‌زمینه",
"settings.backgroundAuto": "خودکار",
"settings.backgroundDark": "تیره",
"settings.backgroundLight": "روشن",
"settings.fetchTheme": "دریافت پوسته",
"settings.importTheme": "وارد کردن پوسته",
"settings.editTheme": "ویرایش پوسته",
"settings.selectTheme": "انتخاب پوسته",
"settings.links.docs": "مستندات",
"settings.links.github": "مخزن GitHub",
"settings.links.telegram": "گروه تلگرام",
"settings.title": "تنظیمات برنامه",
"mihomo.userAgent": "User Agent اشتراک",
"mihomo.userAgentPlaceholder": "پیش‌فرض: mihomo.party/v{{version}} (clash.meta)",
"mihomo.title": "تنظیمات هسته",
"mihomo.restart": "راه‌اندازی مجدد هسته",
"mihomo.memory": "مصرف حافظه",
"mihomo.delayTest.url": "آدرس تست تاخیر",
"mihomo.delayTest.urlPlaceholder": "پیش‌فرض: http://www.gstatic.com/generate_204",
"mihomo.delayTest.concurrency": "همزمانی تست تاخیر",
"mihomo.delayTest.concurrencyPlaceholder": "پیش‌فرض: 50",
"mihomo.delayTest.timeout": "زمان انتظار تست تاخیر",
"mihomo.delayTest.timeoutPlaceholder": "پیش‌فرض: 5000",
"mihomo.gist.title": "همگام‌سازی پیکربندی با Gist",
"mihomo.gist.copyUrl": "کپی آدرس Gist",
"mihomo.gist.token": "توکن GitHub",
"mihomo.proxyColumns.title": "ستون‌های نمایش پراکسی",
"mihomo.proxyColumns.auto": "خودکار",
"mihomo.proxyColumns.one": "یک ستون",
"mihomo.proxyColumns.two": "دو ستون",
"mihomo.proxyColumns.three": "سه ستون",
"mihomo.proxyColumns.four": "چهار ستون",
"mihomo.cpuPriority.title": "اولویت پردازنده هسته",
"mihomo.cpuPriority.realtime": "بلادرنگ",
"mihomo.cpuPriority.high": "بالا",
"mihomo.cpuPriority.aboveNormal": "بالاتر از عادی",
"mihomo.cpuPriority.normal": "عادی",
"mihomo.cpuPriority.belowNormal": "پایین‌تر از عادی",
"mihomo.cpuPriority.low": "پایین",
"mihomo.workDir.title": "استفاده از پوشه کاری مجزا برای اشتراک‌های مختلف",
"mihomo.workDir.tooltip": "برای جلوگیری از تداخل گروه‌های پراکسی با نام یکسان در اشتراک‌های مختلف",
"mihomo.controlDns": "کنترل تنظیمات DNS",
"mihomo.controlSniff": "کنترل تشخیص دامنه",
"mihomo.autoCloseConnection": "بستن خودکار اتصال",
"mihomo.pauseSSID.title": "اتصال مستقیم برای SSIDهای خاص",
"mihomo.pauseSSID.placeholder": "SSID را وارد کنید",
"mihomo.coreVersion": "نسخه هسته",
"mihomo.upgradeCore": "ارتقاء هسته",
"mihomo.CoreAuthLost": "مجوز هسته از دست رفت",
"mihomo.coreUpgradeSuccess": "ارتقاء هسته با موفقیت انجام شد. اگر می‌خواهید از کارت شبکه مجازی (Tun) استفاده کنید، لطفا به صفحه کارت شبکه مجازی بروید و مجددا به صورت دستی به هسته مجوز دهید",
"mihomo.alreadyLatestVersion": "آخرین نسخه نصب شده است",
"mihomo.selectCoreVersion": "انتخاب نسخه هسته",
"mihomo.stableVersion": "نسخه پایدار",
"mihomo.alphaVersion": "نسخه آلفا",
"mihomo.mixedPort": "پورت ترکیبی",
"mihomo.confirm": "تایید",
"mihomo.socksPort": "پورت Socks",
"mihomo.httpPort": "پورت Http",
"mihomo.redirPort": "پورت Redir",
"mihomo.externalController": "آدرس کنترل‌کننده خارجی",
"mihomo.externalControllerSecret": "رمز کنترل‌کننده خارجی",
"mihomo.ipv6": "IPv6",
"mihomo.allowLanConnection": "اجازه اتصال شبکه محلی",
"mihomo.allowedIpSegments": "بخش‌های IP مجاز",
"mihomo.disallowedIpSegments": "بخش‌های IP غیرمجاز",
"mihomo.userVerification": "تایید هویت کاربر",
"mihomo.skipAuthPrefixes": "رد کردن تایید هویت برای بخش‌های IP",
"mihomo.useRttDelayTest": "استفاده از تست تاخیر 1-RTT",
"mihomo.tcpConcurrent": "همزمانی TCP",
"mihomo.storeSelectedNode": "ذخیره گره انتخاب شده",
"mihomo.storeFakeIp": "ذخیره IP جعلی",
"mihomo.disableLoopbackDetector": "غیرفعال کردن تشخیص لوپ‌بک",
"mihomo.skipSafePathCheck": "رد کردن بررسی مسیر امن",
"mihomo.disableEmbedCA": "عدم استفاده از گواهی CA داخلی",
"mihomo.disableSystemCA": "عدم استفاده از گواهی CA سیستم",
"mihomo.logRetentionDays": "روزهای نگهداری لاگ",
"mihomo.logLevel": "سطح لاگ",
"mihomo.selectLogLevel": "انتخاب سطح لاگ",
"mihomo.silent": "بی‌صدا",
"mihomo.error": "خطا",
"mihomo.warning": "هشدار",
"mihomo.info": "اطلاعات",
"mihomo.debug": "اشکال‌زدایی",
"mihomo.error.coreStartFailed": "راه‌اندازی هسته با خطا مواجه شد",
"mihomo.error.profileCheckFailed": "بررسی پروفایل با خطا مواجه شد",
"mihomo.error.externalControllerListenError": "خطا در گوش‌دادن به کنترل‌کننده خارجی",
"mihomo.findProcess": "یافتن فرآیند",
"mihomo.selectFindProcessMode": "انتخاب حالت یافتن فرآیند",
"mihomo.strict": "خودکار",
"mihomo.off": "خاموش",
"mihomo.always": "همیشه",
"mihomo.username.placeholder": "نام کاربری",
"mihomo.password.placeholder": "رمز عبور",
"mihomo.ipSegment.placeholder": "بخش IP",
"mihomo.interface.title": "اطلاعات شبکه",
"substore.title": "Sub-Store",
"substore.checkUpdate": "بررسی به‌روزرسانی",
"substore.updating": "ساب استور در حال به‌روزرسانی است...",
"substore.updateCompleted": "به‌روزرسانی ساب استور تکمیل شد",
"substore.updateFailed": "به‌روزرسانی ساب استور با شکست مواجه شد",
"substore.downloadFailed": "دانلود فایل ساب استور با شکست مواجه شد",
"substore.openInBrowser": "باز کردن در مرورگر",
"substore.enable": "فعال‌سازی ساب استور",
"substore.allowLan": "اجازه دسترسی شبکه محلی",
"substore.useCustomBackend": "استفاده از بک‌اند شخصی ساب استور",
"substore.customBackendUrl.title": "آدرس بک‌اند شخصی ساب استور",
"substore.customBackendUrl.placeholder": "باید شامل پروتکل باشد",
"substore.useProxy": "فعال‌سازی پراکسی برای تمام درخواست‌های ساب استور",
"substore.sync.title": "زمانبندی همگام‌سازی اشتراک/فایل",
"substore.sync.placeholder": "عبارت Cron",
"substore.restore.title": "زمانبندی بازیابی پیکربندی",
"substore.restore.placeholder": "عبارت Cron",
"substore.backup.title": "زمانبندی پشتیبان‌گیری پیکربندی",
"substore.backup.placeholder": "عبارت Cron",
"webdav.title": "پشتیبان‌گیری WebDAV",
"webdav.url": "آدرس WebDAV",
"webdav.dir": "پوشه پشتیبان‌گیری WebDAV",
"webdav.username": "نام کاربری WebDAV",
"webdav.password": "رمز عبور WebDAV",
"webdav.maxBackups": "حداکثر نسخه پشتیبان",
"webdav.noLimit": "بدون محدودیت",
"webdav.backup": "پشتیبان‌گیری",
"webdav.restore.title": "بازیابی پشتیبان",
"webdav.restore.noBackups": "هیچ پشتیبانی موجود نیست",
"webdav.notification.backupSuccess.title": "پشتیبان‌گیری موفق",
"webdav.notification.backupSuccess.body": "فایل پشتیبان در WebDAV بارگذاری شد",
"shortcuts.title": "میانبرهای صفحه کلید",
"shortcuts.toggleWindow": "تغییر وضعیت پنجره",
"shortcuts.toggleFloatingWindow": "تغییر وضعیت پنجره شناور",
"shortcuts.toggleSystemProxy": "تغییر وضعیت پراکسی سیستم",
"shortcuts.toggleTun": "تغییر وضعیت TUN",
"shortcuts.toggleRuleMode": "تغییر وضعیت حالت قانون",
"shortcuts.toggleGlobalMode": "تغییر وضعیت حالت جهانی",
"shortcuts.toggleDirectMode": "تغییر وضعیت حالت مستقیم",
"shortcuts.toggleLightMode": "تغییر وضعیت حالت روشن",
"shortcuts.restartApp": "راه‌اندازی مجدد برنامه",
"shortcuts.input.placeholder": "برای ورود میانبر کلیک کنید",
"sider.title": "تنظیمات نوار کناری",
"sider.cards.systemProxy": "پراکسی سیستم",
"sider.cards.tun": "TUN",
"sider.cards.profiles": "پروفایل‌ها",
"sider.cards.proxies": "گروه‌های پراکسی",
"sider.cards.rules": "قوانین",
"sider.cards.resources": "منابع خارجی",
"sider.cards.override": "جایگزینی",
"sider.cards.connections": "اتصالات",
"sider.cards.core": "تنظیمات هسته",
"sider.cards.dns": "DNS",
"sider.cards.sniff": "تشخیص دامنه",
"sider.cards.logs": "گزارش‌ها",
"sider.cards.substore": "ساب استور",
"sider.cards.config": "پیکربندی اجرا",
"sider.cards.emptyProfile": "پروفایل خالی",
"sider.cards.viewRuntimeConfig": "مشاهده پیکربندی اجرا",
"sider.cards.remote": "از راه دور",
"sider.cards.local": "محلی",
"sider.cards.trafficUsage": "پیشرفت مصرف ترافیک",
"sider.cards.neverExpire": "بدون انقضا",
"sider.cards.outbound.title": "حالت خروجی",
"sider.cards.outbound.rule": "قانون",
"sider.cards.outbound.global": "جهانی",
"sider.cards.outbound.direct": "مستقیم",
"sider.size.large": "بزرگ",
"sider.size.small": "کوچک",
"sider.size.hidden": "مخفی",
"actions.guide.title": "باز کردن راهنما",
"actions.guide.button": "باز کردن راهنما",
"actions.update.title": "بررسی به‌روزرسانی",
"actions.update.button": "بررسی به‌روزرسانی",
"actions.update.upToDate.title": "به‌روز است",
"actions.update.upToDate.body": "به‌روزرسانی جدیدی موجود نیست",
"actions.reset.title": "بازنشانی برنامه",
"actions.reset.button": "بازنشانی برنامه",
"actions.reset.tooltip": "حذف تمام پیکربندی‌ها و بازگرداندن برنامه به حالت اولیه",
"actions.reset.confirm.title": "تایید بازنشانی",
"actions.reset.confirm.content": "آیا از بازنشانی برنامه و حذف تمام پیکربندی‌ها مطمئن هستید؟ فقط بازنشانی برنامه زمانی که برنامه نادرست کار می‌کند، مورد استفاده قرار می‌گیرد. تمام اشتراک ها و جایگزینی‌ها و اسکریپت‌ها حذف خواهند شد!",
"actions.heapSnapshot.title": "ایجاد نمای لحظه‌ای حافظه",
"actions.heapSnapshot.button": "ایجاد نمای لحظه‌ای حافظه",
"actions.heapSnapshot.tooltip": "ایجاد نمای لحظه‌ای از فرآیند اصلی برای اشکال‌زدایی مشکلات حافظه",
"actions.lightMode.title": "حالت سبک",
"actions.lightMode.button": "حالت سبک",
"actions.lightMode.tooltip": "خروج کامل از برنامه و نگهداری تنها فرآیند هسته",
"actions.restartApp": "راه‌اندازی مجدد برنامه",
"actions.quit.title": "خروج از برنامه",
"actions.quit.button": "خروج از برنامه",
"actions.version.title": "نسخه برنامه",
"theme.editor.title": "ویرایش پوسته",
"proxies.title": "گروه‌های پراکسی و گره‌ها",
"proxies.card.title": "گروه‌پراکسی",
"proxies.delay.test": "تست",
"proxies.delay.timeout": "وقفه",
"proxies.unpin": "حذف پین",
"proxies.order.default": "پیش‌فرض",
"proxies.order.delay": "تاخیر",
"proxies.order.name": "نام",
"proxies.mode.full": "اطلاعات کامل",
"proxies.mode.simple": "اطلاعات ساده",
"proxies.mode.direct": "حالت مستقیم",
"proxies.search.placeholder": "جستجوی پراکسی‌ها",
"proxies.locate": "یافتن پراکسی فعلی",
"sniffer.title": "تنظیمات تشخیص دامنه",
"sniffer.parsePureIP": "تشخیص آدرس‌های IP بدون نگاشت",
"sniffer.forceDNSMapping": "تشخیص نگاشت‌های IP واقعی",
"sniffer.overrideDestination": "جایگزینی آدرس اتصال",
"sniffer.sniff.title": "تشخیص پورت HTTP",
"sniffer.sniff.tls": "تشخیص پورت TLS",
"sniffer.sniff.quic": "تشخیص پورت QUIC",
"sniffer.sniff.ports.placeholder": "شماره پورت‌ها، با کاما جدا شده",
"sniffer.skipDomain.title": "رد کردن تشخیص دامنه",
"sniffer.skipDomain.placeholder": "مثال: +.push.apple.com",
"sniffer.forceDomain.title": "اجبار تشخیص دامنه",
"sniffer.forceDomain.placeholder": "مثال: v2ex.com",
"sniffer.skipDstAddress.title": "رد کردن تشخیص آدرس مقصد",
"sniffer.skipDstAddress.placeholder": "مثال: 1.1.1.1/32",
"sniffer.skipSrcAddress.title": "رد کردن تشخیص آدرس مبدا",
"sniffer.skipSrcAddress.placeholder": "مثال: 192.168.1.1/24",
"sysproxy.title": "پراکسی سیستم",
"sysproxy.host.title": "میزبان پراکسی",
"sysproxy.host.placeholder": "پیش‌فرض 127.0.0.1، در صورت عدم نیاز تغییر ندهید",
"sysproxy.mode.title": "حالت پراکسی",
"sysproxy.mode.manual": "دستی",
"sysproxy.mode.pac": "PAC",
"sysproxy.uwp.title": "ابزار UWP",
"sysproxy.uwp.open": "باز کردن ابزار UWP",
"sysproxy.pac.edit": "ویرایش اسکریپت PAC",
"sysproxy.bypass.title": "دور زدن پراکسی",
"sysproxy.bypass.addDefault": "افزودن دور زدن پیش‌فرض",
"sysproxy.bypass.placeholder": "مثال: *.baidu.com",
"tun.title": "TUN",
"tun.firewall.title": "بازنشانی دیوار آتش",
"tun.firewall.reset": "بازنشانی دیوار آتش",
"tun.core.title": "مجوزدهی دستی",
"tun.core.auth": "مجوزدهی به هسته",
"tun.dns.autoSet": "تنظیم خودکار DNS سیستم",
"tun.stack.title": "پشته حالت Tun",
"tun.device.title": "نام دستگاه Tun",
"tun.strictRoute": "مسیریابی سختگیرانه",
"tun.autoRoute": "تنظیم خودکار مسیر جهانی",
"tun.autoRedirect": "تنظیم خودکار بازگردانی TCP",
"tun.autoDetectInterface": "تشخیص خودکار رابط",
"tun.dnsHijack": "ربایش DNS",
"tun.excludeAddress.title": "مستثنی کردن شبکه‌های سفارشی",
"tun.excludeAddress.placeholder": "مثال: 172.20.0.0/16",
"tun.notifications.coreAuthSuccess": "مجوزدهی به هسته با موفقیت انجام شد",
"tun.notifications.firewallResetSuccess": "بازنشانی دیوار آتش با موفقیت انجام شد",
"tun.error.tunPermissionDenied": "راه‌اندازی رابط TUN با شکست مواجه شد، لطفا به صورت دستی به هسته مجوز دهید",
"dns.title": "تنظیمات DNS",
"dns.enhancedMode.title": "حالت نگاشت دامنه",
"dns.enhancedMode.fakeIp": "IP جعلی",
"dns.enhancedMode.redirHost": "IP واقعی",
"dns.enhancedMode.normal": "بدون نگاشت",
"dns.fakeIp.range": "محدوده پاسخ",
"dns.fakeIp.rangePlaceholder": "مثال: 198.18.0.1/16",
"dns.fakeIp.filter": "پاسخ IP واقعی",
"dns.fakeIp.filterPlaceholder": "مثال: +.lan",
"dns.respectRules": "رعایت قوانین",
"dns.defaultNameserver": "سرور DNS برای حل نام دامنه",
"dns.defaultNameserverPlaceholder": "مثال: 223.5.5.5، فقط IP",
"dns.proxyServerNameserver": "سرور DNS برای حل نام پراکسی",
"dns.proxyServerNameserverPlaceholder": "مثال: tls://dns.alidns.com",
"dns.nameserver": "سرور حل نام پیش‌فرض",
"dns.nameserverPlaceholder": "مثال: tls://dns.alidns.com",
"dns.directNameserver": "سرور حل نام مستقیم",
"dns.directNameserverPlaceholder": "مثال: tls://dns.alidns.com",
"dns.nameserverPolicy.title": "سیاست جایگزینی DNS",
"dns.nameserverPolicy.list": "لیست سیاست DNS",
"dns.nameserverPolicy.domainPlaceholder": "دامنه",
"dns.nameserverPolicy.serverPlaceholder": "سرور DNS",
"dns.systemHosts.title": "استفاده از Hosts سیستم",
"dns.customHosts.title": "Hosts سفارشی",
"dns.customHosts.list": "لیست Hosts",
"dns.customHosts.domainPlaceholder": "دامنه",
"dns.customHosts.valuePlaceholder": "دامنه یا IP",
"profiles.title": "مدیریت پروفایل",
"profiles.updateAll": "به‌روزرسانی همه پروفایل‌ها",
"profiles.useProxy": "پراکسی",
"profiles.import": "وارد کردن",
"profiles.open": "باز کردن",
"profiles.new": "جدید",
"profiles.newProfile": "پروفایل جدید",
"profiles.substore.visit": "بازدید از ساب استور",
"profiles.error.unsupportedFileType": "نوع فایل پشتیبانی نمی‌شود",
"profiles.error.urlParamMissing": "پارامتر url وجود ندارد",
"profiles.error.importFailed": "وارد کردن اشتراک با شکست مواجه شد",
"profiles.emptyProfile": "پروفایل خالی",
"profiles.viewRuntimeConfig": "مشاهده پیکربندی اجرای فعلی",
"profiles.neverExpire": "بدون انقضا",
"profiles.remote": "از راه دور",
"profiles.local": "محلی",
"profiles.trafficUsage": "پیشرفت مصرف ترافیک",
"profiles.traffic.usage": "{{used}}/{{total}}",
"profiles.traffic.unlimited": "نامحدود",
"profiles.traffic.expired": "منقضی شده",
"profiles.traffic.remainingDays": "{{days}} روز",
"profiles.traffic.lastUpdate": "آخرین به‌روزرسانی: {{time}}",
"profiles.editInfo.title": "ویرایش اطلاعات",
"profiles.editInfo.name": "نام",
"profiles.editInfo.url": "آدرس اشتراک",
"profiles.editInfo.useProxy": "استفاده از پراکسی برای به‌روزرسانی",
"profiles.editInfo.interval": "فاصله به‌روزرسانی (دقیقه)",
"profiles.editInfo.fixedInterval": "فاصله به‌روزرسانی ثابت",
"profiles.editInfo.override.title": "جایگزینی",
"profiles.editInfo.override.global": "جهانی",
"profiles.editInfo.override.noAvailable": "جایگزینی در دسترس نیست",
"profiles.editInfo.override.add": "افزودن جایگزینی",
"profiles.editFile.title": "ویرایش پروفایل",
"profiles.editFile.notice": "توجه: تغییرات اعمال شده در اینجا پس از به‌روزرسانی پروفایل بازنشانی می‌شوند. برای پیکربندی‌های سفارشی، لطفا از",
"profiles.editFile.override": "جایگزینی",
"profiles.editFile.feature": "ویژگی",
"profiles.openFile": "باز کردن فایل",
"profiles.home": "خانه",
"profiles.notification.importSuccess": "اشتراک با موفقیت وارد شد",
"resources.proxyProviders.title": "ارائه‌دهندگان پراکسی",
"resources.proxyProviders.updateAll": "به‌روزرسانی همه",
"resources.ruleProviders.title": "ارائه‌دهندگان قانون",
"resources.ruleProviders.updateAll": "به‌روزرسانی همه",
"outbound.title": "حالت خروجی",
"outbound.modes.rule": "قانون",
"outbound.modes.global": "جهانی",
"outbound.modes.direct": "مستقیم",
"rules.title": "قوانین",
"rules.filter": "فیلتر قوانین",
"override.title": "جایگزینی",
"override.import": "وارد کردن",
"override.docs": "مستندات",
"override.repository": "مخزن جایگزینی",
"override.unsupportedFileType": "نوع فایل پشتیبانی نمی‌شود",
"override.actions.open": "باز کردن",
"override.actions.newYaml": "YAML جدید",
"override.actions.newJs": "جاوااسکریپت جدید",
"override.defaultContent.yaml": "# https://mihomo.party/docs/guide/override/yaml",
"override.defaultContent.js": "// https://mihomo.party/docs/guide/override/javascript\nfunction main(config) {\n return config\n}",
"override.newFile.yaml": "YAML جدید",
"override.newFile.js": "JS جدید",
"override.editInfo.title": "ویرایش اطلاعات",
"override.editInfo.name": "نام",
"override.editInfo.url": "آدرس",
"override.editInfo.global": "فعال‌سازی جهانی",
"override.editFile.title": "ویرایش جایگزینی {{type}}",
"override.editFile.script": "اسکریپت",
"override.editFile.config": "پیکربندی",
"override.execLog.title": "گزارش اجرا",
"override.execLog.close": "بستن",
"override.menuItems.editInfo": "ویرایش اطلاعات",
"override.menuItems.editFile": "ویرایش فایل",
"override.menuItems.openFile": "باز کردن فایل",
"override.menuItems.execLog": "گزارش اجرا",
"override.menuItems.delete": "حذف",
"override.labels.global": "جهانی",
"connections.title": "اتصالات",
"connections.upload": "آپلود",
"connections.download": "دانلود",
"connections.closeAll": "بستن همه اتصالات",
"connections.active": "فعال",
"connections.closed": "بسته شده",
"connections.filter": "فیلتر",
"connections.orderBy": "مرتب‌سازی بر اساس",
"connections.time": "زمان",
"connections.uploadAmount": "مقدار آپلود",
"connections.downloadAmount": "مقدار دانلود",
"connections.uploadSpeed": "سرعت آپلود",
"connections.downloadSpeed": "سرعت دانلود",
"connections.detail.title": "جزئیات اتصال",
"connections.detail.establishTime": "زمان برقراری",
"connections.detail.rule": "قانون",
"connections.detail.proxyChain": "زنجیره پراکسی",
"connections.detail.connectionType": "نوع اتصال",
"connections.detail.host": "میزبان",
"connections.detail.sniffHost": "میزبان تشخیص داده شده",
"connections.detail.processName": "نام فرآیند",
"connections.detail.processPath": "مسیر فرآیند",
"connections.detail.sourceIP": "IP مبدا",
"connections.detail.sourceGeoIP": "موقعیت جغرافیایی مبدا",
"connections.detail.sourceASN": "ASN مبدا",
"connections.detail.destinationIP": "IP مقصد",
"connections.detail.destinationGeoIP": "موقعیت جغرافیایی مقصد",
"connections.detail.destinationASN": "ASN مقصد",
"connections.detail.sourcePort": "پورت مبدا",
"connections.detail.destinationPort": "پورت مقصد",
"connections.detail.inboundIP": "IP ورودی",
"connections.detail.inboundPort": "پورت ورودی",
"connections.detail.copyRule": "کپی قانون",
"connections.detail.inboundName": "نام ورودی",
"connections.detail.inboundUser": "کاربر ورودی",
"connections.detail.dscp": "DSCP",
"connections.detail.remoteDestination": "مقصد از راه دور",
"connections.detail.dnsMode": "حالت DNS",
"connections.detail.specialProxy": "پراکسی ویژه",
"connections.detail.specialRules": "قوانین ویژه",
"connections.detail.close": "بستن",
"resources.geoData.geoip": "پایگاه داده GeoIP",
"resources.geoData.geosite": "پایگاه داده GeoSite",
"resources.geoData.mmdb": "پایگاه داده MMDB",
"resources.geoData.asn": "پایگاه داده ASN",
"resources.geoData.mode": "حالت داده‌های جغرافیایی",
"resources.geoData.autoUpdate": "به‌روزرسانی خودکار",
"resources.geoData.updateInterval": "فاصله به‌روزرسانی (ساعت)",
"resources.geoData.updateSuccess": "به‌روزرسانی داده‌های جغرافیایی موفق",
"logs.title": "گزارش‌های بلادرنگ",
"logs.filter": "فیلتر گزارش‌ها",
"logs.clear": "پاک کردن گزارش‌ها",
"logs.autoScroll": "پیمایش خودکار",
"tray.showWindow": "نمایش پنجره",
"tray.showFloatingWindow": "نمایش پنجره شناور",
"tray.hideFloatingWindow": "مخفی کردن پنجره شناور",
"tray.ruleMode": "حالت قانون",
"tray.globalMode": "حالت جهانی",
"tray.directMode": "حالت مستقیم",
"tray.systemProxy": "پراکسی سیستم",
"tray.tun": "TUN",
"tray.profiles": "پروفایل‌ها",
"tray.openDirectories.title": "باز کردن پوشه‌ها",
"tray.openDirectories.appDir": "پوشه برنامه",
"tray.openDirectories.workDir": "پوشه کاری",
"tray.openDirectories.coreDir": "پوشه هسته",
"tray.openDirectories.logDir": "پوشه گزارش‌ها",
"tray.copyEnv": "کپی متغیرهای محیطی",
"guide.welcome.title": "به میهومو پارتی خوش آمدید",
"guide.welcome.description": "این یک آموزش تعاملی است. اگر با نرم‌افزار آشنا هستید، می‌توانید دکمه بستن را در گوشه بالا سمت راست کلیک کنید. همیشه می‌توانید این آموزش را دوباره از تنظیمات باز کنید.",
"guide.sider.title": "نوار پیمایش",
"guide.sider.description": "سمت چپ نوار پیمایش برنامه است که به عنوان داشبورد نیز عمل می‌کند. در اینجا می‌توانید بین صفحات مختلف جابجا شوید و نمای کلی از اطلاعات وضعیت پرکاربرد را مشاهده کنید.",
"guide.card.title": "کارت‌ها",
"guide.card.description": "روی کارت‌های نوار پیمایش کلیک کنید تا به صفحه مربوطه بروید. همچنین می‌توانید کارت‌ها را با کشیدن و رها کردن به دلخواه مرتب کنید.",
"guide.main.title": "ناحیه اصلی",
"guide.main.description": "سمت راست ناحیه اصلی برنامه است که محتوای صفحه انتخاب شده از نوار پیمایش را نمایش می‌دهد.",
"guide.profile.title": "مدیریت پروفایل",
"guide.profile.description": "کارت مدیریت پروفایل اطلاعات مربوط به پیکربندی اشتراک در حال اجرا را نشان می‌دهد. برای مدیریت پیکربندی‌های اشتراک خود روی آن کلیک کنید تا وارد صفحه مدیریت پروفایل شوید.",
"guide.import.title": "وارد کردن اشتراک",
"guide.import.description": "میهومو پارتی از روش‌های مختلف وارد کردن اشتراک پشتیبانی می‌کند. لینک اشتراک خود را اینجا وارد کنید و برای وارد کردن پیکربندی اشتراک خود روی وارد کردن کلیک کنید. اگر اشتراک شما برای به‌روزرسانی نیاز به پراکسی دارد، قبل از وارد کردن گزینه 'پراکسی' را علامت بزنید (این نیاز به داشتن یک اشتراک کارآمد دارد).",
"guide.substore.title": "ساب استور",
"guide.substore.description": "میهومو پارتی ادغام عمیقی با ساب استور دارد. می‌توانید با کلیک روی این دکمه وارد ساب استور شوید یا مستقیماً اشتراک‌های مدیریت شده از طریق ساب استور را وارد کنید. میهومو پارتی به طور پیش‌فرض از بک‌اند ساب استور داخلی استفاده می‌کند. اگر بک‌اند ساب استور خود را دارید، می‌توانید آن را در صفحه تنظیمات پیکربندی کنید. اگر از ساب استور استفاده نمی‌کنید، می‌توانید آن را در تنظیمات غیرفعال کنید.",
"guide.localProfile.title": "پروفایل محلی",
"guide.localProfile.description": "روی '+' کلیک کنید تا یک فایل محلی را وارد کنید یا یک پیکربندی خالی جدید برای ویرایش ایجاد کنید.",
"guide.sysproxy.title": "پراکسی سیستم",
"guide.sysproxy.description": "پس از وارد کردن اشتراک، هسته در حال اجرا و گوش دادن به پورت مشخص شده است. اکنون می‌توانید از پراکسی از طریق پورت مشخص شده استفاده کنید. اگر می‌خواهید بیشتر برنامه‌ها به طور خودکار از این پورت پراکسی استفاده کنند، باید کلید پراکسی سیستم را روشن کنید.",
"guide.sysproxySetting.title": "تنظیمات پراکسی سیستم",
"guide.sysproxySetting.description": "در اینجا می‌توانید تنظیمات مربوط به پراکسی سیستم را پیکربندی کنید و حالت پراکسی را انتخاب کنید. برای برنامه‌های ویندوز که از پراکسی سیستم پیروی نمی‌کنند، می‌توانید از 'ابزار UWP' برای حذف محدودیت‌های لوپ‌بک محلی استفاده کنید. برای تفاوت بین 'حالت پراکسی دستی' و 'حالت پراکسی PAC'، لطفاً در اینترنت جستجو کنید.",
"guide.tun.title": "کارت شبکه مجازی",
"guide.tun.description": "کارت شبکه مجازی، که معمولاً در نرم‌افزارهای مشابه به عنوان 'حالت Tun' شناخته می‌شود، به شما اجازه می‌دهد هسته کنترل تمام ترافیک برنامه‌هایی را که از تنظیمات پراکسی سیستم پیروی نمی‌کنند، در دست بگیرد.",
"guide.tunSetting.title": "تنظیمات کارت شبکه مجازی",
"guide.tunSetting.description": "در اینجا می‌توانید تنظیمات کارت شبکه مجازی را تغییر دهید. میهومو پارتی به صورت تئوری تمام مشکلات مجوز را حل کرده است. اگر کارت شبکه مجازی شما هنوز کار نمی‌کند، دیوار آتش را بازنشانی کنید (ویندوز) یا به صورت دستی به هسته مجوز دهید (مک/لینوکس) و سپس هسته را مجدداً راه‌اندازی کنید.",
"guide.override.title": "Override",
"guide.override.description": "میهومو پارتی قابلیت‌های قدرتمند جایگزینی را برای سفارشی‌سازی پیکربندی‌های اشتراک وارد شده، مانند افزودن قوانین و سفارشی‌سازی گروه‌های پراکسی ارائه می‌دهد. می‌توانید مستقیماً فایل‌های جایگزینی نوشته شده توسط دیگران را وارد کنید یا فایل‌های خود را بنویسید. <b>فراموش نکنید که فایل جایگزینی را روی اشتراکی که می‌خواهید جایگزین کنید فعال کنید</b>. برای نحو فایل جایگزینی، لطفاً به <a href=\"https://mihomo.party/docs/guide/override\" target=\"_blank\">مستندات رسمی</a> مراجعه کنید.",
"guide.dns.title": "DNS",
"guide.dns.description": "نرم‌افزار به طور پیش‌فرض کنترل تنظیمات DNS هسته را در دست می‌گیرد. اگر نیاز دارید از تنظیمات DNS پیکربندی اشتراک خود استفاده کنید، می‌توانید 'کنترل تنظیمات DNS' را در تنظیمات برنامه غیرفعال کنید. همین مورد برای تشخیص دامنه نیز صدق می‌کند.",
"guide.end.title": "پایان آموزش",
"guide.end.description": "اکنون که با استفاده اساسی از نرم‌افزار آشنا شدید، اشتراک خود را وارد کنید و از آن استفاده کنید. لذت ببرید!\nهمچنین می‌توانید برای آخرین اخبار به <a href=\"https://t.me/mihomo_party_group\" target=\"_blank\">گروه تلگرام</a> ما بپیوندید."
}

View File

@ -0,0 +1,510 @@
{
"common.settings": "Настройки",
"common.profiles": "Профили",
"common.proxies": "Прокси",
"common.connections": "Соединения",
"common.dns": "DNS",
"common.tun": "TUN",
"common.save": "Сохранить",
"common.cancel": "Отмена",
"common.edit": "Изменить",
"common.delete": "Удалить",
"common.seconds": "секунд",
"common.confirm": "Подтвердить",
"common.auto": "Авто",
"common.default": "По умолчанию",
"common.close": "Закрыть",
"common.pinWindow": "Закрепить окно",
"common.next": "Далее",
"common.prev": "Назад",
"common.done": "Готово",
"common.notification.restartRequired": "Требуется перезапуск для применения изменений",
"common.notification.systemProxyEnabled": "Системный прокси включен",
"common.notification.systemProxyDisabled": "Системный прокси отключен",
"common.notification.tunEnabled": "TUN включен",
"common.notification.tunDisabled": "TUN отключен",
"common.notification.ruleMode": "Режим правил",
"common.notification.globalMode": "Глобальный режим",
"common.notification.directMode": "Прямой режим",
"common.error.appCrash": "Приложение завершилось аварийно :( Пожалуйста, отправьте следующую информацию разработчику для устранения проблемы",
"common.error.copyErrorMessage": "Копировать сообщение об ошибке",
"common.error.invalidCron": "Неверное выражение Cron",
"common.error.getBackupListFailed": "Не удалось получить список резервных копий: {{error}}",
"common.error.restoreFailed": "Не удалось восстановить: {{error}}",
"common.error.deleteFailed": "Не удалось удалить: {{error}}",
"common.error.shortcutRegistrationFailed": "Не удалось зарегистрировать сочетание клавиш",
"common.error.shortcutRegistrationFailedWithError": "Не удалось зарегистрировать сочетание клавиш: {{error}}",
"common.error.adminRequired": "Для первого запуска требуются права администратора",
"common.error.initFailed": "Не удалось инициализировать приложение",
"common.updater.versionReady": "Версия v{{version}} готова",
"common.updater.goToDownload": "Перейти к загрузке",
"common.updater.update": "Обновить",
"settings.general": "Общие настройки",
"settings.mihomo": "Настройки Mihomo",
"settings.language": "Язык",
"settings.theme": "Тема",
"settings.darkMode": "Тёмная тема",
"settings.lightMode": "Светлая тема",
"settings.autoStart": "Автозапуск",
"settings.autoCheckUpdate": "Автоматическая проверка обновлений",
"settings.silentStart": "Тихий запуск",
"settings.autoQuitWithoutCore": "Автоматический облегчённый режим",
"settings.autoQuitWithoutCoreTooltip": "Автоматически переходить в облегчённый режим после закрытия окна",
"settings.autoQuitWithoutCoreDelay": "Задержка включения облегчённого режима",
"settings.envType": "Тип переменных окружения",
"settings.showFloatingWindow": "Показывать плавающее окно",
"settings.spinFloatingIcon": "Анимация иконки при активности сети",
"settings.disableTray": "Отключить значок в трее",
"settings.proxyInTray": "Показывать информацию о прокси в трее",
"settings.showTraffic_windows": "Показывать скорость в панели задач",
"settings.showTraffic_mac": "Показывать скорость в строке состояния",
"settings.showDockIcon": "Показывать значок в доке",
"settings.useWindowFrame": "Использовать системную рамку окна",
"settings.backgroundColor": "Цвет фона",
"settings.backgroundAuto": "Авто",
"settings.backgroundDark": "Тёмный",
"settings.backgroundLight": "Светлый",
"settings.fetchTheme": "Загрузить тему",
"settings.importTheme": "Импортировать тему",
"settings.editTheme": "Редактировать тему",
"settings.selectTheme": "Выбрать тему",
"settings.links.docs": "Документация",
"settings.links.github": "GitHub репозиторий",
"settings.links.telegram": "Telegram группа",
"settings.title": "Настройки приложения",
"mihomo.title": "Настройки ядра",
"mihomo.restart": "Перезапустить ядро",
"mihomo.memory": "Использование памяти",
"mihomo.userAgent": "User Agent подписки",
"mihomo.userAgentPlaceholder": "По умолчанию: mihomo.party/v{{version}} (clash.meta)",
"mihomo.delayTest.url": "URL теста задержки",
"mihomo.delayTest.urlPlaceholder": "По умолчанию: http://www.gstatic.com/generate_204",
"mihomo.delayTest.concurrency": "Параллельность теста задержки",
"mihomo.delayTest.concurrencyPlaceholder": "По умолчанию: 50",
"mihomo.delayTest.timeout": "Таймаут теста задержки",
"mihomo.delayTest.timeoutPlaceholder": "По умолчанию: 5000",
"mihomo.gist.title": "Синхронизация конфигурации в Gist",
"mihomo.gist.copyUrl": "Копировать URL Gist",
"mihomo.gist.token": "GitHub токен",
"mihomo.proxyColumns.title": "Колонки отображения прокси",
"mihomo.proxyColumns.auto": "Авто",
"mihomo.proxyColumns.one": "Одна колонка",
"mihomo.proxyColumns.two": "Две колонки",
"mihomo.proxyColumns.three": "Три колонки",
"mihomo.proxyColumns.four": "Четыре колонки",
"mihomo.cpuPriority.title": "Приоритет процесса ядра",
"mihomo.cpuPriority.realtime": "Реального времени",
"mihomo.cpuPriority.high": "Высокий",
"mihomo.cpuPriority.aboveNormal": "Выше среднего",
"mihomo.cpuPriority.normal": "Нормальный",
"mihomo.cpuPriority.belowNormal": "Ниже среднего",
"mihomo.cpuPriority.low": "Низкий",
"mihomo.workDir.title": "Отдельные рабочие каталоги для разных подписок",
"mihomo.workDir.tooltip": "Включите для избежания конфликтов при наличии групп прокси с одинаковыми именами в разных подписках",
"mihomo.controlDns": "Управление настройками DNS",
"mihomo.controlSniff": "Управление сниффингом доменов",
"mihomo.autoCloseConnection": "Автозакрытие соединений",
"mihomo.pauseSSID.title": "Прямое подключение для определённых WiFi SSID",
"mihomo.pauseSSID.placeholder": "Введите SSID",
"mihomo.coreVersion": "Версия ядра",
"mihomo.upgradeCore": "Обновить ядро",
"mihomo.coreAuthLost": "Потеряна авторизация ядра",
"mihomo.coreUpgradeSuccess": "Ядро успешно обновлено. Если вы хотите использовать виртуальную сетевую карту (Tun), пожалуйста, перейдите на страницу виртуальной сетевой карты для повторной авторизации ядра",
"mihomo.alreadyLatestVersion": "Уже последняя версия",
"mihomo.selectCoreVersion": "Выберите версию ядра",
"mihomo.stableVersion": "Стабильная",
"mihomo.alphaVersion": "Альфа",
"mihomo.mixedPort": "Смешанный порт",
"mihomo.confirm": "Подтвердить",
"mihomo.socksPort": "Порт Socks",
"mihomo.httpPort": "Порт Http",
"mihomo.redirPort": "Порт Redir",
"mihomo.externalController": "Адрес внешнего контроллера",
"mihomo.externalControllerSecret": "Секрет внешнего контроллера",
"mihomo.ipv6": "IPv6",
"mihomo.allowLanConnection": "Разрешить LAN подключения",
"mihomo.allowedIpSegments": "Разрешённые IP сегменты",
"mihomo.disallowedIpSegments": "Запрещённые IP сегменты",
"mihomo.userVerification": "Проверка пользователя",
"mihomo.skipAuthPrefixes": "Пропускать проверку для IP сегментов",
"mihomo.useRttDelayTest": "Использовать 1-RTT тест задержки",
"mihomo.tcpConcurrent": "TCP параллельность",
"mihomo.storeSelectedNode": "Сохранять выбранный узел",
"mihomo.storeFakeIp": "Сохранять Fake IP",
"mihomo.disableLoopbackDetector": "Отключить определение петли",
"mihomo.skipSafePathCheck": "Пропускать проверку безопасного пути",
"mihomo.disableEmbedCA": "Отключить встроенный CA",
"mihomo.disableSystemCA": "Отключить системный CA",
"mihomo.logRetentionDays": "Дни хранения логов",
"mihomo.logLevel": "Уровень логирования",
"mihomo.selectLogLevel": "Выберите уровень логирования",
"mihomo.silent": "Тихий",
"mihomo.error": "Ошибка",
"mihomo.warning": "Предупреждение",
"mihomo.info": "Информация",
"mihomo.debug": "Отладка",
"mihomo.error.coreStartFailed": "Ошибка запуска ядра",
"mihomo.error.profileCheckFailed": "Проверка профиля не удалась",
"mihomo.error.externalControllerListenError": "Ошибка прослушивания внешнего контроллера",
"mihomo.findProcess": "Поиск процесса",
"mihomo.selectFindProcessMode": "Выберите режим поиска процесса",
"mihomo.strict": "Авто",
"mihomo.off": "Выкл",
"mihomo.always": "Всегда",
"mihomo.username.placeholder": "Имя пользователя",
"mihomo.password.placeholder": "Пароль",
"mihomo.ipSegment.placeholder": "IP сегмент",
"mihomo.interface.title": "Сетевая информация",
"substore.title": "Sub-Store",
"substore.checkUpdate": "Проверить обновления",
"substore.updating": "Sub-Store обновляется...",
"substore.updateCompleted": "Обновление Sub-Store завершено",
"substore.updateFailed": "Не удалось обновить Sub-Store",
"substore.downloadFailed": "Не удалось загрузить файл Sub-Store",
"substore.openInBrowser": "Открыть в браузере",
"substore.enable": "Включить Sub-Store",
"substore.allowLan": "Разрешить доступ по LAN",
"substore.useCustomBackend": "Использовать собственный бэкенд Sub-Store",
"substore.customBackendUrl.title": "URL собственного бэкенда Sub-Store",
"substore.customBackendUrl.placeholder": "Должен включать протокол",
"substore.useProxy": "Включить прокси для всех запросов Sub-Store",
"substore.sync.title": "Расписание синхронизации подписок/файлов",
"substore.sync.placeholder": "Cron выражение",
"substore.restore.title": "Расписание восстановления конфигурации",
"substore.restore.placeholder": "Cron выражение",
"substore.backup.title": "Расписание резервного копирования",
"substore.backup.placeholder": "Cron выражение",
"webdav.title": "Резервное копирование WebDAV",
"webdav.url": "URL WebDAV",
"webdav.dir": "Каталог резервных копий WebDAV",
"webdav.username": "Имя пользователя WebDAV",
"webdav.password": "Пароль WebDAV",
"webdav.maxBackups": "Максимум резервных копий",
"webdav.noLimit": "Без ограничений",
"webdav.backup": "Резервное копирование",
"webdav.restore.title": "Восстановление резервной копии",
"webdav.restore.noBackups": "Нет доступных резервных копий",
"webdav.notification.backupSuccess.title": "Резервное копирование успешно",
"webdav.notification.backupSuccess.body": "Файл резервной копии загружен на WebDAV",
"shortcuts.title": "Горячие клавиши",
"shortcuts.toggleWindow": "Показать/скрыть окно",
"shortcuts.toggleFloatingWindow": "Показать/скрыть плавающее окно",
"shortcuts.toggleSystemProxy": "Включить/выключить системный прокси",
"shortcuts.toggleTun": "Включить/выключить TUN",
"shortcuts.toggleRuleMode": "Переключить режим правил",
"shortcuts.toggleGlobalMode": "Переключить глобальный режим",
"shortcuts.toggleDirectMode": "Переключить прямое подключение",
"shortcuts.toggleLightMode": "Переключить облегченный режим",
"shortcuts.restartApp": "Перезапустить приложение",
"shortcuts.input.placeholder": "Нажмите для ввода комбинации",
"sider.title": "Настройки боковой панели",
"sider.cards.systemProxy": "Системный прокси",
"sider.cards.tun": "TUN",
"sider.cards.profiles": "Профили",
"sider.cards.proxies": "Прокси группы",
"sider.cards.rules": "Правила",
"sider.cards.resources": "Внешние ресурсы",
"sider.cards.override": "Переопределения",
"sider.cards.connections": "Подключения",
"sider.cards.core": "Настройки ядра",
"sider.cards.dns": "DNS",
"sider.cards.sniff": "Анализ трафика",
"sider.cards.logs": "Журналы",
"sider.cards.substore": "Sub-Store",
"sider.cards.config": "Конфигурация",
"sider.cards.emptyProfile": "Пустой профиль",
"sider.cards.viewRuntimeConfig": "Просмотр текущей конфигурации",
"sider.cards.remote": "Удаленный",
"sider.cards.local": "Локальный",
"sider.cards.trafficUsage": "Использование трафика",
"sider.cards.neverExpire": "Бессрочно",
"sider.cards.outbound.title": "Режим исходящего трафика",
"sider.cards.outbound.rule": "По правилам",
"sider.cards.outbound.global": "Глобальный",
"sider.cards.outbound.direct": "Прямой",
"sider.size.large": "Большой",
"sider.size.small": "Маленький",
"sider.size.hidden": "Скрытый",
"actions.guide.title": "Открыть руководство",
"actions.guide.button": "Открыть руководство",
"actions.update.title": "Проверить обновления",
"actions.update.button": "Проверить обновления",
"actions.update.upToDate.title": "Актуальная версия",
"actions.update.upToDate.body": "Нет доступных обновлений",
"actions.reset.title": "Сбросить приложение",
"actions.reset.button": "Сбросить приложение",
"actions.reset.tooltip": "Удалить все настройки и вернуть приложение к исходному состоянию",
"actions.reset.confirm.title": "Подтвердить сброс",
"actions.reset.confirm.content": "Вы уверены, что хотите сбросить приложение и удалить все настройки? Сброс приложения следует выполнять только в том случае, если оно не работает должным образом. Ваши все подписки, переопределения и скрипты будут потеряны!",
"actions.heapSnapshot.title": "Создать снимок кучи",
"actions.heapSnapshot.button": "Создать снимок кучи",
"actions.heapSnapshot.tooltip": "Создать снимок кучи основного процесса для отладки проблем с памятью",
"actions.lightMode.title": "Облегченный режим",
"actions.lightMode.button": "Облегченный режим",
"actions.lightMode.tooltip": "Полностью закрыть приложение, оставив только процесс ядра",
"actions.restartApp": "Перезапустить приложение",
"actions.quit.title": "Выйти",
"actions.quit.button": "Выйти",
"actions.version.title": "Версия приложения",
"theme.editor.title": "Редактор темы",
"proxies.title": "Прокси группы и узлы",
"proxies.card.title": "Прокси группы",
"proxies.delay.test": "Тест",
"proxies.delay.timeout": "Таймаут",
"proxies.unpin": "Открепить",
"proxies.order.default": "По умолчанию",
"proxies.order.delay": "По задержке",
"proxies.order.name": "По имени",
"proxies.mode.full": "Подробная информация",
"proxies.mode.simple": "Краткая информация",
"proxies.mode.direct": "Прямое подключение",
"proxies.search.placeholder": "Поиск прокси",
"proxies.locate": "Найти текущий прокси",
"sniffer.title": "Настройки анализа доменов",
"sniffer.parsePureIP": "Анализировать немаппированные IP-адреса",
"sniffer.forceDNSMapping": "Анализировать реальные IP-маппинги",
"sniffer.overrideDestination": "Переопределить адрес подключения",
"sniffer.sniff.title": "Анализ HTTP портов",
"sniffer.sniff.tls": "Анализ TLS портов",
"sniffer.sniff.quic": "Анализ QUIC портов",
"sniffer.sniff.ports.placeholder": "Номера портов, разделенные запятыми",
"sniffer.skipDomain.title": "Пропустить анализ доменов",
"sniffer.skipDomain.placeholder": "Пример: +.push.apple.com",
"sniffer.forceDomain.title": "Принудительный анализ доменов",
"sniffer.forceDomain.placeholder": "Пример: v2ex.com",
"sniffer.skipDstAddress.title": "Пропустить анализ адресов назначения",
"sniffer.skipDstAddress.placeholder": "Пример: 1.1.1.1/32",
"sniffer.skipSrcAddress.title": "Пропустить анализ исходных адресов",
"sniffer.skipSrcAddress.placeholder": "Пример: 192.168.1.1/24",
"sysproxy.title": "Системный прокси",
"sysproxy.host.title": "Хост прокси",
"sysproxy.host.placeholder": "По умолчанию 127.0.0.1, не изменяйте без необходимости",
"sysproxy.mode.title": "Режим прокси",
"sysproxy.mode.manual": "Ручной",
"sysproxy.mode.pac": "PAC",
"sysproxy.uwp.title": "Инструмент UWP",
"sysproxy.uwp.open": "Открыть инструмент UWP",
"sysproxy.pac.edit": "Редактировать PAC скрипт",
"sysproxy.bypass.title": "Исключения прокси",
"sysproxy.bypass.addDefault": "Добавить стандартные исключения",
"sysproxy.bypass.placeholder": "Пример: *.baidu.com",
"tun.title": "TUN",
"tun.firewall.title": "Сбросить брандмауэр",
"tun.firewall.reset": "Сбросить брандмауэр",
"tun.core.title": "Ручная авторизация",
"tun.core.auth": "Авторизовать ядро",
"tun.dns.autoSet": "Автоматическая настройка DNS",
"tun.stack.title": "Стек режима Tun",
"tun.device.title": "Имя устройства Tun",
"tun.strictRoute": "Строгая маршрутизация",
"tun.autoRoute": "Автоматическая глобальная маршрутизация",
"tun.autoRedirect": "Автоматическое перенаправление TCP",
"tun.autoDetectInterface": "Автоопределение интерфейса",
"tun.dnsHijack": "Перехват DNS",
"tun.excludeAddress.title": "Исключить пользовательские сети",
"tun.excludeAddress.placeholder": "Пример: 172.20.0.0/16",
"tun.notifications.coreAuthSuccess": "Авторизация ядра успешна",
"tun.notifications.firewallResetSuccess": "Брандмауэр успешно сброшен",
"tun.error.tunPermissionDenied": "Ошибка запуска TUN, попробуйте вручную предоставить разрешения ядру",
"dns.title": "Настройки DNS",
"dns.enhancedMode.title": "Режим маппинга доменов",
"dns.enhancedMode.fakeIp": "Фиктивный IP",
"dns.enhancedMode.redirHost": "Реальный IP",
"dns.enhancedMode.normal": "Без маппинга",
"dns.fakeIp.range": "Диапазон ответов",
"dns.fakeIp.rangePlaceholder": "Пример: 198.18.0.1/16",
"dns.fakeIp.filter": "Ответ реального IP",
"dns.fakeIp.filterPlaceholder": "Пример: +.lan",
"dns.respectRules": "Соблюдать правила",
"dns.defaultNameserver": "Разрешение доменов DNS сервера",
"dns.defaultNameserverPlaceholder": "Пример: 223.5.5.5, только IP",
"dns.proxyServerNameserver": "Разрешение доменов прокси-сервера",
"dns.proxyServerNameserverPlaceholder": "Пример: tls://dns.alidns.com",
"dns.nameserver": "Сервер разрешения по умолчанию",
"dns.nameserverPlaceholder": "Пример: tls://dns.alidns.com",
"dns.directNameserver": "Сервер прямого разрешения",
"dns.directNameserverPlaceholder": "Пример: tls://dns.alidns.com",
"dns.nameserverPolicy.title": "Переопределение политики DNS",
"dns.nameserverPolicy.list": "Список политик DNS",
"dns.nameserverPolicy.domainPlaceholder": "Домен",
"dns.nameserverPolicy.serverPlaceholder": "DNS сервер",
"dns.systemHosts.title": "Использовать системный Hosts",
"dns.customHosts.title": "Пользовательский Hosts",
"dns.customHosts.list": "Список Hosts",
"dns.customHosts.domainPlaceholder": "Домен",
"dns.customHosts.valuePlaceholder": "Домен или IP",
"profiles.title": "Управление профилями",
"profiles.updateAll": "Обновить все профили",
"profiles.useProxy": "Прокси",
"profiles.import": "Импорт",
"profiles.open": "Открыть",
"profiles.new": "Создать",
"profiles.newProfile": "Новый профиль",
"profiles.substore.visit": "Посетить Sub-Store",
"profiles.error.unsupportedFileType": "Неподдерживаемый тип файла",
"profiles.error.urlParamMissing": "Отсутствует параметр: url",
"profiles.error.importFailed": "Ошибка импорта подписки",
"profiles.emptyProfile": "Пустой профиль",
"profiles.viewRuntimeConfig": "Просмотр текущей конфигурации",
"profiles.neverExpire": "Бессрочно",
"profiles.remote": "Удаленный",
"profiles.local": "Локальный",
"profiles.trafficUsage": "Использование трафика",
"profiles.traffic.usage": "{{used}}/{{total}}",
"profiles.traffic.unlimited": "Безлимитный",
"profiles.traffic.expired": "Истек",
"profiles.traffic.remainingDays": "{{days}} дней",
"profiles.traffic.lastUpdate": "Последнее обновление: {{time}}",
"profiles.editInfo.title": "Редактировать информацию",
"profiles.editInfo.name": "Имя",
"profiles.editInfo.url": "URL подписки",
"profiles.editInfo.useProxy": "Использовать прокси для обновления",
"profiles.editInfo.interval": "Интервал обн. (мин)",
"profiles.editInfo.fixedInterval": "Фиксированный интервал обновления",
"profiles.editInfo.override.title": "Переопределение",
"profiles.editInfo.override.global": "Глобальный",
"profiles.editInfo.override.noAvailable": "Нет доступных переопределений",
"profiles.editInfo.override.add": "Добавить переопределение",
"profiles.editFile.title": "Редактировать профиль",
"profiles.editFile.notice": "Примечание: Изменения, сделанные здесь, будут сброшены после обновления профиля. Для пользовательских настроек используйте",
"profiles.editFile.override": "Переопределение",
"profiles.editFile.feature": "функцию",
"profiles.openFile": "Открыть файл",
"profiles.home": "Главная",
"profiles.notification.importSuccess": "Подписка успешно импортирована",
"resources.proxyProviders.title": "Провайдеры прокси",
"resources.proxyProviders.updateAll": "Обновить все",
"resources.ruleProviders.title": "Провайдеры правил",
"resources.ruleProviders.updateAll": "Обновить все",
"outbound.title": "Режим исходящего трафика",
"outbound.modes.rule": "Правило",
"outbound.modes.global": "Глобальный",
"outbound.modes.direct": "Прямой",
"rules.title": "Правила",
"rules.filter": "Фильтр правил",
"override.title": "Переопределение",
"override.import": "Импорт",
"override.docs": "Документация",
"override.repository": "Репозиторий переопределений",
"override.unsupportedFileType": "Неподдерживаемый тип файла",
"override.actions.open": "Открыть",
"override.actions.newYaml": "Новый YAML",
"override.actions.newJs": "Новый JavaScript",
"override.defaultContent.yaml": "# https://mihomo.party/docs/guide/override/yaml",
"override.defaultContent.js": "// https://mihomo.party/docs/guide/override/javascript\nfunction main(config) {\n return config\n}",
"override.newFile.yaml": "Новый YAML",
"override.newFile.js": "Новый JS",
"override.editInfo.title": "Редактировать информацию",
"override.editInfo.name": "Имя",
"override.editInfo.url": "URL",
"override.editInfo.global": "Глобальное включение",
"override.editFile.title": "Редактировать переопределение {{type}}",
"override.editFile.script": "Скрипт",
"override.editFile.config": "Конфигурация",
"override.execLog.title": "Журнал выполнения",
"override.execLog.close": "Закрыть",
"override.menuItems.editInfo": "Редактировать информацию",
"override.menuItems.editFile": "Редактировать файл",
"override.menuItems.openFile": "Открыть файл",
"override.menuItems.execLog": "Журнал выполнения",
"override.menuItems.delete": "Удалить",
"override.labels.global": "Глобальный",
"connections.title": "Подключения",
"connections.upload": "Загрузка",
"connections.download": "Скачивание",
"connections.closeAll": "Закрыть все подключения",
"connections.active": "Активные",
"connections.closed": "Закрытые",
"connections.filter": "Фильтр",
"connections.orderBy": "Сортировать по",
"connections.time": "Время",
"connections.uploadAmount": "Объем загрузки",
"connections.downloadAmount": "Объем скачивания",
"connections.uploadSpeed": "Скорость загрузки",
"connections.downloadSpeed": "Скорость скачивания",
"connections.detail.title": "Детали подключения",
"connections.detail.establishTime": "Время установления",
"connections.detail.rule": "Правило",
"connections.detail.proxyChain": "Цепочка прокси",
"connections.detail.connectionType": "Тип подключения",
"connections.detail.host": "Хост",
"connections.detail.sniffHost": "Определенный хост",
"connections.detail.processName": "Имя процесса",
"connections.detail.processPath": "Путь процесса",
"connections.detail.sourceIP": "IP источника",
"connections.detail.sourceGeoIP": "GeoIP источника",
"connections.detail.sourceASN": "ASN источника",
"connections.detail.destinationIP": "IP назначения",
"connections.detail.destinationGeoIP": "GeoIP назначения",
"connections.detail.destinationASN": "ASN назначения",
"connections.detail.sourcePort": "Порт источника",
"connections.detail.destinationPort": "Порт назначения",
"connections.detail.inboundIP": "Входящий IP",
"connections.detail.inboundPort": "Входящий порт",
"connections.detail.copyRule": "Копировать правило",
"connections.detail.inboundName": "Имя входящего",
"connections.detail.inboundUser": "Пользователь входящего",
"connections.detail.dscp": "DSCP",
"connections.detail.remoteDestination": "Удаленное назначение",
"connections.detail.dnsMode": "Режим DNS",
"connections.detail.specialProxy": "Специальный прокси",
"connections.detail.specialRules": "Специальные правила",
"connections.detail.close": "Закрыть",
"resources.geoData.geoip": "База данных GeoIP",
"resources.geoData.geosite": "База данных GeoSite",
"resources.geoData.mmdb": "База данных MMDB",
"resources.geoData.asn": "База данных ASN",
"resources.geoData.mode": "Режим GeoData",
"resources.geoData.autoUpdate": "Автообновление",
"resources.geoData.updateInterval": "Интервал обновления (часы)",
"resources.geoData.updateSuccess": "GeoData успешно обновлена",
"logs.title": "Журнал в реальном времени",
"logs.filter": "Фильтр логов",
"logs.clear": "Очистить логи",
"logs.autoScroll": "Автопрокрутка",
"tray.showWindow": "Показать окно",
"tray.showFloatingWindow": "Показать плавающее окно",
"tray.hideFloatingWindow": "Скрыть плавающее окно",
"tray.ruleMode": "Режим правил",
"tray.globalMode": "Глобальный режим",
"tray.directMode": "Прямой режим",
"tray.systemProxy": "Системный прокси",
"tray.tun": "TUN",
"tray.profiles": "Профили",
"tray.openDirectories.title": "Открыть директории",
"tray.openDirectories.appDir": "Директория приложения",
"tray.openDirectories.workDir": "Рабочая директория",
"tray.openDirectories.coreDir": "Директория ядра",
"tray.openDirectories.logDir": "Директория логов",
"tray.copyEnv": "Копировать переменные среды",
"guide.welcome.title": "Добро пожаловать в Mihomo Party",
"guide.welcome.description": "Это интерактивное руководство. Если вы уже знакомы с программой, вы можете закрыть его, нажав кнопку в правом верхнем углу. Вы всегда можете открыть это руководство снова в настройках.",
"guide.sider.title": "Панель навигации",
"guide.sider.description": "Слева находится навигационная панель приложения, которая также служит панелью управления. Здесь вы можете переключаться между различными страницами и получать обзор часто используемой информации о состоянии.",
"guide.card.title": "Карточки",
"guide.card.description": "Нажмите на карточки в навигационной панели, чтобы перейти к соответствующей странице. Вы также можете перетаскивать карточки, чтобы расположить их по своему усмотрению.",
"guide.main.title": "Основная область",
"guide.main.description": "Правая сторона - это основная область приложения, отображающая содержимое выбранной страницы из навигационной панели.",
"guide.profile.title": "Управление профилями",
"guide.profile.description": "Карточка управления профилями показывает информацию о текущей конфигурации подписки. Нажмите, чтобы перейти на страницу управления профилями, где вы можете управлять конфигурациями подписок.",
"guide.import.title": "Импорт подписки",
"guide.import.description": "Mihomo Party поддерживает различные методы импорта подписок. Введите здесь ссылку на вашу подписку и нажмите импорт. Если для обновления подписки требуется прокси, отметьте опцию 'Прокси' перед импортом (для этого необходимо иметь уже работающую подписку).",
"guide.substore.title": "Sub-Store",
"guide.substore.description": "Mihomo Party глубоко интегрирован с Sub-Store. Вы можете нажать эту кнопку, чтобы войти в Sub-Store или напрямую импортировать подписки, управляемые через Sub-Store. Mihomo Party по умолчанию использует встроенный бэкенд Sub-Store. Если у вас есть собственный бэкенд Sub-Store, вы можете настроить его на странице настроек. Если вы не используете Sub-Store, вы также можете отключить его в настройках.",
"guide.localProfile.title": "Локальный профиль",
"guide.localProfile.description": "Нажмите '+', чтобы импортировать локальный файл или создать новую пустую конфигурацию для редактирования.",
"guide.sysproxy.title": "Системный прокси",
"guide.sysproxy.description": "После импорта подписки ядро запущено и прослушивает указанный порт. Теперь вы можете использовать прокси через указанный порт. Если вы хотите, чтобы большинство приложений автоматически использовали этот прокси-порт, вам нужно включить переключатель системного прокси.",
"guide.sysproxySetting.title": "Настройки системного прокси",
"guide.sysproxySetting.description": "Здесь вы можете настроить параметры системного прокси и выбрать режим прокси. Для приложений Windows, которые не следуют системному прокси, вы можете использовать 'UWP Tool' для снятия ограничений локальной петли. О различиях между 'Ручным режимом прокси' и 'Режимом прокси PAC' можно узнать в интернете.",
"guide.tun.title": "Виртуальная сетевая карта",
"guide.tun.description": "Виртуальная сетевая карта, известная как 'Режим Tun' в аналогичном программном обеспечении, позволяет ядру контролировать весь трафик для приложений, которые не следуют настройкам системного прокси.",
"guide.tunSetting.title": "Настройки виртуальной сетевой карты",
"guide.tunSetting.description": "Здесь вы можете изменить настройки виртуальной сетевой карты. Mihomo Party теоретически решил все проблемы с разрешениями. Если ваша виртуальная сетевая карта все еще не работает, попробуйте сбросить брандмауэр (Windows) или вручную авторизовать ядро (MacOS/Linux), а затем перезапустить ядро.",
"guide.override.title": "Переопределение",
"guide.override.description": "Mihomo Party предоставляет мощную функцию переопределения для настройки импортированных конфигураций подписки, таких как добавление правил и настройка групп прокси. Вы можете напрямую импортировать файлы переопределения, написанные другими, или написать свои собственные. <b>Не забудьте включить файл переопределения для подписки, которую вы хотите переопределить</b>. Синтаксис файла переопределения см. в <a href=\"https://mihomo.party/docs/guide/override\" target=\"_blank\">официальной документации</a>.",
"guide.dns.title": "DNS",
"guide.dns.description": "По умолчанию программа контролирует настройки DNS ядра. Если вам нужно использовать настройки DNS из конфигурации подписки, вы можете отключить 'Контроль настроек DNS' в настройках приложения. То же самое относится к сниффингу доменов.",
"guide.end.title": "Руководство завершено",
"guide.end.description": "Теперь, когда вы понимаете основы использования программы, импортируйте свою подписку и начните использовать ее. Приятного использования!\nВы также можете присоединиться к нашей официальной <a href=\"https://t.me/mihomo_party_group\" target=\"_blank\">группе Telegram</a> для получения последних новостей."
}

View File

@ -15,11 +15,17 @@
"common.default": "默认", "common.default": "默认",
"common.close": "关闭", "common.close": "关闭",
"common.pinWindow": "窗口置顶", "common.pinWindow": "窗口置顶",
"common.enterRootPassword": "请输入root密码",
"common.next": "下一步", "common.next": "下一步",
"common.prev": "上一步", "common.prev": "上一步",
"common.done": "完成", "common.done": "完成",
"common.notification.restartRequired": "需要重启应用以使更改生效", "common.notification.restartRequired": "需要重启应用以使更改生效",
"common.notification.systemProxyEnabled": "系统代理已启用",
"common.notification.systemProxyDisabled": "系统代理已关闭",
"common.notification.tunEnabled": "TUN 已启用",
"common.notification.tunDisabled": "TUN 已关闭",
"common.notification.ruleMode": "规则模式",
"common.notification.globalMode": "全局模式",
"common.notification.directMode": "直连模式",
"common.error.appCrash": "应用崩溃了 :( 请将以下信息提交给开发者以排查错误", "common.error.appCrash": "应用崩溃了 :( 请将以下信息提交给开发者以排查错误",
"common.error.copyErrorMessage": "复制报错信息", "common.error.copyErrorMessage": "复制报错信息",
"common.error.invalidCron": "无效的 Cron 表达式", "common.error.invalidCron": "无效的 Cron 表达式",
@ -70,7 +76,7 @@
"mihomo.restart": "重启内核", "mihomo.restart": "重启内核",
"mihomo.memory": "内存使用", "mihomo.memory": "内存使用",
"mihomo.userAgent": "订阅 User Agent", "mihomo.userAgent": "订阅 User Agent",
"mihomo.userAgentPlaceholder": "默认:clash.meta", "mihomo.userAgentPlaceholder": "默认:mihomo.party/v{{version}} (clash.meta)",
"mihomo.delayTest.url": "延迟测试 URL", "mihomo.delayTest.url": "延迟测试 URL",
"mihomo.delayTest.urlPlaceholder": "默认http://www.gstatic.com/generate_204", "mihomo.delayTest.urlPlaceholder": "默认http://www.gstatic.com/generate_204",
"mihomo.delayTest.concurrency": "延迟测试并发数", "mihomo.delayTest.concurrency": "延迟测试并发数",
@ -139,6 +145,7 @@
"mihomo.debug": "调试", "mihomo.debug": "调试",
"mihomo.error.coreStartFailed": "内核启动出错", "mihomo.error.coreStartFailed": "内核启动出错",
"mihomo.error.profileCheckFailed": "配置检查失败", "mihomo.error.profileCheckFailed": "配置检查失败",
"mihomo.error.externalControllerListenError": "外部控制监听错误",
"mihomo.findProcess": "查找进程", "mihomo.findProcess": "查找进程",
"mihomo.selectFindProcessMode": "选择进程查找模式", "mihomo.selectFindProcessMode": "选择进程查找模式",
"mihomo.strict": "自动", "mihomo.strict": "自动",
@ -172,6 +179,8 @@
"webdav.dir": "WebDAV 备份目录", "webdav.dir": "WebDAV 备份目录",
"webdav.username": "WebDAV 用户名", "webdav.username": "WebDAV 用户名",
"webdav.password": "WebDAV 密码", "webdav.password": "WebDAV 密码",
"webdav.maxBackups": "最大备份数",
"webdav.noLimit": "不限制",
"webdav.backup": "备份", "webdav.backup": "备份",
"webdav.restore.title": "恢复备份", "webdav.restore.title": "恢复备份",
"webdav.restore.noBackups": "还没有备份", "webdav.restore.noBackups": "还没有备份",
@ -225,6 +234,8 @@
"actions.reset.title": "重置软件", "actions.reset.title": "重置软件",
"actions.reset.button": "重置软件", "actions.reset.button": "重置软件",
"actions.reset.tooltip": "删除所有配置,将软件恢复初始状态", "actions.reset.tooltip": "删除所有配置,将软件恢复初始状态",
"actions.reset.confirm.title": "确认重置",
"actions.reset.confirm.content": "您确定要重置删除所有配置,将软件恢复初始状态吗?只有软件运行不正常的时候才需重置,您的所有订阅、覆写、脚本将全部丢失!",
"actions.heapSnapshot.title": "创建堆快照", "actions.heapSnapshot.title": "创建堆快照",
"actions.heapSnapshot.button": "创建堆快照", "actions.heapSnapshot.button": "创建堆快照",
"actions.heapSnapshot.tooltip": "创建主进程堆快照,用于排查内存问题", "actions.heapSnapshot.tooltip": "创建主进程堆快照,用于排查内存问题",
@ -349,6 +360,7 @@
"profiles.editInfo.url": "订阅地址", "profiles.editInfo.url": "订阅地址",
"profiles.editInfo.useProxy": "使用代理更新", "profiles.editInfo.useProxy": "使用代理更新",
"profiles.editInfo.interval": "更新间隔(分钟)", "profiles.editInfo.interval": "更新间隔(分钟)",
"profiles.editInfo.fixedInterval": "固定更新间隔",
"profiles.editInfo.override.title": "覆写", "profiles.editInfo.override.title": "覆写",
"profiles.editInfo.override.global": "全局", "profiles.editInfo.override.global": "全局",
"profiles.editInfo.override.noAvailable": "没有可用的覆写", "profiles.editInfo.override.noAvailable": "没有可用的覆写",

View File

@ -236,6 +236,7 @@ const Connections: React.FC = () => {
className="w-[180px] min-w-[131px]" className="w-[180px] min-w-[131px]"
aria-label={t('connections.orderBy')} aria-label={t('connections.orderBy')}
selectedKeys={new Set([connectionOrderBy])} selectedKeys={new Set([connectionOrderBy])}
disallowEmptySelection={true}
onSelectionChange={async (v) => { onSelectionChange={async (v) => {
await patchAppConfig({ await patchAppConfig({
connectionOrderBy: v.currentKey as connectionOrderBy: v.currentKey as

View File

@ -142,9 +142,6 @@ const DNS: React.FC = () => {
className="app-nodrag" className="app-nodrag"
color="primary" color="primary"
onPress={() => { onPress={() => {
const hostsObject = Object.fromEntries(
values.hosts.map(({ domain, value }) => [domain, value])
)
const dnsConfig = { const dnsConfig = {
ipv6: values.ipv6, ipv6: values.ipv6,
'fake-ip-range': values.fakeIPRange, 'fake-ip-range': values.fakeIPRange,
@ -165,10 +162,13 @@ const DNS: React.FC = () => {
values.nameserverPolicy.map(({ domain, value }) => [domain, value]) values.nameserverPolicy.map(({ domain, value }) => [domain, value])
) )
} }
onSave({ const result = { dns: dnsConfig }
dns: dnsConfig, if (values.useHosts) {
hosts: hostsObject result['hosts'] = Object.fromEntries(
}) values.hosts.map(({ domain, value }) => [domain, value])
)
}
onSave(result)
}} }}
> >
{t('common.save')} {t('common.save')}

View File

@ -136,6 +136,7 @@ const Mihomo: React.FC = () => {
size="sm" size="sm"
aria-label={t('mihomo.selectCoreVersion')} aria-label={t('mihomo.selectCoreVersion')}
selectedKeys={new Set([core])} selectedKeys={new Set([core])}
disallowEmptySelection={true}
onSelectionChange={async (v) => { onSelectionChange={async (v) => {
handleConfigChangeWithRestart('core', v.currentKey as 'mihomo' | 'mihomo-alpha') handleConfigChangeWithRestart('core', v.currentKey as 'mihomo' | 'mihomo-alpha')
}} }}
@ -400,7 +401,7 @@ const Mihomo: React.FC = () => {
<Input <Input
size="sm" size="sm"
fullWidth fullWidth
placeholder="IP 段" placeholder={t('mihomo.ipSegment.placeholder')}
value={ipcidr || ''} value={ipcidr || ''}
onValueChange={(v) => { onValueChange={(v) => {
if (index === lanAllowedIpsInput.length) { if (index === lanAllowedIpsInput.length) {
@ -450,7 +451,7 @@ const Mihomo: React.FC = () => {
<Input <Input
size="sm" size="sm"
fullWidth fullWidth
placeholder="IP 段" placeholder={t('mihomo.username.placeholder')}
value={ipcidr || ''} value={ipcidr || ''}
onValueChange={(v) => { onValueChange={(v) => {
if (index === lanDisallowedIpsInput.length) { if (index === lanDisallowedIpsInput.length) {
@ -702,6 +703,7 @@ const Mihomo: React.FC = () => {
size="sm" size="sm"
aria-label={t('mihomo.selectLogLevel')} aria-label={t('mihomo.selectLogLevel')}
selectedKeys={new Set([logLevel])} selectedKeys={new Set([logLevel])}
disallowEmptySelection={true}
onSelectionChange={(v) => { onSelectionChange={(v) => {
onChangeNeedRestart({ 'log-level': v.currentKey as LogLevel }) onChangeNeedRestart({ 'log-level': v.currentKey as LogLevel })
}} }}
@ -720,6 +722,7 @@ const Mihomo: React.FC = () => {
size="sm" size="sm"
aria-label={t('mihomo.selectFindProcessMode')} aria-label={t('mihomo.selectFindProcessMode')}
selectedKeys={new Set([findProcessMode])} selectedKeys={new Set([findProcessMode])}
disallowEmptySelection={true}
onSelectionChange={(v) => { onSelectionChange={(v) => {
onChangeNeedRestart({ 'find-process-mode': v.currentKey as FindProcessMode }) onChangeNeedRestart({ 'find-process-mode': v.currentKey as FindProcessMode })
}} }}

View File

@ -22,19 +22,18 @@ import { includesIgnoreCase } from '@renderer/utils/includes'
import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config' import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
const SCROLL_POSITION_KEY = 'proxy_scroll_position'
const GROUP_EXPAND_STATE_KEY = 'proxy_group_expand_state' const GROUP_EXPAND_STATE_KEY = 'proxy_group_expand_state'
const SCROLL_DEBOUNCE_TIME = 200
const RENDER_DELAY = 100
// 自定义 hook 用于管理滚动位置和展开状态 // 自定义 hook 用于管理展开状态
const useProxyState = (groups: IMihomoMixedGroup[]) => { const useProxyState = (groups: IMihomoMixedGroup[]): {
virtuosoRef: React.RefObject<GroupedVirtuosoHandle>;
isOpen: boolean[];
setIsOpen: React.Dispatch<React.SetStateAction<boolean[]>>;
} => {
const virtuosoRef = useRef<GroupedVirtuosoHandle>(null) const virtuosoRef = useRef<GroupedVirtuosoHandle>(null)
const [scrollPosition, setScrollPosition] = useState<number>(0)
const scrollTimerRef = useRef<NodeJS.Timeout>()
// 初始化展开状态 // 初始化展开状态
const [isOpen, setIsOpen] = useState(() => { const [isOpen, setIsOpen] = useState<boolean[]>(() => {
try { try {
const savedState = localStorage.getItem(GROUP_EXPAND_STATE_KEY) const savedState = localStorage.getItem(GROUP_EXPAND_STATE_KEY)
return savedState ? JSON.parse(savedState) : Array(groups.length).fill(false) return savedState ? JSON.parse(savedState) : Array(groups.length).fill(false)
@ -53,62 +52,10 @@ const useProxyState = (groups: IMihomoMixedGroup[]) => {
} }
}, [isOpen]) }, [isOpen])
// 清理定时器
useEffect(() => {
return () => {
if (scrollTimerRef.current) {
clearTimeout(scrollTimerRef.current)
}
}
}, [])
// 恢复滚动位置
useEffect(() => {
if (groups.length > 0) {
try {
const savedPosition = localStorage.getItem(SCROLL_POSITION_KEY)
if (savedPosition) {
const position = parseInt(savedPosition)
if (!isNaN(position) && position >= 0) {
const timer = setTimeout(() => {
virtuosoRef.current?.scrollTo({ top: position })
}, RENDER_DELAY)
return () => clearTimeout(timer)
}
}
} catch (error) {
console.error('Failed to restore scroll position:', error)
}
}
}, [groups])
const saveScrollPosition = useCallback((position: number) => {
try {
localStorage.setItem(SCROLL_POSITION_KEY, position.toString())
} catch (error) {
console.error('Failed to save scroll position:', error)
}
}, [])
return { return {
virtuosoRef, virtuosoRef,
isOpen, isOpen,
setIsOpen, setIsOpen
scrollPosition,
onScroll: useCallback((e: React.UIEvent<HTMLElement>) => {
const position = (e.target as HTMLElement).scrollTop
setScrollPosition(position)
// 清理之前的定时器
if (scrollTimerRef.current) {
clearTimeout(scrollTimerRef.current)
}
// 使用防抖来减少存储频率
scrollTimerRef.current = setTimeout(() => {
saveScrollPosition(position)
}, SCROLL_DEBOUNCE_TIME)
}, [saveScrollPosition])
} }
} }
@ -127,8 +74,9 @@ const Proxies: React.FC = () => {
} = appConfig || {} } = appConfig || {}
const [cols, setCols] = useState(1) const [cols, setCols] = useState(1)
const { virtuosoRef, isOpen, setIsOpen, onScroll, scrollPosition } = useProxyState(groups) const { virtuosoRef, isOpen, setIsOpen } = useProxyState(groups)
const [delaying, setDelaying] = useState(Array(groups.length).fill(false)) const [delaying, setDelaying] = useState(Array(groups.length).fill(false))
const [proxyDelaying, setProxyDelaying] = useState<Record<string, boolean>>({})
const [searchValue, setSearchValue] = useState(Array(groups.length).fill('')) const [searchValue, setSearchValue] = useState(Array(groups.length).fill(''))
const { groupCounts, allProxies } = useMemo(() => { const { groupCounts, allProxies } = useMemo(() => {
const groupCounts: number[] = [] const groupCounts: number[] = []
@ -168,13 +116,7 @@ const Proxies: React.FC = () => {
await mihomoCloseAllConnections() await mihomoCloseAllConnections()
} }
mutate() mutate()
// 等待 DOM 更新完成后再恢复滚动位置 }, [autoCloseConnection, mutate])
requestAnimationFrame(() => {
requestAnimationFrame(() => {
virtuosoRef.current?.scrollTo({ top: scrollPosition })
})
})
}, [autoCloseConnection, mutate, virtuosoRef, scrollPosition])
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)
@ -194,6 +136,16 @@ const Proxies: React.FC = () => {
return newDelaying return newDelaying
}) })
// 本组测试状态
const groupProxies = allProxies[index]
setProxyDelaying((prev) => {
const newProxyDelaying = { ...prev }
groupProxies.forEach(proxy => {
newProxyDelaying[proxy.name] = true
})
return newProxyDelaying
})
try { try {
// 限制并发数量 // 限制并发数量
const result: Promise<void>[] = [] const result: Promise<void>[] = []
@ -205,6 +157,12 @@ const Proxies: React.FC = () => {
} catch { } catch {
// ignore // ignore
} finally { } finally {
// 立即更新状态
setProxyDelaying((prev) => {
const newProxyDelaying = { ...prev }
delete newProxyDelaying[proxy.name]
return newProxyDelaying
})
mutate() mutate()
} }
}) })
@ -224,6 +182,14 @@ const Proxies: React.FC = () => {
newDelaying[index] = false newDelaying[index] = false
return newDelaying return newDelaying
}) })
// 状态清理
setProxyDelaying((prev) => {
const newProxyDelaying = { ...prev }
groupProxies.forEach(proxy => {
delete newProxyDelaying[proxy.name]
})
return newProxyDelaying
})
} }
}, [allProxies, groups, delayTestConcurrency, mutate]) }, [allProxies, groups, delayTestConcurrency, mutate])
@ -245,7 +211,7 @@ const Proxies: React.FC = () => {
handleResize() // 初始化 handleResize() // 初始化
window.addEventListener('resize', handleResize) window.addEventListener('resize', handleResize)
return () => { return (): void => {
window.removeEventListener('resize', handleResize) window.removeEventListener('resize', handleResize)
} }
}, [calcCols]) }, [calcCols])
@ -311,7 +277,18 @@ const Proxies: React.FC = () => {
<GroupedVirtuoso <GroupedVirtuoso
ref={virtuosoRef} ref={virtuosoRef}
groupCounts={groupCounts} groupCounts={groupCounts}
onScroll={onScroll} defaultItemHeight={80}
increaseViewportBy={{ top: 300, bottom: 300 }}
overscan={500}
computeItemKey={(index, groupIndex) => {
let innerIndex = index
groupCounts.slice(0, groupIndex).forEach((count) => {
innerIndex -= count
})
const proxyIndex = innerIndex * cols
const proxy = allProxies[groupIndex]?.[proxyIndex]
return proxy ? `${groupIndex}-${proxy.name}` : `${groupIndex}-${index}`
}}
groupContent={(index) => { groupContent={(index) => {
if ( if (
groups[index] && groups[index] &&
@ -477,6 +454,7 @@ const Proxies: React.FC = () => {
allProxies[groupIndex][innerIndex * cols + i]?.name === allProxies[groupIndex][innerIndex * cols + i]?.name ===
groups[groupIndex].now groups[groupIndex].now
} }
isGroupTesting={!!proxyDelaying[allProxies[groupIndex][innerIndex * cols + i].name]}
/> />
) )
})} })}

View File

@ -67,6 +67,7 @@ const SubStore: React.FC = () => {
> >
<IoMdCloudDownload className="text-lg" /> <IoMdCloudDownload className="text-lg" />
</Button> </Button>
<Button <Button
title={t('substore.openInBrowser')} title={t('substore.openInBrowser')}
isIconOnly isIconOnly

View File

@ -11,7 +11,14 @@ import React from 'react'
import { MdDeleteForever } from 'react-icons/md' import { MdDeleteForever } from 'react-icons/md'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
const defaultBypass: string[] = const defaultPacScript = `
function FindProxyForURL(url, host) {
return "PROXY 127.0.0.1:%mixed-port%; SOCKS5 127.0.0.1:%mixed-port%; DIRECT;";
}
`
const Sysproxy: React.FC = () => {
const defaultBypass: string[] =
platform === 'linux' platform === 'linux'
? ['localhost', '127.0.0.1', '192.168.0.0/16', '10.0.0.0/8', '172.16.0.0/12', '::1'] ? ['localhost', '127.0.0.1', '192.168.0.0/16', '10.0.0.0/8', '172.16.0.0/12', '::1']
: platform === 'darwin' : platform === 'darwin'
@ -49,13 +56,6 @@ const defaultBypass: string[] =
'<local>' '<local>'
] ]
const defaultPacScript = `
function FindProxyForURL(url, host) {
return "PROXY 127.0.0.1:%mixed-port%; SOCKS5 127.0.0.1:%mixed-port%; DIRECT;";
}
`
const Sysproxy: React.FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { appConfig, patchAppConfig } = useAppConfig() const { appConfig, patchAppConfig } = useAppConfig()
const { sysProxy } = appConfig || ({ sysProxy: { enable: false } } as IAppConfig) const { sysProxy } = appConfig || ({ sysProxy: { enable: false } } as IAppConfig)
@ -92,14 +92,21 @@ const Sysproxy: React.FC = () => {
} }
const onSave = async (): Promise<void> => { const onSave = async (): Promise<void> => {
// check valid TODO
await patchAppConfig({ sysProxy: values })
try {
await triggerSysProxy(true)
await patchAppConfig({ sysProxy: { enable: true } })
setChanged(false) setChanged(false)
// 保存当前的开关状态,以便在失败时恢复
const previousState = values.enable
try {
await patchAppConfig({ sysProxy: values })
await triggerSysProxy(true)
await patchAppConfig({ sysProxy: { enable: true } })
} catch (e) { } catch (e) {
setValues({ ...values, enable: previousState })
setChanged(true)
alert(e) alert(e)
await patchAppConfig({ sysProxy: { enable: false } }) await patchAppConfig({ sysProxy: { enable: false } })
} }
} }

View File

@ -6,7 +6,6 @@ import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-c
import { manualGrantCorePermition, restartCore, setupFirewall } from '@renderer/utils/ipc' import { manualGrantCorePermition, restartCore, setupFirewall } from '@renderer/utils/ipc'
import { platform } from '@renderer/utils/init' import { platform } from '@renderer/utils/init'
import React, { Key, useState } from 'react' import React, { Key, useState } from 'react'
import BasePasswordModal from '@renderer/components/base/base-password-modal'
import { useAppConfig } from '@renderer/hooks/use-app-config' import { useAppConfig } from '@renderer/hooks/use-app-config'
import { MdDeleteForever } from 'react-icons/md' import { MdDeleteForever } from 'react-icons/md'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -18,7 +17,6 @@ const Tun: React.FC = () => {
const { autoSetDNS = true } = appConfig || {} const { autoSetDNS = true } = appConfig || {}
const { tun } = controledMihomoConfig || {} const { tun } = controledMihomoConfig || {}
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [openPasswordModal, setOpenPasswordModal] = useState(false)
const { const {
device = 'Mihomo', device = 'Mihomo',
stack = 'mixed', stack = 'mixed',
@ -71,21 +69,6 @@ const Tun: React.FC = () => {
return ( return (
<> <>
{openPasswordModal && (
<BasePasswordModal
onCancel={() => setOpenPasswordModal(false)}
onConfirm={async (password: string) => {
try {
await manualGrantCorePermition(password)
new Notification(t('tun.notifications.coreAuthSuccess'))
await restartCore()
setOpenPasswordModal(false)
} catch (e) {
alert(e)
}
}}
/>
)}
<BasePage <BasePage
title={t('tun.title')} title={t('tun.title')}
header={ header={
@ -145,7 +128,6 @@ const Tun: React.FC = () => {
size="sm" size="sm"
color="primary" color="primary"
onPress={async () => { onPress={async () => {
if (platform === 'darwin') {
try { try {
await manualGrantCorePermition() await manualGrantCorePermition()
new Notification(t('tun.notifications.coreAuthSuccess')) new Notification(t('tun.notifications.coreAuthSuccess'))
@ -153,9 +135,6 @@ const Tun: React.FC = () => {
} catch (e) { } catch (e) {
alert(e) alert(e)
} }
} else {
setOpenPasswordModal(true)
}
}} }}
> >
{t('tun.core.auth')} {t('tun.core.auth')}

View File

@ -7,7 +7,7 @@ import Profiles from '@renderer/pages/profiles'
import Logs from '@renderer/pages/logs' import Logs from '@renderer/pages/logs'
import Connections from '@renderer/pages/connections' import Connections from '@renderer/pages/connections'
import Mihomo from '@renderer/pages/mihomo' import Mihomo from '@renderer/pages/mihomo'
import Sysproxy from '@renderer/pages/syspeoxy' import Sysproxy from '@renderer/pages/sysproxy'
import Tun from '@renderer/pages/tun' import Tun from '@renderer/pages/tun'
import Resources from '@renderer/pages/resources' import Resources from '@renderer/pages/resources'
import DNS from '@renderer/pages/dns' import DNS from '@renderer/pages/dns'

View File

@ -147,6 +147,10 @@ export async function updateProfileItem(item: IProfileItem): Promise<void> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('updateProfileItem', item)) return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('updateProfileItem', item))
} }
export async function addProfileUpdater(item: IProfileItem): Promise<void> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('addProfileUpdater', item))
}
export async function getProfileStr(id: string): Promise<string> { export async function getProfileStr(id: string): Promise<string> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('getProfileStr', id)) return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('getProfileStr', id))
} }
@ -207,10 +211,8 @@ export async function triggerSysProxy(enable: boolean): Promise<void> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('triggerSysProxy', enable)) return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('triggerSysProxy', enable))
} }
export async function manualGrantCorePermition(password?: string): Promise<void> { export async function manualGrantCorePermition(): Promise<void> {
return ipcErrorWrapper( return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('manualGrantCorePermition'))
await window.electron.ipcRenderer.invoke('manualGrantCorePermition', password)
)
} }
export async function getFilePath(ext: string[]): Promise<string[] | undefined> { export async function getFilePath(ext: string[]): Promise<string[] | undefined> {

View File

@ -1,6 +1,8 @@
import i18next from 'i18next' import i18next from 'i18next'
import enUS from '../renderer/src/locales/en-US.json' import enUS from '../renderer/src/locales/en-US.json'
import zhCN from '../renderer/src/locales/zh-CN.json' import zhCN from '../renderer/src/locales/zh-CN.json'
import ruRU from '../renderer/src/locales/ru-RU.json'
import faIR from '../renderer/src/locales/fa-IR.json'
export const resources = { export const resources = {
'en-US': { 'en-US': {
@ -8,6 +10,12 @@ export const resources = {
}, },
'zh-CN': { 'zh-CN': {
translation: zhCN translation: zhCN
},
'ru-RU': {
translation: ruRU
},
'fa-IR': {
translation: faIR
} }
} }

View File

@ -284,6 +284,7 @@ interface IAppConfig {
webdavDir?: string webdavDir?: string
webdavUsername?: string webdavUsername?: string
webdavPassword?: string webdavPassword?: string
webdavMaxBackups?: number
useNameserverPolicy: boolean useNameserverPolicy: boolean
nameserverPolicy: { [key: string]: string | string[] } nameserverPolicy: { [key: string]: string | string[] }
showWindowShortcut?: string showWindowShortcut?: string
@ -295,7 +296,7 @@ interface IAppConfig {
directModeShortcut?: string directModeShortcut?: string
restartAppShortcut?: string restartAppShortcut?: string
quitWithoutCoreShortcut?: string quitWithoutCoreShortcut?: string
language?: 'zh-CN' | 'en-US' language?: 'zh-CN' | 'en-US' | 'ru-RU' | 'fa-IR'
} }
interface IMihomoTunConfig { interface IMihomoTunConfig {
@ -461,6 +462,7 @@ interface IProfileItem {
useProxy?: boolean useProxy?: boolean
extra?: ISubscriptionUserInfo extra?: ISubscriptionUserInfo
substore?: boolean substore?: boolean
allowFixedInterval?: boolean
} }
interface ISubStoreSub { interface ISubStoreSub {