Compare commits

..

3 Commits

17 changed files with 148 additions and 62 deletions

View File

@ -1,6 +1,7 @@
import { getAppConfig } from '../config'
import dayjs from 'dayjs'
import AdmZip from 'adm-zip'
import https from 'https'
import {
appConfigPath,
controledMihomoConfigPath,
@ -34,13 +35,22 @@ async function getWebDAVClient(): Promise<WebDAVContext> {
webdavUsername = '',
webdavPassword = '',
webdavDir = 'clash-party',
webdavMaxBackups = 0
webdavMaxBackups = 0,
webdavIgnoreCert = false
} = await getAppConfig()
const client = createClient(webdavUrl, {
const clientOptions: Parameters<typeof createClient>[1] = {
username: webdavUsername,
password: webdavPassword
})
}
if (webdavIgnoreCert) {
clientOptions.httpsAgent = new https.Agent({
rejectUnauthorized: false
})
}
const client = createClient(webdavUrl, clientOptions)
return { client, webdavDir, webdavMaxBackups }
}
@ -49,15 +59,33 @@ export async function webdavBackup(): Promise<boolean> {
const { client, webdavDir, webdavMaxBackups } = await getWebDAVClient()
const zip = new AdmZip()
zip.addLocalFile(appConfigPath())
zip.addLocalFile(controledMihomoConfigPath())
zip.addLocalFile(profileConfigPath())
zip.addLocalFile(overrideConfigPath())
zip.addLocalFolder(themesDir(), 'themes')
zip.addLocalFolder(profilesDir(), 'profiles')
zip.addLocalFolder(overrideDir(), 'override')
zip.addLocalFolder(rulesDir(), 'rules')
zip.addLocalFolder(subStoreDir(), 'substore')
if (existsSync(appConfigPath())) {
zip.addLocalFile(appConfigPath())
}
if (existsSync(controledMihomoConfigPath())) {
zip.addLocalFile(controledMihomoConfigPath())
}
if (existsSync(profileConfigPath())) {
zip.addLocalFile(profileConfigPath())
}
if (existsSync(overrideConfigPath())) {
zip.addLocalFile(overrideConfigPath())
}
if (existsSync(themesDir())) {
zip.addLocalFolder(themesDir(), 'themes')
}
if (existsSync(profilesDir())) {
zip.addLocalFolder(profilesDir(), 'profiles')
}
if (existsSync(overrideDir())) {
zip.addLocalFolder(overrideDir(), 'override')
}
if (existsSync(rulesDir())) {
zip.addLocalFolder(rulesDir(), 'rules')
}
if (existsSync(subStoreDir())) {
zip.addLocalFolder(subStoreDir(), 'substore')
}
const date = new Date()
const zipFileName = `${process.platform}_${dayjs(date).format('YYYY-MM-DD_HH-mm-ss')}.zip`

View File

@ -2,7 +2,7 @@ import React, { useState } from 'react'
import SettingCard from '../base/base-setting-card'
import { toast } from '@renderer/components/base/toast'
import SettingItem from '../base/base-setting-item'
import { Button, Input, Select, SelectItem } from '@heroui/react'
import { Button, Input, Select, SelectItem, Switch } from '@heroui/react'
import { listWebdavBackups, webdavBackup, reinitWebdavBackupScheduler } from '@renderer/utils/ipc'
import WebdavRestoreModal from './webdav-restore-modal'
import debounce from '@renderer/utils/debounce'
@ -19,7 +19,8 @@ const WebdavConfig: React.FC = () => {
webdavPassword,
webdavDir = 'clash-party',
webdavMaxBackups = 0,
webdavBackupCron
webdavBackupCron,
webdavIgnoreCert = false
} = appConfig || {}
const [backuping, setBackuping] = useState(false)
const [restoring, setRestoring] = useState(false)
@ -32,7 +33,8 @@ const WebdavConfig: React.FC = () => {
webdavPassword,
webdavDir,
webdavMaxBackups,
webdavBackupCron
webdavBackupCron,
webdavIgnoreCert
})
const setWebdavDebounce = debounce(
({
@ -153,6 +155,16 @@ const WebdavConfig: React.FC = () => {
<SelectItem key="20">20</SelectItem>
</Select>
</SettingItem>
<SettingItem title={t('webdav.ignoreCert')} divider>
<Switch
size="sm"
isSelected={webdav.webdavIgnoreCert}
onValueChange={(v) => {
setWebdav({ ...webdav, webdavIgnoreCert: v })
patchAppConfig({ webdavIgnoreCert: v })
}}
/>
</SettingItem>
<SettingItem title={t('webdav.backup.cron.title')} divider>
<div className="flex w-[60%] gap-2">
{webdavBackupCron !== webdav.webdavBackupCron && (
@ -188,7 +200,7 @@ const WebdavConfig: React.FC = () => {
/>
</div>
</SettingItem>
<div className="flex justify0between">
<div className="flex justify-between">
<Button isLoading={backuping} fullWidth size="sm" className="mr-1" onPress={handleBackup}>
{t('webdav.backup')}
</Button>

View File

@ -25,11 +25,12 @@ export const AppConfigProvider: React.FC<{ children: ReactNode }> = ({ children
}
React.useEffect(() => {
window.electron.ipcRenderer.on('appConfigUpdated', () => {
const handler = (): void => {
mutateAppConfig()
})
}
window.electron.ipcRenderer.on('appConfigUpdated', handler)
return (): void => {
window.electron.ipcRenderer.removeAllListeners('appConfigUpdated')
window.electron.ipcRenderer.removeListener('appConfigUpdated', handler)
}
}, [])

View File

@ -30,11 +30,12 @@ export const ControledMihomoConfigProvider: React.FC<{ children: ReactNode }> =
}
React.useEffect(() => {
window.electron.ipcRenderer.on('controledMihomoConfigUpdated', () => {
const handler = (): void => {
mutateControledMihomoConfig()
})
}
window.electron.ipcRenderer.on('controledMihomoConfigUpdated', handler)
return (): void => {
window.electron.ipcRenderer.removeAllListeners('controledMihomoConfigUpdated')
window.electron.ipcRenderer.removeListener('controledMihomoConfigUpdated', handler)
}
}, [])

View File

@ -20,11 +20,12 @@ export const GroupsProvider: React.FC<{ children: ReactNode }> = ({ children })
})
React.useEffect(() => {
window.electron.ipcRenderer.on('groupsUpdated', () => {
const handler = (): void => {
mutate()
})
}
window.electron.ipcRenderer.on('groupsUpdated', handler)
return (): void => {
window.electron.ipcRenderer.removeAllListeners('groupsUpdated')
window.electron.ipcRenderer.removeListener('groupsUpdated', handler)
}
}, [])

View File

@ -119,11 +119,12 @@ export const ProfileConfigProvider: React.FC<{ children: ReactNode }> = ({ child
}
React.useEffect(() => {
window.electron.ipcRenderer.on('profileConfigUpdated', () => {
const handler = (): void => {
mutateProfileConfig()
})
}
window.electron.ipcRenderer.on('profileConfigUpdated', handler)
return (): void => {
window.electron.ipcRenderer.removeAllListeners('profileConfigUpdated')
window.electron.ipcRenderer.removeListener('profileConfigUpdated', handler)
}
}, [])

View File

@ -16,11 +16,12 @@ export const RulesProvider: React.FC<{ children: ReactNode }> = ({ children }) =
})
React.useEffect(() => {
window.electron.ipcRenderer.on('rulesUpdated', () => {
const handler = (): void => {
mutate()
})
}
window.electron.ipcRenderer.on('rulesUpdated', handler)
return (): void => {
window.electron.ipcRenderer.removeAllListeners('rulesUpdated')
window.electron.ipcRenderer.removeListener('rulesUpdated', handler)
}
}, [])

View File

@ -236,6 +236,7 @@
"webdav.username": "WebDAV Username",
"webdav.password": "WebDAV Password",
"webdav.maxBackups": "Max Backups",
"webdav.ignoreCert": "Ignore Certificate",
"webdav.noLimit": "No Limit",
"webdav.backup": "Backup",
"webdav.backup.cron.title": "Schedule Config Backup",

View File

@ -211,6 +211,7 @@
"webdav.username": "نام کاربری WebDAV",
"webdav.password": "رمز عبور WebDAV",
"webdav.maxBackups": "حداکثر نسخه پشتیبان",
"webdav.ignoreCert": "نادیده گرفتن گواهی",
"webdav.noLimit": "بدون محدودیت",
"webdav.backup": "پشتیبان‌گیری",
"webdav.backup.cron.title": "زمانبندی پشتیبان‌گیری پیکربندی",

View File

@ -211,6 +211,7 @@
"webdav.username": "Имя пользователя WebDAV",
"webdav.password": "Пароль WebDAV",
"webdav.maxBackups": "Максимум резервных копий",
"webdav.ignoreCert": "Игнорировать сертификат",
"webdav.noLimit": "Без ограничений",
"webdav.backup": "Резервное копирование",
"webdav.backup.cron.title": "Расписание резервного копирования",

View File

@ -236,6 +236,7 @@
"webdav.username": "WebDAV 用户名",
"webdav.password": "WebDAV 密码",
"webdav.maxBackups": "最大备份数",
"webdav.ignoreCert": "忽略证书验证",
"webdav.noLimit": "不限制",
"webdav.backup": "备份",
"webdav.backup.cron.title": "定时备份配置",

View File

@ -236,6 +236,7 @@
"webdav.username": "WebDAV 用戶名",
"webdav.password": "WebDAV 密碼",
"webdav.maxBackups": "最大備份數",
"webdav.ignoreCert": "忽略證書驗證",
"webdav.noLimit": "不限制",
"webdav.backup": "備份",
"webdav.backup.cron.title": "定時備份配置",

View File

@ -632,8 +632,10 @@ const Mihomo: React.FC = () => {
type="number"
value={smartCollectorSize.toString()}
onValueChange={async (v: string) => {
let num = parseInt(v)
await patchAppConfig({ smartCollectorSize: num })
const num = parseInt(v)
if (!isNaN(num)) {
await patchAppConfig({ smartCollectorSize: num })
}
}}
onBlur={async (e) => {
let num = parseInt(e.target.value)
@ -706,9 +708,11 @@ const Mihomo: React.FC = () => {
min={0}
onValueChange={(v) => {
const port = parseInt(v)
setMixedPortInput(port)
patchAppConfig({ showMixedPort: port })
setIsManualPortChange(true)
if (!isNaN(port)) {
setMixedPortInput(port)
patchAppConfig({ showMixedPort: port })
setIsManualPortChange(true)
}
}}
/>
<Button
@ -765,9 +769,11 @@ const Mihomo: React.FC = () => {
min={0}
onValueChange={(v) => {
const port = parseInt(v)
setSocksPortInput(port)
patchAppConfig({ showSocksPort: port })
setIsManualPortChange(true)
if (!isNaN(port)) {
setSocksPortInput(port)
patchAppConfig({ showSocksPort: port })
setIsManualPortChange(true)
}
}}
/>
<Button
@ -824,9 +830,11 @@ const Mihomo: React.FC = () => {
min={0}
onValueChange={(v) => {
const port = parseInt(v)
setHttpPortInput(port)
patchAppConfig({ showHttpPort: port })
setIsManualPortChange(true)
if (!isNaN(port)) {
setHttpPortInput(port)
patchAppConfig({ showHttpPort: port })
setIsManualPortChange(true)
}
}}
/>
<Button
@ -884,9 +892,11 @@ const Mihomo: React.FC = () => {
min={0}
onValueChange={(v) => {
const port = parseInt(v)
setRedirPortInput(port)
patchAppConfig({ showRedirPort: port })
setIsManualPortChange(true)
if (!isNaN(port)) {
setRedirPortInput(port)
patchAppConfig({ showRedirPort: port })
setIsManualPortChange(true)
}
}}
/>
<Button
@ -945,9 +955,11 @@ const Mihomo: React.FC = () => {
min={0}
onValueChange={(v) => {
const port = parseInt(v)
setTproxyPortInput(port)
patchAppConfig({ showTproxyPort: port })
setIsManualPortChange(true)
if (!isNaN(port)) {
setTproxyPortInput(port)
patchAppConfig({ showTproxyPort: port })
setIsManualPortChange(true)
}
}}
/>
<Button
@ -1082,7 +1094,7 @@ const Mihomo: React.FC = () => {
{allowLan && (
<>
<SettingItem title={t('mihomo.allowedIpSegments')}>
{lanAllowedIpsInput.join('') !== lanAllowedIps.join('') && (
{JSON.stringify(lanAllowedIpsInput) !== JSON.stringify(lanAllowedIps) && (
<Button
size="sm"
color="primary"
@ -1132,7 +1144,7 @@ const Mihomo: React.FC = () => {
</div>
<Divider className="mb-2" />
<SettingItem title={t('mihomo.disallowedIpSegments')}>
{lanDisallowedIpsInput.join('') !== lanDisallowedIps.join('') && (
{JSON.stringify(lanDisallowedIpsInput) !== JSON.stringify(lanDisallowedIps) && (
<Button
size="sm"
color="primary"
@ -1186,7 +1198,7 @@ const Mihomo: React.FC = () => {
</>
)}
<SettingItem title={t('mihomo.userVerification')}>
{authenticationInput.join('') !== authentication.join('') && (
{JSON.stringify(authenticationInput) !== JSON.stringify(authentication) && (
<Button
size="sm"
color="primary"
@ -1261,7 +1273,7 @@ const Mihomo: React.FC = () => {
</div>
<Divider className="mb-2" />
<SettingItem title={t('mihomo.skipAuthPrefixes')}>
{skipAuthPrefixesInput.join('') !== skipAuthPrefixes.join('') && (
{JSON.stringify(skipAuthPrefixesInput) !== JSON.stringify(skipAuthPrefixes) && (
<Button
size="sm"
color="primary"
@ -1357,7 +1369,10 @@ const Mihomo: React.FC = () => {
className="w-[100px]"
value={maxLogDays.toString()}
onValueChange={(v) => {
patchAppConfig({ maxLogDays: parseInt(v) })
const num = parseInt(v)
if (!isNaN(num)) {
patchAppConfig({ maxLogDays: num })
}
}}
/>
</SettingItem>

View File

@ -149,8 +149,8 @@ const Profiles: React.FC = () => {
const newOrder = sortedItems.slice()
const activeIndex = newOrder.findIndex((item) => item.id === active.id)
const overIndex = newOrder.findIndex((item) => item.id === over.id)
newOrder.splice(activeIndex, 1)
newOrder.splice(overIndex, 0, items[activeIndex])
const [movedItem] = newOrder.splice(activeIndex, 1)
newOrder.splice(overIndex, 0, movedItem)
setSortedItems(newOrder)
await setProfileConfig({ current, items: newOrder })
}

View File

@ -63,19 +63,38 @@ const useProxyState = (
const [isOpen, setIsOpen] = useState<boolean[]>(() => {
try {
const savedState = localStorage.getItem(GROUP_EXPAND_STATE_KEY)
return savedState ? JSON.parse(savedState) : Array(groups.length).fill(false)
if (savedState) {
const parsed = JSON.parse(savedState)
if (Array.isArray(parsed)) {
return parsed
}
}
} catch (error) {
console.error('Failed to load group expand state:', error)
return Array(groups.length).fill(false)
}
return []
})
// 同步展开状态数组长度与 groups 长度
useEffect(() => {
if (groups.length !== isOpen.length) {
setIsOpen((prev) => {
if (groups.length > prev.length) {
return [...prev, ...Array(groups.length - prev.length).fill(false)]
}
return prev.slice(0, groups.length)
})
}
}, [groups.length])
// 保存展开状态
useEffect(() => {
try {
localStorage.setItem(GROUP_EXPAND_STATE_KEY, JSON.stringify(isOpen))
} catch (error) {
console.error('Failed to save group expand state:', error)
if (isOpen.length > 0) {
try {
localStorage.setItem(GROUP_EXPAND_STATE_KEY, JSON.stringify(isOpen))
} catch (error) {
console.error('Failed to save group expand state:', error)
}
}
}, [isOpen])

View File

@ -223,7 +223,8 @@ const Tun: React.FC = () => {
className="w-[100px]"
value={values.mtu.toString()}
onValueChange={(v) => {
setValues({ ...values, mtu: parseInt(v) })
const num = parseInt(v)
setValues({ ...values, mtu: isNaN(num) ? 1500 : num })
}}
/>
</SettingItem>

View File

@ -302,6 +302,7 @@ interface IAppConfig {
webdavPassword?: string
webdavMaxBackups?: number
webdavBackupCron?: string
webdavIgnoreCert?: boolean
useNameserverPolicy: boolean
nameserverPolicy: { [key: string]: string | string[] }
showWindowShortcut?: string