mirror of
https://gh.catmak.name/https://github.com/mihomo-party-org/mihomo-party
synced 2025-12-27 05:00:30 +08:00
Compare commits
39 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 |
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,9 +1,17 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
# 设置日志文件
|
||||||
|
LOG_FILE="/tmp/mihomo-party-install.log"
|
||||||
|
exec > "$LOG_FILE" 2>&1
|
||||||
|
|
||||||
|
log() {
|
||||||
|
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1"
|
||||||
|
}
|
||||||
|
|
||||||
# 检查 root 权限
|
# 检查 root 权限
|
||||||
if [ "$EUID" -ne 0 ]; then
|
if [ "$EUID" -ne 0 ]; then
|
||||||
echo "Please run as root"
|
log "Error: Please run as root"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@ -13,63 +21,153 @@ if [[ $2 == *".app" ]]; then
|
|||||||
else
|
else
|
||||||
APP_PATH="$2/Mihomo Party.app"
|
APP_PATH="$2/Mihomo Party.app"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
HELPER_PATH="/Library/PrivilegedHelperTools/party.mihomo.helper"
|
HELPER_PATH="/Library/PrivilegedHelperTools/party.mihomo.helper"
|
||||||
LAUNCH_DAEMON="/Library/LaunchDaemons/party.mihomo.helper.plist"
|
LAUNCH_DAEMON="/Library/LaunchDaemons/party.mihomo.helper.plist"
|
||||||
|
|
||||||
# 设置核心文件权限
|
log "Starting installation..."
|
||||||
chown root:admin "$APP_PATH/Contents/Resources/sidecar/mihomo"
|
|
||||||
chown root:admin "$APP_PATH/Contents/Resources/sidecar/mihomo-alpha"
|
|
||||||
chmod +s "$APP_PATH/Contents/Resources/sidecar/mihomo"
|
|
||||||
chmod +s "$APP_PATH/Contents/Resources/sidecar/mihomo-alpha"
|
|
||||||
|
|
||||||
# 创建目录并复制 helper
|
# 创建目录并设置权限
|
||||||
mkdir -p /Library/PrivilegedHelperTools
|
log "Creating directories and setting permissions..."
|
||||||
if [ ! -f "$APP_PATH/Contents/Resources/files/party.mihomo.helper" ]; then
|
mkdir -p "/Library/PrivilegedHelperTools"
|
||||||
echo "Helper file not found"
|
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
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cp "$APP_PATH/Contents/Resources/files/party.mihomo.helper" "$HELPER_PATH"
|
|
||||||
chown root:wheel "$HELPER_PATH"
|
|
||||||
chmod 544 "$HELPER_PATH"
|
|
||||||
|
|
||||||
# 创建并配置 LaunchDaemon
|
# 创建并配置 LaunchDaemon
|
||||||
mkdir -p /Library/LaunchDaemons
|
log "Configuring LaunchDaemon..."
|
||||||
|
mkdir -p "/Library/LaunchDaemons"
|
||||||
cat << EOF > "$LAUNCH_DAEMON"
|
cat << EOF > "$LAUNCH_DAEMON"
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?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">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>Label</key>
|
<key>Label</key>
|
||||||
<string>party.mihomo.helper</string>
|
<string>party.mihomo.helper</string>
|
||||||
<key>AssociatedBundleIdentifiers</key>
|
<key>AssociatedBundleIdentifiers</key>
|
||||||
|
<array>
|
||||||
<string>party.mihomo.app</string>
|
<string>party.mihomo.app</string>
|
||||||
<key>KeepAlive</key>
|
</array>
|
||||||
<true/>
|
<key>KeepAlive</key>
|
||||||
<key>Program</key>
|
<true/>
|
||||||
<string>${HELPER_PATH}</string>
|
<key>Program</key>
|
||||||
<key>StandardErrorPath</key>
|
<string>${HELPER_PATH}</string>
|
||||||
<string>/tmp/party.mihomo.helper.err</string>
|
<key>StandardErrorPath</key>
|
||||||
<key>StandardOutPath</key>
|
<string>/tmp/party.mihomo.helper.err</string>
|
||||||
<string>/tmp/party.mihomo.helper.log</string>
|
<key>StandardOutPath</key>
|
||||||
</dict>
|
<string>/tmp/party.mihomo.helper.log</string>
|
||||||
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
chown root:wheel "$LAUNCH_DAEMON"
|
chown root:wheel "$LAUNCH_DAEMON"
|
||||||
chmod 644 "$LAUNCH_DAEMON"
|
chmod 644 "$LAUNCH_DAEMON"
|
||||||
|
log "LaunchDaemon configured"
|
||||||
|
|
||||||
# 加载并启动服务
|
# 验证关键文件
|
||||||
launchctl unload "$LAUNCH_DAEMON" || true
|
log "Verifying installation..."
|
||||||
if ! launchctl load "$LAUNCH_DAEMON"; then
|
if [ ! -x "$HELPER_PATH" ]; then
|
||||||
echo "Failed to load helper service"
|
log "Error: Helper tool is not executable: $HELPER_PATH"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! launchctl start party.mihomo.helper; then
|
# 检查二进制文件有效性
|
||||||
echo "Failed to start helper service"
|
if ! file "$HELPER_PATH" | grep -q "Mach-O"; then
|
||||||
|
log "Error: Helper tool is not a valid Mach-O binary"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Installation completed successfully"
|
# 验证 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
|
||||||
131
changelog.md
131
changelog.md
@ -1,69 +1,72 @@
|
|||||||
## 1.7.2
|
## 1.7.7
|
||||||
|
### 新功能 (Feat)
|
||||||
|
- Mihomo 内核升级 v1.19.12
|
||||||
|
- 新增 Webdav 最大备数设置和清理逻辑
|
||||||
|
|
||||||
|
### 修复 (Fix)
|
||||||
|
- 修复 MacOS 下无法启动的问题(重置工作目录权限)
|
||||||
|
- 尝试修复不同版本 MacOS 下安装软件时候的报错(Input/output error)
|
||||||
|
- 部分遗漏的多国语言翻译
|
||||||
|
|
||||||
|
## 1.7.6
|
||||||
|
|
||||||
|
**此版本修复了 1.7.5 中的几个严重 bug,推荐所有人更新**
|
||||||
|
|
||||||
|
### 修复 (Fix)
|
||||||
|
- 修复了内核1.19.8更新后gist同步失效的问题(#780)
|
||||||
|
- 部分遗漏的多国语言翻译
|
||||||
|
- MacOS 下启动Error: EACCES: permission denied
|
||||||
|
- MacOS 系统代理 bypass 不生效
|
||||||
|
- MacOS 系统代理开启时 500 报错
|
||||||
|
|
||||||
|
## 1.7.5
|
||||||
|
|
||||||
|
### 新功能 (Feat)
|
||||||
|
- 增加组延迟测试时的动画
|
||||||
|
- 订阅卡片可右键点击
|
||||||
|
-
|
||||||
|
|
||||||
|
### 修复 (Fix)
|
||||||
|
- 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)
|
### 新功能 (Feat)
|
||||||
- 添加伊朗语支持 (#507)
|
- Mihomo 内核升级 v1.19.5
|
||||||
- 添加俄语支持 (#503)
|
- MacOS 下添加 Dock 图标动态展现方式 (#594)
|
||||||
- 使用特权助手设置系统代理(解决MacOS下的少有的系统代理设置问题)
|
- 更改默认 UA 并添加版本
|
||||||
|
- 添加固定间隔的配置文件更新按钮 (#670)
|
||||||
|
- 重构Linux上的手动授权内核方式
|
||||||
|
- 将sub-store迁移到工作目录下(#552)
|
||||||
|
- 重置软件增加警告提示
|
||||||
|
|
||||||
### 修复 (Fix)
|
### 修复 (Fix)
|
||||||
- 修复 Linux 上 sub-store 的更新问题 (#545)
|
- 修复代理节点页面因为重复刷新导致的溢出问题
|
||||||
- 修复按钮嵌套的水合错误 (button nesting hydration errors)
|
- 修复由于 Mihomo 核心错误导致启动时窗口丢失 (#601)
|
||||||
|
- 修复macOS下的sub-store更新问题 (#552)
|
||||||
### 其他优化 (Perf)
|
- 修复多语言翻译
|
||||||
- 优化系统代理开关逻辑
|
- 修复 defaultBypass 几乎总是 Windows 默认绕过设置 (#602)
|
||||||
- 优化代理页面性能和滚动体验
|
- 修复重置防火墙时发生的错误,因为没有指定防火墙规则 (#650)
|
||||||
- 简化启动守护进程的配置以支持辅助服务
|
|
||||||
- 改进安装后脚本,增强错误处理和路径灵活性
|
|
||||||
|
|
||||||
## 1.7.1
|
|
||||||
**注意:主题失效,请重新下载一次,因为更新了UI组件,老主题不兼容了**
|
|
||||||
|
|
||||||
### 新功能 (Feat)
|
|
||||||
- 自动检测操作系统语言并设置app
|
|
||||||
|
|
||||||
### 修复 (Fix)
|
|
||||||
- 修复详细模式下节点旗帜不显示的问题
|
|
||||||
- 修复引导页显示问题
|
|
||||||
- 修复缺失的hero-ui参数
|
|
||||||
- 补充丢失的翻译
|
|
||||||
|
|
||||||
### 其他改进 (Chore)
|
|
||||||
- 美化延迟测试结果按钮的显示样式
|
|
||||||
- 默认开1-RTT延迟测试
|
|
||||||
- 替换默认延迟测试链接
|
|
||||||
|
|
||||||
## 1.7.0
|
|
||||||
### 新功能 (Feat)
|
|
||||||
- 增加更多核心设置
|
|
||||||
- 添加内联支持和缺失的翻译
|
|
||||||
- 增加连接的时间排序功能
|
|
||||||
- 添加 i18n 支持,包含英文翻译
|
|
||||||
- 支持应用内更新和重启 Sub-Store
|
|
||||||
|
|
||||||
### 修复 (Fix)
|
|
||||||
- 延迟按钮宽度的自动调整
|
|
||||||
- 渲染 Card 为 div,防止按钮嵌套导致的 Hydration 错误
|
|
||||||
- 解决自动运行受控组件的警告
|
|
||||||
- 解决标题栏覆盖和受控组件的警告
|
|
||||||
- 解决无法点击配置文件的问题
|
|
||||||
- 解决组件层级中的无效按钮嵌套
|
|
||||||
- 在 NextUI 按钮组件中用 `onPress` 替换 `onClick`
|
|
||||||
- 添加 `aria-label` 以解决可访问性警告
|
|
||||||
- UI 中的延迟测试结果支持自动更新,无需点击
|
|
||||||
- 移除 NextUI Card 组件中的嵌套按钮元素
|
|
||||||
- 修复 `useWindowFrame` 切换时的重复重启问题 (#457)
|
|
||||||
|
|
||||||
### 重构 (Refactor)
|
|
||||||
- 按文件类型筛选提供者,而不是按订阅信息筛选
|
|
||||||
- 添加缺失的 `aria-label`,提升可访问性合规性
|
|
||||||
|
|
||||||
### 其他改进 (Chore)
|
|
||||||
- 格式化语言文件并删除未使用的文件
|
|
||||||
- 添加缺失的 i18n 字符串和 UI 调整
|
|
||||||
- 更新依赖项
|
|
||||||
- 记住滚动位置和展开状态
|
|
||||||
- 增加节点详细信息
|
|
||||||
|
|
||||||
### 依赖更新 (Deps)
|
|
||||||
- 更新依赖项,并将 UI 框架从 NextUI 迁移到 HeroUI
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mihomo-party",
|
"name": "mihomo-party",
|
||||||
"version": "1.7.2",
|
"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."
|
||||||
@ -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,7 +164,9 @@ 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']) {
|
||||||
newItem.interval = parseInt(headers['profile-update-interval']) * 60
|
if (!item.allowFixedInterval) {
|
||||||
|
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,12 +84,27 @@ if (process.platform === 'win32' && !is.dev && !process.argv.includes('noadmin')
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const gotTheLock = app.requestSingleInstanceLock()
|
async function initApp(): Promise<void> {
|
||||||
|
await fixUserDataPermissions()
|
||||||
if (!gotTheLock) {
|
|
||||||
app.quit()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initApp()
|
||||||
|
.then(() => {
|
||||||
|
const gotTheLock = app.requestSingleInstanceLock()
|
||||||
|
|
||||||
|
if (!gotTheLock) {
|
||||||
|
app.quit()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// ignore permission fix errors
|
||||||
|
const gotTheLock = app.requestSingleInstanceLock()
|
||||||
|
|
||||||
|
if (!gotTheLock) {
|
||||||
|
app.quit()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
export function customRelaunch(): void {
|
export function customRelaunch(): void {
|
||||||
const script = `while kill -0 ${process.pid} 2>/dev/null; do
|
const script = `while kill -0 ${process.pid} 2>/dev/null; do
|
||||||
sleep 0.1
|
sleep 0.1
|
||||||
@ -269,10 +309,21 @@ export async function createWindow(): Promise<void> {
|
|||||||
mainWindow?.webContents.reload()
|
mainWindow?.webContents.reload()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
mainWindow.on('show', () => {
|
||||||
|
showDockIcon()
|
||||||
|
})
|
||||||
|
|
||||||
mainWindow.on('close', async (event) => {
|
mainWindow.on('close', async (event) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
mainWindow?.hide()
|
mainWindow?.hide()
|
||||||
const { autoQuitWithoutCore = false, autoQuitWithoutCoreDelay = 60 } = await getAppConfig()
|
const {
|
||||||
|
autoQuitWithoutCore = false,
|
||||||
|
autoQuitWithoutCoreDelay = 60,
|
||||||
|
useDockIcon = true
|
||||||
|
} = await getAppConfig()
|
||||||
|
if (!useDockIcon) {
|
||||||
|
hideDockIcon()
|
||||||
|
}
|
||||||
if (autoQuitWithoutCore) {
|
if (autoQuitWithoutCore) {
|
||||||
if (quitTimeout) {
|
if (quitTimeout) {
|
||||||
clearTimeout(quitTimeout)
|
clearTimeout(quitTimeout)
|
||||||
|
|||||||
@ -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,12 +1,6 @@
|
|||||||
import { getAppConfig, getControledMihomoConfig } from '../config'
|
import { getAppConfig, getControledMihomoConfig } from '../config'
|
||||||
import { Worker } from 'worker_threads'
|
import { Worker } from 'worker_threads'
|
||||||
import {
|
import { dataDir, mihomoWorkDir, subStoreDir, substoreLogPath } from '../utils/dirs'
|
||||||
dataDir,
|
|
||||||
mihomoWorkDir,
|
|
||||||
resourcesFilesDir,
|
|
||||||
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'
|
||||||
@ -17,9 +11,6 @@ import { nativeImage } from 'electron'
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import AdmZip from 'adm-zip'
|
import AdmZip from 'adm-zip'
|
||||||
import { promisify } from 'util'
|
|
||||||
import { exec } from 'child_process'
|
|
||||||
import { platform } from 'os'
|
|
||||||
|
|
||||||
export let pacPort: number
|
export let pacPort: number
|
||||||
export let subStorePort: number
|
export let subStorePort: number
|
||||||
@ -86,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,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,
|
||||||
@ -148,12 +139,11 @@ export async function stopSubStoreBackendServer(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function downloadSubStore(password?: string): 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(dataDir(), 'temp')
|
const tempDir = path.join(dataDir(), 'temp')
|
||||||
const execPromise = promisify(exec)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 创建临时目录
|
// 创建临时目录
|
||||||
@ -194,35 +184,12 @@ export async function downloadSubStore(password?: string): Promise<void> {
|
|||||||
// 先解压到临时目录
|
// 先解压到临时目录
|
||||||
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)
|
||||||
// 如果是 Linux 平台,使用 sudo cp 移动文件
|
if (existsSync(frontendDir)) {
|
||||||
if (platform() === 'linux') {
|
await rm(frontendDir, { recursive: true })
|
||||||
try {
|
|
||||||
await execPromise(`echo "${password}" | sudo -S cp "${tempBackendPath}" "${backendPath}"`)
|
|
||||||
// 确保目标目录存在并清空
|
|
||||||
if (existsSync(frontendDir)) {
|
|
||||||
await execPromise(`echo "${password}" | sudo -S rm -r "${frontendDir}"`)
|
|
||||||
}
|
|
||||||
await execPromise(`echo "${password}" | sudo -S mkdir "${frontendDir}"`)
|
|
||||||
// 将 dist 目录中的内容移动到目标目录
|
|
||||||
await execPromise(
|
|
||||||
`echo "${password}" | sudo -S cp -r "${tempFrontendDir}"/* "${frontendDir}/"`
|
|
||||||
)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('substore.downloadFailed:', error)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 非 Linux 平台
|
|
||||||
await cp(tempBackendPath, backendPath)
|
|
||||||
if (existsSync(frontendDir)) {
|
|
||||||
await rm(frontendDir, { recursive: true })
|
|
||||||
}
|
|
||||||
mkdirSync(frontendDir, { recursive: true })
|
|
||||||
await cp(path.join(tempDir, 'dist'), frontendDir, { recursive: true })
|
|
||||||
}
|
}
|
||||||
|
mkdirSync(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')
|
||||||
|
|||||||
@ -7,6 +7,7 @@ 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 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
|
||||||
@ -82,12 +83,14 @@ async function enableSysProxy(): Promise<void> {
|
|||||||
triggerAutoProxy(true, `http://${host || '127.0.0.1'}:${pacPort}/pac`)
|
triggerAutoProxy(true, `http://${host || '127.0.0.1'}:${pacPort}/pac`)
|
||||||
}
|
}
|
||||||
} else if (process.platform === 'darwin') {
|
} else if (process.platform === 'darwin') {
|
||||||
await axios.post(
|
await helperRequest(() =>
|
||||||
'http://localhost/pac',
|
axios.post(
|
||||||
{ url: `http://${host || '127.0.0.1'}:${pacPort}/pac` },
|
'http://localhost/pac',
|
||||||
{
|
{ url: `http://${host || '127.0.0.1'}:${pacPort}/pac` },
|
||||||
socketPath: helperSocketPath
|
{
|
||||||
}
|
socketPath: helperSocketPath
|
||||||
|
}
|
||||||
|
)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
triggerAutoProxy(true, `http://${host || '127.0.0.1'}:${pacPort}/pac`)
|
triggerAutoProxy(true, `http://${host || '127.0.0.1'}:${pacPort}/pac`)
|
||||||
@ -108,12 +111,14 @@ async function enableSysProxy(): Promise<void> {
|
|||||||
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') {
|
} else if (process.platform === 'darwin') {
|
||||||
await axios.post(
|
await helperRequest(() =>
|
||||||
'http://localhost/global',
|
axios.post(
|
||||||
{ host: host || '127.0.0.1', port: port.toString(), bypass: bypass.join(',') },
|
'http://localhost/global',
|
||||||
{
|
{ host: host || '127.0.0.1', port: port.toString(), bypass: bypass.join(',') },
|
||||||
socketPath: helperSocketPath
|
{
|
||||||
}
|
socketPath: helperSocketPath
|
||||||
|
}
|
||||||
|
)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
triggerManualProxy(true, host || '127.0.0.1', port, bypass.join(','))
|
triggerManualProxy(true, host || '127.0.0.1', port, bypass.join(','))
|
||||||
@ -134,11 +139,84 @@ async function disableSysProxy(): Promise<void> {
|
|||||||
triggerManualProxy(false, '', 0, '')
|
triggerManualProxy(false, '', 0, '')
|
||||||
}
|
}
|
||||||
} else if (process.platform === 'darwin') {
|
} else if (process.platform === 'darwin') {
|
||||||
await axios.get('http://localhost/off', {
|
await helperRequest(() =>
|
||||||
socketPath: helperSocketPath
|
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))
|
||||||
@ -203,7 +203,7 @@ export function registerIpcMainHandlers(): void {
|
|||||||
ipcMain.handle('stopSubStoreFrontendServer', () => ipcErrorWrapper(stopSubStoreFrontendServer)())
|
ipcMain.handle('stopSubStoreFrontendServer', () => ipcErrorWrapper(stopSubStoreFrontendServer)())
|
||||||
ipcMain.handle('startSubStoreBackendServer', () => ipcErrorWrapper(startSubStoreBackendServer)())
|
ipcMain.handle('startSubStoreBackendServer', () => ipcErrorWrapper(startSubStoreBackendServer)())
|
||||||
ipcMain.handle('stopSubStoreBackendServer', () => ipcErrorWrapper(stopSubStoreBackendServer)())
|
ipcMain.handle('stopSubStoreBackendServer', () => ipcErrorWrapper(stopSubStoreBackendServer)())
|
||||||
ipcMain.handle('downloadSubStore', (_e, password) => ipcErrorWrapper(downloadSubStore)(password))
|
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
|
|
||||||
@ -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>
|
||||||
|
|||||||
@ -193,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({
|
||||||
@ -389,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')}
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -15,11 +15,17 @@
|
|||||||
"common.default": "پیشفرض",
|
"common.default": "پیشفرض",
|
||||||
"common.close": "بستن",
|
"common.close": "بستن",
|
||||||
"common.pinWindow": "پین کردن پنجره",
|
"common.pinWindow": "پین کردن پنجره",
|
||||||
"common.enterRootPassword": "لطفا رمز عبور روت را وارد کنید",
|
|
||||||
"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 نامعتبر است",
|
||||||
@ -67,7 +73,7 @@
|
|||||||
"settings.links.telegram": "گروه تلگرام",
|
"settings.links.telegram": "گروه تلگرام",
|
||||||
"settings.title": "تنظیمات برنامه",
|
"settings.title": "تنظیمات برنامه",
|
||||||
"mihomo.userAgent": "User Agent اشتراک",
|
"mihomo.userAgent": "User Agent اشتراک",
|
||||||
"mihomo.userAgentPlaceholder": "پیشفرض: clash.meta",
|
"mihomo.userAgentPlaceholder": "پیشفرض: mihomo.party/v{{version}} (clash.meta)",
|
||||||
"mihomo.title": "تنظیمات هسته",
|
"mihomo.title": "تنظیمات هسته",
|
||||||
"mihomo.restart": "راهاندازی مجدد هسته",
|
"mihomo.restart": "راهاندازی مجدد هسته",
|
||||||
"mihomo.memory": "مصرف حافظه",
|
"mihomo.memory": "مصرف حافظه",
|
||||||
@ -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": "جایگزینی در دسترس نیست",
|
||||||
|
|||||||
@ -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": "URL подписки",
|
"profiles.editInfo.url": "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": "Нет доступных переопределений",
|
||||||
|
|||||||
@ -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')}
|
||||||
|
|||||||
@ -27,7 +27,7 @@ const CoreMap = {
|
|||||||
const Mihomo: React.FC = () => {
|
const Mihomo: React.FC = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { appConfig, patchAppConfig } = useAppConfig()
|
const { appConfig, patchAppConfig } = useAppConfig()
|
||||||
const {
|
const {
|
||||||
core = 'mihomo',
|
core = 'mihomo',
|
||||||
maxLogDays = 7,
|
maxLogDays = 7,
|
||||||
sysProxy,
|
sysProxy,
|
||||||
@ -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,24 +22,15 @@ 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>;
|
virtuosoRef: React.RefObject<GroupedVirtuosoHandle>;
|
||||||
isOpen: boolean[];
|
isOpen: boolean[];
|
||||||
setIsOpen: React.Dispatch<React.SetStateAction<boolean[]>>;
|
setIsOpen: React.Dispatch<React.SetStateAction<boolean[]>>;
|
||||||
scrollPosition: number;
|
|
||||||
onScroll: (e: React.UIEvent<HTMLElement>) => void;
|
|
||||||
} => {
|
} => {
|
||||||
const virtuosoRef = useRef<GroupedVirtuosoHandle>(null)
|
const virtuosoRef = useRef<GroupedVirtuosoHandle>(null)
|
||||||
const [scrollPosition, setScrollPosition] = useState<number>(0)
|
|
||||||
const scrollTimerRef = useRef<NodeJS.Timeout>()
|
|
||||||
const isManualScroll = useRef<boolean>(false)
|
|
||||||
const lastGroupsLength = useRef<number>(0)
|
|
||||||
|
|
||||||
// 初始化展开状态
|
// 初始化展开状态
|
||||||
const [isOpen, setIsOpen] = useState<boolean[]>(() => {
|
const [isOpen, setIsOpen] = useState<boolean[]>(() => {
|
||||||
@ -61,84 +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) {
|
|
||||||
// 只在首次加载或groups长度变化时恢复滚动位置
|
|
||||||
if (lastGroupsLength.current === 0 || lastGroupsLength.current !== groups.length) {
|
|
||||||
lastGroupsLength.current = groups.length
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
virtuosoRef.current?.scrollTo({
|
|
||||||
top: position,
|
|
||||||
behavior: 'auto' // 使用auto以避免平滑滚动引起的额外视觉效果
|
|
||||||
})
|
|
||||||
}, RENDER_DELAY)
|
|
||||||
return () => clearTimeout(timer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to restore scroll position:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 记录当前组长度以便跟踪变化
|
|
||||||
lastGroupsLength.current = groups.length
|
|
||||||
}, [groups])
|
|
||||||
|
|
||||||
// 数据刷新时保持滚动位置
|
|
||||||
useEffect(() => {
|
|
||||||
if (groups.length > 0 && scrollPosition > 0 && !isManualScroll.current) {
|
|
||||||
// 只在数据刷新时恢复位置,不是手动滚动触发的
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
virtuosoRef.current?.scrollTo({ top: scrollPosition, behavior: 'auto' })
|
|
||||||
}, 50)
|
|
||||||
return () => clearTimeout(timer)
|
|
||||||
}
|
|
||||||
}, [groups, scrollPosition])
|
|
||||||
|
|
||||||
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
|
|
||||||
isManualScroll.current = true // 标记这是手动滚动
|
|
||||||
setScrollPosition(position)
|
|
||||||
|
|
||||||
// 清理之前的定时器
|
|
||||||
if (scrollTimerRef.current) {
|
|
||||||
clearTimeout(scrollTimerRef.current)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用防抖来减少存储频率
|
|
||||||
scrollTimerRef.current = setTimeout(() => {
|
|
||||||
saveScrollPosition(position)
|
|
||||||
isManualScroll.current = false // 重置标记
|
|
||||||
}, SCROLL_DEBOUNCE_TIME)
|
|
||||||
}, [saveScrollPosition])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -157,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[] = []
|
||||||
@ -193,23 +111,12 @@ const Proxies: React.FC = () => {
|
|||||||
}, [groups, isOpen, proxyDisplayOrder, cols, searchValue])
|
}, [groups, isOpen, proxyDisplayOrder, cols, searchValue])
|
||||||
|
|
||||||
const onChangeProxy = useCallback(async (group: string, proxy: string): Promise<void> => {
|
const onChangeProxy = useCallback(async (group: string, proxy: string): Promise<void> => {
|
||||||
// 保存当前滚动位置以便切换后恢复
|
|
||||||
const currentPosition = scrollPosition;
|
|
||||||
|
|
||||||
await mihomoChangeProxy(group, proxy)
|
await mihomoChangeProxy(group, proxy)
|
||||||
if (autoCloseConnection) {
|
if (autoCloseConnection) {
|
||||||
await mihomoCloseAllConnections()
|
await mihomoCloseAllConnections()
|
||||||
}
|
}
|
||||||
mutate()
|
mutate()
|
||||||
|
}, [autoCloseConnection, mutate])
|
||||||
// 使用单层requestAnimationFrame和更长的延迟来确保DOM更新完成
|
|
||||||
setTimeout(() => {
|
|
||||||
virtuosoRef.current?.scrollTo({
|
|
||||||
top: currentPosition,
|
|
||||||
behavior: 'auto' // 使用auto避免出现平滑滚动导致的额外视觉抖动
|
|
||||||
})
|
|
||||||
}, 150) // 增加延迟让DOM有足够的时间更新
|
|
||||||
}, [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)
|
||||||
@ -229,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>[] = []
|
||||||
@ -240,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()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -259,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])
|
||||||
|
|
||||||
@ -280,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])
|
||||||
@ -346,12 +277,9 @@ const Proxies: React.FC = () => {
|
|||||||
<GroupedVirtuoso
|
<GroupedVirtuoso
|
||||||
ref={virtuosoRef}
|
ref={virtuosoRef}
|
||||||
groupCounts={groupCounts}
|
groupCounts={groupCounts}
|
||||||
onScroll={onScroll}
|
defaultItemHeight={80}
|
||||||
initialTopMostItemIndex={scrollPosition > 0 ? undefined : 0}
|
increaseViewportBy={{ top: 300, bottom: 300 }}
|
||||||
defaultItemHeight={80} // 设置默认高度减少跳动
|
overscan={500}
|
||||||
increaseViewportBy={{ top: 300, bottom: 300 }} // 扩大可视区域减少闪烁
|
|
||||||
overscan={500} // 增加预渲染区域
|
|
||||||
// 使用稳定的key减少不必要的重新渲染
|
|
||||||
computeItemKey={(index, groupIndex) => {
|
computeItemKey={(index, groupIndex) => {
|
||||||
let innerIndex = index
|
let innerIndex = index
|
||||||
groupCounts.slice(0, groupIndex).forEach((count) => {
|
groupCounts.slice(0, groupIndex).forEach((count) => {
|
||||||
@ -526,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]}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
@ -14,8 +14,6 @@ import React, { useEffect, useState } from 'react'
|
|||||||
import { HiExternalLink } from 'react-icons/hi'
|
import { HiExternalLink } from 'react-icons/hi'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { IoMdCloudDownload } from 'react-icons/io'
|
import { IoMdCloudDownload } from 'react-icons/io'
|
||||||
import BasePasswordModal from '@renderer/components/base/base-password-modal'
|
|
||||||
import { platform } from '@renderer/utils/init'
|
|
||||||
|
|
||||||
const SubStore: React.FC = () => {
|
const SubStore: React.FC = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@ -24,7 +22,6 @@ const SubStore: React.FC = () => {
|
|||||||
const [backendPort, setBackendPort] = useState<number | undefined>()
|
const [backendPort, setBackendPort] = useState<number | undefined>()
|
||||||
const [frontendPort, setFrontendPort] = useState<number | undefined>()
|
const [frontendPort, setFrontendPort] = useState<number | undefined>()
|
||||||
const [isUpdating, setIsUpdating] = useState(false)
|
const [isUpdating, setIsUpdating] = useState(false)
|
||||||
const [openPasswordModal, setOpenPasswordModal] = useState(false)
|
|
||||||
const getPort = async (): Promise<void> => {
|
const getPort = async (): Promise<void> => {
|
||||||
setBackendPort(await subStorePort())
|
setBackendPort(await subStorePort())
|
||||||
setFrontendPort(await subStoreFrontendPort())
|
setFrontendPort(await subStoreFrontendPort())
|
||||||
@ -37,85 +34,40 @@ const SubStore: React.FC = () => {
|
|||||||
if (!frontendPort) return null
|
if (!frontendPort) return null
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{openPasswordModal && (
|
|
||||||
<BasePasswordModal
|
|
||||||
onCancel={() => setOpenPasswordModal(false)}
|
|
||||||
onConfirm={async (password: string) => {
|
|
||||||
try {
|
|
||||||
setOpenPasswordModal(false)
|
|
||||||
new Notification(t('substore.updating'))
|
|
||||||
await downloadSubStore(password)
|
|
||||||
await stopSubStoreBackendServer()
|
|
||||||
await startSubStoreBackendServer()
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
|
||||||
setFrontendPort(0)
|
|
||||||
await stopSubStoreFrontendServer()
|
|
||||||
await startSubStoreFrontendServer()
|
|
||||||
await getPort()
|
|
||||||
new Notification(t('substore.updateCompleted'))
|
|
||||||
} catch (e) {
|
|
||||||
alert(e)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<BasePage
|
<BasePage
|
||||||
title={t('substore.title')}
|
title={t('substore.title')}
|
||||||
header={
|
header={
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{platform != 'linux' && (
|
<Button
|
||||||
<Button
|
title={t('substore.checkUpdate')}
|
||||||
title={t('substore.checkUpdate')}
|
isIconOnly
|
||||||
isIconOnly
|
size="sm"
|
||||||
size="sm"
|
className="app-nodrag"
|
||||||
className="app-nodrag"
|
variant="light"
|
||||||
variant="light"
|
isLoading={isUpdating}
|
||||||
isLoading={isUpdating}
|
onPress={async () => {
|
||||||
onPress={async () => {
|
try {
|
||||||
try {
|
new Notification(t('substore.updating'))
|
||||||
new Notification(t('substore.updating'))
|
setIsUpdating(true)
|
||||||
setIsUpdating(true)
|
await downloadSubStore()
|
||||||
await downloadSubStore()
|
await stopSubStoreBackendServer()
|
||||||
await stopSubStoreBackendServer()
|
await startSubStoreBackendServer()
|
||||||
await startSubStoreBackendServer()
|
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
setFrontendPort(0)
|
||||||
setFrontendPort(0)
|
await stopSubStoreFrontendServer()
|
||||||
await stopSubStoreFrontendServer()
|
await startSubStoreFrontendServer()
|
||||||
await startSubStoreFrontendServer()
|
await getPort()
|
||||||
await getPort()
|
new Notification(t('substore.updateCompleted'))
|
||||||
new Notification(t('substore.updateCompleted'))
|
} catch (e) {
|
||||||
} catch (e) {
|
new Notification(`${t('substore.updateFailed')}: ${e}`)
|
||||||
new Notification(`${t('substore.updateFailed')}: ${e}`)
|
} finally {
|
||||||
} finally {
|
setIsUpdating(false)
|
||||||
setIsUpdating(false)
|
}
|
||||||
}
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<IoMdCloudDownload className="text-lg" />
|
||||||
<IoMdCloudDownload className="text-lg" />
|
</Button>
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{platform === 'linux' && (
|
|
||||||
<Button
|
|
||||||
title={t('substore.checkUpdate')}
|
|
||||||
isIconOnly
|
|
||||||
size="sm"
|
|
||||||
className="app-nodrag"
|
|
||||||
variant="light"
|
|
||||||
isLoading={isUpdating}
|
|
||||||
onPress={async () => {
|
|
||||||
try {
|
|
||||||
setIsUpdating(true)
|
|
||||||
setOpenPasswordModal(true)
|
|
||||||
} catch (e) {
|
|
||||||
new Notification(`${t('substore.updateFailed')}: ${e}`)
|
|
||||||
} finally {
|
|
||||||
setIsUpdating(false)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IoMdCloudDownload className="text-lg" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button
|
<Button
|
||||||
title={t('substore.openInBrowser')}
|
title={t('substore.openInBrowser')}
|
||||||
isIconOnly
|
isIconOnly
|
||||||
|
|||||||
@ -11,44 +11,6 @@ import React from 'react'
|
|||||||
import { MdDeleteForever } from 'react-icons/md'
|
import { MdDeleteForever } from 'react-icons/md'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
const defaultBypass: string[] =
|
|
||||||
platform === 'linux'
|
|
||||||
? ['localhost', '127.0.0.1', '192.168.0.0/16', '10.0.0.0/8', '172.16.0.0/12', '::1']
|
|
||||||
: platform === 'darwin'
|
|
||||||
? [
|
|
||||||
'127.0.0.1',
|
|
||||||
'192.168.0.0/16',
|
|
||||||
'10.0.0.0/8',
|
|
||||||
'172.16.0.0/12',
|
|
||||||
'localhost',
|
|
||||||
'*.local',
|
|
||||||
'*.crashlytics.com',
|
|
||||||
'<local>'
|
|
||||||
]
|
|
||||||
: [
|
|
||||||
'localhost',
|
|
||||||
'127.*',
|
|
||||||
'192.168.*',
|
|
||||||
'10.*',
|
|
||||||
'172.16.*',
|
|
||||||
'172.17.*',
|
|
||||||
'172.18.*',
|
|
||||||
'172.19.*',
|
|
||||||
'172.20.*',
|
|
||||||
'172.21.*',
|
|
||||||
'172.22.*',
|
|
||||||
'172.23.*',
|
|
||||||
'172.24.*',
|
|
||||||
'172.25.*',
|
|
||||||
'172.26.*',
|
|
||||||
'172.27.*',
|
|
||||||
'172.28.*',
|
|
||||||
'172.29.*',
|
|
||||||
'172.30.*',
|
|
||||||
'172.31.*',
|
|
||||||
'<local>'
|
|
||||||
]
|
|
||||||
|
|
||||||
const defaultPacScript = `
|
const defaultPacScript = `
|
||||||
function FindProxyForURL(url, host) {
|
function FindProxyForURL(url, host) {
|
||||||
return "PROXY 127.0.0.1:%mixed-port%; SOCKS5 127.0.0.1:%mixed-port%; DIRECT;";
|
return "PROXY 127.0.0.1:%mixed-port%; SOCKS5 127.0.0.1:%mixed-port%; DIRECT;";
|
||||||
@ -56,6 +18,44 @@ function FindProxyForURL(url, host) {
|
|||||||
`
|
`
|
||||||
|
|
||||||
const Sysproxy: React.FC = () => {
|
const Sysproxy: React.FC = () => {
|
||||||
|
const defaultBypass: string[] =
|
||||||
|
platform === 'linux'
|
||||||
|
? ['localhost', '127.0.0.1', '192.168.0.0/16', '10.0.0.0/8', '172.16.0.0/12', '::1']
|
||||||
|
: platform === 'darwin'
|
||||||
|
? [
|
||||||
|
'127.0.0.1',
|
||||||
|
'192.168.0.0/16',
|
||||||
|
'10.0.0.0/8',
|
||||||
|
'172.16.0.0/12',
|
||||||
|
'localhost',
|
||||||
|
'*.local',
|
||||||
|
'*.crashlytics.com',
|
||||||
|
'<local>'
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
'localhost',
|
||||||
|
'127.*',
|
||||||
|
'192.168.*',
|
||||||
|
'10.*',
|
||||||
|
'172.16.*',
|
||||||
|
'172.17.*',
|
||||||
|
'172.18.*',
|
||||||
|
'172.19.*',
|
||||||
|
'172.20.*',
|
||||||
|
'172.21.*',
|
||||||
|
'172.22.*',
|
||||||
|
'172.23.*',
|
||||||
|
'172.24.*',
|
||||||
|
'172.25.*',
|
||||||
|
'172.26.*',
|
||||||
|
'172.27.*',
|
||||||
|
'172.28.*',
|
||||||
|
'172.29.*',
|
||||||
|
'172.30.*',
|
||||||
|
'172.31.*',
|
||||||
|
'<local>'
|
||||||
|
]
|
||||||
|
|
||||||
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)
|
||||||
@ -93,20 +93,20 @@ const Sysproxy: React.FC = () => {
|
|||||||
|
|
||||||
const onSave = async (): Promise<void> => {
|
const onSave = async (): Promise<void> => {
|
||||||
setChanged(false)
|
setChanged(false)
|
||||||
|
|
||||||
// 保存当前的开关状态,以便在失败时恢复
|
// 保存当前的开关状态,以便在失败时恢复
|
||||||
const previousState = values.enable
|
const previousState = values.enable
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await patchAppConfig({ sysProxy: values })
|
await patchAppConfig({ sysProxy: values })
|
||||||
await triggerSysProxy(true)
|
await triggerSysProxy(true)
|
||||||
|
|
||||||
await patchAppConfig({ sysProxy: { enable: true } })
|
await patchAppConfig({ sysProxy: { enable: true } })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setValues({ ...values, enable: previousState })
|
setValues({ ...values, enable: previousState })
|
||||||
setChanged(true)
|
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,16 +128,12 @@ 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'))
|
await restartCore()
|
||||||
await restartCore()
|
} catch (e) {
|
||||||
} catch (e) {
|
alert(e)
|
||||||
alert(e)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setOpenPasswordModal(true)
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -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> {
|
||||||
@ -326,8 +328,8 @@ export async function startSubStoreBackendServer(): Promise<void> {
|
|||||||
export async function stopSubStoreBackendServer(): Promise<void> {
|
export async function stopSubStoreBackendServer(): Promise<void> {
|
||||||
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('stopSubStoreBackendServer'))
|
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('stopSubStoreBackendServer'))
|
||||||
}
|
}
|
||||||
export async function downloadSubStore(password?: string): Promise<void> {
|
export async function downloadSubStore(): Promise<void> {
|
||||||
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('downloadSubStore', password))
|
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('downloadSubStore'))
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function subStorePort(): Promise<number> {
|
export async function subStorePort(): Promise<number> {
|
||||||
|
|||||||
2
src/shared/types.d.ts
vendored
2
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
|
||||||
@ -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