Compare commits

...

3 Commits

17 changed files with 148 additions and 62 deletions

View File

@ -1,6 +1,7 @@
import { getAppConfig } from '../config' import { getAppConfig } from '../config'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import AdmZip from 'adm-zip' import AdmZip from 'adm-zip'
import https from 'https'
import { import {
appConfigPath, appConfigPath,
controledMihomoConfigPath, controledMihomoConfigPath,
@ -34,13 +35,22 @@ async function getWebDAVClient(): Promise<WebDAVContext> {
webdavUsername = '', webdavUsername = '',
webdavPassword = '', webdavPassword = '',
webdavDir = 'clash-party', webdavDir = 'clash-party',
webdavMaxBackups = 0 webdavMaxBackups = 0,
webdavIgnoreCert = false
} = await getAppConfig() } = await getAppConfig()
const client = createClient(webdavUrl, { const clientOptions: Parameters<typeof createClient>[1] = {
username: webdavUsername, username: webdavUsername,
password: webdavPassword password: webdavPassword
}
if (webdavIgnoreCert) {
clientOptions.httpsAgent = new https.Agent({
rejectUnauthorized: false
}) })
}
const client = createClient(webdavUrl, clientOptions)
return { client, webdavDir, webdavMaxBackups } return { client, webdavDir, webdavMaxBackups }
} }
@ -49,15 +59,33 @@ export async function webdavBackup(): Promise<boolean> {
const { client, webdavDir, webdavMaxBackups } = await getWebDAVClient() const { client, webdavDir, webdavMaxBackups } = await getWebDAVClient()
const zip = new AdmZip() const zip = new AdmZip()
if (existsSync(appConfigPath())) {
zip.addLocalFile(appConfigPath()) zip.addLocalFile(appConfigPath())
}
if (existsSync(controledMihomoConfigPath())) {
zip.addLocalFile(controledMihomoConfigPath()) zip.addLocalFile(controledMihomoConfigPath())
}
if (existsSync(profileConfigPath())) {
zip.addLocalFile(profileConfigPath()) zip.addLocalFile(profileConfigPath())
}
if (existsSync(overrideConfigPath())) {
zip.addLocalFile(overrideConfigPath()) zip.addLocalFile(overrideConfigPath())
}
if (existsSync(themesDir())) {
zip.addLocalFolder(themesDir(), 'themes') zip.addLocalFolder(themesDir(), 'themes')
}
if (existsSync(profilesDir())) {
zip.addLocalFolder(profilesDir(), 'profiles') zip.addLocalFolder(profilesDir(), 'profiles')
}
if (existsSync(overrideDir())) {
zip.addLocalFolder(overrideDir(), 'override') zip.addLocalFolder(overrideDir(), 'override')
}
if (existsSync(rulesDir())) {
zip.addLocalFolder(rulesDir(), 'rules') zip.addLocalFolder(rulesDir(), 'rules')
}
if (existsSync(subStoreDir())) {
zip.addLocalFolder(subStoreDir(), 'substore') zip.addLocalFolder(subStoreDir(), 'substore')
}
const date = new Date() const date = new Date()
const zipFileName = `${process.platform}_${dayjs(date).format('YYYY-MM-DD_HH-mm-ss')}.zip` 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 SettingCard from '../base/base-setting-card'
import { toast } from '@renderer/components/base/toast' import { toast } from '@renderer/components/base/toast'
import SettingItem from '../base/base-setting-item' 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 { listWebdavBackups, webdavBackup, reinitWebdavBackupScheduler } 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'
@ -19,7 +19,8 @@ const WebdavConfig: React.FC = () => {
webdavPassword, webdavPassword,
webdavDir = 'clash-party', webdavDir = 'clash-party',
webdavMaxBackups = 0, webdavMaxBackups = 0,
webdavBackupCron webdavBackupCron,
webdavIgnoreCert = false
} = appConfig || {} } = appConfig || {}
const [backuping, setBackuping] = useState(false) const [backuping, setBackuping] = useState(false)
const [restoring, setRestoring] = useState(false) const [restoring, setRestoring] = useState(false)
@ -32,7 +33,8 @@ const WebdavConfig: React.FC = () => {
webdavPassword, webdavPassword,
webdavDir, webdavDir,
webdavMaxBackups, webdavMaxBackups,
webdavBackupCron webdavBackupCron,
webdavIgnoreCert
}) })
const setWebdavDebounce = debounce( const setWebdavDebounce = debounce(
({ ({
@ -153,6 +155,16 @@ const WebdavConfig: React.FC = () => {
<SelectItem key="20">20</SelectItem> <SelectItem key="20">20</SelectItem>
</Select> </Select>
</SettingItem> </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> <SettingItem title={t('webdav.backup.cron.title')} divider>
<div className="flex w-[60%] gap-2"> <div className="flex w-[60%] gap-2">
{webdavBackupCron !== webdav.webdavBackupCron && ( {webdavBackupCron !== webdav.webdavBackupCron && (
@ -188,7 +200,7 @@ const WebdavConfig: React.FC = () => {
/> />
</div> </div>
</SettingItem> </SettingItem>
<div className="flex justify0between"> <div className="flex justify-between">
<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')}
</Button> </Button>

View File

@ -25,11 +25,12 @@ export const AppConfigProvider: React.FC<{ children: ReactNode }> = ({ children
} }
React.useEffect(() => { React.useEffect(() => {
window.electron.ipcRenderer.on('appConfigUpdated', () => { const handler = (): void => {
mutateAppConfig() mutateAppConfig()
}) }
window.electron.ipcRenderer.on('appConfigUpdated', handler)
return (): void => { 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(() => { React.useEffect(() => {
window.electron.ipcRenderer.on('controledMihomoConfigUpdated', () => { const handler = (): void => {
mutateControledMihomoConfig() mutateControledMihomoConfig()
}) }
window.electron.ipcRenderer.on('controledMihomoConfigUpdated', handler)
return (): void => { 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(() => { React.useEffect(() => {
window.electron.ipcRenderer.on('groupsUpdated', () => { const handler = (): void => {
mutate() mutate()
}) }
window.electron.ipcRenderer.on('groupsUpdated', handler)
return (): void => { 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(() => { React.useEffect(() => {
window.electron.ipcRenderer.on('profileConfigUpdated', () => { const handler = (): void => {
mutateProfileConfig() mutateProfileConfig()
}) }
window.electron.ipcRenderer.on('profileConfigUpdated', handler)
return (): void => { 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(() => { React.useEffect(() => {
window.electron.ipcRenderer.on('rulesUpdated', () => { const handler = (): void => {
mutate() mutate()
}) }
window.electron.ipcRenderer.on('rulesUpdated', handler)
return (): void => { return (): void => {
window.electron.ipcRenderer.removeAllListeners('rulesUpdated') window.electron.ipcRenderer.removeListener('rulesUpdated', handler)
} }
}, []) }, [])

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -63,20 +63,39 @@ const useProxyState = (
const [isOpen, setIsOpen] = useState<boolean[]>(() => { const [isOpen, setIsOpen] = useState<boolean[]>(() => {
try { try {
const savedState = localStorage.getItem(GROUP_EXPAND_STATE_KEY) const savedState = localStorage.getItem(GROUP_EXPAND_STATE_KEY)
return savedState ? JSON.parse(savedState) : Array(groups.length).fill(false) if (savedState) {
const parsed = JSON.parse(savedState)
if (Array.isArray(parsed)) {
return parsed
}
}
} catch (error) { } catch (error) {
console.error('Failed to load group expand state:', 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(() => { useEffect(() => {
if (isOpen.length > 0) {
try { try {
localStorage.setItem(GROUP_EXPAND_STATE_KEY, JSON.stringify(isOpen)) localStorage.setItem(GROUP_EXPAND_STATE_KEY, JSON.stringify(isOpen))
} catch (error) { } catch (error) {
console.error('Failed to save group expand state:', error) console.error('Failed to save group expand state:', error)
} }
}
}, [isOpen]) }, [isOpen])
return { return {

View File

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

View File

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