diff --git a/config/nginx.conf.template b/config/nginx.conf.template
index 16f9e16..4f3a2f4 100644
--- a/config/nginx.conf.template
+++ b/config/nginx.conf.template
@@ -20,6 +20,18 @@ http {
try_files $uri $uri/ /index.php?$query_string;
}
+ location /phpmyadmin {
+ alias "{{PHPMYADMIN_DIR}}";
+ index index.php;
+ location ~ ^/phpmyadmin/(.+\.php)$ {
+ alias "{{PHPMYADMIN_DIR}}/$1";
+ fastcgi_pass 127.0.0.1:{{PHP_PORT}};
+ fastcgi_index index.php;
+ fastcgi_param SCRIPT_FILENAME $request_filename;
+ include "{{CONF_DIR}}/fastcgi_params";
+ }
+ }
+
location ~ \.php$ {
fastcgi_pass 127.0.0.1:{{PHP_PORT}};
fastcgi_index index.php;
diff --git a/implementation_plan.md b/implementation_plan.md
index b29d36b..6657bec 100644
--- a/implementation_plan.md
+++ b/implementation_plan.md
@@ -65,6 +65,14 @@ multiphp/
---
+## phpMyAdmin Entegrasyonu
+- phpMyAdmin'in en son sürümünün (all-languages.zip) otomatik indirilmesi ve ayıklanması.
+- Nginx üzerinde `/phpmyadmin` aliası ile yapılandırma.
+- `config.inc.php` dosyasının yerel MySQL ile uyumlu şekilde otomatik oluşturulması.
+- Dashboard üzerinde phpMyAdmin'e hızlı erişim butonu.
+
+---
+
## Doğrulama ve Test Planı
### Otomatik Testler
diff --git a/logo/Backup_of_truncgil-multiphp-server.cdr b/logo/Backup_of_truncgil-multiphp-server.cdr
index c5d8fe9..94e1327 100644
Binary files a/logo/Backup_of_truncgil-multiphp-server.cdr and b/logo/Backup_of_truncgil-multiphp-server.cdr differ
diff --git a/logo/truncgil-multiphp-server-light.svg b/logo/truncgil-multiphp-server-light.svg
new file mode 100644
index 0000000..3809522
--- /dev/null
+++ b/logo/truncgil-multiphp-server-light.svg
@@ -0,0 +1,30 @@
+
+
+
+
diff --git a/logo/truncgil-multiphp-server-yatay-light.svg b/logo/truncgil-multiphp-server-yatay-light.svg
new file mode 100644
index 0000000..976214d
--- /dev/null
+++ b/logo/truncgil-multiphp-server-yatay-light.svg
@@ -0,0 +1,27 @@
+
+
+
+
diff --git a/logo/truncgil-multiphp-server-yatay.svg b/logo/truncgil-multiphp-server-yatay.svg
new file mode 100644
index 0000000..5837156
--- /dev/null
+++ b/logo/truncgil-multiphp-server-yatay.svg
@@ -0,0 +1,35 @@
+
+
+
+
diff --git a/logo/truncgil-multiphp-server.cdr b/logo/truncgil-multiphp-server.cdr
index 23110ae..584a829 100644
Binary files a/logo/truncgil-multiphp-server.cdr and b/logo/truncgil-multiphp-server.cdr differ
diff --git a/src/main/ipc/index.ts b/src/main/ipc/index.ts
index 38b56e0..d533894 100644
--- a/src/main/ipc/index.ts
+++ b/src/main/ipc/index.ts
@@ -7,6 +7,7 @@ import { projectService } from '../services/ProjectService'
import { phpManagerService } from '../services/PhpManagerService'
import { mySqlManagerService } from '../services/MySqlManagerService'
import { nginxManagerService } from '../services/NginxManagerService'
+import { phpMyAdminService } from '../services/PhpMyAdminService'
import path from 'path'
function findExecutable(dir: string, name: string): string | null {
@@ -58,7 +59,8 @@ export function registerIpcHandlers(): void {
PHP_PORT: settings.phpPort,
ROOT: rootPath,
LOG_DIR: logDir.replace(/\\/g, '/'),
- CONF_DIR: confDir.replace(/\\/g, '/')
+ CONF_DIR: confDir.replace(/\\/g, '/'),
+ PHPMYADMIN_DIR: phpMyAdminService.getPath().replace(/\\/g, '/')
})
// Test config first with prefix
@@ -241,6 +243,9 @@ export function registerIpcHandlers(): void {
ipcMain.handle('projects:remove', async (_event, id) => {
return projectService.removeProject(id)
})
+ ipcMain.handle('projects:update', async (_event, project) => {
+ return projectService.updateProject(project)
+ })
// Config management
ipcMain.handle('config:get', async () => {
@@ -250,4 +255,15 @@ export function registerIpcHandlers(): void {
ipcMain.handle('config:save', async (_event, config: any) => {
return { success: true, settings: configService.updateSettings(config) }
})
+
+ // phpMyAdmin
+ ipcMain.handle('pma:status', async () => {
+ return await phpMyAdminService.isInstalled()
+ })
+
+ ipcMain.handle('pma:setup', async (event, mysqlPort: string) => {
+ return await phpMyAdminService.setup(mysqlPort, (p) => {
+ event.sender.send('pma:progress', p)
+ })
+ })
}
diff --git a/src/main/services/DownloadService.ts b/src/main/services/DownloadService.ts
index 81b3567..4bb6bb5 100644
--- a/src/main/services/DownloadService.ts
+++ b/src/main/services/DownloadService.ts
@@ -15,19 +15,14 @@ export class DownloadService {
}
}
- async downloadAndExtract(url: string, targetName: string, onProgress?: (p: number) => void): Promise {
+ async downloadAndExtract(url: string, targetName: string, onProgress?: (p: number) => void, customTargetDir?: string): Promise {
const tempFile = path.join(app.getPath('temp'), `${targetName}.zip`)
- const targetDir = path.join(this.binDir, targetName)
- const phpExe = path.join(targetDir, 'php.exe')
+ const targetDir = customTargetDir || path.join(this.binDir, targetName)
- // If already installed and working, skip
- if (fs.existsSync(phpExe)) {
- return targetDir
- }
-
- // Cleanup failed attempts
- if (fs.existsSync(targetDir)) {
- fs.rmSync(targetDir, { recursive: true, force: true })
+ // Cleanup failed/previous attempts if we want a fresh start
+ // For phpMyAdmin, we always want a fresh extract to avoid version mix
+ if (fs.existsSync(targetDir) && !customTargetDir) {
+ // For PHP/MySQL we might skip, but let's make it smarter
}
const writer = fs.createWriteStream(tempFile)
diff --git a/src/main/services/PhpMyAdminService.ts b/src/main/services/PhpMyAdminService.ts
new file mode 100644
index 0000000..11a878b
--- /dev/null
+++ b/src/main/services/PhpMyAdminService.ts
@@ -0,0 +1,75 @@
+import path from 'path'
+import fs from 'fs'
+import { app } from 'electron'
+import { downloadService } from './DownloadService'
+
+export class PhpMyAdminService {
+ private pmaDir: string
+ private downloadUrl = 'https://www.phpmyadmin.net/downloads/phpMyAdmin-latest-all-languages.zip'
+
+ constructor() {
+ this.pmaDir = path.join(app.getPath('userData'), 'phpmyadmin')
+ }
+
+ async isInstalled(): Promise {
+ return fs.existsSync(path.join(this.pmaDir, 'index.php'))
+ }
+
+ async setup(mysqlPort: string, onProgress?: (progress: number) => void): Promise {
+ if (!fs.existsSync(this.pmaDir)) {
+ fs.mkdirSync(this.pmaDir, { recursive: true })
+ }
+
+ try {
+ await downloadService.downloadAndExtract(this.downloadUrl, this.pmaDir, (p) => {
+ if (onProgress) onProgress(p)
+ })
+
+ // After extraction, the zip usually contains a folder like phpMyAdmin-x.y.z-all-languages
+ // We should move contents to the root of this.pmaDir if they are nested
+ const items = fs.readdirSync(this.pmaDir)
+ if (items.length === 1 && fs.statSync(path.join(this.pmaDir, items[0])).isDirectory()) {
+ const nestedDir = path.join(this.pmaDir, items[0])
+ const nestedItems = fs.readdirSync(nestedDir)
+ for (const item of nestedItems) {
+ fs.renameSync(path.join(nestedDir, item), path.join(this.pmaDir, item))
+ }
+ fs.rmdirSync(nestedDir)
+ }
+
+ // Generate config.inc.php
+ this.generateConfig(mysqlPort)
+
+ return true
+ } catch (error) {
+ console.error('phpMyAdmin setup error:', error)
+ return false
+ }
+ }
+
+ private generateConfig(mysqlPort: string = '3306') {
+ const configPath = path.join(this.pmaDir, 'config.inc.php')
+ const blowfishSecret = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15)
+
+ const configContent = ``
+
+ fs.writeFileSync(configPath, configContent)
+ }
+
+ getPath(): string {
+ return this.pmaDir
+ }
+}
+
+export const phpMyAdminService = new PhpMyAdminService()
diff --git a/src/main/services/PortMappingService.ts b/src/main/services/PortMappingService.ts
new file mode 100644
index 0000000..36cf4e2
--- /dev/null
+++ b/src/main/services/PortMappingService.ts
@@ -0,0 +1,58 @@
+import fs from 'fs'
+import path from 'path'
+import { app } from 'electron'
+
+interface PortMap {
+ php: Record;
+ mysql: Record;
+}
+
+export class PortMappingService {
+ private configFile: string
+ private portMap: PortMap = { php: {}, mysql: {} }
+ private nextPhpPort = 9001
+ private nextMysqlPort = 3306
+
+ constructor() {
+ this.configFile = path.join(app.getPath('userData'), 'port_mappings.json')
+ this.load()
+ }
+
+ private load() {
+ if (fs.existsSync(this.configFile)) {
+ try {
+ this.portMap = JSON.parse(fs.readFileSync(this.configFile, 'utf-8'))
+
+ const phpPorts = Object.values(this.portMap.php)
+ if (phpPorts.length > 0) this.nextPhpPort = Math.max(...phpPorts) + 1
+
+ const mysqlPorts = Object.values(this.portMap.mysql)
+ if (mysqlPorts.length > 0) this.nextMysqlPort = Math.max(...mysqlPorts) + 1
+ } catch (e) {
+ console.error('Failed to load port mappings', e)
+ }
+ }
+ }
+
+ private save() {
+ fs.writeFileSync(this.configFile, JSON.stringify(this.portMap, null, 2))
+ }
+
+ getPhpPort(version: string): number {
+ if (!this.portMap.php[version]) {
+ this.portMap.php[version] = this.nextPhpPort++
+ this.save()
+ }
+ return this.portMap.php[version]
+ }
+
+ getMySqlPort(version: string): number {
+ if (!this.portMap.mysql[version]) {
+ this.portMap.mysql[version] = this.nextMysqlPort++
+ this.save()
+ }
+ return this.portMap.mysql[version]
+ }
+}
+
+export const portMappingService = new PortMappingService()
diff --git a/src/main/services/ProjectService.ts b/src/main/services/ProjectService.ts
index e1462f1..12eb62a 100644
--- a/src/main/services/ProjectService.ts
+++ b/src/main/services/ProjectService.ts
@@ -7,6 +7,7 @@ export interface Project {
name: string
path: string
phpVersion: string
+ mySqlVersion: string
host: string
}
@@ -49,6 +50,14 @@ export class ProjectService {
return newProject
}
+ updateProject(project: Project) {
+ const index = this.projects.findIndex(p => p.id === project.id)
+ if (index !== -1) {
+ this.projects[index] = project
+ this.saveProjects()
+ }
+ }
+
removeProject(id: string) {
this.projects = this.projects.filter(p => p.id !== id)
this.saveProjects()
diff --git a/src/preload/index.ts b/src/preload/index.ts
index bb8aead..4580b09 100644
--- a/src/preload/index.ts
+++ b/src/preload/index.ts
@@ -12,6 +12,7 @@ const api = {
getProjects: () => electronAPI.ipcRenderer.invoke('projects:list'),
addProject: (project: any) => electronAPI.ipcRenderer.invoke('projects:add', project),
removeProject: (id: string) => electronAPI.ipcRenderer.invoke('projects:remove', id),
+ updateProject: (project: any) => electronAPI.ipcRenderer.invoke('projects:update', project),
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)),
@@ -23,7 +24,10 @@ const api = {
saveSettings: (settings: any) => electronAPI.ipcRenderer.invoke('config:save', settings),
getNginxVersions: () => electronAPI.ipcRenderer.invoke('nginx:versions'),
downloadNginxVersion: (id: string) => electronAPI.ipcRenderer.invoke('nginx:download', id),
- onNginxProgress: (callback: any) => electronAPI.ipcRenderer.on('nginx:progress', (_event, data) => callback(data))
+ onNginxProgress: (callback: any) => electronAPI.ipcRenderer.on('nginx:progress', (_event, data) => callback(data)),
+ pmaStatus: () => electronAPI.ipcRenderer.invoke('pma:status'),
+ pmaSetup: (mysqlPort: string) => electronAPI.ipcRenderer.invoke('pma:setup', mysqlPort),
+ onPmaProgress: (callback: any) => electronAPI.ipcRenderer.on('pma:progress', (_event, data) => callback(data))
}
// Use `contextBridge` APIs to expose Electron APIs to
diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx
index be34a44..4196849 100644
--- a/src/renderer/src/App.tsx
+++ b/src/renderer/src/App.tsx
@@ -57,7 +57,9 @@ import {
Terminal as TerminalIcon,
ContentCopy as CopyIcon,
Close as CloseIcon,
- ContentCopy as ContentCopyIcon
+ ContentCopy as ContentCopyIcon,
+ OpenInNew as OpenIcon,
+ Edit as EditIcon
} from '@mui/icons-material'
import { useTranslation } from 'react-i18next'
@@ -97,7 +99,8 @@ function App(): JSX.Element {
const [nginxVersions, setNginxVersions] = useState([])
const [loadingVersions, setLoadingVersions] = useState(false)
const [isProjectDialogOpen, setIsProjectDialogOpen] = useState(false)
- const [newProject, setNewProject] = useState({ name: '', path: '', phpVersion: '', host: 'localhost' })
+ const slugify = (text: string) => text.toLowerCase().trim().replace(/[^\w\s-]/g, '').replace(/[\s_-]+/g, '-').replace(/^-+|-+$/g, '');
+ const [newProject, setNewProject] = useState({ name: '', path: '', phpVersion: '', mySqlVersion: '', host: 'localhost' })
const [settings, setSettings] = useState({
nginxPort: '80',
phpPort: '9001',
@@ -114,6 +117,9 @@ function App(): JSX.Element {
const [logDialogOpen, setLogDialogOpen] = useState(false)
const [selectedService, setSelectedService] = useState('')
const [logs, setLogs] = useState([])
+ const [pmaInstalled, setPmaInstalled] = useState(false)
+ const [pmaLoading, setPmaLoading] = useState(false)
+ const [pmaProgress, setPmaProgress] = useState(0)
const fetchStatus = async () => {
if (window.api && window.api.getServiceStatus) {
@@ -174,23 +180,39 @@ function App(): JSX.Element {
}
useEffect(() => {
- if (isProjectDialogOpen && !newProject.phpVersion && phpVersions.length > 0) {
- const installed = phpVersions.find(v => v.status === 'installed')
- if (installed) {
- setNewProject(prev => ({ ...prev, phpVersion: installed.version }))
+ if (isProjectDialogOpen && !newProject.id) {
+ const updates: Partial = {}
+ if (!newProject.phpVersion && phpVersions.length > 0) {
+ const installedPhp = phpVersions.find(v => v.status === 'installed')
+ if (installedPhp) updates.phpVersion = installedPhp.version
+ }
+ if (!newProject.mySqlVersion && mySqlVersions.length > 0) {
+ const installedMysql = mySqlVersions.find(v => v.status === 'installed')
+ if (installedMysql) updates.mySqlVersion = installedMysql.version
+ }
+ if (Object.keys(updates).length > 0) {
+ setNewProject(prev => ({ ...prev, ...updates }))
}
}
- }, [isProjectDialogOpen, phpVersions])
+ }, [isProjectDialogOpen, phpVersions, mySqlVersions])
useEffect(() => {
if (!window.api) return
+ const fetchPmaStatus = async () => {
+ if (window.api && window.api.pmaStatus) {
+ const installed = await window.api.pmaStatus()
+ setPmaInstalled(installed)
+ }
+ }
+
fetchStatus()
fetchProjects()
fetchPhpVersions()
fetchMySqlVersions()
fetchNginxVersions()
fetchSettings()
+ fetchPmaStatus()
const removePhpListener = window.api.onBinaryProgress ? window.api.onBinaryProgress((data) => {
if (!data) return
@@ -210,6 +232,10 @@ function App(): JSX.Element {
setNginxVersions(prev => prev.map(v => v.id === id ? { ...v, progress, status: progress === 100 ? 'installed' : 'downloading' } : v))
}) : null
+ const removePmaListener = window.api.onPmaProgress ? window.api.onPmaProgress((p: number) => {
+ setPmaProgress(p)
+ }) : null
+
const interval = setInterval(() => {
fetchStatus()
fetchProjects()
@@ -231,6 +257,7 @@ function App(): JSX.Element {
if (typeof removePhpListener === 'function') removePhpListener()
if (typeof removeMySqlListener === 'function') removeMySqlListener()
if (typeof removeNginxListener === 'function') removeNginxListener()
+ if (typeof removePmaListener === 'function') removePmaListener()
}
}, [logDialogOpen, selectedService])
@@ -243,9 +270,11 @@ function App(): JSX.Element {
const handleSelectPath = async () => {
if (window.api && window.api.selectDirectory) {
- const path = await window.api.selectDirectory()
- if (path) {
- setNewProject(prev => ({ ...prev, path }))
+ const pathStr = await window.api.selectDirectory()
+ if (pathStr) {
+ const folderName = pathStr.split(/[/\\]/).pop() || ''
+ const slug = slugify(folderName)
+ setNewProject(prev => ({ ...prev, path: pathStr, name: folderName, host: slug }))
}
}
}
@@ -331,10 +360,14 @@ function App(): JSX.Element {
const handleAddProject = async () => {
if (!window.api) return
- await window.api.addProject(newProject)
+ if (newProject.id) {
+ await window.api.updateProject(newProject)
+ } else {
+ await window.api.addProject(newProject)
+ }
setIsProjectDialogOpen(false)
- setNewProject({ name: '', path: '', phpVersion: '', host: 'localhost' })
- setNotification({ open: true, message: 'Proje başarıyla eklendi.', severity: 'success' })
+ setNewProject({ name: '', path: '', phpVersion: '', mySqlVersion: '', host: 'localhost' })
+ setNotification({ open: true, message: newProject.id ? 'Proje güncellendi.' : 'Proje başarıyla eklendi.', severity: 'success' })
fetchProjects()
}
@@ -378,6 +411,27 @@ function App(): JSX.Element {
fetchNginxVersions()
}
+ const handleSetupPma = async () => {
+ if (!window.api) return
+ setPmaLoading(true)
+ setPmaProgress(0)
+ try {
+ const success = await window.api.pmaSetup()
+ if (success) {
+ setPmaInstalled(true)
+ setNotification({ open: true, message: 'phpMyAdmin başarıyla kuruldu.', severity: 'success' })
+ } else {
+ setNotification({ open: true, message: 'phpMyAdmin kurulumu sırasında hata oluştu.', severity: 'error' })
+ }
+ } finally {
+ setPmaLoading(false)
+ }
+ }
+
+ const handleOpenPma = () => {
+ window.open(`http://localhost:${settings.nginxPort}/phpmyadmin`, '_blank')
+ }
+
const installedPhpVersions = phpVersions.filter(v => v.status === 'installed')
return (
@@ -501,51 +555,97 @@ function App(): JSX.Element {
)}
{settingsTab === 1 && (
-
-
- MySQL Sürümleri
- Farklı MySQL sürümlerini indirebilir ve sunucunuzda kullanabilirsiniz.
-
-
- {mySqlVersions.length === 0 && !loadingVersions && (
-
+ <>
+
+
+ MySQL Sürümleri
+ Farklı MySQL sürümlerini indirebilir ve sunucunuzda kullanabilirsiniz.
+
+
+ {mySqlVersions.length === 0 && !loadingVersions && (
+
+ )}
+ {mySqlVersions.map((v) => (
+
+
+ {v.status === 'installed' ? : }
+
+
+ {`MySQL ${v.version} (${v.cycle})`}
+ {v.description && (
+
+ )}
+
+ }
+ secondary={v.status === 'downloading' ? `İndiriliyor... %${Math.round(v.progress)}` : v.status === 'installed' ? 'Kurulu' : 'İndirilebilir'}
+ sx={{ flexGrow: 1 }}
+ />
+
+ {v.status === 'available' && (
+ } onClick={() => handleDownloadMySql(v.id)}>
+ İNDİR
+
+ )}
+ {v.status === 'downloading' && (
+
+
+
+ )}
+ {v.status === 'installed' && (
+
+ )}
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+ phpMyAdmin
+
+ {pmaInstalled ? 'Veritabanı yönetimi için phpMyAdmin hazır.' : 'Veritabanlarını web üzerinden yönetmek için phpMyAdmin kurun.'}
+
+
+
+ {!pmaInstalled ? (
+ : }
+ >
+ {pmaLoading ? 'KURULUYOR...' : 'KURULUMU BAŞLAT'}
+
+ ) : (
+ }
+ >
+ AÇ
+
+ )}
+
+
+ {pmaLoading && (
+
+
+
+ İndiriliyor... %{Math.round(pmaProgress)}
+
+
)}
- {mySqlVersions.map((v) => (
-
-
- {v.status === 'installed' ? : }
-
-
- {`MySQL ${v.version} (${v.cycle})`}
- {v.description && (
-
- )}
-
- }
- secondary={v.status === 'downloading' ? `İndiriliyor... %${Math.round(v.progress)}` : v.status === 'installed' ? 'Kurulu' : 'İndirilebilir'}
- sx={{ flexGrow: 1 }}
- />
-
- {v.status === 'available' && (
- } onClick={() => handleDownloadMySql(v.id)}>
- İNDİR
-
- )}
- {v.status === 'downloading' && (
-
-
-
- )}
- {v.status === 'installed' && (
-
- )}
-
-
- ))}
-
-
+
+ >
)}
{settingsTab === 2 && (
@@ -715,15 +815,23 @@ function App(): JSX.Element {
handleRemoveProject(project.id)}>
-
-
+
+ {
+ setNewProject(project)
+ setIsProjectDialogOpen(true)
+ }} sx={{ mr: 1 }}>
+
+
+ handleRemoveProject(project.id)}>
+
+
+
}
>
@@ -856,8 +964,11 @@ function App(): JSX.Element {
-
- {/* Log Dialog */}
- setLogDialogOpen(false)} fullWidth maxWidth="md">
-
-
-
- {selectedService.toUpperCase()} Konsol Çıktısı
-
-
- navigator.clipboard.writeText(logs.join('\n'))}>
-
-
-
-
-
-
- {logs.length === 0 ? (
- Henüz log kaydı yok...
- ) : (
- logs.map((line, i) => (
-
- {line}
-
- ))
- )}
-
-
-
-
-
-
-
- )
+ {/* Log Dialog */}
+ setLogDialogOpen(false)} fullWidth maxWidth="md">
+
+
+
+ {selectedService.toUpperCase()} Konsol Çıktısı
+
+
+ navigator.clipboard.writeText(logs.join('\n'))}>
+
+
+
+
+
+
+ {logs.length === 0 ? (
+ Henüz log kaydı yok...
+ ) : (
+ logs.map((line, i) => (
+
+ {line}
+
+ ))
+ )}
+
+
+
+
+
+
+
+ )
}
-export default App
+ export default App