feat: implement core IPC handlers and service management for Nginx, Apache, and project configuration

This commit is contained in:
Ümit Tunç
2026-04-03 22:37:44 +03:00
parent 612fd2042b
commit a6967a7c4c
15 changed files with 453 additions and 30 deletions
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 471 KiB

+12
View File
@@ -0,0 +1,12 @@
const fs = require('fs');
const { execSync } = require('child_process');
try {
console.log('Converting icon using npx png-to-ico...');
const buffer = execSync('npx -y png-to-ico logo/icon.png');
fs.writeFileSync('build/icon.ico', buffer);
console.log('Successfully created build/icon.ico (' + buffer.length + ' bytes)');
} catch (err) {
console.error('Conversion failed:', err.message);
process.exit(1);
}
+106 -4
View File
@@ -21,6 +21,8 @@
"@types/prismjs": "^1.26.6", "@types/prismjs": "^1.26.6",
"axios": "^1.14.0", "axios": "^1.14.0",
"decompress": "^4.2.1", "decompress": "^4.2.1",
"electron-log": "^5.4.3",
"electron-updater": "^6.8.3",
"i18next": "^23.1.0", "i18next": "^23.1.0",
"pidusage": "^4.0.1", "pidusage": "^4.0.1",
"prismjs": "^1.30.0", "prismjs": "^1.30.0",
@@ -3376,7 +3378,6 @@
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true,
"license": "Python-2.0" "license": "Python-2.0"
}, },
"node_modules/array-buffer-byte-length": { "node_modules/array-buffer-byte-length": {
@@ -5207,6 +5208,15 @@
"node": ">= 10.0.0" "node": ">= 10.0.0"
} }
}, },
"node_modules/electron-log": {
"version": "5.4.3",
"resolved": "https://registry.npmjs.org/electron-log/-/electron-log-5.4.3.tgz",
"integrity": "sha512-sOUsM3LjZdugatazSQ/XTyNcw8dfvH1SYhXWiJyfYodAAKOZdHs0txPiLDXFzOZbhXgAgshQkshH2ccq0feyLQ==",
"license": "MIT",
"engines": {
"node": ">= 14"
}
},
"node_modules/electron-publish": { "node_modules/electron-publish": {
"version": "24.13.1", "version": "24.13.1",
"resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-24.13.1.tgz", "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-24.13.1.tgz",
@@ -5278,6 +5288,82 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/electron-updater": {
"version": "6.8.3",
"resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.8.3.tgz",
"integrity": "sha512-Z6sgw3jgbikWKXei1ENdqFOxBP0WlXg3TtKfz0rgw2vIZFJUyI4pD7ZN7jrkm7EoMK+tcm/qTnPUdqfZukBlBQ==",
"license": "MIT",
"dependencies": {
"builder-util-runtime": "9.5.1",
"fs-extra": "^10.1.0",
"js-yaml": "^4.1.0",
"lazy-val": "^1.0.5",
"lodash.escaperegexp": "^4.1.2",
"lodash.isequal": "^4.5.0",
"semver": "~7.7.3",
"tiny-typed-emitter": "^2.1.0"
}
},
"node_modules/electron-updater/node_modules/builder-util-runtime": {
"version": "9.5.1",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz",
"integrity": "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==",
"license": "MIT",
"dependencies": {
"debug": "^4.3.4",
"sax": "^1.2.4"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/electron-updater/node_modules/fs-extra": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/electron-updater/node_modules/jsonfile": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
"license": "MIT",
"dependencies": {
"universalify": "^2.0.0"
},
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/electron-updater/node_modules/semver": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/electron-updater/node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"license": "MIT",
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/electron-vite": { "node_modules/electron-vite": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/electron-vite/-/electron-vite-2.3.0.tgz", "resolved": "https://registry.npmjs.org/electron-vite/-/electron-vite-2.3.0.tgz",
@@ -7451,7 +7537,6 @@
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"argparse": "^2.0.1" "argparse": "^2.0.1"
@@ -7556,7 +7641,6 @@
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz",
"integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/lazystream": { "node_modules/lazystream": {
@@ -7676,6 +7760,12 @@
"license": "MIT", "license": "MIT",
"peer": true "peer": true
}, },
"node_modules/lodash.escaperegexp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz",
"integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==",
"license": "MIT"
},
"node_modules/lodash.flatten": { "node_modules/lodash.flatten": {
"version": "4.4.0", "version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
@@ -7684,6 +7774,13 @@
"license": "MIT", "license": "MIT",
"peer": true "peer": true
}, },
"node_modules/lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
"license": "MIT"
},
"node_modules/lodash.isplainobject": { "node_modules/lodash.isplainobject": {
"version": "4.0.6", "version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
@@ -9120,7 +9217,6 @@
"version": "1.6.0", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz",
"integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==",
"dev": true,
"license": "BlueOak-1.0.0", "license": "BlueOak-1.0.0",
"engines": { "engines": {
"node": ">=11.0.0" "node": ">=11.0.0"
@@ -9854,6 +9950,12 @@
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/tiny-typed-emitter": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz",
"integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==",
"license": "MIT"
},
"node_modules/tmp": { "node_modules/tmp": {
"version": "0.2.5", "version": "0.2.5",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
+40 -11
View File
@@ -17,6 +17,8 @@
"postinstall": "electron-builder install-app-deps", "postinstall": "electron-builder install-app-deps",
"build:unpack": "npm run build && electron-builder --dir", "build:unpack": "npm run build && electron-builder --dir",
"build:win": "npm run build && electron-builder --win", "build:win": "npm run build && electron-builder --win",
"dist": "npm run build && electron-builder --win",
"publish:win": "npm run build && electron-builder --win --publish always",
"build:mac": "npm run build && electron-builder --mac", "build:mac": "npm run build && electron-builder --mac",
"build:linux": "npm run build && electron-builder --linux" "build:linux": "npm run build && electron-builder --linux"
}, },
@@ -33,6 +35,8 @@
"@types/prismjs": "^1.26.6", "@types/prismjs": "^1.26.6",
"axios": "^1.14.0", "axios": "^1.14.0",
"decompress": "^4.2.1", "decompress": "^4.2.1",
"electron-log": "^5.4.3",
"electron-updater": "^6.8.3",
"i18next": "^23.1.0", "i18next": "^23.1.0",
"pidusage": "^4.0.1", "pidusage": "^4.0.1",
"prismjs": "^1.30.0", "prismjs": "^1.30.0",
@@ -63,8 +67,15 @@
"vite": "^5.3.1" "vite": "^5.3.1"
}, },
"build": { "build": {
"appId": "com.multiphp", "appId": "com.truncgil.multiphp",
"productName": "Multi-PHP", "productName": "Trunçgil MultiPHP",
"publish": [
{
"provider": "github",
"owner": "truncgil",
"repo": "multiphp"
}
],
"directories": { "directories": {
"output": "dist" "output": "dist"
}, },
@@ -76,21 +87,39 @@
], ],
"asar": true, "asar": true,
"win": { "win": {
"icon": "resources/icon.png", "icon": "build/icon.ico",
"target": [ "target": [
{ "nsis"
"target": "nsis", ]
"arch": [ },
"x64" "nsis": {
] "oneClick": false,
} "allowToChangeInstallationDirectory": true,
"createDesktopShortcut": true,
"createStartMenuShortcut": true,
"shortcutName": "Trunçgil MultiPHP",
"installerIcon": "build/icon.ico",
"uninstallerIcon": "build/icon.ico",
"installerHeaderIcon": "build/icon.ico",
"perMachine": false,
"multiLanguageInstaller": true,
"installerLanguages": [
"tr_TR",
"en_US",
"de_DE",
"ru_RU"
] ]
}, },
"mac": { "mac": {
"icon": "resources/icon.png" "category": "public.app-category.developer-tools",
"target": [
"dmg"
]
}, },
"linux": { "linux": {
"icon": "resources/icon.png" "target": [
"AppImage"
]
} }
} }
} }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 471 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 KiB

