356 lines
15 KiB
TypeScript
356 lines
15 KiB
TypeScript
import React, { useState, useEffect } from 'react'
|
|
import {
|
|
Dialog,
|
|
DialogTitle,
|
|
DialogContent,
|
|
DialogActions,
|
|
Button,
|
|
List,
|
|
ListItem,
|
|
ListItemText,
|
|
Switch,
|
|
TextField,
|
|
InputAdornment,
|
|
CircularProgress,
|
|
Box,
|
|
Typography,
|
|
Chip,
|
|
IconButton
|
|
} from '@mui/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 { useTranslation } from 'react-i18next'
|
|
import PhpIniCodeEditor from './PhpIniCodeEditor'
|
|
|
|
interface PhpExtension {
|
|
name: string
|
|
dll: string
|
|
enabled: boolean
|
|
}
|
|
|
|
interface PhpExtensionManagerProps {
|
|
open: boolean
|
|
onClose: () => void
|
|
phpVersion: string
|
|
}
|
|
|
|
const PhpExtensionManager: React.FC<PhpExtensionManagerProps> = ({ open, onClose, phpVersion }) => {
|
|
const { t } = useTranslation()
|
|
const [extensions, setExtensions] = useState<PhpExtension[]>([])
|
|
const [loading, setLoading] = useState(false)
|
|
const [searchTerm, setSearchTerm] = useState('')
|
|
const [saving, setSaving] = useState<string | null>(null)
|
|
const [rawConfig, setRawConfig] = useState('')
|
|
const [isRawEditorOpen, setIsRawEditorOpen] = useState(false)
|
|
const [isSavingRaw, setIsSavingRaw] = useState(false)
|
|
const [configPath, setConfigPath] = useState('')
|
|
const [rawSearchTerm, setRawSearchTerm] = useState('')
|
|
|
|
const fetchExtensions = async () => {
|
|
if (!phpVersion) return
|
|
setLoading(true)
|
|
try {
|
|
const data = await window.api.invoke('php:extensions:get', phpVersion)
|
|
setExtensions(data || [])
|
|
} catch (error) {
|
|
console.error('Failed to fetch extensions:', error)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const fetchRawConfig = async () => {
|
|
if (!phpVersion) return
|
|
try {
|
|
const data = await window.api.invoke('php:config:get', phpVersion)
|
|
setRawConfig(data.content)
|
|
setConfigPath(data.path)
|
|
setIsRawEditorOpen(true)
|
|
} catch (error) {
|
|
console.error('Failed to fetch raw config:', error)
|
|
}
|
|
}
|
|
|
|
const handleSaveRaw = async () => {
|
|
setIsSavingRaw(true)
|
|
Swal.fire({
|
|
title: t('php.extensions.saving'),
|
|
text: t('php.extensions.restarting'),
|
|
allowOutsideClick: false,
|
|
didOpen: () => {
|
|
Swal.showLoading()
|
|
},
|
|
background: '#1e1e1e',
|
|
color: '#fff'
|
|
})
|
|
|
|
try {
|
|
const result = await window.api.invoke('php:config:save', { version: phpVersion, content: rawConfig })
|
|
if (result.success) {
|
|
// Trigger restart
|
|
await window.api.invoke('services:start', `php:${phpVersion}`)
|
|
|
|
Swal.fire({
|
|
title: t('common.success'),
|
|
text: t('php.extensions.success_msg'),
|
|
icon: 'success',
|
|
timer: 2000,
|
|
showConfirmButton: false,
|
|
background: '#1e1e1e',
|
|
color: '#fff'
|
|
})
|
|
setIsRawEditorOpen(false)
|
|
fetchExtensions()
|
|
}
|
|
} catch (error: any) {
|
|
Swal.fire({ title: 'Hata', text: error.message, icon: 'error', background: '#1e1e1e', color: '#fff' })
|
|
} finally {
|
|
setIsSavingRaw(false)
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (open && phpVersion) {
|
|
fetchExtensions()
|
|
}
|
|
}, [open, phpVersion])
|
|
|
|
const handleToggle = async (extName: string, enabled: boolean) => {
|
|
setSaving(extName)
|
|
|
|
// Show a non-blocking toast or a small overlay if we want, but for "Restart" let's be more explicit if it was running
|
|
const wasRunning = await window.api.invoke('service:status', `php:${phpVersion}`) === 'running'
|
|
|
|
if (wasRunning) {
|
|
Swal.fire({
|
|
title: t('php.extensions.adjusting'),
|
|
text: t('php.extensions.adjusting_desc', {
|
|
extName,
|
|
mode: enabled ? t('common.starting').toLowerCase() : t('common.stop').toLowerCase()
|
|
}),
|
|
allowOutsideClick: false,
|
|
didOpen: () => {
|
|
Swal.showLoading()
|
|
},
|
|
background: '#1e1e1e',
|
|
color: '#fff'
|
|
})
|
|
}
|
|
|
|
try {
|
|
const result = await window.api.invoke('php:extensions:toggle', { version: phpVersion, extName, enabled })
|
|
if (result.success) {
|
|
setExtensions(prev => prev.map(ext => ext.name === extName ? { ...ext, enabled } : ext))
|
|
|
|
if (wasRunning) {
|
|
await window.api.invoke('services:start', `php:${phpVersion}`)
|
|
Swal.close()
|
|
}
|
|
} else {
|
|
Swal.fire({ title: t('common.error'), text: result.message, icon: 'error', background: '#1e1e1e', color: '#fff' })
|
|
}
|
|
} catch (error: any) {
|
|
Swal.fire({ title: t('common.error'), text: error.message, icon: 'error', background: '#1e1e1e', color: '#fff' })
|
|
} finally {
|
|
setSaving(null)
|
|
}
|
|
}
|
|
|
|
const filteredExtensions = extensions.filter(ext =>
|
|
ext.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
ext.dll.toLowerCase().includes(searchTerm.toLowerCase())
|
|
)
|
|
|
|
return (
|
|
<>
|
|
<Dialog
|
|
open={open}
|
|
onClose={onClose}
|
|
maxWidth="sm"
|
|
fullWidth
|
|
PaperProps={{
|
|
sx: {
|
|
bgcolor: '#1e1e1e',
|
|
color: '#fff',
|
|
backgroundImage: 'none',
|
|
borderRadius: 2,
|
|
border: '1px solid rgba(255,255,255,0.1)'
|
|
}
|
|
}}
|
|
>
|
|
<DialogTitle sx={{ m: 0, p: 2, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
<Box>
|
|
<Typography variant="h6">{t('php.extensions.title', { version: phpVersion })}</Typography>
|
|
<Typography variant="caption" color="text.secondary">{t('php.extensions.desc')}</Typography>
|
|
</Box>
|
|
<IconButton onClick={onClose} sx={{ color: 'text.secondary' }}>
|
|
<CloseIcon />
|
|
</IconButton>
|
|
</DialogTitle>
|
|
<DialogContent dividers sx={{ borderColor: 'rgba(255,255,255,0.1)', p: 0 }}>
|
|
<Box sx={{ p: 2, bgcolor: 'rgba(255,255,255,0.03)', display: 'flex', gap: 1 }}>
|
|
<TextField
|
|
fullWidth
|
|
size="small"
|
|
placeholder={t('php.extensions.search_placeholder')}
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
InputProps={{
|
|
startAdornment: (
|
|
<InputAdornment position="start">
|
|
<SearchIcon sx={{ color: 'text.secondary' }} />
|
|
</InputAdornment>
|
|
),
|
|
sx: { color: '#fff', bgcolor: 'rgba(0,0,0,0.2)' }
|
|
}}
|
|
/>
|
|
<Button
|
|
variant="outlined"
|
|
startIcon={<RawIcon />}
|
|
onClick={fetchRawConfig}
|
|
sx={{ whiteSpace: 'nowrap', borderColor: 'rgba(255,255,255,0.2)', color: '#fff' }}
|
|
>
|
|
{t('php.extensions.raw_editor')}
|
|
</Button>
|
|
</Box>
|
|
|
|
{loading ? (
|
|
<Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}>
|
|
<CircularProgress />
|
|
</Box>
|
|
) : (
|
|
<List sx={{ maxHeight: 400, overflow: 'auto' }}>
|
|
{filteredExtensions.length === 0 ? (
|
|
<ListItem>
|
|
<ListItemText
|
|
primary={searchTerm ? t('php.extensions.not_found') : t('php.extensions.none')}
|
|
sx={{ textAlign: 'center', color: 'text.secondary' }}
|
|
/>
|
|
</ListItem>
|
|
) : (
|
|
filteredExtensions.map((ext) => (
|
|
<ListItem
|
|
key={ext.name}
|
|
divider
|
|
sx={{ borderColor: 'rgba(255,255,255,0.05)' }}
|
|
secondaryAction={
|
|
saving === ext.name ? (
|
|
<CircularProgress size={24} />
|
|
) : (
|
|
<Switch
|
|
edge="end"
|
|
checked={ext.enabled}
|
|
onChange={(e) => handleToggle(ext.name, e.target.checked)}
|
|
color="primary"
|
|
/>
|
|
)
|
|
}
|
|
>
|
|
<ListItemText
|
|
primary={
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
{ext.name}
|
|
{ext.enabled && <Chip label={t('php.extensions.active')} size="small" color="success" sx={{ height: 18, fontSize: '0.65rem' }} />}
|
|
</Box>
|
|
}
|
|
secondary={ext.dll}
|
|
secondaryTypographyProps={{ sx: { color: 'text.secondary', fontSize: '0.75rem' } }}
|
|
/>
|
|
</ListItem>
|
|
))
|
|
)}
|
|
</List>
|
|
)}
|
|
</DialogContent>
|
|
<DialogActions sx={{ p: 2, bgcolor: 'rgba(255,255,255,0.02)' }}>
|
|
<Button onClick={fetchExtensions} startIcon={<RefreshIcon />} sx={{ mr: 'auto' }}>
|
|
{t('common.refresh').toUpperCase()}
|
|
</Button>
|
|
<Button onClick={onClose} variant="outlined" color="primary">
|
|
{t('common.close').toUpperCase()}
|
|
</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
|
|
<Dialog
|
|
open={isRawEditorOpen}
|
|
onClose={() => setIsRawEditorOpen(false)}
|
|
maxWidth="md"
|
|
fullWidth
|
|
PaperProps={{
|
|
sx: {
|
|
bgcolor: '#1a1a1a',
|
|
color: '#fff',
|
|
borderRadius: 2,
|
|
}
|
|
}}
|
|
>
|
|
<DialogTitle sx={{ display: 'flex', flexDirection: 'column', gap: 0.5, p: 2 }}>
|
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
<Typography variant="h6">{t('php.extensions.ini_edit', { version: phpVersion })}</Typography>
|
|
<IconButton onClick={() => setIsRawEditorOpen(false)} sx={{ color: 'rgba(255,255,255,0.5)' }}>
|
|
<CloseIcon />
|
|
</IconButton>
|
|
</Box>
|
|
<Typography
|
|
variant="caption"
|
|
sx={{
|
|
color: 'primary.main',
|
|
wordBreak: 'break-all',
|
|
cursor: 'pointer',
|
|
'&:hover': { textDecoration: 'underline', color: 'primary.light' }
|
|
}}
|
|
onClick={() => window.api.invoke('shell:open-path', configPath)}
|
|
title={t('php.extensions.file_open_tip')}
|
|
>
|
|
{configPath}
|
|
</Typography>
|
|
</DialogTitle>
|
|
<Box sx={{ px: 2, pb: 2, bgcolor: '#1a1a1a' }}>
|
|
<TextField
|
|
fullWidth
|
|
size="small"
|
|
placeholder={t('php.extensions.search_file_placeholder')}
|
|
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}
|
|
/>
|
|
</DialogContent>
|
|
<DialogActions sx={{ p: 2, bgcolor: 'rgba(0,0,0,0.2)' }}>
|
|
<Button onClick={() => setIsRawEditorOpen(false)} sx={{ color: 'rgba(255,255,255,0.5)' }}>
|
|
{t('common.cancel').toUpperCase()}
|
|
</Button>
|
|
<Button
|
|
onClick={handleSaveRaw}
|
|
variant="outlined"
|
|
color="primary"
|
|
startIcon={isSavingRaw ? <CircularProgress size={18} color="inherit" /> : <SaveIcon />}
|
|
disabled={isSavingRaw}
|
|
>
|
|
{isSavingRaw ? t('php.extensions.saving_btn') : t('php.extensions.save_restart_btn')}
|
|
</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
</>
|
|
)
|
|
}
|
|
|
|
export default PhpExtensionManager
|