diff --git a/build/icon.ico b/build/icon.ico new file mode 100644 index 0000000..4fac4d9 Binary files /dev/null and b/build/icon.ico differ diff --git a/build/icon.png b/build/icon.png new file mode 100644 index 0000000..9ddad5a Binary files /dev/null and b/build/icon.png differ diff --git a/convert_icon.js b/convert_icon.js new file mode 100644 index 0000000..66266fe --- /dev/null +++ b/convert_icon.js @@ -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); +} diff --git a/package-lock.json b/package-lock.json index c8d60c1..c5e738b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,8 @@ "@types/prismjs": "^1.26.6", "axios": "^1.14.0", "decompress": "^4.2.1", + "electron-log": "^5.4.3", + "electron-updater": "^6.8.3", "i18next": "^23.1.0", "pidusage": "^4.0.1", "prismjs": "^1.30.0", @@ -3376,7 +3378,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/array-buffer-byte-length": { @@ -5207,6 +5208,15 @@ "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": { "version": "24.13.1", "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-24.13.1.tgz", @@ -5278,6 +5288,82 @@ "dev": true, "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": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/electron-vite/-/electron-vite-2.3.0.tgz", @@ -7451,7 +7537,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -7556,7 +7641,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", - "dev": true, "license": "MIT" }, "node_modules/lazystream": { @@ -7676,6 +7760,12 @@ "license": "MIT", "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": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", @@ -7684,6 +7774,13 @@ "license": "MIT", "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": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", @@ -9120,7 +9217,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", - "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=11.0.0" @@ -9854,6 +9950,12 @@ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "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": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", diff --git a/package.json b/package.json index ea2f7f9..c29b53d 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,8 @@ "postinstall": "electron-builder install-app-deps", "build:unpack": "npm run build && electron-builder --dir", "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:linux": "npm run build && electron-builder --linux" }, @@ -33,6 +35,8 @@ "@types/prismjs": "^1.26.6", "axios": "^1.14.0", "decompress": "^4.2.1", + "electron-log": "^5.4.3", + "electron-updater": "^6.8.3", "i18next": "^23.1.0", "pidusage": "^4.0.1", "prismjs": "^1.30.0", @@ -63,8 +67,15 @@ "vite": "^5.3.1" }, "build": { - "appId": "com.multiphp", - "productName": "Multi-PHP", + "appId": "com.truncgil.multiphp", + "productName": "Trunçgil MultiPHP", + "publish": [ + { + "provider": "github", + "owner": "truncgil", + "repo": "multiphp" + } + ], "directories": { "output": "dist" }, @@ -76,21 +87,39 @@ ], "asar": true, "win": { - "icon": "resources/icon.png", + "icon": "build/icon.ico", "target": [ - { - "target": "nsis", - "arch": [ - "x64" - ] - } + "nsis" + ] + }, + "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": { - "icon": "resources/icon.png" + "category": "public.app-category.developer-tools", + "target": [ + "dmg" + ] }, "linux": { - "icon": "resources/icon.png" + "target": [ + "AppImage" + ] } } } diff --git a/resources/icon.png b/resources/icon.png index 1c6651e..9ddad5a 100644 Binary files a/resources/icon.png and b/resources/icon.png differ diff --git a/resources/icon_256.png b/resources/icon_256.png new file mode 100644 index 0000000..84a9971 Binary files /dev/null and b/resources/icon_256.png differ diff --git a/src/main/index.ts b/src/main/index.ts index 5ccc820..273fff1 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -3,6 +3,7 @@ import { join } from 'path' import { electronApp, optimizer, is } from '@electron-toolkit/utils' import { registerIpcHandlers } from './ipc' import { cliAliasService } from './services/CliAliasService' +import { updateService } from './services/UpdateService' let tray: Tray | null = null let mainWindow: BrowserWindow | null = null @@ -86,6 +87,10 @@ app.whenReady().then(() => { registerIpcHandlers() createWindow() + if (mainWindow) { + updateService.setMainWindow(mainWindow) + updateService.init() + } createTray() // Generate CLI aliases on start diff --git a/src/main/ipc/index.ts b/src/main/ipc/index.ts index eaaa4bd..9243429 100644 --- a/src/main/ipc/index.ts +++ b/src/main/ipc/index.ts @@ -949,6 +949,18 @@ export function registerIpcHandlers(): void { 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) => { return new Promise((resolve) => { exec(`code "${pathStr}"`, (error) => { @@ -1444,13 +1456,13 @@ export function registerIpcHandlers(): void { ipcMain.handle('docs:open-user-guide', async (_event, locale: string) => { 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)) { await shell.openPath(guidePath) return { success: true } } else { // 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)) { await shell.openPath(langDir) return { success: true } @@ -1465,7 +1477,7 @@ export function registerIpcHandlers(): void { ipcMain.handle('docs:list-topics', async (_event, locale: string) => { 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 [] 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 }) => { 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)) { return fs.readFileSync(filePath, 'utf8') } diff --git a/src/main/services/ConfigService.ts b/src/main/services/ConfigService.ts index b162445..43fe1c4 100644 --- a/src/main/services/ConfigService.ts +++ b/src/main/services/ConfigService.ts @@ -23,24 +23,67 @@ export class ConfigService { constructor() { this.templateDir = path.join(app.getAppPath(), 'config') - const base = this.getBasePath() + const base = this.getDataPath() this.configDir = path.join(base, 'generated_configs') this.settingsPath = path.join(base, 'settings.json') + // Ensure directories exist 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)) { - 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 { - if (app.isPackaged) { - return path.dirname(app.getPath('exe')) - } - return process.cwd() + return this.getDataPath() + } + + /** + * 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): Promise { diff --git a/src/main/services/MariaDbManagerService.ts b/src/main/services/MariaDbManagerService.ts index 46696c4..93f6d1c 100644 --- a/src/main/services/MariaDbManagerService.ts +++ b/src/main/services/MariaDbManagerService.ts @@ -22,10 +22,19 @@ export class MariaDbManagerService { constructor() { this.binDir = path.join(configService.getBasePath(), 'bin') - this.catalogPath = path.join(app.getAppPath(), 'resources/mariadb_catalog.json') - if (!fs.existsSync(this.catalogPath)) { - this.catalogPath = path.join(process.cwd(), 'resources/mariadb_catalog.json') - } + + // Path resolution for both development and production (packaged) + 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() } diff --git a/src/main/services/UpdateService.ts b/src/main/services/UpdateService.ts new file mode 100644 index 0000000..1aa4b7a --- /dev/null +++ b/src/main/services/UpdateService.ts @@ -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() diff --git a/src/preload/index.ts b/src/preload/index.ts index c28717c..582a577 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -44,6 +44,19 @@ const api = { electronAPI.ipcRenderer.on('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) } diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 07d2eb0..bef14c3 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -160,6 +160,10 @@ function App(): JSX.Element { severity: 'success' }) + const [updateStatus, setUpdateStatus] = useState('idle') + const [updateInfo, setUpdateInfo] = useState(null) + const [updateProgress, setUpdateProgress] = useState(null) + // Log States const [logDialogOpen, setLogDialogOpen] = useState(false) const [selectedService] = useState('') @@ -324,6 +328,15 @@ function App(): JSX.Element { fetchCliConfig() 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) => { if (!data) return const { id, progress } = data @@ -379,6 +392,8 @@ function App(): JSX.Element { if (typeof removeNginxListener === 'function') removeNginxListener() if (typeof removeApacheListener === 'function') removeApacheListener() if (typeof removePmaListener === 'function') removePmaListener() + if (typeof removeUpdateStatusListener === 'function') removeUpdateStatusListener() + if (typeof removeUpdateProgressListener === 'function') removeUpdateProgressListener() } }, [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') @@ -987,6 +1012,16 @@ function App(): JSX.Element { + + + window.api.invoke('www:open')} sx={{ py: 0.5 }}> + + + + @@ -1029,6 +1064,7 @@ function App(): JSX.Element { } iconPosition="start" /> } iconPosition="start" /> } iconPosition="start" /> + } iconPosition="start" /> @@ -1421,6 +1457,77 @@ function App(): JSX.Element { )} + + {settingsTab === 6 && ( + + + + + {t('updates.title')} + v1.0.0 + + + + + {updateStatus === 'idle' && ( + + {t('updates.not_available')} + + + )} + + {updateStatus === 'checking' && ( + + + {t('updates.checking')} + + )} + + {updateStatus === 'available' && ( + + + {t('updates.available', { version: updateInfo?.version })} + + + )} + + {updateStatus === 'downloading' && ( + + {t('updates.downloading')} + + + %{Math.round(updateProgress?.percent || 0)} - {Math.round((updateProgress?.bytesPerSecond || 0) / 1024)} KB/s + + + )} + + {updateStatus === 'downloaded' && ( + + + {t('updates.downloaded')} + + + )} + + {updateStatus === 'error' && ( + + {t('updates.error', { error: updateInfo })} + + + )} + + + + {t('settings.cli_restart_warning')} + + + )} )} diff --git a/src/renderer/src/locales/tr.json b/src/renderer/src/locales/tr.json index daa5f37..3261982 100644 --- a/src/renderer/src/locales/tr.json +++ b/src/renderer/src/locales/tr.json @@ -358,5 +358,16 @@ "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?", "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}}" } } \ No newline at end of file