+5
View File
@@ -3,6 +3,7 @@ import { join } from 'path'
import { electronApp, optimizer, is } from '@electron-toolkit/utils' import { electronApp, optimizer, is } from '@electron-toolkit/utils'
import { registerIpcHandlers } from './ipc' import { registerIpcHandlers } from './ipc'
import { cliAliasService } from './services/CliAliasService' import { cliAliasService } from './services/CliAliasService'
import { updateService } from './services/UpdateService'
let tray: Tray | null = null let tray: Tray | null = null
let mainWindow: BrowserWindow | null = null let mainWindow: BrowserWindow | null = null
@@ -86,6 +87,10 @@ app.whenReady().then(() => {
registerIpcHandlers() registerIpcHandlers()
createWindow() createWindow()
if (mainWindow) {
updateService.setMainWindow(mainWindow)
updateService.init()
}
createTray() createTray()
// Generate CLI aliases on start // Generate CLI aliases on start
+16 -4
View File
@@ -949,6 +949,18 @@ export function registerIpcHandlers(): void {
return await shell.openPath(pathStr) return await shell.openPath(pathStr)
}) })
ipcMain.handle('www:open', async () => {
try {
const wwwPath = path.join(configService.getBasePath(), 'www')
if (!fs.existsSync(wwwPath)) fs.mkdirSync(wwwPath, { recursive: true })
await shell.openPath(wwwPath)
return { success: true }
} catch (error: any) {
console.error('Failed to open www path:', error)
return { success: false, message: error.message }
}
})
ipcMain.handle('open-vscode', async (_event, pathStr: string) => { ipcMain.handle('open-vscode', async (_event, pathStr: string) => {
return new Promise((resolve) => { return new Promise((resolve) => {
exec(`code "${pathStr}"`, (error) => { exec(`code "${pathStr}"`, (error) => {
@@ -1444,13 +1456,13 @@ export function registerIpcHandlers(): void {
ipcMain.handle('docs:open-user-guide', async (_event, locale: string) => { ipcMain.handle('docs:open-user-guide', async (_event, locale: string) => {
try { try {
const guidePath = path.join(process.cwd(), 'docs', 'user-guide', locale, 'index.md') const guidePath = path.join(configService.getAppPath(), 'docs', 'user-guide', locale, 'index.md')
if (fs.existsSync(guidePath)) { if (fs.existsSync(guidePath)) {
await shell.openPath(guidePath) await shell.openPath(guidePath)
return { success: true } return { success: true }
} else { } else {
// Fallback to directory or English if specific locale file doesn't exist // Fallback to directory or English if specific locale file doesn't exist
const langDir = path.join(process.cwd(), 'docs', 'user-guide', locale) const langDir = path.join(configService.getAppPath(), 'docs', 'user-guide', locale)
if (fs.existsSync(langDir)) { if (fs.existsSync(langDir)) {
await shell.openPath(langDir) await shell.openPath(langDir)
return { success: true } return { success: true }
@@ -1465,7 +1477,7 @@ export function registerIpcHandlers(): void {
ipcMain.handle('docs:list-topics', async (_event, locale: string) => { ipcMain.handle('docs:list-topics', async (_event, locale: string) => {
try { try {
const localeDir = path.join(process.cwd(), 'docs', 'user-guide', locale) const localeDir = path.join(configService.getAppPath(), 'docs', 'user-guide', locale)
if (!fs.existsSync(localeDir)) return [] if (!fs.existsSync(localeDir)) return []
const files = fs.readdirSync(localeDir).filter(f => f.endsWith('.md')) const files = fs.readdirSync(localeDir).filter(f => f.endsWith('.md'))
@@ -1493,7 +1505,7 @@ export function registerIpcHandlers(): void {
ipcMain.handle('docs:read-topic', async (_event, { locale, file }) => { ipcMain.handle('docs:read-topic', async (_event, { locale, file }) => {
try { try {
const filePath = path.join(process.cwd(), 'docs', 'user-guide', locale, file) const filePath = path.join(configService.getAppPath(), 'docs', 'user-guide', locale, file)
if (fs.existsSync(filePath)) { if (fs.existsSync(filePath)) {
return fs.readFileSync(filePath, 'utf8') return fs.readFileSync(filePath, 'utf8')
} }
+50 -7
View File
@@ -23,24 +23,67 @@ export class ConfigService {
constructor() { constructor() {
this.templateDir = path.join(app.getAppPath(), 'config') this.templateDir = path.join(app.getAppPath(), 'config')
const base = this.getBasePath() const base = this.getDataPath()
this.configDir = path.join(base, 'generated_configs') this.configDir = path.join(base, 'generated_configs')
this.settingsPath = path.join(base, 'settings.json') this.settingsPath = path.join(base, 'settings.json')
// Ensure directories exist
if (!fs.existsSync(this.configDir)) { if (!fs.existsSync(this.configDir)) {
fs.mkdirSync(this.configDir, { recursive: true }) try {
fs.mkdirSync(this.configDir, { recursive: true })
} catch (e) {
console.error('Failed to create config directory:', e)
}
} }
if (!fs.existsSync(this.settingsPath)) { if (!fs.existsSync(this.settingsPath)) {
fs.writeFileSync(this.settingsPath, JSON.stringify(this.defaultSettings, null, 2)) try {
fs.writeFileSync(this.settingsPath, JSON.stringify(this.defaultSettings, null, 2))
} catch (e) {
console.error('Failed to create settings file:', e)
}
}
}
/**
* Returns the base path for writable data (configs, binaries, logs).
* If packaged, checks for write access to the executable directory.
* If not writable (e.g. Program Files), falls back to app.getPath('userData').
*/
getDataPath(): string {
if (!app.isPackaged) {
return process.cwd()
}
const exeDir = path.dirname(app.getPath('exe'))
// Check if we are in Program Files
const isProgramFiles = exeDir.toLowerCase().includes('program files')
if (isProgramFiles) {
return app.getPath('userData')
}
// Check for write access
try {
const testFile = path.join(exeDir, '.write_test')
fs.writeFileSync(testFile, 'test')
fs.unlinkSync(testFile)
return exeDir // Writable (Portable Mode)
} catch (e) {
return app.getPath('userData') // Not writable, use AppData
} }
} }
getBasePath(): string { getBasePath(): string {
if (app.isPackaged) { return this.getDataPath()
return path.dirname(app.getPath('exe')) }
}
return process.cwd() /**
* Returns the read-only application path (where templates, docs, etc. are located).
*/
getAppPath(): string {
return app.getAppPath()
} }
async generateConfig(templateName: string, targetName: string, variables: Record<string, string>): Promise<string> { async generateConfig(templateName: string, targetName: string, variables: Record<string, string>): Promise<string> {
+13 -4
View File
@@ -22,10 +22,19 @@ export class MariaDbManagerService {
constructor() { constructor() {
this.binDir = path.join(configService.getBasePath(), 'bin') this.binDir = path.join(configService.getBasePath(), 'bin')
this.catalogPath = path.join(app.getAppPath(), 'resources/mariadb_catalog.json')
if (!fs.existsSync(this.catalogPath)) { // Path resolution for both development and production (packaged)
this.catalogPath = path.join(process.cwd(), 'resources/mariadb_catalog.json') const appPath = app.getAppPath()
} const possiblePaths = [
path.join(appPath, 'resources/mariadb_catalog.json'),
path.join(appPath, '../../resources/mariadb_catalog.json'),
path.join(process.resourcesPath, 'mariadb_catalog.json'),
path.join(process.resourcesPath, 'resources/mariadb_catalog.json'),
path.join(process.cwd(), 'resources/mariadb_catalog.json')
]
this.catalogPath = possiblePaths.find(p => fs.existsSync(p)) || possiblePaths[0]
this.init() this.init()
} }
+80
View File
@@ -0,0 +1,80 @@
import { autoUpdater } from 'electron-updater'
import { ipcMain, BrowserWindow } from 'electron'
import log from 'electron-log'
// Configure logging
autoUpdater.logger = log
// @ts-ignore
autoUpdater.logger.transports.file.level = 'info'
export class UpdateService {
private mainWindow: BrowserWindow | null = null
constructor() {
this.setupHandlers()
}
setMainWindow(window: BrowserWindow) {
this.mainWindow = window
}
private setupHandlers() {
autoUpdater.on('checking-for-update', () => {
this.sendToRenderer('update-status', 'checking')
})
autoUpdater.on('update-available', (info) => {
this.sendToRenderer('update-status', 'available', info)
})
autoUpdater.on('update-not-available', (info) => {
this.sendToRenderer('update-status', 'not-available', info)
})
autoUpdater.on('error', (err) => {
let message = err.message
if (message.includes('No published versions on GitHub')) {
message = 'Henüz GitHub üzerinde yayınlanmış bir sürüm bulunmamaktadır. (No published versions on GitHub)'
}
this.sendToRenderer('update-status', 'error', message)
})
autoUpdater.on('download-progress', (progressObj) => {
this.sendToRenderer('update-progress', progressObj)
})
autoUpdater.on('update-downloaded', (info) => {
this.sendToRenderer('update-status', 'downloaded', info)
})
// IPC handlers for manual control
ipcMain.handle('check-for-updates', async () => {
try {
return await autoUpdater.checkForUpdatesAndNotify()
} catch (error: any) {
return { error: error.message }
}
})
ipcMain.handle('download-update', async () => {
return await autoUpdater.downloadUpdate()
})
ipcMain.handle('quit-and-install', () => {
autoUpdater.quitAndInstall()
})
}
private sendToRenderer(channel: string, ...args: any[]) {
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
this.mainWindow.webContents.send(channel, ...args)
}
}
init() {
// You can enable semi-automatic updates here if desired
// autoUpdater.checkForUpdatesAndNotify()
}
}
export const updateService = new UpdateService()
+13
View File
@@ -44,6 +44,19 @@ const api = {
electronAPI.ipcRenderer.on('mariadb:export:log', listener) electronAPI.ipcRenderer.on('mariadb:export:log', listener)
return () => electronAPI.ipcRenderer.removeListener('mariadb:export:log', listener) return () => electronAPI.ipcRenderer.removeListener('mariadb:export:log', listener)
}, },
checkForUpdates: () => electronAPI.ipcRenderer.invoke('check-for-updates'),
downloadUpdate: () => electronAPI.ipcRenderer.invoke('download-update'),
quitAndInstall: () => electronAPI.ipcRenderer.invoke('quit-and-install'),
onUpdateStatus: (callback: any) => {
const listener = (_event, status: string, info: any) => callback(status, info)
electronAPI.ipcRenderer.on('update-status', listener)
return () => electronAPI.ipcRenderer.removeListener('update-status', listener)
},
onUpdateProgress: (callback: any) => {
const listener = (_event, progress: any) => callback(progress)
electronAPI.ipcRenderer.on('update-progress', listener)
return () => electronAPI.ipcRenderer.removeListener('update-progress', listener)
},
invoke: (channel: string, ...args: any[]) => electronAPI.ipcRenderer.invoke(channel, ...args) invoke: (channel: string, ...args: any[]) => electronAPI.ipcRenderer.invoke(channel, ...args)
} }
+107
View File
@@ -160,6 +160,10 @@ function App(): JSX.Element {
severity: 'success' severity: 'success'
}) })
const [updateStatus, setUpdateStatus] = useState<string>('idle')
const [updateInfo, setUpdateInfo] = useState<any>(null)
const [updateProgress, setUpdateProgress] = useState<any>(null)
// Log States // Log States
const [logDialogOpen, setLogDialogOpen] = useState(false) const [logDialogOpen, setLogDialogOpen] = useState(false)
const [selectedService] = useState('') const [selectedService] = useState('')
@@ -324,6 +328,15 @@ function App(): JSX.Element {
fetchCliConfig() fetchCliConfig()
fetchPmaStatus() fetchPmaStatus()
const removeUpdateStatusListener = window.api.onUpdateStatus ? window.api.onUpdateStatus((st: string, info: any) => {
setUpdateStatus(st)
if (info) setUpdateInfo(info)
}) : null
const removeUpdateProgressListener = window.api.onUpdateProgress ? window.api.onUpdateProgress((p: any) => {
setUpdateProgress(p)
}) : null
const removePhpListener = window.api.onBinaryProgress ? window.api.onBinaryProgress((data) => { const removePhpListener = window.api.onBinaryProgress ? window.api.onBinaryProgress((data) => {
if (!data) return if (!data) return
const { id, progress } = data const { id, progress } = data
@@ -379,6 +392,8 @@ function App(): JSX.Element {
if (typeof removeNginxListener === 'function') removeNginxListener() if (typeof removeNginxListener === 'function') removeNginxListener()
if (typeof removeApacheListener === 'function') removeApacheListener() if (typeof removeApacheListener === 'function') removeApacheListener()
if (typeof removePmaListener === 'function') removePmaListener() if (typeof removePmaListener === 'function') removePmaListener()
if (typeof removeUpdateStatusListener === 'function') removeUpdateStatusListener()
if (typeof removeUpdateProgressListener === 'function') removeUpdateProgressListener()
} }
}, [logDialogOpen, selectedService]) }, [logDialogOpen, selectedService])
@@ -779,6 +794,16 @@ function App(): JSX.Element {
} }
} }
const handleCheckForUpdates = async () => {
if (!window.api) return
setUpdateStatus('checking')
const result = await window.api.checkForUpdates()
if (result && result.error) {
setUpdateStatus('error')
setNotification({ open: true, message: result.error, severity: 'error' })
}
}
const installedPhpVersions = phpVersions.filter(v => v.status === 'installed') const installedPhpVersions = phpVersions.filter(v => v.status === 'installed')
@@ -987,6 +1012,16 @@ function App(): JSX.Element {
<ListItemText primary={t('mariadb_process_monitor.title')} primaryTypographyProps={{ className: 'text-sm font-medium' }} /> <ListItemText primary={t('mariadb_process_monitor.title')} primaryTypographyProps={{ className: 'text-sm font-medium' }} />
</ListItemButton> </ListItemButton>
</ListItem> </ListItem>
<Divider sx={{ opacity: 0.05, my: 0.5 }} />
<ListItem disablePadding>
<ListItemButton onClick={() => window.api.invoke('www:open')} sx={{ py: 0.5 }}>
<ListItemIcon sx={{ minWidth: 40 }}><FolderOpenIcon fontSize="small" sx={{ color: 'var(--color-primary)' }} /></ListItemIcon>
<ListItemText
primary={t('common.open_www', 'WWW Klasörünü Aç')}
primaryTypographyProps={{ className: 'text-sm font-medium', sx: { color: 'var(--color-primary)' } }}
/>
</ListItemButton>
</ListItem>
</List> </List>
<Divider sx={{ opacity: 0.05 }} /> <Divider sx={{ opacity: 0.05 }} />
<List> <List>
@@ -1029,6 +1064,7 @@ function App(): JSX.Element {
<Tab label="Apache" icon={<ServerIcon />} iconPosition="start" /> <Tab label="Apache" icon={<ServerIcon />} iconPosition="start" />
<Tab label={t('settings.server_settings')} icon={<SettingsIcon />} iconPosition="start" /> <Tab label={t('settings.server_settings')} icon={<SettingsIcon />} iconPosition="start" />
<Tab label={t('settings.cli_env')} icon={<TerminalIcon />} iconPosition="start" /> <Tab label={t('settings.cli_env')} icon={<TerminalIcon />} iconPosition="start" />
<Tab label={t('updates.title')} icon={<RefreshIcon />} iconPosition="start" />
</Tabs> </Tabs>
</Box> </Box>
@@ -1421,6 +1457,77 @@ function App(): JSX.Element {
</Paper> </Paper>
</Box> </Box>
)} )}
{settingsTab === 6 && (
<Paper sx={{ p: 4, bgcolor: 'rgba(255, 255, 255, 0.03)', borderRadius: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 3, gap: 2 }}>
<RefreshIcon color="primary" sx={{ fontSize: 32 }} />
<Box>
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>{t('updates.title')}</Typography>
<Typography variant="body2" color="text.secondary">v1.0.0</Typography>
</Box>
</Box>
<Box sx={{ p: 3, bgcolor: 'rgba(0,0,0,0.2)', borderRadius: 2, border: '1px solid rgba(255,255,255,0.05)', mb: 3 }}>
{updateStatus === 'idle' && (
<Box sx={{ textAlign: 'center', py: 2 }}>
<Typography variant="body1" sx={{ mb: 2 }}>{t('updates.not_available')}</Typography>
<Button variant="contained" onClick={handleCheckForUpdates} startIcon={<RefreshIcon />}>
{t('updates.check_btn')}
</Button>
</Box>
)}
{updateStatus === 'checking' && (
<Box sx={{ textAlign: 'center', py: 2 }}>
<CircularProgress size={24} sx={{ mb: 2 }} />
<Typography variant="body1">{t('updates.checking')}</Typography>
</Box>
)}
{updateStatus === 'available' && (
<Box sx={{ textAlign: 'center', py: 2 }}>
<AvailableIcon color="primary" sx={{ fontSize: 48, mb: 1 }} />
<Typography variant="h6" sx={{ mb: 1 }}>{t('updates.available', { version: updateInfo?.version })}</Typography>
<Button variant="contained" onClick={() => window.api.downloadUpdate()} startIcon={<DownloadIcon />}>
{t('common.download')}
</Button>
</Box>
)}
{updateStatus === 'downloading' && (
<Box sx={{ py: 2 }}>
<Typography variant="body1" sx={{ mb: 1, textAlign: 'center' }}>{t('updates.downloading')}</Typography>
<LinearProgress variant="determinate" value={updateProgress?.percent || 0} sx={{ height: 10, borderRadius: 5, mb: 1 }} />
<Typography variant="caption" sx={{ display: 'block', textAlign: 'center' }}>
%{Math.round(updateProgress?.percent || 0)} - {Math.round((updateProgress?.bytesPerSecond || 0) / 1024)} KB/s
</Typography>
</Box>
)}
{updateStatus === 'downloaded' && (
<Box sx={{ textAlign: 'center', py: 2 }}>
<InstalledIcon color="success" sx={{ fontSize: 48, mb: 1 }} />
<Typography variant="body1" sx={{ mb: 2 }}>{t('updates.downloaded')}</Typography>
<Button variant="contained" color="success" onClick={() => window.api.quitAndInstall()} startIcon={<RefreshIcon />}>
{t('updates.install_btn')}
</Button>
</Box>
)}
{updateStatus === 'error' && (
<Box sx={{ textAlign: 'center', py: 2 }}>
<Typography color="error" sx={{ mb: 2 }}>{t('updates.error', { error: updateInfo })}</Typography>
<Button variant="outlined" onClick={handleCheckForUpdates}>{t('updates.check_btn')}</Button>
</Box>
)}
</Box>
<Alert severity="info" sx={{ bgcolor: 'rgba(2, 136, 209, 0.05)', color: '#91d5ff', '& .MuiAlert-icon': { color: '#91d5ff' } }}>
{t('settings.cli_restart_warning')}
</Alert>
</Paper>
)}
</Box> </Box>
)} )}
+11
View File
@@ -358,5 +358,16 @@
"delete_db_confirm": "'{{db}}' veritabanını silmek istediğinize emin misiniz? Bu işlem geri alınamaz!", "delete_db_confirm": "'{{db}}' veritabanını silmek istediğinize emin misiniz? Bu işlem geri alınamaz!",
"delete_user_confirm": "'{{user}}' kullanıcısını silmek istediğinize emin misiniz?", "delete_user_confirm": "'{{user}}' kullanıcısını silmek istediğinize emin misiniz?",
"no_active_version": "Lütfen önce yüklü bir MariaDB sürümü seçin veya bir sürüm indirin." "no_active_version": "Lütfen önce yüklü bir MariaDB sürümü seçin veya bir sürüm indirin."
},
"updates": {
"title": "Güncellemeler",
"check_btn": "Güncellemeleri Denetle",
"checking": "Güncellemeler denetleniyor...",
"available": "Yeni bir güncelleme mevcut: {{version}}",
"not_available": "Uygulama zaten güncel.",
"downloading": "Güncelleme indiriliyor...",
"downloaded": "Güncelleme indirildi. Yüklemek için yeniden başlatın.",
"install_btn": "Yeniden Başlat ve Yükle",
"error": "Güncelleme hatası: {{error}}"
} }
} }