diff --git a/.scripts/configure.electron.ts b/.scripts/configure.electron.ts new file mode 100644 index 000000000..daaa8a419 --- /dev/null +++ b/.scripts/configure.electron.ts @@ -0,0 +1,64 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { hideBin } from 'yargs/helpers'; +import yargs from 'yargs'; + +const argv: any = yargs(hideBin(process.argv)).argv; + +function modifiedNextServer() { + const filePath = path.resolve(__dirname, '../apps/server-web/release/app/dist/standalone/apps/web/server.js'); + + let fileContent = fs.readFileSync(filePath, 'utf8'); + const searchString = 'process.env.__NEXT_PRIVATE_STANDALONE_CONFIG'; + const codeToInsert = ` + nextConfig.serverRuntimeConfig = { + "GAUZY_API_SERVER_URL": process.env.GAUZY_API_SERVER_URL, + "NEXT_PUBLIC_GAUZY_API_SERVER_URL": process.env.NEXT_PUBLIC_GAUZY_API_SERVER_URL + } + `; + + let lines = fileContent.split('\n'); + const index = lines.findIndex((line) => line.includes(searchString)); + + if (index !== -1) { + lines.splice(index - 1, 0, codeToInsert); + + fileContent = lines.join('\n'); + fs.writeFileSync(filePath, fileContent, 'utf8'); + console.log('Line of code successfully inserted.'); + } else { + console.log(`The string "${searchString}" was not found in the file.`); + } +} + +function modifiedWebConstant() { + const filePath = path.resolve(__dirname, '../apps/web/app/constants.ts'); + + let fileContent = fs.readFileSync(filePath, 'utf8'); + const searchString = `export const IS_DESKTOP_APP = process.env.IS_DESKTOP_APP === 'true';`; + const codeToReplace = `export const IS_DESKTOP_APP = true;`; + + fileContent = fileContent.replace(searchString, codeToReplace); + + fs.writeFileSync(filePath, fileContent, 'utf8'); +} + +function revertWebConstant() { + const filePath = path.resolve(__dirname, '../apps/web/app/constants.ts'); + + let fileContent = fs.readFileSync(filePath, 'utf8'); + const codeToReplace = `export const IS_DESKTOP_APP = process.env.IS_DESKTOP_APP === 'true';`; + const searchString = `export const IS_DESKTOP_APP = true;`; + + fileContent = fileContent.replace(searchString, codeToReplace); + + fs.writeFileSync(filePath, fileContent, 'utf8'); +} + + +if (argv.type === 'server') { + modifiedNextServer(); + revertWebConstant(); +} else if (argv.type === 'constant') { + modifiedWebConstant(); +} diff --git a/apps/server-web/src/locales/i18n/bg/translation.json b/apps/server-web/src/locales/i18n/bg/translation.json index bcec1eff5..28ca92eeb 100644 --- a/apps/server-web/src/locales/i18n/bg/translation.json +++ b/apps/server-web/src/locales/i18n/bg/translation.json @@ -15,19 +15,19 @@ "SERVER_WINDOW": "Прозорец на сървъра" }, "MENU_APP": { - "ABOUT": "Относно", - "QUIT": "Изход", - "WINDOW": "Прозорец", - "SUBMENU": { - "SETTING": "Настройки", - "SERVER_WINDOW": "Сървърен прозорец", - "LEARN_MORE": "Научете повече", - "DOC": "Документация", - "SETTING_DEV": "Настройки за разработчици", - "SERVER_DEV": "Сървър за разработчици" + "APP_ABOUT": "Относно", + "APP_QUIT": "Изход", + "APP_WINDOW": "Прозорец", + "APP_SUBMENU": { + "APP_SETTING": "Настройки", + "APP_SERVER_WINDOW": "Сървърен прозорец", + "APP_LEARN_MORE": "Научете повече", + "APP_DOC": "Документация", + "APP_SETTING_DEV": "Настройки за разработчици", + "APP_SERVER_DEV": "Сървър за разработчици" }, - "DEV": "Разработчик", - "HELP": "Помощ" + "APP_DEV": "Разработчик", + "APP_HELP": "Помощ" }, "FORM": { "FIELDS": { diff --git a/apps/server-web/src/locales/i18n/en/translation.json b/apps/server-web/src/locales/i18n/en/translation.json index c18265dde..440e56bec 100644 --- a/apps/server-web/src/locales/i18n/en/translation.json +++ b/apps/server-web/src/locales/i18n/en/translation.json @@ -15,19 +15,19 @@ "SERVER_WINDOW": "Server Window" }, "MENU_APP": { - "ABOUT": "About", - "QUIT": "Quit", - "WINDOW": "Window", - "SUBMENU": { - "SETTING": "Setting", - "SERVER_WINDOW": "Server Window", - "LEARN_MORE": "Learn More", - "DOC": "Documentation", - "SETTING_DEV": "Setting Dev.", - "SERVER_DEV": "Server Dev." + "APP_ABOUT": "About", + "APP_QUIT": "Quit", + "APP_WINDOW": "Window", + "APP_SUBMENU": { + "APP_SETTING": "Setting", + "APP_SERVER_WINDOW": "Server Window", + "APP_LEARN_MORE": "Learn More", + "APP_DOC": "Documentation", + "APP_SETTING_DEV": "Setting Dev.", + "APP_SERVER_DEV": "Server Dev." }, - "DEV": "Developer", - "HELP": "Help" + "APP_DEV": "Developer", + "APP_HELP": "Help" }, "FORM": { "FIELDS": { diff --git a/apps/server-web/src/main/helpers/constant.ts b/apps/server-web/src/main/helpers/constant.ts index b2ebfcb96..b7d47c829 100644 --- a/apps/server-web/src/main/helpers/constant.ts +++ b/apps/server-web/src/main/helpers/constant.ts @@ -17,7 +17,9 @@ export const EventLists = { SERVER_WINDOW: 'SERVER_WINDOW', RESTART_SERVER: 'RESTART_SERVER', CHANGE_THEME: 'CHANGE_THEME', - SETUP_WINDOW: 'SETUP_WINDOW' + SETUP_WINDOW: 'SETUP_WINDOW', + SETTING_WINDOW_DEV: 'SETTING_WINDOW_DEV', + SERVER_WINDOW_DEV: 'SERVER_WINDOW_DEV' } export const SettingPageTypeMessage = { @@ -38,7 +40,8 @@ export const SettingPageTypeMessage = { updateSettingResponse: 'update-setting-response', updateCancel: 'update-cancel', restartServer: 'restart-server', - themeChange: 'theme-change' + themeChange: 'theme-change', + autoUpdateSetting: 'auto-update-setting' } export const ServerPageTypeMessage = { diff --git a/apps/server-web/src/main/helpers/interfaces/i-constant.ts b/apps/server-web/src/main/helpers/interfaces/i-constant.ts index 421769ce3..e8e439af1 100644 --- a/apps/server-web/src/main/helpers/interfaces/i-constant.ts +++ b/apps/server-web/src/main/helpers/interfaces/i-constant.ts @@ -1 +1 @@ -export type Channels = 'setting-page' | 'ipc-renderer' | 'language-set' | 'updater-page' | 'server-page' | 'theme-change' | 'current-theme'; +export type Channels = 'setting-page' | 'ipc-renderer' | 'language-set' | 'updater-page' | 'server-page' | 'theme-change' | 'current-theme' | 'current-language'; diff --git a/apps/server-web/src/main/helpers/interfaces/i-menu.ts b/apps/server-web/src/main/helpers/interfaces/i-menu.ts new file mode 100644 index 000000000..a12984192 --- /dev/null +++ b/apps/server-web/src/main/helpers/interfaces/i-menu.ts @@ -0,0 +1,15 @@ +export interface AppMenu { + id?: string; + label?: string; + click?: () => void; + submenu?: AppSubMenu[] +} + +export interface AppSubMenu { + id?: string; + label?: string; + click?: () => void; + selector?: string; + type?: string; + accelerator?: string; +} diff --git a/apps/server-web/src/main/helpers/interfaces/i-server.ts b/apps/server-web/src/main/helpers/interfaces/i-server.ts index 43f188166..a51b06d35 100644 --- a/apps/server-web/src/main/helpers/interfaces/i-server.ts +++ b/apps/server-web/src/main/helpers/interfaces/i-server.ts @@ -1,4 +1,4 @@ -interface GeneralConfig { +export interface GeneralConfig { lang?: string autoUpdate?: boolean updateCheckPeriode?: string @@ -7,7 +7,7 @@ interface GeneralConfig { [key: string]: any } -interface ServerConfig { +export interface ServerConfig { PORT: number; NEXT_PUBLIC_GAUZY_API_SERVER_URL: string; GAUZY_API_SERVER_URL: string; diff --git a/apps/server-web/src/main/helpers/interfaces/index.ts b/apps/server-web/src/main/helpers/interfaces/index.ts index 8f45b8de0..de47665c2 100644 --- a/apps/server-web/src/main/helpers/interfaces/index.ts +++ b/apps/server-web/src/main/helpers/interfaces/index.ts @@ -1,3 +1,4 @@ export * from './i-server'; export * from './i-desktop-dialog'; export * from './i-constant'; +export * from './i-menu'; diff --git a/apps/server-web/src/main/helpers/replace-config.ts b/apps/server-web/src/main/helpers/replace-config.ts index 98e12f9af..155f6aa18 100644 --- a/apps/server-web/src/main/helpers/replace-config.ts +++ b/apps/server-web/src/main/helpers/replace-config.ts @@ -42,3 +42,15 @@ export const replaceConfig = async (folderPath: string, envOptions: EnvOptions) console.log('error on replacing file', error); } } + +export const clearDesktopConfig = (folderPath: string) => { + const fileNames = ['desktop-server.body', 'desktop-server.meta']; + try { + // remove cached desktop server config + fileNames.forEach((file) => { + fs.unlinkSync(path.join(folderPath, file)); + }) + } catch (error) { + console.log('skip unlink file on not exists'); + } +} diff --git a/apps/server-web/src/main/helpers/services/desktop-server-factory.ts b/apps/server-web/src/main/helpers/services/desktop-server-factory.ts index 1a9502238..4f0b09a10 100644 --- a/apps/server-web/src/main/helpers/services/desktop-server-factory.ts +++ b/apps/server-web/src/main/helpers/services/desktop-server-factory.ts @@ -8,6 +8,7 @@ export class DesktopServerFactory { if (!this.apiInstance && !!env) { this.apiInstance = new WebService(path, env, win, signal, eventEmitter); } + this.apiInstance.env = env; return this.apiInstance; } } diff --git a/apps/server-web/src/main/helpers/services/libs/desktop-store.ts b/apps/server-web/src/main/helpers/services/libs/desktop-store.ts index 01c708563..eb21ec651 100644 --- a/apps/server-web/src/main/helpers/services/libs/desktop-store.ts +++ b/apps/server-web/src/main/helpers/services/libs/desktop-store.ts @@ -1,7 +1,7 @@ import Store from 'electron-store'; -import { WebServer } from '../../interfaces'; +import { WebServer, ServerConfig } from '../../interfaces'; const store = new Store(); -const DEFAULT_CONFIG:any = { +const DEFAULT_CONFIG:WebServer = { server: { PORT: 3002, GAUZY_API_SERVER_URL: 'http://localhost:3000', @@ -11,7 +11,7 @@ const DEFAULT_CONFIG:any = { general: { lang: 'en', autoUpdate: true, - updateCheckPeriode: '1140' + updateCheckPeriode: '1140' // Time in minutes } } export const LocalStore = { @@ -35,17 +35,41 @@ export const LocalStore = { }); }, + deepMerge(target: T, source: Partial): T { + const result: T = { ...target }; + Object.keys(source).forEach(key => { + const value = source[key as keyof T]; + if (value && typeof value === 'object') { + result[key as keyof T] = this.deepMerge( + target[key as keyof T], + value as any + ); + } else if (value !== undefined) { + result[key as keyof T] = value as any; + } + }); + return result; + }, - setDefaultServerConfig: () => { - const defaultConfig: WebServer | any = store.get('config') || {}; - Object.keys(DEFAULT_CONFIG).forEach((key) => { - Object.keys(DEFAULT_CONFIG[key]).forEach((keySub) => { - defaultConfig[key] = defaultConfig[key] || {}; - defaultConfig[key][keySub] = defaultConfig[key][keySub] || DEFAULT_CONFIG[key][keySub]; - }) - }) - store.set({ - config: defaultConfig + validateConfig(config: WebServer): void { + const required = ['PORT', 'GAUZY_API_SERVER_URL']; + required.forEach(field => { + if (!config || !config.server || !config?.server[field as keyof ServerConfig]) { + throw new Error(`Missing required field: ${field}`); + } }); + }, + + setDefaultServerConfig() { + try { + const storedConfig = store.get('config') as Partial || {}; + const mergedConfig = this.deepMerge(DEFAULT_CONFIG, storedConfig) + this.validateConfig(mergedConfig || {}); + + store.set({ config: mergedConfig }); + } catch (error) { + console.error('Failed to set default configuration:', error); + store.set({ config: DEFAULT_CONFIG }); + } } }; diff --git a/apps/server-web/src/main/helpers/services/web-service.ts b/apps/server-web/src/main/helpers/services/web-service.ts index 80d02a4ff..03ef62036 100644 --- a/apps/server-web/src/main/helpers/services/web-service.ts +++ b/apps/server-web/src/main/helpers/services/web-service.ts @@ -5,7 +5,7 @@ import { EventEmitter } from 'stream'; export class WebService extends ServerTask { constructor( readonly path: string, - readonly env: any, + public env: any, readonly window: BrowserWindow, readonly signal: AbortSignal, readonly eventEmitter: EventEmitter @@ -38,11 +38,7 @@ export class WebService extends ServerTask { } } - private setApiConfig(): void { - // Object.assign(this.args, { - // API_HOST: '0.0.0.0', - // API_PORT: this.config.setting.PORT, - // API_BASE_URL: this.config.apiUrl - // }); + public setApiConfig(): void { + Object.assign(this.args, {...this.env}); } } diff --git a/apps/server-web/src/main/main.ts b/apps/server-web/src/main/main.ts index cba85f718..404f20d97 100644 --- a/apps/server-web/src/main/main.ts +++ b/apps/server-web/src/main/main.ts @@ -10,11 +10,12 @@ import Updater from './updater'; import { mainBindings } from 'i18next-electron-fs-backend'; import i18nextMainBackend from '../configs/i18n.mainconfig'; import fs from 'fs'; -import { WebServer } from './helpers/interfaces'; -import { replaceConfig } from './helpers'; +import { WebServer, AppMenu, ServerConfig } from './helpers/interfaces'; +import { clearDesktopConfig } from './helpers'; import Log from 'electron-log'; import MenuBuilder from './menu'; import { config } from '../configs/config'; +import { debounce } from 'lodash'; console.log = Log.log; @@ -39,8 +40,6 @@ let tray: Tray; let settingWindow: BrowserWindow | null = null; let logWindow: BrowserWindow | null = null; let setupWindow: BrowserWindow | any = null; -let SettingMenu: any = null; -let ServerWindowMenu: any = null; const appMenu = new MenuBuilder(eventEmitter) Log.hooks.push((message: any, transport) => { @@ -92,7 +91,7 @@ i18nextMainBackend.on('initialized', () => { }); let trayMenuItems: any = []; -let appMenuItems: any = []; +let appMenuItems: AppMenu[] = []; const RESOURCES_PATH = app.isPackaged ? path.join(process.resourcesPath, 'assets') @@ -180,7 +179,6 @@ const createWindow = async (type: 'SETTING_WINDOW' | 'LOG_WINDOW' | 'SETUP_WINDO mainBindings(ipcMain, settingWindow, fs); settingWindow.on('closed', () => { settingWindow = null; - SettingMenu = null }); Menu.setApplicationMenu(appMenu.buildDefaultTemplate(appMenuItems, i18nextMainBackend)) break; @@ -191,7 +189,6 @@ const createWindow = async (type: 'SETTING_WINDOW' | 'LOG_WINDOW' | 'SETUP_WINDO mainBindings(ipcMain, logWindow, fs); logWindow.on('closed', () => { logWindow = null; - ServerWindowMenu = null }) Menu.setApplicationMenu(appMenu.buildDefaultTemplate(appMenuItems, i18nextMainBackend)) break; @@ -200,6 +197,8 @@ const createWindow = async (type: 'SETTING_WINDOW' | 'LOG_WINDOW' | 'SETUP_WINDO url = resolveHtmlPath('index.html', 'setup'); setupWindow?.loadURL(url); mainBindings(ipcMain, setupWindow, fs); + setupWindow?.setMenuBarVisibility(false); + Menu.setApplicationMenu(Menu.buildFromTemplate([])); setupWindow.on('closed', () => { setupWindow = null; }) @@ -212,12 +211,17 @@ const createWindow = async (type: 'SETTING_WINDOW' | 'LOG_WINDOW' | 'SETUP_WINDO const runServer = async () => { console.log('Run the Server...'); try { - const envVal: any = getEnvApi(); + const envVal: ServerConfig | undefined = getEnvApi(); + const folderPath = getWebDirPath(); + clearDesktopConfig(folderPath); // Instantiate API and UI servers await desktopServer.start( { api: serverPath }, - envVal, + { + ...envVal, + IS_DESKTOP_APP: true + }, undefined, signal ); @@ -243,7 +247,7 @@ const restartServer = async () => { }, 1000) } -const getEnvApi = () => { +const getEnvApi = (): ServerConfig | undefined => { const setting: WebServer = LocalStore.getStore('config') return setting?.server; }; @@ -257,24 +261,14 @@ const SendMessageToSettingWindow = (type: string, data: any) => { const onInitApplication = () => { // check and set default config - LocalStore.setDefaultServerConfig(); - createIntervalAutoUpdate() - trayMenuItems = trayMenuItems.length ? trayMenuItems : defaultTrayMenuItem(eventEmitter); - appMenuItems = appMenuItems.length ? appMenuItems : appMenu.defaultMenu(); - tray = _initTray(trayMenuItems, getAssetPath('icons/icon.png')); - i18nextMainBackend.on('languageChanged', (lng) => { - if (i18nextMainBackend.isInitialized) { - + const storeConfig:WebServer = LocalStore.getStore('config'); + i18nextMainBackend.on('languageChanged', debounce((lng) => { + if (i18nextMainBackend.isInitialized && storeConfig.general?.setup) { trayMenuItems = trayMenuItems.length ? trayMenuItems : defaultTrayMenuItem(eventEmitter); updateTrayMenu('none', {}, eventEmitter, tray, trayMenuItems, i18nextMainBackend); Menu.setApplicationMenu(appMenu.buildDefaultTemplate(appMenuItems, i18nextMainBackend)) } - }); - eventEmitter.on(EventLists.webServerStart, async () => { - updateTrayMenu('SERVER_START', { enabled: false }, eventEmitter, tray, trayMenuItems, i18nextMainBackend); - isServerRun = true; - await runServer(); - }) + }, 250)); eventEmitter.on(EventLists.webServerStop, async () => { await stopServer(); @@ -400,6 +394,7 @@ const onInitApplication = () => { eventEmitter.on(EventLists.SERVER_WINDOW, async () => { if (!logWindow) { + initTrayMenu() await createWindow('LOG_WINDOW'); } const serverSetting = LocalStore.getStore('config'); @@ -419,7 +414,7 @@ const onInitApplication = () => { }) eventEmitter.on(EventLists.OPEN_WEB, () => { - const envConfig = getEnvApi(); + const envConfig: ServerConfig | undefined = getEnvApi(); const url = `http://127.0.0.1:${envConfig?.PORT}` shell.openExternal(url) }) @@ -431,21 +426,48 @@ const onInitApplication = () => { eventEmitter.on(EventLists.SERVER_WINDOW_DEV, () => { logWindow?.webContents.toggleDevTools(); }) +} + +const initTrayMenu = () => { + try { + LocalStore.setDefaultServerConfig(); + createIntervalAutoUpdate() + trayMenuItems = trayMenuItems.length ? trayMenuItems : defaultTrayMenuItem(eventEmitter); + appMenuItems = appMenuItems.length ? appMenuItems : appMenu.defaultMenu(); + tray = _initTray(trayMenuItems, getAssetPath('icons/icon.png')); + } catch (error) { + console.error('Failed to initialize application:', error); + dialog.showErrorBox('Initialization Error', 'Failed to initialize application'); + } + + eventEmitter.on(EventLists.webServerStart, async () => { + updateTrayMenu('SERVER_START', { enabled: false }, eventEmitter, tray, trayMenuItems, i18nextMainBackend); + isServerRun = true; + await runServer(); + }) - eventEmitter.emit(EventLists.SERVER_WINDOW); + trayMenuItems = trayMenuItems.length ? trayMenuItems : defaultTrayMenuItem(eventEmitter); + updateTrayMenu('none', {}, eventEmitter, tray, trayMenuItems, i18nextMainBackend); } (async () => { await app.whenReady() const storeConfig:WebServer = LocalStore.getStore('config'); + onInitApplication(); if (storeConfig?.general?.setup) { - onInitApplication(); + eventEmitter.emit(EventLists.SERVER_WINDOW); } else { if (!setupWindow) { await createWindow('SETUP_WINDOW'); } if (setupWindow) { - setupWindow?.show() + setupWindow?.show(); + setupWindow?. + setupWindow?.webContents.once('did-finish-load', () => { + setTimeout(() => { + setupWindow?.webContents.send('languageSignal', storeConfig.general?.lang); + }, 50) + }) } } })() @@ -459,27 +481,23 @@ ipcMain.on('message', async (event, arg) => { event.reply('message', `${arg} World!`) }) +const getWebDirPath = () => { + const dirFiles = 'standalone/apps/web/.next/server/app/api'; + const devDirFilesPath = path.join(__dirname, resourceDir.webServer, dirFiles); + const packDirFilesPath = path.join(process.resourcesPath, 'release', 'app', 'dist', dirFiles) + const diFilesPath = isPack ? packDirFilesPath : devDirFilesPath; + return diFilesPath; +} + ipcMain.on(IPC_TYPES.SETTING_PAGE, async (event, arg) => { switch (arg.type) { case SettingPageTypeMessage.saveSetting: - const existingConfig = getEnvApi(); LocalStore.updateConfigSetting({ server: arg.data }); - const dirFiles = 'standalone/apps/web/.next'; - const devDirFilesPath = path.join(__dirname, resourceDir.webServer, dirFiles); - const packDirFilesPath = path.join(process.resourcesPath, 'release', 'app', 'dist', dirFiles) - const diFilesPath = isPack ? packDirFilesPath : devDirFilesPath; - await replaceConfig( - diFilesPath, - { - before: { - NEXT_PUBLIC_GAUZY_API_SERVER_URL: existingConfig?.NEXT_PUBLIC_GAUZY_API_SERVER_URL || config.NEXT_PUBLIC_GAUZY_API_SERVER_URL - }, - after: { - NEXT_PUBLIC_GAUZY_API_SERVER_URL: arg.data.NEXT_PUBLIC_GAUZY_API_SERVER_URL || config.NEXT_PUBLIC_GAUZY_API_SERVER_URL - } - } + const diFilesPath = getWebDirPath(); + clearDesktopConfig( + diFilesPath ) if (arg.isSetup) { LocalStore.updateConfigSetting({ @@ -488,7 +506,6 @@ ipcMain.on(IPC_TYPES.SETTING_PAGE, async (event, arg) => { } }); setupWindow?.close(); - onInitApplication(); eventEmitter.emit(EventLists.SERVER_WINDOW); } else { event.sender.send(IPC_TYPES.SETTING_PAGE, { @@ -554,6 +571,11 @@ ipcMain.handle('current-theme', async () => { return setting?.general?.theme;; }) +ipcMain.handle('current-language', async (): Promise => { + const setting: WebServer = LocalStore.getStore('config'); + return setting?.general?.lang || 'en'; +}) + const createIntervalAutoUpdate = () => { if (intervalUpdate) { clearInterval(intervalUpdate) diff --git a/apps/server-web/src/main/menu.ts b/apps/server-web/src/main/menu.ts index cec72f148..c374a32fe 100644 --- a/apps/server-web/src/main/menu.ts +++ b/apps/server-web/src/main/menu.ts @@ -7,6 +7,7 @@ import { config } from '../configs/config'; import { EventEmitter } from 'events'; import { EventLists } from './helpers/constant'; import i18n from 'i18next'; +import { AppMenu } from './helpers/interfaces'; export default class MenuBuilder { eventEmitter: EventEmitter @@ -15,7 +16,7 @@ export default class MenuBuilder { this.eventEmitter = eventEmitter } - defaultMenu() { + defaultMenu(): AppMenu[] { const isDarwin = process.platform === 'darwin'; return [ { @@ -24,7 +25,7 @@ export default class MenuBuilder { submenu: [ { id: 'MENU_APP_ABOUT', - label: `MENU_APP.ABOUT`, + label: `MENU_APP.APP_ABOUT`, selector: 'orderFrontStandardAboutPanel:', click: () => { this.eventEmitter.emit(EventLists.gotoAbout) @@ -33,7 +34,7 @@ export default class MenuBuilder { { type: 'separator' }, { id: 'MENU_APP_QUIT', - label: 'MENU_APP.QUIT', + label: 'MENU_APP.APP_QUIT', accelerator: isDarwin ? 'Command+Q' : 'Alt+F4', click: () => { app.quit(); @@ -43,18 +44,18 @@ export default class MenuBuilder { }, { id: 'MENU_APP_WINDOW', - label: 'MENU_APP.WINDOW', + label: 'MENU_APP.APP_WINDOW', submenu: [ { id: 'SUBMENU_SETTING', - label: 'MENU_APP.SUBMENU.SETTING', + label: 'MENU_APP.APP_SUBMENU.APP_SETTING', click: () => { this.eventEmitter.emit(EventLists.gotoSetting); } }, { id: 'SUBMENU_SERVER', - label: 'MENU_APP.SUBMENU.SERVER_WINDOW', + label: 'MENU_APP.APP_SUBMENU.APP_SERVER_WINDOW', click: () => { this.eventEmitter.emit(EventLists.SERVER_WINDOW); } @@ -63,18 +64,18 @@ export default class MenuBuilder { }, { id: 'MENU_APP_HELP', - label: 'MENU_APP.HELP', + label: 'MENU_APP.APP_HELP', submenu: [ { id: 'SUBMENU_LEARN_MORE', - label: 'MENU_APP.SUBMENU.LEARN_MORE', + label: 'MENU_APP.APP_SUBMENU.APP_LEARN_MORE', click() { shell.openExternal(config.COMPANY_SITE_LINK); }, }, { id: 'SUBMENU_DOC', - label: 'MENU_APP.SUBMENU.DOC', + label: 'MENU_APP.APP_SUBMENU.APP_DOC', click() { shell.openExternal( config.COMPANY_GITHUB_LINK @@ -85,18 +86,18 @@ export default class MenuBuilder { }, { id: 'MENU_APP_DEV', - label: 'MENU_APP.DEV', + label: 'MENU_APP.APP_DEV', submenu: [ { id: 'SUBMENU_SETTING_DEV', - label: 'MENU_APP.SUBMENU.SETTING_DEV', + label: 'MENU_APP.APP_SUBMENU.APP_SETTING_DEV', click: () => { this.eventEmitter.emit(EventLists.SETTING_WINDOW_DEV); }, }, { id: 'SUBMENU_SERVER_DEV', - label: 'MENU_APP.SUBMENU.SERVER_DEV', + label: 'MENU_APP.APP_SUBMENU.APP_SERVER_DEV', click: () => { this.eventEmitter.emit(EventLists.SERVER_WINDOW_DEV); }, diff --git a/apps/server-web/src/renderer/components/LanguageSelector.tsx b/apps/server-web/src/renderer/components/LanguageSelector.tsx new file mode 100644 index 000000000..fb816f536 --- /dev/null +++ b/apps/server-web/src/renderer/components/LanguageSelector.tsx @@ -0,0 +1,54 @@ +import { useTranslation } from 'react-i18next'; +import { SelectComponent } from './Select'; +import { useState } from 'react'; +import { ILanguages } from '../libs/interfaces'; +import { SettingPageTypeMessage } from '../../main/helpers/constant'; + +type LanguageSelector = { + lang: string; +}; +const LanguageSelector = ({ lang }: LanguageSelector) => { + const [langs] = useState([ + { + code: 'en', + label: 'English', + }, + { + code: 'bg', + label: 'Bulgarian', + }, + ]); + const { t } = useTranslation(); + + const changeLanguage = (data: ILanguages) => { + window.electron.ipcRenderer.sendMessage('setting-page', { + type: SettingPageTypeMessage.langChange, + data: data.code, + }); + setLng(data.code); + }; + + const [lng, setLng] = useState(lang); + + const language = langs.find((lg) => lg.code === lng) || { + code: 'en', + label: 'English', + }; + return ( + ({ + value: i.code, + label: `LANGUAGES.${i.code}`, + }))} + title={t('FORM.LABELS.LANGUAGES')} + defaultValue={language.code} + onValueChange={(val) => { + changeLanguage({ code: val }); + }} + disabled={false} + value={language.code} + /> + ); +}; + +export default LanguageSelector; diff --git a/apps/server-web/src/renderer/components/SideBar.tsx b/apps/server-web/src/renderer/components/SideBar.tsx index fd4c80ebe..f0b94c3d7 100644 --- a/apps/server-web/src/renderer/components/SideBar.tsx +++ b/apps/server-web/src/renderer/components/SideBar.tsx @@ -1,22 +1,16 @@ import { Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { ISidebarComponent } from '../libs/interfaces'; -import { SelectComponent } from './Select'; import { ThemeToggler } from './Toggler'; +import LanguageSelector from './LanguageSelector'; export function SideBar({ children, menus, menuChange, - langs, lang, - onLangChange, }: ISidebarComponent) { const { t } = useTranslation(); - const language = langs.find((lg) => lg.code === lang) || { - code: 'en', - label: 'English', - }; return (
@@ -43,19 +37,7 @@ export function SideBar({
- ({ - value: i.code, - label: `LANGUAGES.${i.code}`, - }))} - title={t('FORM.LABELS.LANGUAGES')} - defaultValue={language.code} - onValueChange={(lang) => { - onLangChange({ code: lang }); - }} - disabled={false} - value={language.code} - /> +
diff --git a/apps/server-web/src/renderer/libs/interfaces/i-components.ts b/apps/server-web/src/renderer/libs/interfaces/i-components.ts index 607e7a8dd..fba435bbe 100644 --- a/apps/server-web/src/renderer/libs/interfaces/i-components.ts +++ b/apps/server-web/src/renderer/libs/interfaces/i-components.ts @@ -1,4 +1,4 @@ -import { IUpdaterStates, IUpdateSetting, ISideMenu, ISelectItems, IServerSetting, ILanguages } from './i-setting'; +import { IUpdaterStates, IUpdateSetting, ISideMenu, ISelectItems, IServerSetting } from './i-setting'; type IToastComponent = { title: string; @@ -51,8 +51,6 @@ type ISidebarComponent = { children: string | JSX.Element | JSX.Element[]; menus: ISideMenu[]; menuChange: (key: string) => void; - langs: ILanguages[]; - onLangChange: (lang: any) => void; lang: string; }; diff --git a/apps/server-web/src/renderer/libs/interfaces/i-setting.ts b/apps/server-web/src/renderer/libs/interfaces/i-setting.ts index f09118e9e..e54d327a6 100644 --- a/apps/server-web/src/renderer/libs/interfaces/i-setting.ts +++ b/apps/server-web/src/renderer/libs/interfaces/i-setting.ts @@ -40,7 +40,7 @@ interface IPopup { interface ILanguages { code: string; - label: string; + label?: string; } type IUpdateSetting = { diff --git a/apps/server-web/src/renderer/pages/Setting.tsx b/apps/server-web/src/renderer/pages/Setting.tsx index 90b699ac7..477d0a84f 100644 --- a/apps/server-web/src/renderer/pages/Setting.tsx +++ b/apps/server-web/src/renderer/pages/Setting.tsx @@ -305,13 +305,7 @@ export function Setting() { }, []); return ( - + ); diff --git a/apps/server-web/src/renderer/pages/setup/Landing.tsx b/apps/server-web/src/renderer/pages/setup/Landing.tsx index e683418ae..e0e8ad77f 100644 --- a/apps/server-web/src/renderer/pages/setup/Landing.tsx +++ b/apps/server-web/src/renderer/pages/setup/Landing.tsx @@ -1,18 +1,27 @@ import { EverTeamsLogo } from '../../components/svgs/index'; import { config } from '../../../configs/config'; import { useTranslation } from 'react-i18next'; +import LanguageSelector from '../../components/LanguageSelector'; +import { useEffect, useState } from 'react'; type props = { nextAction: () => void; }; const Landing = (props: props) => { const { t } = useTranslation(); + const [defaultLang, setDefaultLang] = useState('en'); + + const getCurrentLanguage = async () => { + const lang = await window.electron.ipcRenderer.invoke('current-language'); + setDefaultLang(lang); + }; + + useEffect(() => { + getCurrentLanguage(); + }, []); return (
- +
diff --git a/apps/web/app/api/desktop-server/route.ts b/apps/web/app/api/desktop-server/route.ts new file mode 100644 index 000000000..c9405224c --- /dev/null +++ b/apps/web/app/api/desktop-server/route.ts @@ -0,0 +1,6 @@ +import { getDesktopConfig } from '@app/services/server/requests/desktop-source'; +import { NextResponse } from 'next/server'; + +export async function GET() { + return NextResponse.json(await getDesktopConfig()); +} diff --git a/apps/web/app/constants.ts b/apps/web/app/constants.ts index b7d0ad636..c4f477e63 100644 --- a/apps/web/app/constants.ts +++ b/apps/web/app/constants.ts @@ -1,6 +1,6 @@ import { JitsuOptions } from '@jitsu/jitsu-react/dist/useJitsu'; import { I_SMTPRequest } from './interfaces/ISmtp'; -import { getNextPublicEnv } from './env'; +import { getNextPublicEnv, getServerSideProps } from './env'; import enLanguage from '../locales/en.json'; import { BG, CN, DE, ES, FR, IS, IT, NL, PL, PT, RU, SA, US } from 'country-flag-icons/react/1x1'; import { ManualTimeReasons } from './interfaces/timer/IManualTimeReasons'; @@ -30,13 +30,20 @@ export const ACTIVE_TIMEZONE_COOKIE_NAME = 'auth-timezone'; export const NO_TEAM_POPUP_SHOW_COOKIE_NAME = 'no-team-popup-show'; export const ACTIVE_PROJECT_COOKIE_NAME = 'auth-active-project'; +export const IS_DESKTOP_APP = process.env.IS_DESKTOP_APP === 'true'; + // Recaptcha export const RECAPTCHA_SITE_KEY = getNextPublicEnv( 'NEXT_PUBLIC_CAPTCHA_SITE_KEY', process.env.NEXT_PUBLIC_CAPTCHA_SITE_KEY ); export const RECAPTCHA_SECRET_KEY = process.env.CAPTCHA_SECRET_KEY; -const basePath = process.env.GAUZY_API_SERVER_URL ? process.env.GAUZY_API_SERVER_URL : 'https://api.ever.team'; +let basePath = process.env.GAUZY_API_SERVER_URL ? process.env.GAUZY_API_SERVER_URL : 'https://api.ever.team'; +if (IS_DESKTOP_APP) { + const serverRuntimeConfig = getServerSideProps() + basePath = serverRuntimeConfig?.GAUZY_API_SERVER_URL || basePath; +} + export const GAUZY_API_SERVER_URL = basePath + '/api'; export const GAUZY_API_BASE_SERVER_URL = getNextPublicEnv( @@ -319,8 +326,6 @@ export const SLACK_CLIENT_SECRET = process.env.SLACK_CLIENT_SECRET; export const TWITTER_CLIENT_ID = process.env.TWITTER_CLIENT_ID; export const TWITTER_CLIENT_SECRET = process.env.TWITTER_CLIENT_SECRET; -export const IS_DESKTOP_APP = process.env.IS_DESKTOP_APP === 'true'; - // Add manual timer reason export const manualTimeReasons: ManualTimeReasons[] = [ diff --git a/apps/web/app/env.ts b/apps/web/app/env.ts index 2de6fd64b..187f7bfeb 100644 --- a/apps/web/app/env.ts +++ b/apps/web/app/env.ts @@ -1,3 +1,4 @@ +import getConfig from 'next/config'; const NEXT_PUBLIC_ENVS: { value: Env } = { value: {} }; type Env = Record; @@ -38,6 +39,18 @@ export function getNextPublicEnv>(name: string, optio }; } +export function getServerSideProps() { + try { + const { serverRuntimeConfig } = getConfig(); + + return { + GAUZY_API_SERVER_URL: serverRuntimeConfig.GAUZY_API_SERVER_URL, + }; + } catch(e) { + console.log('skip get config on call from client'); + } +} + export function setNextPublicEnv(envs: Env) { if (envs) { NEXT_PUBLIC_ENVS.value = { diff --git a/apps/web/app/services/client/axios.ts b/apps/web/app/services/client/axios.ts index eff23ac40..df61a233d 100644 --- a/apps/web/app/services/client/axios.ts +++ b/apps/web/app/services/client/axios.ts @@ -1,5 +1,5 @@ /* eslint-disable no-mixed-spaces-and-tabs */ -import { API_BASE_URL, APPLICATION_LANGUAGES_CODE, DEFAULT_APP_PATH, GAUZY_API_BASE_SERVER_URL } from '@app/constants'; +import { API_BASE_URL, APPLICATION_LANGUAGES_CODE, DEFAULT_APP_PATH, GAUZY_API_BASE_SERVER_URL, IS_DESKTOP_APP } from '@app/constants'; import { getAccessTokenCookie, getActiveTeamIdCookie, @@ -87,11 +87,29 @@ apiDirect.interceptors.response.use( type APIConfig = AxiosRequestConfig & { tenantId?: string; directAPI?: boolean }; -function apiConfig(config?: APIConfig) { +async function desktopServerOverride() { + if (typeof window !== 'undefined') { + try { + const serverConfig = await api.get('/desktop-server'); + return serverConfig?.data?.NEXT_PUBLIC_GAUZY_API_SERVER_URL; + } catch (error) { + return GAUZY_API_BASE_SERVER_URL + } + } + return GAUZY_API_BASE_SERVER_URL; +} + +async function apiConfig(config?: APIConfig) { const tenantId = getTenantIdCookie(); const organizationId = getOrganizationIdCookie(); let baseURL: string | undefined = GAUZY_API_BASE_SERVER_URL.value; + + if (IS_DESKTOP_APP) { // dynamic api host while on desktop mode + const runtimeConfig = await desktopServerOverride(); + baseURL = runtimeConfig; + } + baseURL = baseURL ? `${baseURL}/api` : undefined; apiDirect.defaults.baseURL = baseURL; @@ -109,22 +127,22 @@ function apiConfig(config?: APIConfig) { }; } -function get(endpoint: string, config?: APIConfig) { - const { baseURL, headers } = apiConfig(config); +async function get(endpoint: string, config?: APIConfig) { + const { baseURL, headers } = await apiConfig(config); const { directAPI = true } = config || {}; return baseURL && directAPI ? apiDirect.get(endpoint, { ...config, headers }) : api.get(endpoint); } -function deleteApi(endpoint: string, config?: APIConfig) { - const { baseURL, headers } = apiConfig(config); +async function deleteApi(endpoint: string, config?: APIConfig) { + const { baseURL, headers } = await apiConfig(config); const { directAPI = true } = config || {}; return baseURL && directAPI ? apiDirect.delete(endpoint, { ...config, headers }) : api.delete(endpoint); } -function post(url: string, data?: Record | FormData, config?: APIConfig) { - const { baseURL, headers, tenantId, organizationId } = apiConfig(config); +async function post(url: string, data?: Record | FormData, config?: APIConfig) { + const { baseURL, headers, tenantId, organizationId } = await apiConfig(config); const { directAPI = true } = config || {}; if (baseURL && directAPI && data && !(data instanceof FormData)) { @@ -139,8 +157,8 @@ function post(url: string, data?: Record | FormData, config?: AP return baseURL && directAPI ? apiDirect.post(url, data, { ...config, headers }) : api.post(url, data); } -function put(url: string, data?: Record | FormData, config?: APIConfig) { - const { baseURL, headers, tenantId, organizationId } = apiConfig(config); +async function put(url: string, data?: Record | FormData, config?: APIConfig) { + const { baseURL, headers, tenantId, organizationId } = await apiConfig(config); const { directAPI = true } = config || {}; if (baseURL && directAPI && data && !(data instanceof FormData)) { @@ -155,8 +173,8 @@ function put(url: string, data?: Record | FormData, config?: API return baseURL && directAPI ? apiDirect.put(url, data, { ...config, headers }) : api.put(url, data); } -function patch(url: string, data?: Record | FormData, config?: APIConfig) { - const { baseURL, headers, tenantId, organizationId } = apiConfig(config); +async function patch(url: string, data?: Record | FormData, config?: APIConfig) { + const { baseURL, headers, tenantId, organizationId } = await apiConfig(config); const { directAPI = true } = config || {}; if (baseURL && directAPI && data && !(data instanceof FormData)) { diff --git a/apps/web/app/services/server/requests/desktop-source.ts b/apps/web/app/services/server/requests/desktop-source.ts new file mode 100644 index 000000000..24eb4fb0e --- /dev/null +++ b/apps/web/app/services/server/requests/desktop-source.ts @@ -0,0 +1,11 @@ +import getConfig from 'next/config'; + +export function getDesktopConfig() { + try { + const { serverRuntimeConfig } = getConfig(); + return serverRuntimeConfig; + } catch (error) { + console.log('skip get server runtime config'); + return {}; + } +} diff --git a/package.json b/package.json index 4948aeb7a..e1d99a5f5 100644 --- a/package.json +++ b/package.json @@ -81,12 +81,13 @@ "spell": "cspell . --config .cspell.json", "spellcheck": "cspell .", "config:electron": "yarn ts-node ./.scripts/electron.env.ts", + "config:electron:web:build": "yarn ts-node ./.scripts/configure.electron.ts", "pack": "yarn ts-node .scripts/electron-package-utils/package-util.ts", "pack:server-web": "yarn run pack --desktop=server-web", "config:server-web": "yarn run config:electron -- --environment=prod --desktop=server-web", "dev:server-web": "cross-env NODE_ENV=production yarn prepare:config:server-web && yarn build:web:desktop && yarn run prepare:server-web && yarn copy:build:web && yarn run start:server-web", - "build:web:desktop": "cross-env NEXT_BUILD_OUTPUT_TYPE=standalone NEXT_SHARP_PATH=/temp/node_modules/sharp yarn build:web", - "copy:build:web": "pwd && cp -r apps/web/.next/standalone apps/server-web/release/app/dist && cp -r apps/web/.next/static apps/server-web/release/app/dist/standalone/apps/web/.next && cp -r apps/web/public apps/server-web/release/app/dist/standalone/apps/web/", + "build:web:desktop": "yarn config:electron:web:build -- --type=constant && cross-env NEXT_BUILD_OUTPUT_TYPE=standalone NEXT_SHARP_PATH=/tmp/node_modules/sharp yarn build:web", + "copy:build:web": "pwd && cp -r apps/web/.next/standalone apps/server-web/release/app/dist && cp -r apps/web/.next/static apps/server-web/release/app/dist/standalone/apps/web/.next && cp -r apps/web/public apps/server-web/release/app/dist/standalone/apps/web/ && yarn config:electron:web:build -- --type=server", "start:server-web": "cd apps/server-web && yarn run start", "prepare:server-web": "cd apps/server-web && yarn run prepare:electron && yarn run build", "pack:server-web:mac": "cd apps/server-web && yarn run pack:mac",