mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-14 21:20:48 +08:00
* refactor(ipc): generate bindings with Specta * fix(profile): project IPC inputs to consumed fields * fix(events): remove unused refresh payloads * fix(webdav): return only consumed backup metadata * fix(network): trim unused interface metadata * fix(sysinfo): return only displayed system fields * chore(knip): check generated runtime exports * fix(proxy): confirm runtime chain updates before UI changes * fix(clipboard): propagate proxy environment copy failures * refactor(dns): remove ignored runtime patch * refactor(core): trim unused upgrade response fields * refactor(ipc): avoid redundant response copies * refactor(proxy): clarify automatic proxy query naming * chore(tests): remove added IPC regression tests * refactor(ipc): enforce typed command boundaries * chore(contract): check generated bindings before push * refactor(ipc): remove unused proxy liveness payloads
294 lines
9.2 KiB
TypeScript
294 lines
9.2 KiB
TypeScript
import Visibility from '@mui/icons-material/Visibility'
|
|
import VisibilityOff from '@mui/icons-material/VisibilityOff'
|
|
import {
|
|
TextField,
|
|
Button,
|
|
Grid,
|
|
Stack,
|
|
IconButton,
|
|
InputAdornment,
|
|
} from '@mui/material'
|
|
import { useLockFn } from 'ahooks'
|
|
import { useState, useRef, memo, useEffect } from 'react'
|
|
import { useForm } from 'react-hook-form'
|
|
import { useTranslation } from 'react-i18next'
|
|
|
|
import { useVerge } from '@/hooks/use-verge'
|
|
import { saveWebdavConfig, createWebdavBackup } from '@/services/cmds'
|
|
import { mutate } from '@/services/mutate'
|
|
import { errorDetail, showNotice } from '@/services/notice-service'
|
|
import {
|
|
buildWebdavSignature,
|
|
getWebdavStatus,
|
|
setWebdavStatus,
|
|
} from '@/services/webdav-status'
|
|
import { isValidUrl } from '@/utils/network'
|
|
|
|
interface BackupConfigViewerProps {
|
|
onBackupSuccess: () => Promise<void>
|
|
onSaveSuccess: (signature?: string) => Promise<void>
|
|
onRefresh: () => Promise<void>
|
|
onInit: () => Promise<void>
|
|
setLoading: (loading: boolean) => void
|
|
}
|
|
|
|
export const BackupConfigViewer = memo(
|
|
({
|
|
onBackupSuccess,
|
|
onSaveSuccess,
|
|
onRefresh,
|
|
onInit,
|
|
setLoading,
|
|
}: BackupConfigViewerProps) => {
|
|
const { t } = useTranslation()
|
|
const { verge, mutateVerge } = useVerge()
|
|
const { webdav_url, webdav_username, webdav_password } = verge || {}
|
|
const [showPassword, setShowPassword] = useState(false)
|
|
const usernameRef = useRef<HTMLInputElement>(null)
|
|
const passwordRef = useRef<HTMLInputElement>(null)
|
|
const urlRef = useRef<HTMLInputElement>(null)
|
|
|
|
const { register, handleSubmit, watch } = useForm<IWebDavConfig>({
|
|
defaultValues: {
|
|
url: webdav_url!,
|
|
username: webdav_username!,
|
|
password: webdav_password!,
|
|
},
|
|
})
|
|
const url = watch('url')
|
|
const username = watch('username')
|
|
const password = watch('password')
|
|
|
|
const webdavChanged =
|
|
webdav_url !== url ||
|
|
webdav_username !== username ||
|
|
webdav_password !== password
|
|
|
|
const webdavSignature = buildWebdavSignature(verge)
|
|
const webdavStatus = getWebdavStatus(webdavSignature)
|
|
const shouldAutoInit = webdavStatus !== 'failed'
|
|
|
|
const handleClickShowPassword = () => {
|
|
setShowPassword((prev) => !prev)
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (!webdav_url || !webdav_username || !webdav_password) {
|
|
return
|
|
}
|
|
if (!shouldAutoInit) {
|
|
return
|
|
}
|
|
void onInit()
|
|
}, [webdav_url, webdav_username, webdav_password, onInit, shouldAutoInit])
|
|
|
|
const checkForm = () => {
|
|
const username = usernameRef.current?.value
|
|
const password = passwordRef.current?.value
|
|
const url = urlRef.current?.value
|
|
|
|
if (!url) {
|
|
urlRef.current?.focus()
|
|
showNotice.error('settings.modals.backup.messages.webdavUrlRequired')
|
|
throw new Error(t('settings.modals.backup.messages.webdavUrlRequired'))
|
|
} else if (!isValidUrl(url)) {
|
|
urlRef.current?.focus()
|
|
showNotice.error('settings.modals.backup.messages.invalidWebdavUrl')
|
|
throw new Error(t('settings.modals.backup.messages.invalidWebdavUrl'))
|
|
}
|
|
if (!username) {
|
|
usernameRef.current?.focus()
|
|
showNotice.error('settings.modals.backup.messages.usernameRequired')
|
|
throw new Error(t('settings.modals.backup.messages.usernameRequired'))
|
|
}
|
|
if (!password) {
|
|
passwordRef.current?.focus()
|
|
showNotice.error('settings.modals.backup.messages.passwordRequired')
|
|
throw new Error(t('settings.modals.backup.messages.passwordRequired'))
|
|
}
|
|
}
|
|
|
|
const save = useLockFn(async (data: IWebDavConfig) => {
|
|
checkForm()
|
|
const signature = buildWebdavSignature({
|
|
webdav_url: data.url,
|
|
webdav_username: data.username,
|
|
webdav_password: data.password,
|
|
})
|
|
const trimmedUrl = data.url.trim()
|
|
const trimmedUsername = data.username.trim()
|
|
|
|
try {
|
|
setLoading(true)
|
|
await mutate(
|
|
() => saveWebdavConfig(trimmedUrl, trimmedUsername, data.password),
|
|
{
|
|
id: 'save-webdav-config',
|
|
errorNotice: false,
|
|
},
|
|
)
|
|
await mutateVerge(
|
|
(current) =>
|
|
current
|
|
? {
|
|
...current,
|
|
webdav_url: trimmedUrl,
|
|
webdav_username: trimmedUsername,
|
|
webdav_password: data.password,
|
|
}
|
|
: current,
|
|
false,
|
|
)
|
|
setWebdavStatus(signature, 'unknown')
|
|
showNotice.success('settings.modals.backup.messages.webdavConfigSaved')
|
|
await onSaveSuccess(signature)
|
|
} catch (error) {
|
|
showNotice.error(
|
|
'settings.modals.backup.messages.webdavConfigSaveFailed',
|
|
{ error: errorDetail(error) },
|
|
3000,
|
|
)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
})
|
|
|
|
const handleBackup = useLockFn(async () => {
|
|
checkForm()
|
|
const signature = buildWebdavSignature({
|
|
webdav_url: url,
|
|
webdav_username: username,
|
|
webdav_password: password,
|
|
})
|
|
|
|
try {
|
|
setLoading(true)
|
|
await mutate(() => createWebdavBackup(), {
|
|
id: 'create-webdav-backup',
|
|
errorNotice: false,
|
|
}).then(async () => {
|
|
showNotice.success('settings.modals.backup.messages.backupCreated')
|
|
await onBackupSuccess()
|
|
})
|
|
setWebdavStatus(signature, 'ready')
|
|
} catch (error) {
|
|
showNotice.error('settings.modals.backup.messages.backupFailed', {
|
|
error,
|
|
})
|
|
setWebdavStatus(signature, 'failed')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
})
|
|
|
|
return (
|
|
<form onSubmit={(e) => e.preventDefault()}>
|
|
<Grid container spacing={2}>
|
|
<Grid size={{ xs: 12, sm: 9 }}>
|
|
<Grid container spacing={2}>
|
|
<Grid size={{ xs: 12 }}>
|
|
<TextField
|
|
fullWidth
|
|
label={t('settings.modals.backup.fields.webdavUrl')}
|
|
variant="outlined"
|
|
size="small"
|
|
{...register('url')}
|
|
autoCorrect="off"
|
|
autoCapitalize="off"
|
|
spellCheck="false"
|
|
inputRef={urlRef}
|
|
sx={{ mt: 1 }}
|
|
/>
|
|
</Grid>
|
|
<Grid size={{ xs: 6 }}>
|
|
<TextField
|
|
label={t('settings.modals.backup.fields.username')}
|
|
variant="outlined"
|
|
size="small"
|
|
{...register('username')}
|
|
autoCorrect="off"
|
|
autoCapitalize="off"
|
|
spellCheck="false"
|
|
inputRef={usernameRef}
|
|
/>
|
|
</Grid>
|
|
<Grid size={{ xs: 6 }}>
|
|
<TextField
|
|
label={t('shared.labels.password')}
|
|
type={showPassword ? 'text' : 'password'}
|
|
variant="outlined"
|
|
size="small"
|
|
autoCorrect="off"
|
|
autoCapitalize="off"
|
|
spellCheck="false"
|
|
inputRef={passwordRef}
|
|
{...register('password')}
|
|
slotProps={{
|
|
input: {
|
|
endAdornment: (
|
|
<InputAdornment position="end">
|
|
<IconButton
|
|
onClick={handleClickShowPassword}
|
|
edge="end"
|
|
>
|
|
{showPassword ? <VisibilityOff /> : <Visibility />}
|
|
</IconButton>
|
|
</InputAdornment>
|
|
),
|
|
},
|
|
}}
|
|
/>
|
|
</Grid>
|
|
</Grid>
|
|
</Grid>
|
|
<Grid size={{ xs: 12, sm: 3 }}>
|
|
<Stack
|
|
direction="column"
|
|
sx={{
|
|
justifyContent: 'space-between',
|
|
alignItems: 'stretch',
|
|
height: '100%',
|
|
}}
|
|
>
|
|
{webdavChanged ||
|
|
webdav_url === undefined ||
|
|
webdav_username === undefined ||
|
|
webdav_password === undefined ? (
|
|
<Button
|
|
variant="contained"
|
|
color={'primary'}
|
|
sx={{ height: '100%' }}
|
|
type="button"
|
|
onClick={handleSubmit(save)}
|
|
>
|
|
{t('shared.actions.save')}
|
|
</Button>
|
|
) : (
|
|
<>
|
|
<Button
|
|
variant="contained"
|
|
color="success"
|
|
onClick={handleBackup}
|
|
type="button"
|
|
size="large"
|
|
>
|
|
{t('settings.modals.backup.actions.backup')}
|
|
</Button>
|
|
<Button
|
|
variant="outlined"
|
|
onClick={onRefresh}
|
|
type="button"
|
|
size="large"
|
|
>
|
|
{t('shared.actions.refresh')}
|
|
</Button>
|
|
</>
|
|
)}
|
|
</Stack>
|
|
</Grid>
|
|
</Grid>
|
|
</form>
|
|
)
|
|
},
|
|
)
|