mirror of
https://gh.catmak.name/https://github.com/mihomo-party-org/mihomo-party
synced 2025-12-28 13:40:29 +08:00
Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
364578f210 | ||
|
|
674cefcc29 | ||
|
|
827e744601 | ||
|
|
151726fcce | ||
|
|
6f3845151d | ||
|
|
a9b5887e15 | ||
|
|
727fd48684 | ||
|
|
e2ab88f4e2 | ||
|
|
1a5c001dbd | ||
|
|
6744e14c66 | ||
|
|
62a04cc5ad | ||
|
|
555130001b | ||
|
|
78f9211ebe | ||
|
|
c6d0e05851 | ||
|
|
caf962f921 | ||
|
|
0b8c77d200 | ||
|
|
d6e456302e | ||
|
|
daa8e7ba7e | ||
|
|
27ab6d1b5c | ||
|
|
da5336ee36 | ||
|
|
bdb7fb6489 | ||
|
|
1a1992c617 | ||
|
|
511eb0c7fa | ||
|
|
f54ffcf42b | ||
|
|
5a84d6485e | ||
|
|
afe93774b0 | ||
|
|
00be605e6e | ||
|
|
944475d791 | ||
|
|
08f15c7e01 | ||
|
|
0218f72c5f | ||
|
|
c54ce3577d | ||
|
|
6cb432dea9 | ||
|
|
a6a3afd3bb | ||
|
|
a2faf0fc8f | ||
|
|
b15fc6ce3a | ||
|
|
fcb323a17a | ||
|
|
69e65a3959 | ||
|
|
6ffcf544b8 | ||
|
|
36746074da | ||
|
|
d763e93984 | ||
|
|
6ab920ddbd | ||
|
|
59bd7e8a08 | ||
|
|
f30596231b | ||
|
|
4b238d4dc2 | ||
|
|
81bb2c44e0 | ||
|
|
6cf1ae2c25 | ||
|
|
c906a10562 | ||
|
|
eb12f13525 | ||
|
|
502c089f86 |
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
26
build/pkg-scripts/preinstall
Normal file
26
build/pkg-scripts/preinstall
Normal 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
|
||||||
104
changelog.md
104
changelog.md
@ -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)
|
||||||
|
|||||||
@ -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
27
scripts/cleanup-mac.sh
Executable 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."
|
||||||
@ -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 {
|
||||||
|
|||||||
@ -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'])
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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}"`
|
||||||
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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,11 +84,26 @@ if (process.platform === 'win32' && !is.dev && !process.argv.includes('noadmin')
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function initApp(): Promise<void> {
|
||||||
|
await fixUserDataPermissions()
|
||||||
|
}
|
||||||
|
|
||||||
|
initApp()
|
||||||
|
.then(() => {
|
||||||
const gotTheLock = app.requestSingleInstanceLock()
|
const gotTheLock = app.requestSingleInstanceLock()
|
||||||
|
|
||||||
if (!gotTheLock) {
|
if (!gotTheLock) {
|
||||||
app.quit()
|
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
|
||||||
@ -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)
|
||||||
|
|||||||
@ -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> {
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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`
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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')
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
|
|||||||
@ -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 })
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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')
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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)())
|
||||||
|
|||||||
37
src/renderer/src/components/base/base-confirm-modal.tsx
Normal file
37
src/renderer/src/components/base/base-confirm-modal.tsx
Normal 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
|
||||||
@ -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
|
|
||||||
@ -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={() => {
|
||||||
|
|||||||
@ -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')}>
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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)}
|
||||||
|
|||||||
@ -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))
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 })
|
||||||
|
|||||||
@ -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({
|
||||||
|
|||||||
@ -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')}
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
510
src/renderer/src/locales/fa-IR.json
Normal file
510
src/renderer/src/locales/fa-IR.json
Normal 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> ما بپیوندید."
|
||||||
|
}
|
||||||
510
src/renderer/src/locales/ru-RU.json
Normal file
510
src/renderer/src/locales/ru-RU.json
Normal 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> для получения последних новостей."
|
||||||
|
}
|
||||||
@ -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": "没有可用的覆写",
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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')}
|
||||||
|
|||||||
@ -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 })
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -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]}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -11,6 +11,13 @@ 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 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[] =
|
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']
|
||||||
@ -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 } })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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')}
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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> {
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
4
src/shared/types.d.ts
vendored
4
src/shared/types.d.ts
vendored
@ -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 {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user