feat: implement core UI dashboard and backend service management for multi-PHP environment
This commit is contained in:
@@ -2,6 +2,8 @@ import { ipcMain, app } from 'electron'
|
|||||||
import { processManager } from '../services/ProcessManager'
|
import { processManager } from '../services/ProcessManager'
|
||||||
import { configService } from '../services/ConfigService'
|
import { configService } from '../services/ConfigService'
|
||||||
import { downloadService } from '../services/DownloadService'
|
import { downloadService } from '../services/DownloadService'
|
||||||
|
import { projectService } from '../services/ProjectService'
|
||||||
|
import { phpManagerService } from '../services/PhpManagerService'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
|
||||||
export function registerIpcHandlers(): void {
|
export function registerIpcHandlers(): void {
|
||||||
@@ -54,6 +56,33 @@ export function registerIpcHandlers(): void {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Binary management
|
// Binary management
|
||||||
|
ipcMain.handle('binaries:list-php', async () => {
|
||||||
|
return phpManagerService.getVersions()
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('binaries:download-php', async (event, id: string) => {
|
||||||
|
const versions = await phpManagerService.getVersions()
|
||||||
|
const version = versions.find(v => v.id === id)
|
||||||
|
if (!version) return { success: false, message: 'Geçersiz sürüm.' }
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Signal immediate start
|
||||||
|
phpManagerService.updateProgress(id, 0, 'downloading')
|
||||||
|
event.sender.send('binaries:progress', { id, progress: 0 })
|
||||||
|
|
||||||
|
await downloadService.downloadAndExtract(version.url, id, (p) => {
|
||||||
|
phpManagerService.updateProgress(id, p)
|
||||||
|
event.sender.send('binaries:progress', { id, progress: p })
|
||||||
|
})
|
||||||
|
|
||||||
|
phpManagerService.updateProgress(id, 100, 'installed')
|
||||||
|
return { success: true, message: 'İndirme tamamlandı.' }
|
||||||
|
} catch (error: any) {
|
||||||
|
phpManagerService.updateProgress(id, 0, 'available')
|
||||||
|
return { success: false, message: `Hata: ${error.message}` }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
ipcMain.handle('binaries:download', async (_event, { name, url }) => {
|
ipcMain.handle('binaries:download', async (_event, { name, url }) => {
|
||||||
return downloadService.downloadAndExtract(url, name, (p) => {
|
return downloadService.downloadAndExtract(url, name, (p) => {
|
||||||
// TODO: Emit progress to renderer via webContents.send
|
// TODO: Emit progress to renderer via webContents.send
|
||||||
@@ -61,6 +90,19 @@ export function registerIpcHandlers(): void {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Project management
|
||||||
|
ipcMain.handle('projects:list', async () => {
|
||||||
|
return projectService.getProjects()
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('projects:add', async (_event, project) => {
|
||||||
|
return projectService.addProject(project)
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('projects:remove', async (_event, id) => {
|
||||||
|
return projectService.removeProject(id)
|
||||||
|
})
|
||||||
|
|
||||||
// Config management
|
// Config management
|
||||||
ipcMain.handle('config:get', async (_event, key: string) => {
|
ipcMain.handle('config:get', async (_event, key: string) => {
|
||||||
return null
|
return null
|
||||||
|
|||||||
@@ -2,13 +2,14 @@ import axios from 'axios'
|
|||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { app } from 'electron'
|
import { app } from 'electron'
|
||||||
import * as decompress from 'decompress'
|
const decompress = require('decompress')
|
||||||
|
|
||||||
export class DownloadService {
|
export class DownloadService {
|
||||||
private binDir: string
|
private binDir: string
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.binDir = path.join(app.getAppPath(), 'bin')
|
// Use userData for binaries to avoid permission issues in Program Files
|
||||||
|
this.binDir = path.join(app.getPath('userData'), 'bin')
|
||||||
if (!fs.existsSync(this.binDir)) {
|
if (!fs.existsSync(this.binDir)) {
|
||||||
fs.mkdirSync(this.binDir, { recursive: true })
|
fs.mkdirSync(this.binDir, { recursive: true })
|
||||||
}
|
}
|
||||||
@@ -17,24 +18,36 @@ export class DownloadService {
|
|||||||
async downloadAndExtract(url: string, targetName: string, onProgress?: (p: number) => void): Promise<string> {
|
async downloadAndExtract(url: string, targetName: string, onProgress?: (p: number) => void): Promise<string> {
|
||||||
const tempFile = path.join(app.getPath('temp'), `${targetName}.zip`)
|
const tempFile = path.join(app.getPath('temp'), `${targetName}.zip`)
|
||||||
const targetDir = path.join(this.binDir, targetName)
|
const targetDir = path.join(this.binDir, targetName)
|
||||||
|
const phpExe = path.join(targetDir, 'php.exe')
|
||||||
|
|
||||||
if (fs.existsSync(targetDir)) {
|
// If already installed and working, skip
|
||||||
|
if (fs.existsSync(phpExe)) {
|
||||||
return targetDir
|
return targetDir
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cleanup failed attempts
|
||||||
|
if (fs.existsSync(targetDir)) {
|
||||||
|
fs.rmSync(targetDir, { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
|
||||||
const writer = fs.createWriteStream(tempFile)
|
const writer = fs.createWriteStream(tempFile)
|
||||||
|
|
||||||
|
try {
|
||||||
const response = await axios({
|
const response = await axios({
|
||||||
url,
|
url,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
responseType: 'stream'
|
responseType: 'stream',
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const totalLength = response.headers['content-length']
|
const totalLength = parseInt(response.headers['content-length'] || '0')
|
||||||
let downloadedLength = 0
|
let downloadedLength = 0
|
||||||
|
|
||||||
response.data.on('data', (chunk: any) => {
|
response.data.on('data', (chunk: any) => {
|
||||||
downloadedLength += chunk.length
|
downloadedLength += chunk.length
|
||||||
if (onProgress && totalLength) {
|
if (onProgress && totalLength > 0) {
|
||||||
onProgress(Math.round((downloadedLength / totalLength) * 100))
|
onProgress(Math.round((downloadedLength / totalLength) * 100))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -45,7 +58,7 @@ export class DownloadService {
|
|||||||
writer.on('finish', async () => {
|
writer.on('finish', async () => {
|
||||||
try {
|
try {
|
||||||
await decompress(tempFile, targetDir)
|
await decompress(tempFile, targetDir)
|
||||||
fs.unlinkSync(tempFile)
|
if (fs.existsSync(tempFile)) fs.unlinkSync(tempFile)
|
||||||
resolve(targetDir)
|
resolve(targetDir)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
reject(err)
|
reject(err)
|
||||||
@@ -53,6 +66,10 @@ export class DownloadService {
|
|||||||
})
|
})
|
||||||
writer.on('error', reject)
|
writer.on('error', reject)
|
||||||
})
|
})
|
||||||
|
} catch (error) {
|
||||||
|
if (fs.existsSync(tempFile)) fs.unlinkSync(tempFile)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getBinPath(service: string, executable: string): string {
|
getBinPath(service: string, executable: string): string {
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
import { app } from 'electron'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
export interface PhpVersion {
|
||||||
|
id: string
|
||||||
|
version: string
|
||||||
|
url: string
|
||||||
|
status: 'available' | 'downloading' | 'installed'
|
||||||
|
progress: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PhpManagerService {
|
||||||
|
private versions: PhpVersion[] = []
|
||||||
|
private binDir: string
|
||||||
|
private winPhpUrl = 'https://windows.php.net/downloads/releases/'
|
||||||
|
private winReleasesJsonUrl = 'https://windows.php.net/downloads/releases/releases.json'
|
||||||
|
private mainReleasesJsonUrl = 'https://www.php.net/releases/index.php?json'
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.binDir = path.join(app.getPath('userData'), 'bin')
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
private async init() {
|
||||||
|
if (!fs.existsSync(this.binDir)) {
|
||||||
|
fs.mkdirSync(this.binDir, { recursive: true })
|
||||||
|
}
|
||||||
|
await this.fetchAvailableVersions()
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fetchAvailableVersions() {
|
||||||
|
try {
|
||||||
|
// Fetch from windows.php.net for actual paths
|
||||||
|
const winResponse = await axios.get(this.winReleasesJsonUrl, {
|
||||||
|
headers: { 'User-Agent': 'Mozilla/5.0' }
|
||||||
|
})
|
||||||
|
const winData = winResponse.data
|
||||||
|
|
||||||
|
// Fetch from php.net for supported versions list
|
||||||
|
const mainResponse = await axios.get(this.mainReleasesJsonUrl, {
|
||||||
|
headers: { 'User-Agent': 'Mozilla/5.0' }
|
||||||
|
})
|
||||||
|
const mainData = mainResponse.data
|
||||||
|
|
||||||
|
const dynamicVersions: PhpVersion[] = []
|
||||||
|
|
||||||
|
// Method 1: Versions explicitly available in windows releases.json
|
||||||
|
for (const majorMinor in winData) {
|
||||||
|
const release = winData[majorMinor]
|
||||||
|
const version = release.version
|
||||||
|
const tsBuild = release['ts-vs16-x64'] || release['ts-vs17-x64'] || release['ts-vc15-x64']
|
||||||
|
|
||||||
|
if (tsBuild && tsBuild.zip) {
|
||||||
|
dynamicVersions.push({
|
||||||
|
id: `php-${version}`,
|
||||||
|
version: version,
|
||||||
|
url: this.winPhpUrl + tsBuild.zip.path,
|
||||||
|
status: 'available',
|
||||||
|
progress: 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method 2: Check supported_versions from main php.net
|
||||||
|
// This helps if windows releases.json doesn't group some newer versions yet
|
||||||
|
if (mainData['8'] && mainData['8'].supported_versions) {
|
||||||
|
for (const ver of mainData['8'].supported_versions) {
|
||||||
|
// ver is "8.2", "8.3" etc.
|
||||||
|
// If we already have a version for this branch, we are good.
|
||||||
|
// Otherwise, we could try to guess or find more.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// De-duplicate by version string
|
||||||
|
const uniqueVersionsMap = new Map<string, PhpVersion>()
|
||||||
|
dynamicVersions.forEach(v => uniqueVersionsMap.set(v.version, v))
|
||||||
|
|
||||||
|
this.versions = Array.from(uniqueVersionsMap.values()).sort((a, b) => b.version.localeCompare(a.version))
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to fetch PHP versions:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private refreshInstalledStatus() {
|
||||||
|
this.versions = this.versions.map(v => {
|
||||||
|
const targetDir = path.join(this.binDir, v.id)
|
||||||
|
const phpExe = path.join(targetDir, 'php.exe')
|
||||||
|
if (fs.existsSync(phpExe)) {
|
||||||
|
return { ...v, status: 'installed', progress: 100 }
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async getVersions(): Promise<PhpVersion[]> {
|
||||||
|
if (this.versions.length === 0) {
|
||||||
|
await this.fetchAvailableVersions()
|
||||||
|
}
|
||||||
|
this.refreshInstalledStatus()
|
||||||
|
return this.versions
|
||||||
|
}
|
||||||
|
|
||||||
|
updateProgress(id: string, progress: number, status: PhpVersion['status'] = 'downloading') {
|
||||||
|
const version = this.versions.find(v => v.id === id)
|
||||||
|
if (version) {
|
||||||
|
version.progress = progress
|
||||||
|
version.status = status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const phpManagerService = new PhpManagerService()
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
import { app } from 'electron'
|
||||||
|
|
||||||
|
export interface Project {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
path: string
|
||||||
|
phpVersion: string
|
||||||
|
host: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ProjectService {
|
||||||
|
private projectsFile: string
|
||||||
|
private projects: Project[] = []
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.projectsFile = path.join(app.getPath('userData'), 'projects.json')
|
||||||
|
this.loadProjects()
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadProjects() {
|
||||||
|
if (fs.existsSync(this.projectsFile)) {
|
||||||
|
try {
|
||||||
|
const data = fs.readFileSync(this.projectsFile, 'utf-8')
|
||||||
|
this.projects = JSON.parse(data)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load projects:', e)
|
||||||
|
this.projects = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private saveProjects() {
|
||||||
|
fs.writeFileSync(this.projectsFile, JSON.stringify(this.projects, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
getProjects(): Project[] {
|
||||||
|
return this.projects
|
||||||
|
}
|
||||||
|
|
||||||
|
addProject(project: Omit<Project, 'id'>): Project {
|
||||||
|
const newProject = {
|
||||||
|
...project,
|
||||||
|
id: Date.now().toString()
|
||||||
|
}
|
||||||
|
this.projects.push(newProject)
|
||||||
|
this.saveProjects()
|
||||||
|
return newProject
|
||||||
|
}
|
||||||
|
|
||||||
|
removeProject(id: string) {
|
||||||
|
this.projects = this.projects.filter(p => p.id !== id)
|
||||||
|
this.saveProjects()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const projectService = new ProjectService()
|
||||||
@@ -7,7 +7,13 @@ const api = {
|
|||||||
stopService: (name: string) => electronAPI.ipcRenderer.invoke('services:stop', name),
|
stopService: (name: string) => electronAPI.ipcRenderer.invoke('services:stop', name),
|
||||||
getServiceStatus: () => electronAPI.ipcRenderer.invoke('services:status'),
|
getServiceStatus: () => electronAPI.ipcRenderer.invoke('services:status'),
|
||||||
getConfig: (key: string) => electronAPI.ipcRenderer.invoke('config:get', key),
|
getConfig: (key: string) => electronAPI.ipcRenderer.invoke('config:get', key),
|
||||||
saveConfig: (config: any) => electronAPI.ipcRenderer.invoke('config:save', config)
|
saveConfig: (config: any) => electronAPI.ipcRenderer.invoke('config:save', config),
|
||||||
|
getProjects: () => electronAPI.ipcRenderer.invoke('projects:list'),
|
||||||
|
addProject: (project: any) => electronAPI.ipcRenderer.invoke('projects:add', project),
|
||||||
|
removeProject: (id: string) => electronAPI.ipcRenderer.invoke('projects:remove', id),
|
||||||
|
getPhpVersions: () => electronAPI.ipcRenderer.invoke('binaries:list-php'),
|
||||||
|
downloadPhpVersion: (id: string) => electronAPI.ipcRenderer.invoke('binaries:download-php', id),
|
||||||
|
onBinaryProgress: (callback: any) => electronAPI.ipcRenderer.on('binaries:progress', (_event, data) => callback(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use `contextBridge` APIs to expose Electron APIs to
|
// Use `contextBridge` APIs to expose Electron APIs to
|
||||||
|
|||||||
+202
-8
@@ -19,7 +19,17 @@ import {
|
|||||||
Divider,
|
Divider,
|
||||||
Button,
|
Button,
|
||||||
Snackbar,
|
Snackbar,
|
||||||
Alert
|
Alert,
|
||||||
|
Dialog,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
DialogActions,
|
||||||
|
TextField,
|
||||||
|
Select,
|
||||||
|
MenuItem,
|
||||||
|
FormControl,
|
||||||
|
InputLabel,
|
||||||
|
LinearProgress
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import {
|
import {
|
||||||
Dashboard as DashboardIcon,
|
Dashboard as DashboardIcon,
|
||||||
@@ -31,9 +41,13 @@ import {
|
|||||||
Code as PhpIcon,
|
Code as PhpIcon,
|
||||||
PlayArrow as StartIcon,
|
PlayArrow as StartIcon,
|
||||||
Stop as StopIcon,
|
Stop as StopIcon,
|
||||||
Refresh as RestartIcon
|
Delete as DeleteIcon,
|
||||||
|
Download as DownloadIcon,
|
||||||
|
CheckCircle as InstalledIcon,
|
||||||
|
CloudDownload as AvailableIcon
|
||||||
} from '@mui/icons-material'
|
} from '@mui/icons-material'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
api: any
|
api: any
|
||||||
@@ -56,21 +70,58 @@ function App(): JSX.Element {
|
|||||||
mysql: 'stopped'
|
mysql: 'stopped'
|
||||||
})
|
})
|
||||||
const [activeTab, setActiveTab] = useState('dashboard')
|
const [activeTab, setActiveTab] = useState('dashboard')
|
||||||
|
const [projects, setProjects] = useState<any[]>([])
|
||||||
|
const [phpVersions, setPhpVersions] = useState<any[]>([])
|
||||||
|
const [isProjectDialogOpen, setIsProjectDialogOpen] = useState(false)
|
||||||
|
const [newProject, setNewProject] = useState({ name: '', path: '', phpVersion: '8.2', host: 'localhost' })
|
||||||
const [notification, setNotification] = useState<{ open: boolean, message: string, severity: 'success' | 'error' }>({
|
const [notification, setNotification] = useState<{ open: boolean, message: string, severity: 'success' | 'error' }>({
|
||||||
open: false,
|
open: false,
|
||||||
message: '',
|
message: '',
|
||||||
severity: 'success'
|
severity: 'success'
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchStatus = async () => {
|
const fetchStatus = async () => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const currentStatus = await window.api.getServiceStatus()
|
const currentStatus = await window.api.getServiceStatus()
|
||||||
if (currentStatus) setStatus(currentStatus)
|
if (currentStatus) setStatus(currentStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fetchProjects = async () => {
|
||||||
|
// @ts-ignore
|
||||||
|
const list = await window.api.getProjects()
|
||||||
|
setProjects(list || [])
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchPhpVersions = async () => {
|
||||||
|
// @ts-ignore
|
||||||
|
const list = await window.api.getPhpVersions()
|
||||||
|
setPhpVersions(list || [])
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!window.api) return
|
||||||
|
|
||||||
fetchStatus()
|
fetchStatus()
|
||||||
const interval = setInterval(fetchStatus, 3000)
|
fetchProjects()
|
||||||
return () => clearInterval(interval)
|
fetchPhpVersions()
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const removeListener = window.api.onBinaryProgress((data) => {
|
||||||
|
if (!data) return
|
||||||
|
const { id, progress } = data
|
||||||
|
setPhpVersions(prev => prev.map(v => v.id === id ? { ...v, progress, status: progress === 100 ? 'installed' : 'downloading' } : v))
|
||||||
|
})
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
fetchStatus()
|
||||||
|
fetchProjects()
|
||||||
|
fetchPhpVersions()
|
||||||
|
}, 3000)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(interval)
|
||||||
|
if (typeof removeListener === 'function') removeListener()
|
||||||
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleToggleService = async (service: keyof ServiceStatus) => {
|
const handleToggleService = async (service: keyof ServiceStatus) => {
|
||||||
@@ -118,6 +169,33 @@ function App(): JSX.Element {
|
|||||||
return <Chip label={s.toUpperCase()} color={color} size="small" sx={{ fontWeight: 'bold' }} />
|
return <Chip label={s.toUpperCase()} color={color} size="small" sx={{ fontWeight: 'bold' }} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleAddProject = async () => {
|
||||||
|
// @ts-ignore
|
||||||
|
await window.api.addProject(newProject)
|
||||||
|
setIsProjectDialogOpen(false)
|
||||||
|
setNewProject({ name: '', path: '', phpVersion: '8.2', host: 'localhost' })
|
||||||
|
setNotification({ open: true, message: 'Proje başarıyla eklendi.', severity: 'success' })
|
||||||
|
fetchProjects()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRemoveProject = async (id: string) => {
|
||||||
|
// @ts-ignore
|
||||||
|
await window.api.removeProject(id)
|
||||||
|
setNotification({ open: true, message: 'Proje silindi.', severity: 'success' })
|
||||||
|
fetchProjects()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDownloadPhp = async (id: string) => {
|
||||||
|
// @ts-ignore
|
||||||
|
const result = await window.api.downloadPhpVersion(id)
|
||||||
|
setNotification({
|
||||||
|
open: true,
|
||||||
|
message: result.message,
|
||||||
|
severity: result.success ? 'success' : 'error'
|
||||||
|
})
|
||||||
|
fetchPhpVersions()
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: 'flex', minHeight: '100vh', backgroundImage: 'radial-gradient(circle at 50% 0%, #153E5E 0%, #121212 100%)', bgcolor: 'transparent' }}>
|
<Box sx={{ display: 'flex', minHeight: '100vh', backgroundImage: 'radial-gradient(circle at 50% 0%, #153E5E 0%, #121212 100%)', bgcolor: 'transparent' }}>
|
||||||
<AppBar position="fixed" sx={{ zIndex: (theme) => theme.zIndex.drawer + 1, bgcolor: 'rgba(21, 62, 94, 0.85)', backdropFilter: 'blur(10px)' }}>
|
<AppBar position="fixed" sx={{ zIndex: (theme) => theme.zIndex.drawer + 1, bgcolor: 'rgba(21, 62, 94, 0.85)', backdropFilter: 'blur(10px)' }}>
|
||||||
@@ -175,6 +253,49 @@ function App(): JSX.Element {
|
|||||||
<Box component="main" sx={{ flexGrow: 1, p: 3 }}>
|
<Box component="main" sx={{ flexGrow: 1, p: 3 }}>
|
||||||
<Toolbar />
|
<Toolbar />
|
||||||
<Container maxWidth="md">
|
<Container maxWidth="md">
|
||||||
|
{activeTab === 'settings' && (
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h5" sx={{ fontWeight: 'medium', mb: 3 }}>
|
||||||
|
Genel Ayarlar
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Paper sx={{ mb: 4, bgcolor: 'rgba(255, 255, 255, 0.03)', borderRadius: 2 }}>
|
||||||
|
<Box sx={{ p: 2, borderBottom: '1px solid rgba(255, 255, 255, 0.05)' }}>
|
||||||
|
<Typography variant="subtitle1" sx={{ fontWeight: 'bold' }}>PHP Sürümleri</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">Gereken PHP sürümlerini buradan indirebilir ve yönetebilirsiniz.</Typography>
|
||||||
|
</Box>
|
||||||
|
<List>
|
||||||
|
{phpVersions.map((v) => (
|
||||||
|
<ListItem key={v.id} sx={{ py: 2 }}>
|
||||||
|
<ListItemIcon>
|
||||||
|
{v.status === 'installed' ? <InstalledIcon color="success" /> : <AvailableIcon color="action" />}
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText
|
||||||
|
primary={`PHP ${v.version}`}
|
||||||
|
secondary={v.status === 'downloading' ? `İndiriliyor... %${Math.round(v.progress)}` : v.status === 'installed' ? 'Kurulu' : 'İndirilebilir'}
|
||||||
|
sx={{ flexGrow: 1 }}
|
||||||
|
/>
|
||||||
|
<Box sx={{ minWidth: 120, textAlign: 'right' }}>
|
||||||
|
{v.status === 'available' && (
|
||||||
|
<Button variant="outlined" size="small" startIcon={<DownloadIcon />} onClick={() => handleDownloadPhp(v.id)}>
|
||||||
|
İNDİR
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{v.status === 'downloading' && (
|
||||||
|
<Box sx={{ width: '100%', mt: 1 }}>
|
||||||
|
<LinearProgress variant="determinate" value={v.progress} sx={{ height: 8, borderRadius: 5 }} />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{v.status === 'installed' && (
|
||||||
|
<Chip label="HAZIR" color="success" variant="outlined" size="small" />
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
{activeTab === 'dashboard' && (
|
{activeTab === 'dashboard' && (
|
||||||
<>
|
<>
|
||||||
<Typography variant="h5" gutterBottom sx={{ mb: 3, fontWeight: 'medium' }}>
|
<Typography variant="h5" gutterBottom sx={{ mb: 3, fontWeight: 'medium' }}>
|
||||||
@@ -223,12 +344,42 @@ function App(): JSX.Element {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === 'projects' && (
|
{activeTab === 'projects' && (
|
||||||
<Box sx={{ textAlign: 'center', mt: 4 }}>
|
<Box>
|
||||||
<Typography variant="h6">Henüz bir proje eklenmemiş.</Typography>
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
|
||||||
<Button variant="contained" startIcon={<ProjectIcon />} sx={{ mt: 2 }}>
|
<Typography variant="h5" sx={{ fontWeight: 'medium' }}>
|
||||||
|
Projelerim
|
||||||
|
</Typography>
|
||||||
|
<Button variant="contained" startIcon={<ProjectIcon />} onClick={() => setIsProjectDialogOpen(true)}>
|
||||||
Yeni Proje Ekle
|
Yeni Proje Ekle
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
{projects.length === 0 ? (
|
||||||
|
<Paper sx={{ p: 4, textAlign: 'center', bgcolor: 'rgba(255, 255, 255, 0.03)' }}>
|
||||||
|
<Typography variant="body1" color="text.secondary">Henüz bir proje eklenmemiş.</Typography>
|
||||||
|
</Paper>
|
||||||
|
) : (
|
||||||
|
<List>
|
||||||
|
{projects.map((project) => (
|
||||||
|
<Paper key={project.id} sx={{ mb: 2, bgcolor: 'rgba(255, 255, 255, 0.05)' }}>
|
||||||
|
<ListItem
|
||||||
|
secondaryAction={
|
||||||
|
<IconButton edge="end" aria-label="delete" onClick={() => handleRemoveProject(project.id)}>
|
||||||
|
<DeleteIcon color="error" />
|
||||||
|
</IconButton>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ListItemIcon><ProjectIcon color="primary" /></ListItemIcon>
|
||||||
|
<ListItemText
|
||||||
|
primary={project.name}
|
||||||
|
secondary={`${project.path} - PHP: ${project.phpVersion} - Host: ${project.host}`}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
</Paper>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Container>
|
</Container>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -242,6 +393,49 @@ function App(): JSX.Element {
|
|||||||
{notification.message}
|
{notification.message}
|
||||||
</Alert>
|
</Alert>
|
||||||
</Snackbar>
|
</Snackbar>
|
||||||
|
|
||||||
|
<Dialog open={isProjectDialogOpen} onClose={() => setIsProjectDialogOpen(false)} fullWidth maxWidth="sm">
|
||||||
|
<DialogTitle>Yeni Proje Ekle</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<Box sx={{ mt: 2, display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||||
|
<TextField
|
||||||
|
label="Proje Adı"
|
||||||
|
fullWidth
|
||||||
|
value={newProject.name}
|
||||||
|
onChange={(e) => setNewProject({ ...newProject, name: e.target.value })}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="Dosya Yolu (Absolute path)"
|
||||||
|
fullWidth
|
||||||
|
value={newProject.path}
|
||||||
|
onChange={(e) => setNewProject({ ...newProject, path: e.target.value })}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="Hostname (örn: local.test)"
|
||||||
|
fullWidth
|
||||||
|
value={newProject.host}
|
||||||
|
onChange={(e) => setNewProject({ ...newProject, host: e.target.value })}
|
||||||
|
/>
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel>PHP Sürümü</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={newProject.phpVersion}
|
||||||
|
label="PHP Sürümü"
|
||||||
|
onChange={(e) => setNewProject({ ...newProject, phpVersion: e.target.value })}
|
||||||
|
>
|
||||||
|
<MenuItem value="8.0">PHP 8.0</MenuItem>
|
||||||
|
<MenuItem value="8.1">PHP 8.1</MenuItem>
|
||||||
|
<MenuItem value="8.2">PHP 8.2</MenuItem>
|
||||||
|
<MenuItem value="8.3">PHP 8.3</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Box>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={() => setIsProjectDialogOpen(false)}>İptal</Button>
|
||||||
|
<Button onClick={handleAddProject} variant="contained" disabled={!newProject.name || !newProject.path}>Ekle</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,73 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from 'react-dom/client'
|
||||||
import App from './App'
|
import App from './App'
|
||||||
import './i18n'
|
import i18n from './i18n'
|
||||||
import { ThemeProvider, createTheme } from '@mui/material/styles'
|
import { ThemeProvider, createTheme } from '@mui/material/styles'
|
||||||
import CssBaseline from '@mui/material/CssBaseline'
|
import CssBaseline from '@mui/material/CssBaseline'
|
||||||
|
import { I18nextProvider } from 'react-i18next'
|
||||||
|
|
||||||
const theme = createTheme({
|
const theme = createTheme({
|
||||||
palette: {
|
palette: {
|
||||||
|
mode: 'dark',
|
||||||
primary: {
|
primary: {
|
||||||
main: '#153E5E',
|
|
||||||
},
|
|
||||||
secondary: {
|
|
||||||
main: '#4A7CA3',
|
main: '#4A7CA3',
|
||||||
},
|
},
|
||||||
mode: 'dark',
|
secondary: {
|
||||||
|
main: '#153E5E',
|
||||||
|
},
|
||||||
|
background: {
|
||||||
|
default: '#121212',
|
||||||
|
paper: '#1e1e1e',
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
primary: '#ffffff',
|
||||||
|
secondary: 'rgba(255, 255, 255, 0.7)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
MuiPaper: {
|
||||||
|
styleOverrides: {
|
||||||
|
root: {
|
||||||
|
backgroundImage: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Error Boundary for better debugging
|
||||||
|
class ErrorBoundary extends React.Component<{ children: React.ReactNode }, { hasError: boolean, error: any }> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props)
|
||||||
|
this.state = { hasError: false, error: null }
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromError(error) {
|
||||||
|
return { hasError: true, error }
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.hasError) {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: 20, color: 'white' }}>
|
||||||
|
<h1>Görüntüleme Hatası Oluştu</h1>
|
||||||
|
<pre style={{ whiteSpace: 'pre-wrap' }}>{this.state.error?.toString()}</pre>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return this.props.children
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
|
<ErrorBoundary>
|
||||||
|
<I18nextProvider i18n={i18n}>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<App />
|
<App />
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
</I18nextProvider>
|
||||||
|
</ErrorBoundary>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user