feat: initialize Multi-PHP server project with Electron, React, and service management infrastructure
This commit is contained in:
+35
@@ -0,0 +1,35 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Build outputs
|
||||||
|
dist/
|
||||||
|
out/
|
||||||
|
bin/
|
||||||
|
|
||||||
|
# IDE files
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# TypeScript
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Service generated files
|
||||||
|
generated_configs/
|
||||||
|
mysql_data/
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[mysqld]
|
||||||
|
port=3306
|
||||||
|
datadir="{{DATADIR}}"
|
||||||
|
socket="{{SOCKET}}"
|
||||||
|
character-set-server=utf8mb4
|
||||||
|
collation-server=utf8mb4_unicode_ci
|
||||||
|
|
||||||
|
[client]
|
||||||
|
port=3306
|
||||||
|
socket="{{SOCKET}}"
|
||||||
|
default-character-set=utf8mb4
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
worker_processes 1;
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
http {
|
||||||
|
include mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
sendfile on;
|
||||||
|
keepalive_timeout 65;
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen {{PORT}};
|
||||||
|
server_name localhost;
|
||||||
|
root "{{ROOT}}";
|
||||||
|
index index.php index.html index.htm;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.php?$query_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ \.php$ {
|
||||||
|
fastcgi_pass 127.0.0.1:9001;
|
||||||
|
fastcgi_index index.php;
|
||||||
|
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||||
|
include fastcgi_params;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
[PHP]
|
||||||
|
engine = On
|
||||||
|
short_open_tag = Off
|
||||||
|
precision = 14
|
||||||
|
output_buffering = 4096
|
||||||
|
zlib.output_compression = Off
|
||||||
|
implicit_flush = Off
|
||||||
|
unserialize_callback_func =
|
||||||
|
serialize_precision = -1
|
||||||
|
disable_functions =
|
||||||
|
disable_classes =
|
||||||
|
zend.enable_gc = On
|
||||||
|
expose_php = On
|
||||||
|
max_execution_time = 30
|
||||||
|
max_input_time = 60
|
||||||
|
memory_limit = 128M
|
||||||
|
error_reporting = E_ALL
|
||||||
|
display_errors = On
|
||||||
|
display_startup_errors = On
|
||||||
|
log_errors = On
|
||||||
|
log_errors_max_len = 1024
|
||||||
|
ignore_repeated_errors = Off
|
||||||
|
ignore_repeated_source = Off
|
||||||
|
report_memleaks = On
|
||||||
|
html_errors = On
|
||||||
|
variables_order = "GPCS"
|
||||||
|
request_order = "GP"
|
||||||
|
register_argc_argv = Off
|
||||||
|
auto_globals_jit = On
|
||||||
|
post_max_size = 8M
|
||||||
|
auto_prepend_file =
|
||||||
|
auto_append_file =
|
||||||
|
default_mimetype = "text/html"
|
||||||
|
default_charset = "UTF-8"
|
||||||
|
doc_root =
|
||||||
|
user_dir =
|
||||||
|
enable_dl = Off
|
||||||
|
file_uploads = On
|
||||||
|
upload_max_filesize = 2M
|
||||||
|
max_file_uploads = 20
|
||||||
|
allow_url_fopen = On
|
||||||
|
allow_url_include = Off
|
||||||
|
default_socket_timeout = 60
|
||||||
|
|
||||||
|
[ExtensionList]
|
||||||
|
extension=curl
|
||||||
|
extension=fileinfo
|
||||||
|
extension=gd
|
||||||
|
extension=gettext
|
||||||
|
extension=mbstring
|
||||||
|
extension=exif
|
||||||
|
extension=mysqli
|
||||||
|
extension=openssl
|
||||||
|
extension=pdo_mysql
|
||||||
|
extension=pdo_sqlite
|
||||||
|
extension=sqlite3
|
||||||
|
|
||||||
|
[Date]
|
||||||
|
date.timezone = UTC
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { resolve } from 'path'
|
||||||
|
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
main: {
|
||||||
|
plugins: [externalizeDepsPlugin()]
|
||||||
|
},
|
||||||
|
preload: {
|
||||||
|
plugins: [externalizeDepsPlugin()]
|
||||||
|
},
|
||||||
|
renderer: {
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@renderer': resolve('src/renderer/src'),
|
||||||
|
'@locales': resolve('locales')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: [react()]
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"common": {
|
||||||
|
"start": "Starten",
|
||||||
|
"stop": "Stoppen",
|
||||||
|
"restart": "Neustart",
|
||||||
|
"settings": "Einstellungen",
|
||||||
|
"close": "Schließen",
|
||||||
|
"quit": "Beenden"
|
||||||
|
},
|
||||||
|
"dashboard": {
|
||||||
|
"title": "Trunçgil Multi-PHP Server",
|
||||||
|
"status": "Serverstatus",
|
||||||
|
"services": "Dienste",
|
||||||
|
"active_version": "Aktive PHP-Version"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"common": {
|
||||||
|
"start": "Start",
|
||||||
|
"stop": "Stop",
|
||||||
|
"restart": "Restart",
|
||||||
|
"settings": "Settings",
|
||||||
|
"close": "Close",
|
||||||
|
"quit": "Quit"
|
||||||
|
},
|
||||||
|
"dashboard": {
|
||||||
|
"title": "Trunçgil Multi-PHP Server",
|
||||||
|
"status": "Server Status",
|
||||||
|
"services": "Services",
|
||||||
|
"active_version": "Active PHP Version"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"common": {
|
||||||
|
"start": "Başlat",
|
||||||
|
"stop": "Durdur",
|
||||||
|
"restart": "Yeniden Başlat",
|
||||||
|
"settings": "Ayarlar",
|
||||||
|
"close": "Kapat",
|
||||||
|
"quit": "Çıkış"
|
||||||
|
},
|
||||||
|
"dashboard": {
|
||||||
|
"title": "Trunçgil Multi-PHP Server",
|
||||||
|
"status": "Sunucu Durumu",
|
||||||
|
"services": "Servisler",
|
||||||
|
"active_version": "Aktif PHP Sürümü"
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.5 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 99 KiB |
@@ -0,0 +1,28 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<!-- Creator: CorelDRAW -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="134.816mm" height="134.816mm" version="1.1" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd"
|
||||||
|
viewBox="0 0 2611.47 2611.47"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
xmlns:xodm="http://www.corel.com/coreldraw/odm/2003">
|
||||||
|
<defs>
|
||||||
|
<style type="text/css">
|
||||||
|
<![CDATA[
|
||||||
|
.fil0 {fill:none}
|
||||||
|
.fil2 {fill:#153E5E}
|
||||||
|
.fil1 {fill:#4A7CA3}
|
||||||
|
]]>
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<g id="Layer_x0020_1">
|
||||||
|
<metadata id="CorelCorpID_0Corel-Layer"/>
|
||||||
|
<rect class="fil0" x="-0" y="-0" width="2611.47" height="2611.47"/>
|
||||||
|
<g id="_2472301098192">
|
||||||
|
<path class="fil1" d="M1306.48 237.97c194.46,162.17 391.81,273.55 641.12,345.94 43.54,12.64 87.1,24.55 131.37,34.14 31.53,6.83 112.6,16 136.33,25.1 24.03,182.74 4.39,452.05 -32.33,623.08 -20.47,95.35 -44.74,176.01 -74.94,261.64 -41.57,117.85 -126.77,282.1 -204.18,381.09 -12.62,16.14 -23.7,32.32 -36.17,47.9 -51.41,64.29 -144.33,163.58 -206.65,213.76 -15.33,12.34 -30.17,23.69 -47,37.09 -67.56,53.78 -239.27,173.41 -308.59,197.18l-0.05 73.36c0.21,24.23 2.85,55.66 0.64,78.19l131.29 -65.78c154.48,-91.3 238.69,-150.14 366.82,-264.23 9.08,-8.08 14.36,-16.49 23.63,-24.84l96.11 -98.06c295.07,-332.43 429.55,-768.25 436.71,-1213.62 1.12,-69.52 -1.98,-311.75 -21.15,-367.4 -54.71,-9.45 -109.74,-15.42 -163.2,-24.61 -307.75,-52.95 -535.06,-168.26 -770.83,-355.05 -17.48,-13.85 -84.44,-70.37 -96.86,-87.85l-6.35 3.39c9.89,7.57 2.06,-2.77 4.01,9.94l-1.07 77.55c-0.5,29.47 -0.34,63.1 1.33,92.07z"/>
|
||||||
|
<path class="fil1" d="M1149.94 1391.32l158.69 -2.62c-4.95,24.97 -4.03,120.24 -3.1,154.68 0.24,8.76 0.67,17.14 1.14,25.87 0.5,9.19 0.71,12.68 -4.48,19.7 183.12,-3.59 295.04,-29.36 428.46,-141.13 9.96,-8.34 9.28,-8.64 16.91,-18.77 15.48,-20.54 17.72,-13.7 46.74,-60.06 66.24,-105.83 82.01,-223.22 56.4,-348.15 -15.89,-77.5 -67.88,-134.16 -116.01,-168.87 -54.4,-39.23 -130.81,-63.96 -210.45,-73.45 -46.37,-5.53 -542.39,-9.05 -561.95,-2.83 -1.65,2.24 -2.07,2.35 -3.84,6.71 -1.64,4.05 -1.94,5.53 -2.86,10.05 -1.34,6.56 -2.2,14.95 -3.33,22.57l-50.55 295.78c-14.61,88.01 -51.6,262.22 -59.12,338.98l56.43 -47.25c38.63,-29.48 90.42,-54.48 140.15,-66.85 37.6,-9.35 56.12,-10.17 75.4,-14.6l8.19 -2.6c16.19,-5.57 52.71,-331.28 63.53,-340.73 13.75,-5.94 128.78,-1.87 154.69,-1.85 51.09,0.04 102.6,-2.68 148.12,11.97 177.44,57.09 127.52,316.42 -41.22,377.76 -89.58,32.56 -188.3,15.54 -278.54,20.41 -15.09,0.82 -10.77,-1.75 -19.42,5.29z"/>
|
||||||
|
<path class="fil2" d="M1306.48 237.97c-1.67,-28.97 -1.83,-62.6 -1.33,-92.07l1.07 -77.55c-1.96,-12.71 5.88,-2.37 -4.01,-9.94 -38.3,31.85 -56.38,54.5 -98,86.5 -35.58,27.36 -67.05,56.05 -106.45,81.05 -44.4,28.17 -53.94,39.48 -113.88,73.77 -175.06,100.13 -345.78,162 -549.16,198.47 -35.53,6.37 -137.56,14.85 -162.44,24.63 -41.54,233.19 -19,588.9 32.74,820.36 27.06,121.04 68.48,229.39 113.07,336.84 32.69,78.78 71.69,156.4 154.22,182.82 47.21,15.11 56.01,5.94 98.65,-0.83 -6.22,-20.64 -55.22,-92.46 -70.54,-121.36 -22.28,-42.02 -41.7,-81.84 -60.98,-126.6 -85.55,-198.66 -129.87,-390.44 -146.35,-610.96 -6.66,-89.1 -11.15,-276.47 4.48,-361.2 45.12,-9.05 92.56,-14.66 136.75,-24.2 269.06,-58.11 514.14,-173.99 725.96,-342.23l46.22 -37.48z"/>
|
||||||
|
<path class="fil2" d="M1302.2 1588.95c5.19,-7.01 4.98,-10.51 4.48,-19.7 -0.48,-8.73 -0.9,-17.1 -1.14,-25.87 -0.93,-34.44 -1.85,-129.71 3.1,-154.68l-158.69 2.62c-19.34,-10.23 -114.59,16.75 -135.23,25.98 -310.88,138.92 -138.63,409.03 -313.06,505.54 -39.74,21.99 -92.36,23.85 -138.46,9.83 -20.23,-6.15 -40.16,-13.4 -58.15,-22.63 -19.06,-9.77 -34.13,-22.71 -51.45,-31.65 31.82,93.72 107.08,161.93 202.07,185.58 96.19,23.95 224.85,-1 292.9,-68.67 84.32,-83.85 93.08,-145.15 112.91,-268.68 21.51,-134.07 20.89,-145.96 240.73,-137.66z"/>
|
||||||
|
<path class="fil2" d="M1306.03 2556.45c2.21,-22.53 -0.44,-53.96 -0.64,-78.19l0.05 -73.36c-34.48,-12.8 -113.35,-61.59 -143.36,-80.88 -47.41,-30.47 -88.52,-58.43 -131.95,-91.4 -25.66,-19.48 -100.95,-77.27 -122.52,-100.65 -25.31,-27.43 -12.83,-13.7 -79.21,1.58 -37.71,8.68 -60.66,7.92 -98.94,9.95 3.93,11.6 30.66,36.25 46.3,52.45 30.56,31.64 114.04,100.57 147.58,127.4 88.9,71.13 197.15,138.59 299.43,192.43 24.96,13.14 57.22,32.13 83.26,40.66z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.3 KiB |
Generated
+9553
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"name": "multiphp",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Multi-PHP & MySQL Server (Electron)",
|
||||||
|
"main": "./out/main/index.js",
|
||||||
|
"author": "Trunçgil",
|
||||||
|
"homepage": "https://electron-vite.org",
|
||||||
|
"scripts": {
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
|
||||||
|
"typecheck:node": "tsc -p tsconfig.node.json --noEmit",
|
||||||
|
"typecheck:web": "tsc -p tsconfig.web.json --noEmit",
|
||||||
|
"typecheck": "npm run typecheck:node && npm run typecheck:web",
|
||||||
|
"start": "electron-vite preview",
|
||||||
|
"dev": "electron-vite dev",
|
||||||
|
"build": "npm run typecheck && electron-vite build",
|
||||||
|
"postinstall": "electron-builder install-app-deps",
|
||||||
|
"build:unpack": "npm run build && electron-builder --dir",
|
||||||
|
"build:win": "npm run build && electron-builder --win",
|
||||||
|
"build:mac": "npm run build && electron-builder --mac",
|
||||||
|
"build:linux": "npm run build && electron-builder --linux"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@electron-toolkit/preload": "^3.0.1",
|
||||||
|
"@electron-toolkit/utils": "^3.0.0",
|
||||||
|
"@emotion/react": "^11.11.0",
|
||||||
|
"@emotion/styled": "^11.11.0",
|
||||||
|
"@mui/icons-material": "^6.0.0",
|
||||||
|
"@mui/material": "^6.0.0",
|
||||||
|
"axios": "^1.14.0",
|
||||||
|
"decompress": "^4.2.1",
|
||||||
|
"i18next": "^23.1.0",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-i18next": "^13.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@electron-toolkit/eslint-config": "^1.0.2",
|
||||||
|
"@electron-toolkit/eslint-config-prettier": "^2.0.0",
|
||||||
|
"@electron-toolkit/tsconfig": "^1.0.1",
|
||||||
|
"@types/decompress": "^4.2.7",
|
||||||
|
"@types/node": "^20.14.8",
|
||||||
|
"@types/react": "^18.3.3",
|
||||||
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
|
"electron": "^31.7.7",
|
||||||
|
"electron-builder": "^24.13.3",
|
||||||
|
"electron-vite": "^2.3.0",
|
||||||
|
"eslint": "^8.57.0",
|
||||||
|
"eslint-plugin-react": "^7.34.3",
|
||||||
|
"prettier": "^3.3.2",
|
||||||
|
"typescript": "^5.5.2",
|
||||||
|
"vite": "^5.3.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import { app, shell, BrowserWindow, Tray, Menu, nativeImage } from 'electron'
|
||||||
|
import { join } from 'path'
|
||||||
|
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
|
||||||
|
import { registerIpcHandlers } from './ipc'
|
||||||
|
|
||||||
|
let tray: Tray | null = null
|
||||||
|
let mainWindow: BrowserWindow | null = null
|
||||||
|
|
||||||
|
function createTray(): void {
|
||||||
|
// Use a simple colored pixel if no icon is available for now
|
||||||
|
const icon = nativeImage.createFromPath(join(__dirname, '../../resources/icon.png'))
|
||||||
|
tray = new Tray(icon.isEmpty() ? nativeImage.createEmpty() : icon)
|
||||||
|
|
||||||
|
const contextMenu = Menu.buildFromTemplate([
|
||||||
|
{ label: 'Trunçgil Multi-PHP', enabled: false },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{
|
||||||
|
label: 'Göster/Gizle', click: () => {
|
||||||
|
if (mainWindow) {
|
||||||
|
if (mainWindow.isVisible()) mainWindow.hide()
|
||||||
|
else mainWindow.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ type: 'separator' },
|
||||||
|
{ label: 'Başlat (Tüm Servisler)', click: () => { /* TODO: Start services */ } },
|
||||||
|
{ label: 'Durdur (Tüm Servisler)', click: () => { /* TODO: Stop services */ } },
|
||||||
|
{ type: 'separator' },
|
||||||
|
{
|
||||||
|
label: 'Çıkış', click: () => {
|
||||||
|
app.quit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
tray.setToolTip('Trunçgil Multi-PHP Server')
|
||||||
|
tray.setContextMenu(contextMenu)
|
||||||
|
|
||||||
|
tray.on('double-click', () => {
|
||||||
|
if (mainWindow) mainWindow.show()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function createWindow(): void {
|
||||||
|
mainWindow = new BrowserWindow({
|
||||||
|
width: 1000,
|
||||||
|
height: 750,
|
||||||
|
show: false,
|
||||||
|
autoHideMenuBar: true,
|
||||||
|
webPreferences: {
|
||||||
|
preload: join(__dirname, '../preload/index.js'),
|
||||||
|
sandbox: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
mainWindow.on('ready-to-show', () => {
|
||||||
|
if (mainWindow) mainWindow.show()
|
||||||
|
})
|
||||||
|
|
||||||
|
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||||
|
shell.openExternal(details.url)
|
||||||
|
return { action: 'deny' }
|
||||||
|
})
|
||||||
|
|
||||||
|
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
||||||
|
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
|
||||||
|
} else {
|
||||||
|
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app.whenReady().then(() => {
|
||||||
|
electronApp.setAppUserModelId('com.electron')
|
||||||
|
|
||||||
|
app.on('browser-window-created', (_, window) => {
|
||||||
|
optimizer.watchWindowShortcuts(window)
|
||||||
|
})
|
||||||
|
|
||||||
|
createWindow()
|
||||||
|
createTray()
|
||||||
|
registerIpcHandlers()
|
||||||
|
|
||||||
|
app.on('activate', function () {
|
||||||
|
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.on('window-all-closed', () => {
|
||||||
|
// Overriding default behavior: stay in tray if all windows closed
|
||||||
|
// unless on certain platforms where we really want to quit.
|
||||||
|
// For this server app, we often want it to stay alive in Tray.
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
// macOS behavior
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { ipcMain, app } from 'electron'
|
||||||
|
import { processManager } from '../services/ProcessManager'
|
||||||
|
import { configService } from '../services/ConfigService'
|
||||||
|
import { downloadService } from '../services/DownloadService'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
export function registerIpcHandlers(): void {
|
||||||
|
// Service management
|
||||||
|
ipcMain.handle('services:start', async (_event, serviceName: string) => {
|
||||||
|
try {
|
||||||
|
if (serviceName === 'nginx') {
|
||||||
|
const rootPath = path.join(app.getAppPath(), 'www')
|
||||||
|
const configPath = await configService.generateConfig('nginx.conf.template', 'nginx.conf', {
|
||||||
|
PORT: '80',
|
||||||
|
ROOT: rootPath
|
||||||
|
})
|
||||||
|
const binPath = downloadService.getBinPath('nginx', 'nginx.exe')
|
||||||
|
const success = await processManager.startService('nginx', binPath, ['-c', configPath])
|
||||||
|
return { success, message: success ? 'Nginx başlatıldı.' : 'Nginx başlatılamadı.' }
|
||||||
|
}
|
||||||
|
|
||||||
|
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ı.' }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serviceName === 'mysql') {
|
||||||
|
const dataDir = path.join(app.getPath('userData'), 'mysql_data')
|
||||||
|
const configPath = await configService.generateConfig('my.ini.template', 'my.ini', {
|
||||||
|
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: false, message: 'Bilinmeyen servis.' }
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Service start error:', error)
|
||||||
|
return { success: false, message: `Hata: ${error.message}` }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('services:stop', async (_event, serviceName: string) => {
|
||||||
|
const success = await processManager.stopService(serviceName)
|
||||||
|
return { success, message: success ? `${serviceName} durduruldu.` : `${serviceName} zaten durmuş.` }
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('services:status', async () => {
|
||||||
|
return processManager.getStatuses()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Binary management
|
||||||
|
ipcMain.handle('binaries:download', async (_event, { name, url }) => {
|
||||||
|
return downloadService.downloadAndExtract(url, name, (p) => {
|
||||||
|
// TODO: Emit progress to renderer via webContents.send
|
||||||
|
console.log(`Download progress for ${name}: ${p}%`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Config management
|
||||||
|
ipcMain.handle('config:get', async (_event, key: string) => {
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('config:save', async (_event, config: any) => {
|
||||||
|
return { success: true }
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
import { app } from 'electron'
|
||||||
|
|
||||||
|
export class ConfigService {
|
||||||
|
private configDir: string
|
||||||
|
private templateDir: string
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.templateDir = path.join(app.getAppPath(), 'config')
|
||||||
|
this.configDir = path.join(app.getPath('userData'), 'generated_configs')
|
||||||
|
|
||||||
|
if (!fs.existsSync(this.configDir)) {
|
||||||
|
fs.mkdirSync(this.configDir, { recursive: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateConfig(templateName: string, targetName: string, variables: Record<string, string>): Promise<string> {
|
||||||
|
const templatePath = path.join(this.templateDir, templateName)
|
||||||
|
const targetPath = path.join(this.configDir, targetName)
|
||||||
|
|
||||||
|
if (!fs.existsSync(templatePath)) {
|
||||||
|
throw new Error(`Template not found: ${templatePath}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = fs.readFileSync(templatePath, 'utf-8')
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(variables)) {
|
||||||
|
const escapedValue = value.replace(/\\/g, '/') // Use forward slashes for cross-platform compatibility in configs
|
||||||
|
content = content.replace(new RegExp(`{{${key}}}`, 'g'), escapedValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(targetPath, content)
|
||||||
|
return targetPath
|
||||||
|
}
|
||||||
|
|
||||||
|
getGeneratedPath(targetName: string): string {
|
||||||
|
return path.join(this.configDir, targetName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const configService = new ConfigService()
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
import { app } from 'electron'
|
||||||
|
import * as decompress from 'decompress'
|
||||||
|
|
||||||
|
export class DownloadService {
|
||||||
|
private binDir: string
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.binDir = path.join(app.getAppPath(), 'bin')
|
||||||
|
if (!fs.existsSync(this.binDir)) {
|
||||||
|
fs.mkdirSync(this.binDir, { recursive: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async downloadAndExtract(url: string, targetName: string, onProgress?: (p: number) => void): Promise<string> {
|
||||||
|
const tempFile = path.join(app.getPath('temp'), `${targetName}.zip`)
|
||||||
|
const targetDir = path.join(this.binDir, targetName)
|
||||||
|
|
||||||
|
if (fs.existsSync(targetDir)) {
|
||||||
|
return targetDir
|
||||||
|
}
|
||||||
|
|
||||||
|
const writer = fs.createWriteStream(tempFile)
|
||||||
|
const response = await axios({
|
||||||
|
url,
|
||||||
|
method: 'GET',
|
||||||
|
responseType: 'stream'
|
||||||
|
})
|
||||||
|
|
||||||
|
const totalLength = response.headers['content-length']
|
||||||
|
let downloadedLength = 0
|
||||||
|
|
||||||
|
response.data.on('data', (chunk: any) => {
|
||||||
|
downloadedLength += chunk.length
|
||||||
|
if (onProgress && totalLength) {
|
||||||
|
onProgress(Math.round((downloadedLength / totalLength) * 100))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
response.data.pipe(writer)
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
writer.on('finish', async () => {
|
||||||
|
try {
|
||||||
|
await decompress(tempFile, targetDir)
|
||||||
|
fs.unlinkSync(tempFile)
|
||||||
|
resolve(targetDir)
|
||||||
|
} catch (err) {
|
||||||
|
reject(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
writer.on('error', reject)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
getBinPath(service: string, executable: string): string {
|
||||||
|
return path.join(this.binDir, service, executable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const downloadService = new DownloadService()
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { exec } from 'child_process'
|
||||||
|
import { promisify } from 'util'
|
||||||
|
|
||||||
|
const execAsync = promisify(exec)
|
||||||
|
|
||||||
|
export interface OSOperations {
|
||||||
|
addHostEntry(ip: string, host: string): Promise<boolean>
|
||||||
|
isElevated(): Promise<boolean>
|
||||||
|
runElevated(command: string): Promise<{ stdout: string, stderr: string }>
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WindowsOperations implements OSOperations {
|
||||||
|
async addHostEntry(ip: string, host: string): Promise<boolean> {
|
||||||
|
const entry = `${ip} ${host}`
|
||||||
|
const hostsPath = 'C:\\Windows\\System32\\drivers\\etc\\hosts'
|
||||||
|
try {
|
||||||
|
// Use powershell to append to hosts file if not exists
|
||||||
|
const command = `powershell -Command "if (!(Get-Content ${hostsPath} | Select-String '${host}')) { Add-Content ${hostsPath} '${entry}' }"`
|
||||||
|
await this.runElevated(command)
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to add host entry:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async isElevated(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await execAsync('net session')
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async runElevated(command: string): Promise<{ stdout: string, stderr: string }> {
|
||||||
|
// In a real app, we would use sudo-prompt or a similar library.
|
||||||
|
// For now, we assume the app might be run as admin or we use a basic exec.
|
||||||
|
return execAsync(command)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const osOps: OSOperations = new WindowsOperations()
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import { spawn, ChildProcess } from 'child_process'
|
||||||
|
import { join } from 'path'
|
||||||
|
import { app } from 'electron'
|
||||||
|
import fs from 'fs'
|
||||||
|
|
||||||
|
export class ProcessManager {
|
||||||
|
private processes: Map<string, ChildProcess> = new Map()
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.processes = new Map()
|
||||||
|
}
|
||||||
|
|
||||||
|
async startService(name: string, binPath: string, args: string[]): Promise<boolean> {
|
||||||
|
if (this.processes.has(name)) {
|
||||||
|
console.log(`Service ${name} is already running.`)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(binPath)) {
|
||||||
|
console.warn(`[DEV] Executable not found: ${binPath}. Mocking service ${name}.`)
|
||||||
|
const mockChild = {
|
||||||
|
kill: () => { },
|
||||||
|
on: () => { },
|
||||||
|
unref: () => { }
|
||||||
|
} as unknown as ChildProcess
|
||||||
|
this.processes.set(name, mockChild)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const child = spawn(binPath, args, {
|
||||||
|
detached: true,
|
||||||
|
stdio: 'ignore'
|
||||||
|
})
|
||||||
|
|
||||||
|
child.on('error', (err) => {
|
||||||
|
console.error(`Failed to start service ${name}:`, err)
|
||||||
|
this.processes.delete(name)
|
||||||
|
})
|
||||||
|
|
||||||
|
child.on('exit', (code) => {
|
||||||
|
console.log(`Service ${name} exited with code ${code}`)
|
||||||
|
this.processes.delete(name)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.processes.set(name, child)
|
||||||
|
child.unref() // Allow the parent to exit independently
|
||||||
|
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error starting service ${name}:`, error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async stopService(name: string): Promise<boolean> {
|
||||||
|
const child = this.processes.get(name)
|
||||||
|
if (child) {
|
||||||
|
child.kill()
|
||||||
|
this.processes.delete(name)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
isServiceRunning(name: string): boolean {
|
||||||
|
return this.processes.has(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
getStatuses(): Record<string, string> {
|
||||||
|
return {
|
||||||
|
nginx: this.isServiceRunning('nginx') ? 'running' : 'stopped',
|
||||||
|
php: this.isServiceRunning('php') ? 'running' : 'stopped',
|
||||||
|
mysql: this.isServiceRunning('mysql') ? 'running' : 'stopped'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stopAll(): void {
|
||||||
|
for (const name of this.processes.keys()) {
|
||||||
|
this.stopService(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const processManager = new ProcessManager()
|
||||||
|
app.on('before-quit', () => processManager.stopAll())
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { contextBridge } from 'electron'
|
||||||
|
import { electronAPI } from '@electron-toolkit/preload'
|
||||||
|
|
||||||
|
// Custom APIs for renderer
|
||||||
|
const api = {
|
||||||
|
startService: (name: string) => electronAPI.ipcRenderer.invoke('services:start', name),
|
||||||
|
stopService: (name: string) => electronAPI.ipcRenderer.invoke('services:stop', name),
|
||||||
|
getServiceStatus: () => electronAPI.ipcRenderer.invoke('services:status'),
|
||||||
|
getConfig: (key: string) => electronAPI.ipcRenderer.invoke('config:get', key),
|
||||||
|
saveConfig: (config: any) => electronAPI.ipcRenderer.invoke('config:save', config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use `contextBridge` APIs to expose Electron APIs to
|
||||||
|
// renderer only if context isolation is enabled, otherwise
|
||||||
|
// just add to the DOM global.
|
||||||
|
if (process.contextIsolated) {
|
||||||
|
try {
|
||||||
|
contextBridge.exposeInMainWorld('electron', electronAPI)
|
||||||
|
contextBridge.exposeInMainWorld('api', api)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// @ts-ignore (define in dts)
|
||||||
|
window.electron = electronAPI
|
||||||
|
// @ts-ignore (define in dts)
|
||||||
|
window.api = api
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Trunçgil Multi-PHP & MySQL Server</title>
|
||||||
|
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
|
||||||
|
<meta
|
||||||
|
http-equiv="Content-Security-Policy"
|
||||||
|
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,249 @@
|
|||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Typography,
|
||||||
|
Drawer,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemButton,
|
||||||
|
ListItemIcon,
|
||||||
|
ListItemText,
|
||||||
|
AppBar,
|
||||||
|
Toolbar,
|
||||||
|
IconButton,
|
||||||
|
Tooltip,
|
||||||
|
Container,
|
||||||
|
Paper,
|
||||||
|
Chip,
|
||||||
|
Switch,
|
||||||
|
Divider,
|
||||||
|
Button,
|
||||||
|
Snackbar,
|
||||||
|
Alert
|
||||||
|
} from '@mui/material'
|
||||||
|
import {
|
||||||
|
Dashboard as DashboardIcon,
|
||||||
|
Folder as ProjectIcon,
|
||||||
|
Settings as SettingsIcon,
|
||||||
|
Language as LanguageIcon,
|
||||||
|
Storage as DbIcon,
|
||||||
|
Dns as ServerIcon,
|
||||||
|
Code as PhpIcon,
|
||||||
|
PlayArrow as StartIcon,
|
||||||
|
Stop as StopIcon,
|
||||||
|
Refresh as RestartIcon
|
||||||
|
} from '@mui/icons-material'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
api: any
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const drawerWidth = 240
|
||||||
|
|
||||||
|
interface ServiceStatus {
|
||||||
|
nginx: string
|
||||||
|
php: string
|
||||||
|
mysql: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function App(): JSX.Element {
|
||||||
|
const { t, i18n } = useTranslation()
|
||||||
|
const [status, setStatus] = useState<ServiceStatus>({
|
||||||
|
nginx: 'stopped',
|
||||||
|
php: 'stopped',
|
||||||
|
mysql: 'stopped'
|
||||||
|
})
|
||||||
|
const [activeTab, setActiveTab] = useState('dashboard')
|
||||||
|
const [notification, setNotification] = useState<{ open: boolean, message: string, severity: 'success' | 'error' }>({
|
||||||
|
open: false,
|
||||||
|
message: '',
|
||||||
|
severity: 'success'
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchStatus = async () => {
|
||||||
|
// @ts-ignore
|
||||||
|
const currentStatus = await window.api.getServiceStatus()
|
||||||
|
if (currentStatus) setStatus(currentStatus)
|
||||||
|
}
|
||||||
|
fetchStatus()
|
||||||
|
const interval = setInterval(fetchStatus, 3000)
|
||||||
|
return () => clearInterval(interval)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleToggleService = async (service: keyof ServiceStatus) => {
|
||||||
|
// @ts-ignore
|
||||||
|
const result = status[service] === 'running'
|
||||||
|
? await window.api.stopService(service)
|
||||||
|
: await window.api.startService(service)
|
||||||
|
|
||||||
|
setNotification({
|
||||||
|
open: true,
|
||||||
|
message: result.message,
|
||||||
|
severity: result.success ? 'success' : 'error'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleLanguage = () => {
|
||||||
|
const newLang = i18n.language === 'tr' ? 'en' : i18n.language === 'en' ? 'de' : 'tr'
|
||||||
|
i18n.changeLanguage(newLang)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleStartAll = async () => {
|
||||||
|
const services: (keyof ServiceStatus)[] = ['nginx', 'php', 'mysql']
|
||||||
|
for (const s of services) {
|
||||||
|
if (status[s] !== 'running') {
|
||||||
|
// @ts-ignore
|
||||||
|
await window.api.startService(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setNotification({ open: true, message: 'Tüm servisler başlatıldı.', severity: 'success' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleStopAll = async () => {
|
||||||
|
const services: (keyof ServiceStatus)[] = ['nginx', 'php', 'mysql']
|
||||||
|
for (const s of services) {
|
||||||
|
if (status[s] === 'running') {
|
||||||
|
// @ts-ignore
|
||||||
|
await window.api.stopService(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setNotification({ open: true, message: 'Tüm servisler durduruldu.', severity: 'success' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusChip = (s: string) => {
|
||||||
|
const color = s === 'running' ? 'success' : s === 'stopped' ? 'error' : 'warning'
|
||||||
|
return <Chip label={s.toUpperCase()} color={color} size="small" sx={{ fontWeight: 'bold' }} />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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)' }}>
|
||||||
|
<Toolbar>
|
||||||
|
<Typography variant="h6" noWrap component="div" sx={{ flexGrow: 1, fontWeight: 'bold' }}>
|
||||||
|
Trunçgil Multi-PHP Server
|
||||||
|
</Typography>
|
||||||
|
<Tooltip title="Dil Değiştir">
|
||||||
|
<IconButton onClick={toggleLanguage} color="inherit">
|
||||||
|
<LanguageIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<IconButton color="inherit">
|
||||||
|
<SettingsIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Toolbar>
|
||||||
|
</AppBar>
|
||||||
|
|
||||||
|
<Drawer
|
||||||
|
variant="permanent"
|
||||||
|
sx={{
|
||||||
|
width: drawerWidth,
|
||||||
|
flexShrink: 0,
|
||||||
|
[`& .MuiDrawer-paper`]: { width: drawerWidth, boxSizing: 'border-box', bgcolor: 'rgba(30, 30, 30, 0.95)', borderRight: '1px solid rgba(255, 255, 255, 0.05)' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Toolbar />
|
||||||
|
<Box sx={{ overflow: 'auto' }}>
|
||||||
|
<List>
|
||||||
|
<ListItem disablePadding>
|
||||||
|
<ListItemButton selected={activeTab === 'dashboard'} onClick={() => setActiveTab('dashboard')}>
|
||||||
|
<ListItemIcon><DashboardIcon color={activeTab === 'dashboard' ? 'primary' : 'inherit'} /></ListItemIcon>
|
||||||
|
<ListItemText primary="Dashboard" />
|
||||||
|
</ListItemButton>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem disablePadding>
|
||||||
|
<ListItemButton selected={activeTab === 'projects'} onClick={() => setActiveTab('projects')}>
|
||||||
|
<ListItemIcon><ProjectIcon color={activeTab === 'projects' ? 'primary' : 'inherit'} /></ListItemIcon>
|
||||||
|
<ListItemText primary="Projeler" />
|
||||||
|
</ListItemButton>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
<Divider />
|
||||||
|
<List>
|
||||||
|
<ListItem disablePadding>
|
||||||
|
<ListItemButton selected={activeTab === 'settings'} onClick={() => setActiveTab('settings')}>
|
||||||
|
<ListItemIcon><SettingsIcon color={activeTab === 'settings' ? 'primary' : 'inherit'} /></ListItemIcon>
|
||||||
|
<ListItemText primary="Genel Ayarlar" />
|
||||||
|
</ListItemButton>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
</Box>
|
||||||
|
</Drawer>
|
||||||
|
|
||||||
|
<Box component="main" sx={{ flexGrow: 1, p: 3 }}>
|
||||||
|
<Toolbar />
|
||||||
|
<Container maxWidth="md">
|
||||||
|
{activeTab === 'dashboard' && (
|
||||||
|
<>
|
||||||
|
<Typography variant="h5" gutterBottom sx={{ mb: 3, fontWeight: 'medium' }}>
|
||||||
|
Hızlı Bakış
|
||||||
|
</Typography>
|
||||||
|
<Paper elevation={4} sx={{ mb: 4, borderRadius: 3, overflow: 'hidden', bgcolor: 'rgba(255, 255, 255, 0.03)', backdropFilter: 'blur(10px)', border: '1px solid rgba(255, 255, 255, 0.05)' }}>
|
||||||
|
<List dense>
|
||||||
|
<ListItem sx={{ py: 1.5 }}>
|
||||||
|
<ListItemIcon><ServerIcon color="primary" /></ListItemIcon>
|
||||||
|
<ListItemText primary="Nginx Web Sunucusu" secondary="Port: 80, 443" />
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||||
|
{getStatusChip(status.nginx)}
|
||||||
|
<Switch checked={status.nginx === 'running'} onChange={() => handleToggleService('nginx')} />
|
||||||
|
</Box>
|
||||||
|
</ListItem>
|
||||||
|
<Divider variant="inset" />
|
||||||
|
<ListItem sx={{ py: 1.5 }}>
|
||||||
|
<ListItemIcon><PhpIcon color="primary" /></ListItemIcon>
|
||||||
|
<ListItemText primary="PHP FastCGI" secondary="Sürüm: Seçilmedi" />
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||||
|
{getStatusChip(status.php)}
|
||||||
|
<Switch checked={status.php === 'running'} onChange={() => handleToggleService('php')} />
|
||||||
|
</Box>
|
||||||
|
</ListItem>
|
||||||
|
<Divider variant="inset" />
|
||||||
|
<ListItem sx={{ py: 1.5 }}>
|
||||||
|
<ListItemIcon><DbIcon color="primary" /></ListItemIcon>
|
||||||
|
<ListItemText primary="MySQL / MariaDB" secondary="Port: 3306" />
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||||
|
{getStatusChip(status.mysql)}
|
||||||
|
<Switch checked={status.mysql === 'running'} onChange={() => handleToggleService('mysql')} />
|
||||||
|
</Box>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||||
|
<Button variant="contained" fullWidth startIcon={<StartIcon />} size="large" onClick={handleStartAll} sx={{ borderRadius: 2, padding: '12px', background: 'linear-gradient(45deg, #153E5E 30%, #4A7CA3 90%)', boxShadow: '0 3px 5px 2px rgba(21, 62, 94, .3)' }}>
|
||||||
|
{t('common.start')}
|
||||||
|
</Button>
|
||||||
|
<Button variant="outlined" fullWidth startIcon={<StopIcon />} size="large" color="error" onClick={handleStopAll} sx={{ borderRadius: 2, padding: '12px', borderWidth: 2, '&:hover': { borderWidth: 2 } }}>
|
||||||
|
{t('common.stop')}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'projects' && (
|
||||||
|
<Box sx={{ textAlign: 'center', mt: 4 }}>
|
||||||
|
<Typography variant="h6">Henüz bir proje eklenmemiş.</Typography>
|
||||||
|
<Button variant="contained" startIcon={<ProjectIcon />} sx={{ mt: 2 }}>
|
||||||
|
Yeni Proje Ekle
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Snackbar
|
||||||
|
open={notification.open}
|
||||||
|
autoHideDuration={4000}
|
||||||
|
onClose={() => setNotification({ ...notification, open: false })}
|
||||||
|
>
|
||||||
|
<Alert severity={notification.severity} variant="filled" sx={{ width: '100%' }}>
|
||||||
|
{notification.message}
|
||||||
|
</Alert>
|
||||||
|
</Snackbar>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import i18n from 'i18next'
|
||||||
|
import { initReactI18next } from 'react-i18next'
|
||||||
|
import tr from '@locales/tr.json'
|
||||||
|
import en from '@locales/en.json'
|
||||||
|
import de from '@locales/de.json'
|
||||||
|
|
||||||
|
i18n
|
||||||
|
.use(initReactI18next)
|
||||||
|
.init({
|
||||||
|
resources: {
|
||||||
|
tr: { translation: tr },
|
||||||
|
en: { translation: en },
|
||||||
|
de: { translation: de }
|
||||||
|
},
|
||||||
|
lng: 'tr',
|
||||||
|
fallbackLng: 'en',
|
||||||
|
interpolation: {
|
||||||
|
escapeValue: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default i18n
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom/client'
|
||||||
|
import App from './App'
|
||||||
|
import './i18n'
|
||||||
|
import { ThemeProvider, createTheme } from '@mui/material/styles'
|
||||||
|
import CssBaseline from '@mui/material/CssBaseline'
|
||||||
|
|
||||||
|
const theme = createTheme({
|
||||||
|
palette: {
|
||||||
|
primary: {
|
||||||
|
main: '#153E5E',
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
main: '#4A7CA3',
|
||||||
|
},
|
||||||
|
mode: 'dark',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<ThemeProvider theme={theme}>
|
||||||
|
<CssBaseline />
|
||||||
|
<App />
|
||||||
|
</ThemeProvider>
|
||||||
|
</React.StrictMode>
|
||||||
|
)
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.node.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.web.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"extends": "@electron-toolkit/tsconfig/tsconfig.node.json",
|
||||||
|
"include": [
|
||||||
|
"src/main/**/*",
|
||||||
|
"src/preload/**/*",
|
||||||
|
"electron.vite.config.ts"
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"downlevelIteration": true,
|
||||||
|
"types": [
|
||||||
|
"electron-vite/node"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"extends": "@electron-toolkit/tsconfig/tsconfig.web.json",
|
||||||
|
"include": [
|
||||||
|
"src/renderer/src/**/*",
|
||||||
|
"src/renderer/src/**/*.tsx",
|
||||||
|
"src/preload/**/*"
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"paths": {
|
||||||
|
"@renderer/*": [
|
||||||
|
"src/renderer/src/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user