feat: add PHP INI editor component, extension manager UI, and Nginx IPC service handlers
This commit is contained in:
@@ -389,9 +389,12 @@ export function registerIpcHandlers(): void {
|
|||||||
const phpDir = path.join(configService.getBasePath(), 'bin', `php-${version}`)
|
const phpDir = path.join(configService.getBasePath(), 'bin', `php-${version}`)
|
||||||
const iniPath = path.join(phpDir, 'php.ini')
|
const iniPath = path.join(phpDir, 'php.ini')
|
||||||
if (fs.existsSync(iniPath)) {
|
if (fs.existsSync(iniPath)) {
|
||||||
return fs.readFileSync(iniPath, 'utf8')
|
return {
|
||||||
|
content: fs.readFileSync(iniPath, 'utf8'),
|
||||||
|
path: iniPath
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return ''
|
return { content: '', path: iniPath }
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle('php:config:save', async (_event, { version, content }) => {
|
ipcMain.handle('php:config:save', async (_event, { version, content }) => {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import { Search as SearchIcon, Close as CloseIcon, Refresh as RefreshIcon, EditNote as RawIcon, Save as SaveIcon } from '@mui/icons-material'
|
import { Search as SearchIcon, Close as CloseIcon, Refresh as RefreshIcon, EditNote as RawIcon, Save as SaveIcon } from '@mui/icons-material'
|
||||||
import Swal from 'sweetalert2'
|
import Swal from 'sweetalert2'
|
||||||
|
import PhpIniCodeEditor from './PhpIniCodeEditor'
|
||||||
|
|
||||||
interface PhpExtension {
|
interface PhpExtension {
|
||||||
name: string
|
name: string
|
||||||
@@ -40,6 +41,8 @@ const PhpExtensionManager: React.FC<PhpExtensionManagerProps> = ({ open, onClose
|
|||||||
const [rawConfig, setRawConfig] = useState('')
|
const [rawConfig, setRawConfig] = useState('')
|
||||||
const [isRawEditorOpen, setIsRawEditorOpen] = useState(false)
|
const [isRawEditorOpen, setIsRawEditorOpen] = useState(false)
|
||||||
const [isSavingRaw, setIsSavingRaw] = useState(false)
|
const [isSavingRaw, setIsSavingRaw] = useState(false)
|
||||||
|
const [configPath, setConfigPath] = useState('')
|
||||||
|
const [rawSearchTerm, setRawSearchTerm] = useState('')
|
||||||
|
|
||||||
const fetchExtensions = async () => {
|
const fetchExtensions = async () => {
|
||||||
if (!phpVersion) return
|
if (!phpVersion) return
|
||||||
@@ -58,7 +61,8 @@ const PhpExtensionManager: React.FC<PhpExtensionManagerProps> = ({ open, onClose
|
|||||||
if (!phpVersion) return
|
if (!phpVersion) return
|
||||||
try {
|
try {
|
||||||
const data = await window.api.invoke('php:config:get', phpVersion)
|
const data = await window.api.invoke('php:config:get', phpVersion)
|
||||||
setRawConfig(data)
|
setRawConfig(data.content)
|
||||||
|
setConfigPath(data.path)
|
||||||
setIsRawEditorOpen(true)
|
setIsRawEditorOpen(true)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch raw config:', error)
|
console.error('Failed to fetch raw config:', error)
|
||||||
@@ -259,30 +263,53 @@ const PhpExtensionManager: React.FC<PhpExtensionManagerProps> = ({ open, onClose
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogTitle sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
<DialogTitle sx={{ display: 'flex', flexDirection: 'column', gap: 0.5, p: 2 }}>
|
||||||
<Typography variant="h6">php.ini Düzenle ({phpVersion})</Typography>
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
<IconButton onClick={() => setIsRawEditorOpen(false)} sx={{ color: 'rgba(255,255,255,0.5)' }}>
|
<Typography variant="h6">php.ini Düzenle ({phpVersion})</Typography>
|
||||||
<CloseIcon />
|
<IconButton onClick={() => setIsRawEditorOpen(false)} sx={{ color: 'rgba(255,255,255,0.5)' }}>
|
||||||
</IconButton>
|
<CloseIcon />
|
||||||
</DialogTitle>
|
</IconButton>
|
||||||
<DialogContent sx={{ p: 0 }}>
|
</Box>
|
||||||
<TextField
|
<Typography
|
||||||
multiline
|
variant="caption"
|
||||||
fullWidth
|
sx={{
|
||||||
rows={20}
|
color: 'primary.main',
|
||||||
value={rawConfig}
|
wordBreak: 'break-all',
|
||||||
onChange={(e) => setRawConfig(e.target.value)}
|
cursor: 'pointer',
|
||||||
variant="filled"
|
'&:hover': { textDecoration: 'underline', color: 'primary.light' }
|
||||||
InputProps={{
|
|
||||||
sx: {
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
fontSize: '0.85rem',
|
|
||||||
color: '#d4d4d4',
|
|
||||||
bgcolor: '#1e1e1e',
|
|
||||||
'& textarea': { p: 2 }
|
|
||||||
},
|
|
||||||
disableUnderline: true
|
|
||||||
}}
|
}}
|
||||||
|
onClick={() => window.api.invoke('shell:open-path', configPath)}
|
||||||
|
title="Dosyayı sistemde aç"
|
||||||
|
>
|
||||||
|
{configPath}
|
||||||
|
</Typography>
|
||||||
|
</DialogTitle>
|
||||||
|
<Box sx={{ px: 2, pb: 2, bgcolor: '#1a1a1a' }}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
size="small"
|
||||||
|
placeholder="Dosya içerisinde ara..."
|
||||||
|
value={rawSearchTerm}
|
||||||
|
onChange={(e) => setRawSearchTerm(e.target.value)}
|
||||||
|
InputProps={{
|
||||||
|
startAdornment: (
|
||||||
|
<InputAdornment position="start">
|
||||||
|
<SearchIcon sx={{ color: 'text.secondary', fontSize: '1.2rem' }} />
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
sx: {
|
||||||
|
color: '#fff',
|
||||||
|
bgcolor: 'rgba(255,255,255,0.05)',
|
||||||
|
'& fieldset': { borderColor: 'rgba(255,255,255,0.1)' }
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<DialogContent sx={{ p: 0, minHeight: 400, borderTop: '1px solid rgba(255,255,255,0.1)' }}>
|
||||||
|
<PhpIniCodeEditor
|
||||||
|
value={rawConfig}
|
||||||
|
onChange={setRawConfig}
|
||||||
|
searchTerm={rawSearchTerm}
|
||||||
/>
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions sx={{ p: 2, bgcolor: 'rgba(0,0,0,0.2)' }}>
|
<DialogActions sx={{ p: 2, bgcolor: 'rgba(0,0,0,0.2)' }}>
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import Editor from 'react-simple-code-editor'
|
||||||
|
import Prism from 'prismjs'
|
||||||
|
import 'prismjs/components/prism-ini'
|
||||||
|
import 'prismjs/themes/prism-tomorrow.css'
|
||||||
|
|
||||||
|
interface PhpIniCodeEditorProps {
|
||||||
|
value: string
|
||||||
|
onChange: (value: string) => void
|
||||||
|
searchTerm?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const PhpIniCodeEditor: React.FC<PhpIniCodeEditorProps> = ({ value, onChange, searchTerm }) => {
|
||||||
|
const editorRef = React.useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
const highlightWithSearch = (code: string) => {
|
||||||
|
// 1. Get Prism highlighted HTML
|
||||||
|
let highlighted = Prism.highlight(code, Prism.languages.ini, 'ini')
|
||||||
|
|
||||||
|
// 2. If search term exists, wrap it in <mark>
|
||||||
|
// Use a regex that avoids matching inside existing HTML tags
|
||||||
|
if (searchTerm && searchTerm.trim().length > 1) {
|
||||||
|
const escapedSearch = searchTerm.trim().replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||||
|
// This regex matches the search term only if it's not inside a tag (negative lookahead for > before <)
|
||||||
|
const regex = new RegExp(`(${escapedSearch})(?![^<]*>)`, 'gi')
|
||||||
|
highlighted = highlighted.replace(regex, '<mark class="search-match">$1</mark>')
|
||||||
|
}
|
||||||
|
|
||||||
|
return highlighted
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scroll to first match
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (searchTerm && searchTerm.trim().length > 1 && editorRef.current) {
|
||||||
|
const index = value.toLowerCase().indexOf(searchTerm.toLowerCase().trim())
|
||||||
|
if (index !== -1) {
|
||||||
|
const container = editorRef.current
|
||||||
|
const linesBefore = value.substring(0, index).split('\n').length
|
||||||
|
const lineHeight = 21 // Approx for 14px font
|
||||||
|
// Scroll the container, not the textarea
|
||||||
|
container.scrollTop = (linesBefore - 3) * lineHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [searchTerm])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={editorRef}
|
||||||
|
style={{
|
||||||
|
borderRadius: '4px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
border: '1px solid rgba(255,255,255,0.1)',
|
||||||
|
backgroundColor: '#1a1a1a',
|
||||||
|
fontFamily: '"Fira code", "Fira Mono", monospace',
|
||||||
|
fontSize: '14px',
|
||||||
|
height: '450px',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
scrollBehavior: 'smooth'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Editor
|
||||||
|
key={searchTerm} // Force re-render/re-highlight when searchTerm changes
|
||||||
|
value={value}
|
||||||
|
onValueChange={code => onChange(code)}
|
||||||
|
highlight={highlightWithSearch}
|
||||||
|
padding={15}
|
||||||
|
style={{
|
||||||
|
fontFamily: '"Fira code", "Fira Mono", monospace',
|
||||||
|
fontSize: 14,
|
||||||
|
minHeight: '100%',
|
||||||
|
outline: 'none'
|
||||||
|
}}
|
||||||
|
textareaClassName="ini-editor-textarea"
|
||||||
|
preClassName="ini-editor-pre"
|
||||||
|
/>
|
||||||
|
<style>{`
|
||||||
|
.ini-editor-textarea {
|
||||||
|
outline: none !important;
|
||||||
|
caret-color: #fff !important;
|
||||||
|
}
|
||||||
|
.ini-editor-pre {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.token.comment { color: #6a9955; }
|
||||||
|
.token.section { color: #569cd6; font-weight: bold; }
|
||||||
|
.token.attr-name { color: #9cdcfe; }
|
||||||
|
.token.attr-value { color: #ce9178; }
|
||||||
|
.token.punctuation { color: #d4d4d4; }
|
||||||
|
.token.important { color: #569cd6; }
|
||||||
|
.search-match {
|
||||||
|
background: #ffcf00 !important;
|
||||||
|
color: #000 !important;
|
||||||
|
border-radius: 2px;
|
||||||
|
box-shadow: 0 0 8px rgba(255, 207, 0, 0.6);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PhpIniCodeEditor
|
||||||
Reference in New Issue
Block a user