feat: add PHP INI editor component, extension manager UI, and Nginx IPC service handlers

This commit is contained in:
Ümit Tunç
2026-03-30 16:51:16 +03:00
parent f4cff4ac68
commit 8d687a159f
3 changed files with 159 additions and 26 deletions
+5 -2
View File
@@ -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