mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-15 07:12:09 +08:00
296 lines
8.9 KiB
TypeScript
296 lines
8.9 KiB
TypeScript
import { Delete as DeleteIcon } from '@mui/icons-material'
|
|
import { Box, Button, Divider, List, ListItem, TextField } from '@mui/material'
|
|
import { useLockFn, useRequest } from 'ahooks'
|
|
import { forwardRef, useRef, useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
|
|
import { BaseDialog, Switch } from '@/components/base'
|
|
import { useClash } from '@/hooks/use-clash'
|
|
import { restartCore } from '@/services/cmds'
|
|
import { mutate } from '@/services/mutate'
|
|
import { showNotice } from '@/services/notice-service'
|
|
|
|
import { useSettingDialog } from '../settings-search-context'
|
|
|
|
// Development origins must never be persisted into production configuration.
|
|
const DEV_URLS = [
|
|
'tauri://localhost',
|
|
'http://tauri.localhost',
|
|
'http://localhost:3000',
|
|
]
|
|
|
|
const getFullOrigins = (origins: string[]) => {
|
|
const allOrigins = [...origins, ...DEV_URLS]
|
|
const uniqueOrigins = [...new Set(allOrigins)]
|
|
return uniqueOrigins
|
|
}
|
|
|
|
const filterBaseOriginsForUI = (origins: string[]) => {
|
|
return origins.filter((origin: string) => !DEV_URLS.includes(origin.trim()))
|
|
}
|
|
|
|
const buttonStyle = {
|
|
borderRadius: '8px',
|
|
textTransform: 'none',
|
|
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
|
|
transition: 'all 0.3s ease',
|
|
'&:hover': {
|
|
boxShadow: '0 4px 8px rgba(0,0,0,0.15)',
|
|
transform: 'translateY(-1px)',
|
|
},
|
|
'&:active': {
|
|
transform: 'translateY(0)',
|
|
},
|
|
}
|
|
|
|
const addButtonStyle = {
|
|
...buttonStyle,
|
|
backgroundColor: '#4CAF50',
|
|
color: 'white',
|
|
'&:hover': {
|
|
backgroundColor: '#388E3C',
|
|
},
|
|
}
|
|
|
|
const deleteButtonStyle = {
|
|
...buttonStyle,
|
|
backgroundColor: '#FF5252',
|
|
color: 'white',
|
|
'&:hover': {
|
|
backgroundColor: '#D32F2F',
|
|
},
|
|
}
|
|
|
|
interface ClashHeaderConfigingRef {
|
|
open: () => void
|
|
close: () => void
|
|
}
|
|
|
|
interface AllowOriginItem {
|
|
key: number
|
|
value: string
|
|
}
|
|
|
|
export const HeaderConfiguration = forwardRef<ClashHeaderConfigingRef>(
|
|
(props, ref) => {
|
|
const { t } = useTranslation()
|
|
const { clash, mutateClash, patchClash } = useClash()
|
|
const [open, setOpen] = useState(false)
|
|
|
|
const lastKeyRef = useRef(0) // 用于生成唯一的key
|
|
|
|
const [corsConfig, setCorsConfig] = useState<{
|
|
allowPrivateNetwork: boolean
|
|
allowOrigins: AllowOriginItem[]
|
|
}>(() => {
|
|
const cors = clash?.['external-controller-cors']
|
|
const origins = cors?.['allow-origins'] ?? []
|
|
return {
|
|
allowPrivateNetwork: cors?.['allow-private-network'] ?? true,
|
|
allowOrigins: filterBaseOriginsForUI(origins).map((origin) => {
|
|
lastKeyRef.current += 1
|
|
return { key: lastKeyRef.current, value: origin }
|
|
}),
|
|
}
|
|
})
|
|
|
|
const handleCorsConfigChange = (
|
|
key: 'allowPrivateNetwork' | 'allowOrigins',
|
|
value: boolean | AllowOriginItem[],
|
|
) => {
|
|
setCorsConfig((prev) => ({
|
|
...prev,
|
|
[key]: value,
|
|
}))
|
|
}
|
|
|
|
const handleAddOrigin = () => {
|
|
lastKeyRef.current += 1
|
|
handleCorsConfigChange('allowOrigins', [
|
|
...corsConfig.allowOrigins,
|
|
{ key: lastKeyRef.current, value: '' },
|
|
])
|
|
}
|
|
|
|
const handleUpdateOrigin = (index: number, value: string) => {
|
|
const newOrigins = [...corsConfig.allowOrigins]
|
|
newOrigins[index] = { ...newOrigins[index], value }
|
|
handleCorsConfigChange('allowOrigins', newOrigins)
|
|
}
|
|
|
|
const handleDeleteOrigin = (index: number) => {
|
|
const newOrigins = [...corsConfig.allowOrigins]
|
|
newOrigins.splice(index, 1)
|
|
handleCorsConfigChange('allowOrigins', newOrigins)
|
|
}
|
|
|
|
const { loading, run: saveConfig } = useRequest(
|
|
async () => {
|
|
const fullOrigins = getFullOrigins(
|
|
corsConfig.allowOrigins.map((origin) => origin.value),
|
|
)
|
|
|
|
await patchClash({
|
|
'external-controller-cors': {
|
|
'allow-private-network': corsConfig.allowPrivateNetwork,
|
|
'allow-origins': fullOrigins.filter(
|
|
(origin: string) => origin.trim() !== '',
|
|
),
|
|
},
|
|
})
|
|
await mutate(() => restartCore(), {
|
|
id: 'restart-core',
|
|
errorNotice: false,
|
|
})
|
|
await mutateClash()
|
|
},
|
|
{
|
|
manual: true,
|
|
onSuccess: () => {
|
|
setOpen(false)
|
|
showNotice.success('shared.feedback.notifications.common.saveSuccess')
|
|
},
|
|
onError: () => {
|
|
showNotice.error('shared.feedback.notifications.common.saveFailed')
|
|
},
|
|
},
|
|
)
|
|
|
|
useSettingDialog(ref, 'external-controller-cors', open, () => ({
|
|
open: () => {
|
|
const cors = clash?.['external-controller-cors']
|
|
const origins = cors?.['allow-origins'] ?? []
|
|
lastKeyRef.current = 0
|
|
setCorsConfig({
|
|
allowPrivateNetwork: cors?.['allow-private-network'] ?? true,
|
|
allowOrigins: filterBaseOriginsForUI(origins).map((origin) => {
|
|
lastKeyRef.current += 1
|
|
return { key: lastKeyRef.current, value: origin }
|
|
}),
|
|
})
|
|
setOpen(true)
|
|
},
|
|
close: () => setOpen(false),
|
|
}))
|
|
|
|
const handleSave = useLockFn(async () => {
|
|
await saveConfig()
|
|
})
|
|
|
|
return (
|
|
<BaseDialog
|
|
open={open}
|
|
title={t('settings.sections.externalCors.title')}
|
|
contentSx={{ width: 500 }}
|
|
okBtn={loading ? t('shared.statuses.saving') : t('shared.actions.save')}
|
|
cancelBtn={t('shared.actions.cancel')}
|
|
onClose={() => setOpen(false)}
|
|
onCancel={() => setOpen(false)}
|
|
onOk={handleSave}
|
|
>
|
|
<span data-settings-dialog="external-controller-cors" />
|
|
<List sx={{ width: '90%', padding: 2 }}>
|
|
<ListItem
|
|
data-setting-target="settings.sections.externalCors.fields.allowPrivateNetwork"
|
|
sx={{ padding: '8px 0' }}
|
|
>
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
width: '100%',
|
|
}}
|
|
>
|
|
<span style={{ fontWeight: 'normal' }}>
|
|
{t('settings.sections.externalCors.fields.allowPrivateNetwork')}
|
|
</span>
|
|
<Switch
|
|
edge="end"
|
|
checked={corsConfig.allowPrivateNetwork}
|
|
onChange={(e) =>
|
|
handleCorsConfigChange(
|
|
'allowPrivateNetwork',
|
|
e.target.checked,
|
|
)
|
|
}
|
|
/>
|
|
</Box>
|
|
</ListItem>
|
|
|
|
<Divider sx={{ my: 2 }} />
|
|
|
|
<ListItem
|
|
data-setting-target="settings.sections.externalCors.fields.allowedOrigins"
|
|
sx={{ padding: '8px 0' }}
|
|
>
|
|
<div style={{ width: '100%' }}>
|
|
<div style={{ marginBottom: 8, fontWeight: 'bold' }}>
|
|
{t('settings.sections.externalCors.fields.allowedOrigins')}
|
|
</div>
|
|
{corsConfig.allowOrigins.map(({ key, value: origin }, index) => (
|
|
<div
|
|
key={key}
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
marginBottom: 8,
|
|
}}
|
|
>
|
|
<TextField
|
|
fullWidth
|
|
size="small"
|
|
sx={{ fontSize: 14, marginRight: 2 }}
|
|
value={origin}
|
|
onChange={(e) => handleUpdateOrigin(index, e.target.value)}
|
|
placeholder={t(
|
|
'settings.sections.externalCors.placeholders.origin',
|
|
)}
|
|
slotProps={{ htmlInput: { style: { fontSize: 14 } } }}
|
|
/>
|
|
<Button
|
|
variant="contained"
|
|
color="error"
|
|
size="small"
|
|
onClick={() => handleDeleteOrigin(index)}
|
|
disabled={corsConfig.allowOrigins.length <= 0}
|
|
sx={deleteButtonStyle}
|
|
>
|
|
<DeleteIcon fontSize="small" />
|
|
</Button>
|
|
</div>
|
|
))}
|
|
<Button
|
|
data-setting-target="settings.sections.externalCors.actions.add"
|
|
variant="contained"
|
|
size="small"
|
|
onClick={handleAddOrigin}
|
|
sx={addButtonStyle}
|
|
>
|
|
{t('settings.sections.externalCors.actions.add')}
|
|
</Button>
|
|
|
|
<div
|
|
style={{
|
|
marginTop: 12,
|
|
padding: 8,
|
|
backgroundColor: '#f5f5f5',
|
|
borderRadius: 4,
|
|
}}
|
|
>
|
|
<div
|
|
style={{ color: '#666', fontSize: 12, fontStyle: 'italic' }}
|
|
>
|
|
{t('settings.sections.externalCors.messages.alwaysIncluded', {
|
|
urls: DEV_URLS.join(', '),
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</ListItem>
|
|
</List>
|
|
</BaseDialog>
|
|
)
|
|
},
|
|
)
|