From 164fabc1745ab68a987737dce76d8b27141e01e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Cmit=20Tun=C3=A7?= Date: Sat, 28 Mar 2026 09:58:49 +0300 Subject: [PATCH] feat: implement core architecture and dashboard UI for service and project management --- config/my.ini.template | 4 +- config/nginx.conf.template | 2 +- src/main/ipc/index.ts | 64 +++++++++++++++---- src/main/services/ConfigService.ts | 36 +++++++++++ src/main/services/ProcessManager.ts | 12 ++++ src/preload/index.ts | 4 +- src/renderer/src/App.tsx | 95 ++++++++++++++++++++++++++--- 7 files changed, 192 insertions(+), 25 deletions(-) diff --git a/config/my.ini.template b/config/my.ini.template index 22ef3e3..9a38d8b 100644 --- a/config/my.ini.template +++ b/config/my.ini.template @@ -1,11 +1,11 @@ [mysqld] -port=3306 +port={{PORT}} datadir="{{DATADIR}}" socket="{{SOCKET}}" character-set-server=utf8mb4 collation-server=utf8mb4_unicode_ci [client] -port=3306 +port={{PORT}} socket="{{SOCKET}}" default-character-set=utf8mb4 diff --git a/config/nginx.conf.template b/config/nginx.conf.template index c86aea0..b4e0856 100644 --- a/config/nginx.conf.template +++ b/config/nginx.conf.template @@ -19,7 +19,7 @@ http { } location ~ \.php$ { - fastcgi_pass 127.0.0.1:9001; + fastcgi_pass 127.0.0.1:{{PHP_PORT}}; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; diff --git a/src/main/ipc/index.ts b/src/main/ipc/index.ts index 77fe0c4..30456c1 100644 --- a/src/main/ipc/index.ts +++ b/src/main/ipc/index.ts @@ -1,4 +1,5 @@ import { ipcMain, app, dialog } from 'electron' +import fs from 'fs' import { processManager } from '../services/ProcessManager' import { configService } from '../services/ConfigService' import { downloadService } from '../services/DownloadService' @@ -7,37 +8,78 @@ import { phpManagerService } from '../services/PhpManagerService' import { mySqlManagerService } from '../services/MySqlManagerService' import path from 'path' +function findExecutable(dir: string, name: string): string | null { + if (!fs.existsSync(dir)) return null + const files = fs.readdirSync(dir) + for (const file of files) { + const fullPath = path.join(dir, file) + const stat = fs.statSync(fullPath) + if (stat.isDirectory()) { + const found = findExecutable(fullPath, name) + if (found) return found + } else if (file.toLowerCase() === name.toLowerCase()) { + return fullPath + } + } + return null +} + export function registerIpcHandlers(): void { // Service management ipcMain.handle('services:start', async (_event, serviceName: string) => { try { + const settings = configService.getSettings() + const binDir = path.join(app.getPath('userData'), 'bin') + if (serviceName === 'nginx') { const rootPath = path.join(app.getAppPath(), 'www') const configPath = await configService.generateConfig('nginx.conf.template', 'nginx.conf', { - PORT: '80', + PORT: settings.nginxPort, + PHP_PORT: settings.phpPort, ROOT: rootPath }) - const binPath = downloadService.getBinPath('nginx', 'nginx.exe') + + const nginxDir = path.join(binDir, 'nginx') + const binPath = findExecutable(nginxDir, 'nginx.exe') + + if (!binPath) return { success: false, message: 'Nginx executable bulunamadı. Lütfen indirin.' } + const success = await processManager.startService('nginx', binPath, ['-c', configPath]) - return { success, message: success ? 'Nginx başlatıldı.' : 'Nginx başlatılamadı.' } + return { success, message: success ? 'Nginx başlatıldı.' : 'Nginx dosyaları bulunamadı veya port meşgul.' } } if (serviceName === 'php') { const configPath = await configService.generateConfig('php.ini.template', 'php.ini', {}) - const binPath = downloadService.getBinPath('php8.2', 'php-cgi.exe') - const success = await processManager.startService('php', binPath, ['-b', '127.0.0.1:9001', '-c', configPath]) - return { success, message: success ? 'PHP başlatıldı.' : 'PHP başlatılamadı.' } + // Find any installed PHP version + const binPath = findExecutable(binDir, 'php-cgi.exe') + + if (!binPath) return { success: false, message: 'PHP executable bulunamadı. Lütfen en az bir sürüm indirin.' } + + const success = await processManager.startService('php', binPath, ['-b', `127.0.0.1:${settings.phpPort}`, '-c', configPath]) + return { success, message: success ? 'PHP başlatıldı.' : 'PHP dosyaları bulunamadı veya port meşgul.' } } if (serviceName === 'mysql') { const dataDir = path.join(app.getPath('userData'), 'mysql_data') + const binPath = findExecutable(binDir, 'mysqld.exe') + + if (!binPath) return { success: false, message: 'MySQL executable bulunamadı. Lütfen indirin.' } + + // Initialize if data directory is empty + if (!fs.existsSync(dataDir) || fs.readdirSync(dataDir).length === 0) { + if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true }) + console.log('Initializing MySQL data directory...') + await processManager.runOnce(binPath, ['--initialize-insecure', `--datadir=${dataDir.replace(/\\/g, '/')}`]) + } + const configPath = await configService.generateConfig('my.ini.template', 'my.ini', { + PORT: settings.mysqlPort, DATADIR: dataDir, SOCKET: '/tmp/mysql.sock' }) - const binPath = downloadService.getBinPath('mysql', 'bin/mysqld.exe') + const success = await processManager.startService('mysql', binPath, ['--defaults-file=' + configPath]) - return { success, message: success ? 'MySQL başlatıldı.' : 'MySQL başlatılamadı.' } + return { success, message: success ? 'MySQL başlatıldı.' : 'MySQL dosyaları bulunamadı veya port meşgul.' } } return { success: false, message: 'Bilinmeyen servis.' } @@ -140,11 +182,11 @@ export function registerIpcHandlers(): void { }) // Config management - ipcMain.handle('config:get', async (_event, key: string) => { - return null + ipcMain.handle('config:get', async () => { + return configService.getSettings() }) ipcMain.handle('config:save', async (_event, config: any) => { - return { success: true } + return { success: true, settings: configService.updateSettings(config) } }) } diff --git a/src/main/services/ConfigService.ts b/src/main/services/ConfigService.ts index e6f784c..b9e2eb2 100644 --- a/src/main/services/ConfigService.ts +++ b/src/main/services/ConfigService.ts @@ -5,14 +5,26 @@ import { app } from 'electron' export class ConfigService { private configDir: string private templateDir: string + private settingsPath: string + private defaultSettings = { + nginxPort: '80', + phpPort: '9001', + mysqlPort: '3306', + language: 'tr' + } constructor() { this.templateDir = path.join(app.getAppPath(), 'config') this.configDir = path.join(app.getPath('userData'), 'generated_configs') + this.settingsPath = path.join(app.getPath('userData'), 'settings.json') if (!fs.existsSync(this.configDir)) { fs.mkdirSync(this.configDir, { recursive: true }) } + + if (!fs.existsSync(this.settingsPath)) { + fs.writeFileSync(this.settingsPath, JSON.stringify(this.defaultSettings, null, 2)) + } } async generateConfig(templateName: string, targetName: string, variables: Record): Promise { @@ -37,6 +49,30 @@ export class ConfigService { getGeneratedPath(targetName: string): string { return path.join(this.configDir, targetName) } + + getSettings(): any { + try { + if (fs.existsSync(this.settingsPath)) { + const data = fs.readFileSync(this.settingsPath, 'utf-8') + return { ...this.defaultSettings, ...JSON.parse(data) } + } + } catch (e) { + console.error('Failed to read settings:', e) + } + return this.defaultSettings + } + + updateSettings(newSettings: any) { + try { + const current = this.getSettings() + const updated = { ...current, ...newSettings } + fs.writeFileSync(this.settingsPath, JSON.stringify(updated, null, 2)) + return updated + } catch (e) { + console.error('Failed to save settings:', e) + throw e + } + } } export const configService = new ConfigService() diff --git a/src/main/services/ProcessManager.ts b/src/main/services/ProcessManager.ts index 7a375af..e36946a 100644 --- a/src/main/services/ProcessManager.ts +++ b/src/main/services/ProcessManager.ts @@ -10,6 +10,18 @@ export class ProcessManager { this.processes = new Map() } + async runOnce(binPath: string, args: string[]): Promise { + return new Promise((resolve) => { + const child = spawn(binPath, args, { stdio: 'inherit' }) + child.on('close', (code) => { + resolve(code === 0) + }) + child.on('error', () => { + resolve(false) + }) + }) + } + async startService(name: string, binPath: string, args: string[]): Promise { if (this.processes.has(name)) { console.log(`Service ${name} is already running.`) diff --git a/src/preload/index.ts b/src/preload/index.ts index eb3743e..e8172a6 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -17,7 +17,9 @@ const api = { getMySqlVersions: () => electronAPI.ipcRenderer.invoke('mysql:versions'), downloadMySqlVersion: (id: string) => electronAPI.ipcRenderer.invoke('mysql:download', id), onMySqlProgress: (callback: any) => electronAPI.ipcRenderer.on('mysql:progress', (_event, data) => callback(data)), - selectDirectory: () => electronAPI.ipcRenderer.invoke('dialog:select-directory') + selectDirectory: () => electronAPI.ipcRenderer.invoke('dialog:select-directory'), + getSettings: () => electronAPI.ipcRenderer.invoke('config:get'), + saveSettings: (settings: any) => electronAPI.ipcRenderer.invoke('config:save', settings) } // Use `contextBridge` APIs to expose Electron APIs to diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 3d81fa5..40f516e 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -33,7 +33,8 @@ import { Tabs, Tab, InputAdornment, - CircularProgress + CircularProgress, + Stack } from '@mui/material' import { Dashboard as DashboardIcon, @@ -50,7 +51,9 @@ import { CheckCircle as InstalledIcon, CloudDownload as AvailableIcon, Refresh as RefreshIcon, - FolderOpen as FolderOpenIcon + FolderOpen as FolderOpenIcon, + Save as SaveIcon, + Terminal as TerminalIcon } from '@mui/icons-material' import { useTranslation } from 'react-i18next' @@ -68,6 +71,13 @@ interface ServiceStatus { mysql: string } +interface AppSettings { + nginxPort: string + phpPort: string + mysqlPort: string + language: string +} + function App(): JSX.Element { const { t, i18n } = useTranslation() const [status, setStatus] = useState({ @@ -83,6 +93,12 @@ function App(): JSX.Element { const [loadingVersions, setLoadingVersions] = useState(false) const [isProjectDialogOpen, setIsProjectDialogOpen] = useState(false) const [newProject, setNewProject] = useState({ name: '', path: '', phpVersion: '', host: 'localhost' }) + const [settings, setSettings] = useState({ + nginxPort: '80', + phpPort: '9001', + mysqlPort: '3306', + language: 'tr' + }) const [notification, setNotification] = useState<{ open: boolean, message: string, severity: 'success' | 'error' }>({ open: false, message: '', @@ -96,6 +112,13 @@ function App(): JSX.Element { } } + const fetchSettings = async () => { + if (window.api && window.api.getSettings) { + const s = await window.api.getSettings() + if (s) setSettings(s) + } + } + const fetchProjects = async () => { if (window.api && window.api.getProjects) { const list = await window.api.getProjects() @@ -142,6 +165,7 @@ function App(): JSX.Element { fetchProjects() fetchPhpVersions() fetchMySqlVersions() + fetchSettings() const removePhpListener = window.api.onBinaryProgress ? window.api.onBinaryProgress((data) => { if (!data) return @@ -160,7 +184,7 @@ function App(): JSX.Element { fetchProjects() fetchPhpVersions() fetchMySqlVersions() - }, 3000) + }, 5000) return () => { clearInterval(interval) @@ -191,6 +215,15 @@ function App(): JSX.Element { }) } + const handleSaveSettings = async () => { + if (window.api && window.api.saveSettings) { + const result = await window.api.saveSettings(settings) + if (result.success) { + setNotification({ open: true, message: 'Ayarlar kaydedildi.', severity: 'success' }) + } + } + } + const toggleLanguage = () => { const newLang = i18n.language === 'tr' ? 'en' : i18n.language === 'en' ? 'de' : 'tr' i18n.changeLanguage(newLang) @@ -326,15 +359,18 @@ function App(): JSX.Element { Genel Ayarlar - + {settingsTab < 2 && ( + + )} - setSettingsTab(v)} textColor="primary" indicatorColor="primary"> + setSettingsTab(v)} textColor="primary" indicatorColor="primary" variant="scrollable" scrollButtons="auto"> } iconPosition="start" /> } iconPosition="start" /> + } iconPosition="start" /> @@ -426,6 +462,45 @@ function App(): JSX.Element { )} + + {settingsTab === 2 && ( + + + Port Ayarları + + setSettings({ ...settings, nginxPort: e.target.value })} + variant="filled" + size="small" + helperText="Varsayılan: 80" + /> + setSettings({ ...settings, phpPort: e.target.value })} + variant="filled" + size="small" + helperText="Varsayılan: 9001" + /> + setSettings({ ...settings, mysqlPort: e.target.value })} + variant="filled" + size="small" + helperText="Varsayılan: 3306" + /> + + + + + + + )} )} {activeTab === 'dashboard' && ( @@ -437,7 +512,7 @@ function App(): JSX.Element { - + {getStatusChip(status.nginx)} handleToggleService('nginx')} /> @@ -446,7 +521,7 @@ function App(): JSX.Element { - + {getStatusChip(status.php)} handleToggleService('php')} /> @@ -455,7 +530,7 @@ function App(): JSX.Element { - + {getStatusChip(status.mysql)} handleToggleService('mysql')} />