mirror of
https://gh.catmak.name/https://github.com/mihomo-party-org/mihomo-party
synced 2025-12-27 21:20:29 +08:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
364578f210 | ||
|
|
674cefcc29 | ||
|
|
827e744601 | ||
|
|
151726fcce | ||
|
|
6f3845151d | ||
|
|
a9b5887e15 | ||
|
|
727fd48684 | ||
|
|
e2ab88f4e2 | ||
|
|
1a5c001dbd | ||
|
|
6744e14c66 |
@ -74,7 +74,9 @@ cat << EOF > "$LAUNCH_DAEMON"
|
|||||||
<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>
|
||||||
|
</array>
|
||||||
<key>KeepAlive</key>
|
<key>KeepAlive</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>Program</key>
|
<key>Program</key>
|
||||||
@ -91,18 +93,81 @@ chown root:wheel "$LAUNCH_DAEMON"
|
|||||||
chmod 644 "$LAUNCH_DAEMON"
|
chmod 644 "$LAUNCH_DAEMON"
|
||||||
log "LaunchDaemon configured"
|
log "LaunchDaemon configured"
|
||||||
|
|
||||||
# 加载并启动服务
|
# 验证关键文件
|
||||||
log "Loading and starting service..."
|
log "Verifying installation..."
|
||||||
launchctl unload "$LAUNCH_DAEMON" 2>/dev/null || true
|
if [ ! -x "$HELPER_PATH" ]; then
|
||||||
if ! launchctl load "$LAUNCH_DAEMON"; then
|
log "Error: Helper tool is not executable: $HELPER_PATH"
|
||||||
log "Error: Failed to load helper service"
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! launchctl start party.mihomo.helper; then
|
# 检查二进制文件有效性
|
||||||
log "Error: 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
|
||||||
|
|
||||||
|
# 验证 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"
|
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
|
||||||
|
|||||||
24
changelog.md
24
changelog.md
@ -1,7 +1,25 @@
|
|||||||
## 1.7.5
|
## 1.7.7
|
||||||
|
### 新功能 (Feat)
|
||||||
|
- Mihomo 内核升级 v1.19.12
|
||||||
|
- 新增 Webdav 最大备数设置和清理逻辑
|
||||||
|
|
||||||
**此版本修复了 1.7.4 中的几个严重 bug,推荐所有人更新**
|
### 修复 (Fix)
|
||||||
另外,软件改名涉及多个功能、以及 logo重绘等多个任务的大量工作,还需要一些时间,已在开发环境进行中。
|
- 修复 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)
|
### 新功能 (Feat)
|
||||||
- 增加组延迟测试时的动画
|
- 增加组延迟测试时的动画
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mihomo-party",
|
"name": "mihomo-party",
|
||||||
"version": "1.7.5",
|
"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",
|
||||||
|
|||||||
@ -158,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')
|
||||||
|
|||||||
@ -10,8 +10,10 @@ import { createTray, hideDockIcon, showDockIcon } from './resolve/tray'
|
|||||||
import { init } from './utils/init'
|
import { init } from './utils/init'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { initShortcut } from './resolve/shortcut'
|
import { initShortcut } from './resolve/shortcut'
|
||||||
import { execSync, spawn } from 'child_process'
|
import { execSync, spawn, exec } from 'child_process'
|
||||||
import { createElevateTask } from './sys/misc'
|
import { createElevateTask } from './sys/misc'
|
||||||
|
import { promisify } from 'util'
|
||||||
|
import { stat } from 'fs/promises'
|
||||||
import { initProfileUpdater } from './core/profileUpdater'
|
import { initProfileUpdater } from './core/profileUpdater'
|
||||||
import { existsSync, writeFileSync } from 'fs'
|
import { existsSync, writeFileSync } from 'fs'
|
||||||
import { exePath, taskDir } from './utils/dirs'
|
import { exePath, taskDir } from './utils/dirs'
|
||||||
@ -22,6 +24,29 @@ import iconv from 'iconv-lite'
|
|||||||
import { initI18n } from '../shared/i18n'
|
import { initI18n } from '../shared/i18n'
|
||||||
import i18next from 'i18next'
|
import i18next from 'i18next'
|
||||||
|
|
||||||
|
async function fixUserDataPermissions(): Promise<void> {
|
||||||
|
if (process.platform !== 'darwin') return
|
||||||
|
|
||||||
|
const userDataPath = app.getPath('userData')
|
||||||
|
if (!existsSync(userDataPath)) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stats = await stat(userDataPath)
|
||||||
|
const currentUid = process.getuid?.() || 0
|
||||||
|
|
||||||
|
if (stats.uid === 0 && currentUid !== 0) {
|
||||||
|
const execPromise = promisify(exec)
|
||||||
|
const username = process.env.USER || process.env.LOGNAME
|
||||||
|
if (username) {
|
||||||
|
await execPromise(`chown -R "${username}:staff" "${userDataPath}"`)
|
||||||
|
await execPromise(`chmod -R u+rwX "${userDataPath}"`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let quitTimeout: NodeJS.Timeout | null = null
|
let quitTimeout: NodeJS.Timeout | null = null
|
||||||
export let mainWindow: BrowserWindow | null = null
|
export let mainWindow: BrowserWindow | null = null
|
||||||
|
|
||||||
@ -59,11 +84,26 @@ if (process.platform === 'win32' && !is.dev && !process.argv.includes('noadmin')
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function initApp(): Promise<void> {
|
||||||
|
await fixUserDataPermissions()
|
||||||
|
}
|
||||||
|
|
||||||
|
initApp()
|
||||||
|
.then(() => {
|
||||||
const gotTheLock = app.requestSingleInstanceLock()
|
const gotTheLock = app.requestSingleInstanceLock()
|
||||||
|
|
||||||
if (!gotTheLock) {
|
if (!gotTheLock) {
|
||||||
app.quit()
|
app.quit()
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// ignore permission fix errors
|
||||||
|
const gotTheLock = app.requestSingleInstanceLock()
|
||||||
|
|
||||||
|
if (!gotTheLock) {
|
||||||
|
app.quit()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
export function customRelaunch(): void {
|
export function customRelaunch(): void {
|
||||||
const script = `while kill -0 ${process.pid} 2>/dev/null; do
|
const script = `while kill -0 ${process.pid} 2>/dev/null; do
|
||||||
|
|||||||
@ -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> {
|
||||||
|
|||||||
@ -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`
|
||||||
)
|
)
|
||||||
|
|||||||
@ -68,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>
|
||||||
@ -104,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')
|
||||||
|
|||||||
@ -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,rm, readdir, cp } 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())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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')}
|
||||||
|
|||||||
@ -19,6 +19,13 @@
|
|||||||
"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",
|
||||||
@ -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",
|
||||||
|
|||||||
@ -19,6 +19,13 @@
|
|||||||
"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 نامعتبر است",
|
||||||
@ -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": "هیچ پشتیبانی موجود نیست",
|
||||||
|
|||||||
@ -19,6 +19,13 @@
|
|||||||
"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",
|
||||||
@ -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": "Нет доступных резервных копий",
|
||||||
|
|||||||
@ -19,6 +19,13 @@
|
|||||||
"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 表达式",
|
||||||
@ -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": "还没有备份",
|
||||||
|
|||||||
@ -401,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) {
|
||||||
@ -451,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) {
|
||||||
|
|||||||
1
src/shared/types.d.ts
vendored
1
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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user