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, useImperativeHandle, 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' // 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( (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') }, }, ) useImperativeHandle(ref, () => ({ 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 ( setOpen(false)} onCancel={() => setOpen(false)} onOk={handleSave} > {t('settings.sections.externalCors.fields.allowPrivateNetwork')} handleCorsConfigChange( 'allowPrivateNetwork', e.target.checked, ) } />
{t('settings.sections.externalCors.fields.allowedOrigins')}
{corsConfig.allowOrigins.map(({ key, value: origin }, index) => (
handleUpdateOrigin(index, e.target.value)} placeholder={t( 'settings.sections.externalCors.placeholders.origin', )} slotProps={{ htmlInput: { style: { fontSize: 14 } } }} />
))}
{t('settings.sections.externalCors.messages.alwaysIncluded', { urls: DEV_URLS.join(', '), })}
) }, )