Files
clash-verge-rev/src/components/setting/mods/backup-config-viewer.tsx
T
Tunglies 6e08a77a84 refactor(backend): specta ipc bindings (#8195)
* 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
2026-10-12 06:16:05 +00:00

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>
)
},
)