feat: implement core IPC handlers and service management for Nginx, Apache, and project configuration
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 279 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 471 KiB |
@@ -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);
|
||||||
|
}
|
||||||
Generated
+106
-4
@@ -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
@@ -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 |
@@ -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
@@ -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')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -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}}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user