Initial TrueGrowth source import
This commit is contained in:
16
apps/electron/README.md
Normal file
16
apps/electron/README.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# TrueGrowth Electron Shell
|
||||
|
||||
This directory is the V1 desktop shell. The shell loads the React renderer and
|
||||
starts the lightweight Local API Gateway at
|
||||
`http://127.0.0.1:48177/local-api`.
|
||||
|
||||
AIGCPanel is vendored under `vendor/aigcpanel` and is intentionally treated as
|
||||
the local runtime reference/source. Its Vue renderer is not mounted in
|
||||
TrueGrowth V1.
|
||||
|
||||
Run the web renderer first, then start the desktop shell:
|
||||
|
||||
```bash
|
||||
NX_DAEMON=false pnpm nx serve web --host 127.0.0.1 --port 7200
|
||||
pnpm electron:dev
|
||||
```
|
||||
BIN
apps/electron/assets/truegrowth-dock-icon.png
Normal file
BIN
apps/electron/assets/truegrowth-dock-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
BIN
apps/electron/build/icon.icns
Normal file
BIN
apps/electron/build/icon.icns
Normal file
Binary file not shown.
BIN
apps/electron/build/icon.ico
Normal file
BIN
apps/electron/build/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 109 KiB |
BIN
apps/electron/build/icon.png
Normal file
BIN
apps/electron/build/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
360
apps/electron/main.mjs
Normal file
360
apps/electron/main.mjs
Normal file
@@ -0,0 +1,360 @@
|
||||
import {
|
||||
app,
|
||||
BrowserWindow,
|
||||
ipcMain,
|
||||
Menu,
|
||||
nativeTheme,
|
||||
shell,
|
||||
utilityProcess,
|
||||
} from 'electron';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const brandName = 'TrueGrowth';
|
||||
const rendererUrl = process.env.VITE_DEV_SERVER_URL;
|
||||
const localApiPort = Number(process.env.TRUEGROWTH_LOCAL_API_PORT || 48177);
|
||||
const userDataPath =
|
||||
process.env.TRUEGROWTH_ELECTRON_USER_DATA ||
|
||||
path.join(app.getPath('appData'), brandName);
|
||||
const isDev = Boolean(rendererUrl);
|
||||
const appRoot = app.isPackaged
|
||||
? app.getAppPath()
|
||||
: path.resolve(__dirname, '../..');
|
||||
const brandIconPath = resolveAppPath(
|
||||
'apps/electron/assets/truegrowth-dock-icon.png'
|
||||
);
|
||||
|
||||
let mainWindow = null;
|
||||
let localApi = null;
|
||||
|
||||
function readAppVersion() {
|
||||
try {
|
||||
const packageInfo = JSON.parse(
|
||||
fs.readFileSync(resolveAppPath('package.json'), 'utf8')
|
||||
);
|
||||
return packageInfo.version || app.getVersion();
|
||||
} catch {
|
||||
return app.getVersion();
|
||||
}
|
||||
}
|
||||
|
||||
const appVersion = readAppVersion();
|
||||
|
||||
function resolveAppPath(...segments) {
|
||||
return path.join(appRoot, ...segments);
|
||||
}
|
||||
|
||||
function createDesktopLocalRendererEntry(indexHtmlPath) {
|
||||
const html = fs.readFileSync(indexHtmlPath, 'utf8');
|
||||
const appAssetPattern =
|
||||
/https:\/\/cdn\.jsdelivr\.net\/npm\/aitu-app@[^/"'>\s]+\/([^"'<>\s]+)/g;
|
||||
|
||||
return html.replace(appAssetPattern, (_url, assetPath) => `./${assetPath}`);
|
||||
}
|
||||
|
||||
process.title = brandName;
|
||||
app.setName(brandName);
|
||||
app.setAppUserModelId('ai.truegrowth.desktop');
|
||||
app.setPath('userData', userDataPath);
|
||||
process.env.TRUEGROWTH_ELECTRON_USER_DATA = userDataPath;
|
||||
process.env.TRUEGROWTH_RUNTIME_STATE_DIR =
|
||||
process.env.TRUEGROWTH_RUNTIME_STATE_DIR ||
|
||||
path.join(userDataPath, 'runtime');
|
||||
if (!isDev) {
|
||||
process.env.TRUEGROWTH_DESKTOP_PACKAGED =
|
||||
process.env.TRUEGROWTH_DESKTOP_PACKAGED || '1';
|
||||
}
|
||||
process.env.TRUEGROWTH_DESKTOP_APP_ROOT =
|
||||
process.env.TRUEGROWTH_DESKTOP_APP_ROOT || appRoot;
|
||||
process.env.TRUEGROWTH_DESKTOP_RESOURCE_DIR =
|
||||
process.env.TRUEGROWTH_DESKTOP_RESOURCE_DIR ||
|
||||
(app.isPackaged ? process.resourcesPath : appRoot);
|
||||
|
||||
const hasSingleInstanceLock = app.requestSingleInstanceLock({
|
||||
rendererUrl,
|
||||
userDataPath,
|
||||
});
|
||||
|
||||
if (!hasSingleInstanceLock) {
|
||||
app.quit();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
app.on('second-instance', () => {
|
||||
if (!mainWindow) {
|
||||
return;
|
||||
}
|
||||
if (mainWindow.isMinimized()) {
|
||||
mainWindow.restore();
|
||||
}
|
||||
mainWindow.focus();
|
||||
mainWindow.show();
|
||||
});
|
||||
|
||||
function getWindowState(window) {
|
||||
return {
|
||||
isFullScreen: window?.isFullScreen?.() ?? false,
|
||||
isMaximized: window?.isMaximized?.() ?? false,
|
||||
platform: process.platform,
|
||||
};
|
||||
}
|
||||
|
||||
function getSystemThemeState() {
|
||||
const theme = nativeTheme.shouldUseDarkColors ? 'dark' : 'light';
|
||||
return {
|
||||
theme,
|
||||
shouldUseDarkColors: theme === 'dark',
|
||||
source: 'system',
|
||||
};
|
||||
}
|
||||
|
||||
function sendWindowState(window) {
|
||||
if (!window || window.isDestroyed()) {
|
||||
return;
|
||||
}
|
||||
window.webContents.send('truegrowth-window:state', getWindowState(window));
|
||||
}
|
||||
|
||||
function sendSystemThemeState(window) {
|
||||
if (!window || window.isDestroyed()) {
|
||||
return;
|
||||
}
|
||||
const systemTheme = getSystemThemeState();
|
||||
window.setBackgroundColor(
|
||||
systemTheme.theme === 'dark' ? '#060606' : '#f7f7f5'
|
||||
);
|
||||
window.webContents.send('truegrowth-theme:system', systemTheme);
|
||||
}
|
||||
|
||||
function getWindowFromIpcEvent(event) {
|
||||
const window = BrowserWindow.fromWebContents(event.sender);
|
||||
return window && !window.isDestroyed() ? window : null;
|
||||
}
|
||||
|
||||
function installWindowControlsIpc() {
|
||||
ipcMain.handle('truegrowth-window:minimize', (event) => {
|
||||
const window = getWindowFromIpcEvent(event);
|
||||
window?.minimize();
|
||||
return window ? getWindowState(window) : getWindowState(mainWindow);
|
||||
});
|
||||
|
||||
ipcMain.handle('truegrowth-window:toggle-maximize', (event) => {
|
||||
const window = getWindowFromIpcEvent(event);
|
||||
if (!window) {
|
||||
return getWindowState(mainWindow);
|
||||
}
|
||||
if (window.isFullScreen()) {
|
||||
window.setFullScreen(false);
|
||||
} else if (window.isMaximized()) {
|
||||
window.unmaximize();
|
||||
} else {
|
||||
window.maximize();
|
||||
}
|
||||
return getWindowState(window);
|
||||
});
|
||||
|
||||
ipcMain.handle('truegrowth-window:close', (event) => {
|
||||
const window = getWindowFromIpcEvent(event);
|
||||
window?.close();
|
||||
return { closed: Boolean(window) };
|
||||
});
|
||||
|
||||
ipcMain.handle('truegrowth-window:is-maximized', (event) => {
|
||||
const window = getWindowFromIpcEvent(event);
|
||||
return getWindowState(window || mainWindow);
|
||||
});
|
||||
}
|
||||
|
||||
function installThemeIpc() {
|
||||
ipcMain.handle('truegrowth-theme:get-system', () => getSystemThemeState());
|
||||
|
||||
nativeTheme.on('updated', () => {
|
||||
BrowserWindow.getAllWindows().forEach(sendSystemThemeState);
|
||||
});
|
||||
}
|
||||
|
||||
function installApplicationMenu() {
|
||||
const editMenu = [
|
||||
{ role: 'undo' },
|
||||
{ role: 'redo' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'cut' },
|
||||
{ role: 'copy' },
|
||||
{ role: 'paste' },
|
||||
{ role: 'selectAll' },
|
||||
];
|
||||
const viewMenu = [
|
||||
{ role: 'reload' },
|
||||
{ role: 'forceReload' },
|
||||
{ role: 'toggleDevTools' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'resetZoom' },
|
||||
{ role: 'zoomIn' },
|
||||
{ role: 'zoomOut' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'togglefullscreen' },
|
||||
];
|
||||
const windowMenu = [
|
||||
{ role: 'minimize' },
|
||||
{ role: 'zoom' },
|
||||
...(process.platform === 'darwin'
|
||||
? [{ type: 'separator' }, { role: 'front' }]
|
||||
: [{ role: 'close' }]),
|
||||
];
|
||||
const template = [
|
||||
...(process.platform === 'darwin'
|
||||
? [
|
||||
{
|
||||
label: brandName,
|
||||
submenu: [
|
||||
{ role: 'about', label: `About ${brandName}` },
|
||||
{ type: 'separator' },
|
||||
{ role: 'services' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'hide', label: `Hide ${brandName}` },
|
||||
{ role: 'hideOthers' },
|
||||
{ role: 'unhide' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'quit', label: `Quit ${brandName}` },
|
||||
],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: 'File',
|
||||
submenu: [
|
||||
process.platform === 'darwin' ? { role: 'close' } : { role: 'quit' },
|
||||
],
|
||||
},
|
||||
{ label: 'Edit', submenu: editMenu },
|
||||
{ label: 'View', submenu: viewMenu },
|
||||
{ label: 'Window', submenu: windowMenu },
|
||||
{ label: 'Help', submenu: [] },
|
||||
];
|
||||
|
||||
Menu.setApplicationMenu(Menu.buildFromTemplate(template));
|
||||
}
|
||||
|
||||
function createWindow() {
|
||||
const initialSystemTheme = getSystemThemeState().theme;
|
||||
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1440,
|
||||
height: 920,
|
||||
minWidth: 1180,
|
||||
minHeight: 760,
|
||||
title: brandName,
|
||||
icon: brandIconPath,
|
||||
frame: false,
|
||||
autoHideMenuBar: true,
|
||||
backgroundColor: initialSystemTheme === 'dark' ? '#060606' : '#f7f7f5',
|
||||
webPreferences: {
|
||||
preload: resolveAppPath('apps/electron/preload.cjs'),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
sandbox: false,
|
||||
},
|
||||
});
|
||||
|
||||
if (process.platform !== 'darwin') {
|
||||
mainWindow.setMenuBarVisibility(false);
|
||||
}
|
||||
|
||||
mainWindow.on('closed', () => {
|
||||
mainWindow = null;
|
||||
});
|
||||
|
||||
[
|
||||
'enter-full-screen',
|
||||
'leave-full-screen',
|
||||
'maximize',
|
||||
'resize',
|
||||
'restore',
|
||||
'unmaximize',
|
||||
].forEach((eventName) => {
|
||||
mainWindow.on(eventName, () => sendWindowState(mainWindow));
|
||||
});
|
||||
|
||||
mainWindow.webContents.once('did-finish-load', () => {
|
||||
sendWindowState(mainWindow);
|
||||
sendSystemThemeState(mainWindow);
|
||||
});
|
||||
|
||||
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
||||
void shell.openExternal(url);
|
||||
return { action: 'deny' };
|
||||
});
|
||||
|
||||
if (isDev && rendererUrl) {
|
||||
void mainWindow.loadURL(rendererUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
const indexHtmlPath = resolveAppPath('dist/apps/web/index.html');
|
||||
const entryHtml = createDesktopLocalRendererEntry(indexHtmlPath);
|
||||
|
||||
void mainWindow.loadURL(
|
||||
`data:text/html;charset=utf-8,${encodeURIComponent(entryHtml)}`,
|
||||
{
|
||||
baseURLForDataURL: `${pathToFileURL(
|
||||
path.dirname(indexHtmlPath)
|
||||
).toString()}/`,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function startLocalApi() {
|
||||
const { createLocalApiServer } = await import(
|
||||
pathToFileURL(resolveAppPath('scripts/truegrowth-local-api.mjs')).toString()
|
||||
);
|
||||
localApi = createLocalApiServer({ utilityProcess });
|
||||
localApi.on('error', (error) => {
|
||||
if (error?.code === 'EADDRINUSE') {
|
||||
console.log(
|
||||
`TrueGrowth local API already available at http://127.0.0.1:${localApiPort}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
localApi.listen(localApiPort, '127.0.0.1', () => {
|
||||
console.log(
|
||||
`TrueGrowth local API listening at http://127.0.0.1:${localApiPort}`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
installApplicationMenu();
|
||||
installWindowControlsIpc();
|
||||
installThemeIpc();
|
||||
if (process.platform === 'darwin' && app.dock) {
|
||||
app.dock.setIcon(brandIconPath);
|
||||
}
|
||||
app.setAboutPanelOptions({
|
||||
applicationName: brandName,
|
||||
applicationVersion: appVersion,
|
||||
iconPath: brandIconPath,
|
||||
copyright: `Copyright © ${new Date().getFullYear()} TrueGrowth`,
|
||||
});
|
||||
await startLocalApi();
|
||||
createWindow();
|
||||
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on('before-quit', () => {
|
||||
localApi?.close();
|
||||
});
|
||||
315
apps/electron/main.ts
Normal file
315
apps/electron/main.ts
Normal file
@@ -0,0 +1,315 @@
|
||||
import {
|
||||
app,
|
||||
BrowserWindow,
|
||||
ipcMain,
|
||||
Menu,
|
||||
shell,
|
||||
utilityProcess,
|
||||
} from 'electron';
|
||||
import type {
|
||||
BrowserWindow as BrowserWindowType,
|
||||
IpcMainInvokeEvent,
|
||||
MenuItemConstructorOptions,
|
||||
} from 'electron';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const appRoot = path.resolve(__dirname, '../..');
|
||||
const brandName = 'TrueGrowth';
|
||||
const brandIconPath = path.join(
|
||||
appRoot,
|
||||
'apps/electron/assets/truegrowth-dock-icon.png'
|
||||
);
|
||||
const localApiPort = Number(process.env.TRUEGROWTH_LOCAL_API_PORT || 48177);
|
||||
const userDataPath =
|
||||
process.env.TRUEGROWTH_ELECTRON_USER_DATA ||
|
||||
path.join(app.getPath('appData'), brandName);
|
||||
const isDev = Boolean(process.env.VITE_DEV_SERVER_URL);
|
||||
let localApi: { close: () => void; listen: (...args: unknown[]) => unknown; on: (...args: unknown[]) => unknown } | null = null;
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
|
||||
function readAppVersion(): string {
|
||||
try {
|
||||
const packageInfo = JSON.parse(
|
||||
fs.readFileSync(path.join(appRoot, 'package.json'), 'utf8')
|
||||
) as { version?: string };
|
||||
return packageInfo.version || app.getVersion();
|
||||
} catch {
|
||||
return app.getVersion();
|
||||
}
|
||||
}
|
||||
|
||||
const appVersion = readAppVersion();
|
||||
|
||||
function createDesktopLocalRendererEntry(indexHtmlPath: string) {
|
||||
const html = fs.readFileSync(indexHtmlPath, 'utf8');
|
||||
const appAssetPattern =
|
||||
/https:\/\/cdn\.jsdelivr\.net\/npm\/aitu-app@[^/"'>\s]+\/([^"'<>\s]+)/g;
|
||||
|
||||
return html.replace(appAssetPattern, (_url, assetPath) => `./${assetPath}`);
|
||||
}
|
||||
|
||||
process.title = brandName;
|
||||
app.setName(brandName);
|
||||
app.setAppUserModelId('ai.truegrowth.desktop');
|
||||
app.setPath('userData', userDataPath);
|
||||
process.env.TRUEGROWTH_ELECTRON_USER_DATA = userDataPath;
|
||||
process.env.TRUEGROWTH_RUNTIME_STATE_DIR =
|
||||
process.env.TRUEGROWTH_RUNTIME_STATE_DIR ||
|
||||
path.join(userDataPath, 'runtime');
|
||||
if (!isDev) {
|
||||
process.env.TRUEGROWTH_DESKTOP_PACKAGED =
|
||||
process.env.TRUEGROWTH_DESKTOP_PACKAGED || '1';
|
||||
}
|
||||
|
||||
const hasSingleInstanceLock = app.requestSingleInstanceLock({
|
||||
userDataPath,
|
||||
});
|
||||
|
||||
if (!hasSingleInstanceLock) {
|
||||
app.quit();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
app.on('second-instance', () => {
|
||||
if (!mainWindow) {
|
||||
return;
|
||||
}
|
||||
if (mainWindow.isMinimized()) {
|
||||
mainWindow.restore();
|
||||
}
|
||||
mainWindow.focus();
|
||||
mainWindow.show();
|
||||
});
|
||||
|
||||
function getWindowState(window: BrowserWindowType | null) {
|
||||
return {
|
||||
isFullScreen: window?.isFullScreen?.() ?? false,
|
||||
isMaximized: window?.isMaximized?.() ?? false,
|
||||
platform: process.platform,
|
||||
};
|
||||
}
|
||||
|
||||
function sendWindowState(window: BrowserWindowType | null) {
|
||||
if (!window || window.isDestroyed()) {
|
||||
return;
|
||||
}
|
||||
window.webContents.send('truegrowth-window:state', getWindowState(window));
|
||||
}
|
||||
|
||||
function getWindowFromIpcEvent(event: IpcMainInvokeEvent) {
|
||||
const window = BrowserWindow.fromWebContents(event.sender);
|
||||
return window && !window.isDestroyed() ? window : null;
|
||||
}
|
||||
|
||||
function installWindowControlsIpc() {
|
||||
ipcMain.handle('truegrowth-window:minimize', (event) => {
|
||||
const window = getWindowFromIpcEvent(event);
|
||||
window?.minimize();
|
||||
return window ? getWindowState(window) : getWindowState(mainWindow);
|
||||
});
|
||||
|
||||
ipcMain.handle('truegrowth-window:toggle-maximize', (event) => {
|
||||
const window = getWindowFromIpcEvent(event);
|
||||
if (!window) {
|
||||
return getWindowState(mainWindow);
|
||||
}
|
||||
if (window.isFullScreen()) {
|
||||
window.setFullScreen(false);
|
||||
} else if (window.isMaximized()) {
|
||||
window.unmaximize();
|
||||
} else {
|
||||
window.maximize();
|
||||
}
|
||||
return getWindowState(window);
|
||||
});
|
||||
|
||||
ipcMain.handle('truegrowth-window:close', (event) => {
|
||||
const window = getWindowFromIpcEvent(event);
|
||||
window?.close();
|
||||
return { closed: Boolean(window) };
|
||||
});
|
||||
|
||||
ipcMain.handle('truegrowth-window:is-maximized', (event) => {
|
||||
const window = getWindowFromIpcEvent(event);
|
||||
return getWindowState(window || mainWindow);
|
||||
});
|
||||
}
|
||||
|
||||
function installApplicationMenu() {
|
||||
const editMenu: MenuItemConstructorOptions[] = [
|
||||
{ role: 'undo' },
|
||||
{ role: 'redo' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'cut' },
|
||||
{ role: 'copy' },
|
||||
{ role: 'paste' },
|
||||
{ role: 'selectAll' },
|
||||
];
|
||||
const viewMenu: MenuItemConstructorOptions[] = [
|
||||
{ role: 'reload' },
|
||||
{ role: 'forceReload' },
|
||||
{ role: 'toggleDevTools' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'resetZoom' },
|
||||
{ role: 'zoomIn' },
|
||||
{ role: 'zoomOut' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'togglefullscreen' },
|
||||
];
|
||||
const windowMenu: MenuItemConstructorOptions[] = [
|
||||
{ role: 'minimize' },
|
||||
{ role: 'zoom' },
|
||||
...(process.platform === 'darwin'
|
||||
? [{ type: 'separator' as const }, { role: 'front' as const }]
|
||||
: [{ role: 'close' as const }]),
|
||||
];
|
||||
const template: MenuItemConstructorOptions[] = [
|
||||
...(process.platform === 'darwin'
|
||||
? [
|
||||
{
|
||||
label: brandName,
|
||||
submenu: [
|
||||
{ role: 'about', label: `About ${brandName}` },
|
||||
{ type: 'separator' },
|
||||
{ role: 'services' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'hide', label: `Hide ${brandName}` },
|
||||
{ role: 'hideOthers' },
|
||||
{ role: 'unhide' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'quit', label: `Quit ${brandName}` },
|
||||
],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: 'File',
|
||||
submenu: [
|
||||
process.platform === 'darwin' ? { role: 'close' } : { role: 'quit' },
|
||||
],
|
||||
},
|
||||
{ label: 'Edit', submenu: editMenu },
|
||||
{ label: 'View', submenu: viewMenu },
|
||||
{ label: 'Window', submenu: windowMenu },
|
||||
{ label: 'Help', submenu: [] },
|
||||
];
|
||||
|
||||
Menu.setApplicationMenu(Menu.buildFromTemplate(template));
|
||||
}
|
||||
|
||||
function createWindow() {
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1440,
|
||||
height: 920,
|
||||
minWidth: 1180,
|
||||
minHeight: 760,
|
||||
title: brandName,
|
||||
icon: brandIconPath,
|
||||
frame: false,
|
||||
autoHideMenuBar: true,
|
||||
backgroundColor: '#f7f7f5',
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
sandbox: false,
|
||||
},
|
||||
});
|
||||
|
||||
if (process.platform !== 'darwin') {
|
||||
mainWindow.setMenuBarVisibility(false);
|
||||
}
|
||||
|
||||
mainWindow.on('closed', () => {
|
||||
mainWindow = null;
|
||||
});
|
||||
|
||||
[
|
||||
'enter-full-screen',
|
||||
'leave-full-screen',
|
||||
'maximize',
|
||||
'resize',
|
||||
'restore',
|
||||
'unmaximize',
|
||||
].forEach((eventName) => {
|
||||
mainWindow?.on(eventName as 'resize', () => sendWindowState(mainWindow));
|
||||
});
|
||||
|
||||
mainWindow.webContents.once('did-finish-load', () => {
|
||||
sendWindowState(mainWindow);
|
||||
});
|
||||
|
||||
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
||||
void shell.openExternal(url);
|
||||
return { action: 'deny' };
|
||||
});
|
||||
|
||||
if (isDev && process.env.VITE_DEV_SERVER_URL) {
|
||||
void mainWindow.loadURL(process.env.VITE_DEV_SERVER_URL);
|
||||
return;
|
||||
}
|
||||
|
||||
const indexHtmlPath = path.join(__dirname, '../../dist/apps/web/index.html');
|
||||
const entryHtml = createDesktopLocalRendererEntry(indexHtmlPath);
|
||||
|
||||
void mainWindow.loadURL(
|
||||
`data:text/html;charset=utf-8,${encodeURIComponent(entryHtml)}`,
|
||||
{
|
||||
baseURLForDataURL: `${pathToFileURL(
|
||||
path.dirname(indexHtmlPath)
|
||||
).toString()}/`,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function startLocalApi() {
|
||||
const { createLocalApiServer } = await import(
|
||||
'../../scripts/truegrowth-local-api.mjs'
|
||||
);
|
||||
localApi = createLocalApiServer({ utilityProcess });
|
||||
localApi.on('error', (error: NodeJS.ErrnoException) => {
|
||||
if (error?.code === 'EADDRINUSE') {
|
||||
console.log(
|
||||
`TrueGrowth local API already available at http://127.0.0.1:${localApiPort}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
localApi.listen(localApiPort, '127.0.0.1');
|
||||
}
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
installApplicationMenu();
|
||||
installWindowControlsIpc();
|
||||
if (process.platform === 'darwin' && app.dock) {
|
||||
app.dock.setIcon(brandIconPath);
|
||||
}
|
||||
app.setAboutPanelOptions({
|
||||
applicationName: brandName,
|
||||
applicationVersion: appVersion,
|
||||
iconPath: brandIconPath,
|
||||
copyright: `Copyright © ${new Date().getFullYear()} TrueGrowth`,
|
||||
});
|
||||
await startLocalApi();
|
||||
createWindow();
|
||||
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on('before-quit', () => {
|
||||
localApi?.close();
|
||||
});
|
||||
49
apps/electron/preload.cjs
Normal file
49
apps/electron/preload.cjs
Normal file
@@ -0,0 +1,49 @@
|
||||
const { contextBridge, ipcRenderer } = require('electron');
|
||||
|
||||
const localCdnPreference = {
|
||||
cdn: 'local',
|
||||
latency: 0,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
const windowControls = {
|
||||
minimize: () => ipcRenderer.invoke('truegrowth-window:minimize'),
|
||||
toggleMaximize: () => ipcRenderer.invoke('truegrowth-window:toggle-maximize'),
|
||||
close: () => ipcRenderer.invoke('truegrowth-window:close'),
|
||||
getState: () => ipcRenderer.invoke('truegrowth-window:is-maximized'),
|
||||
onStateChange: (callback) => {
|
||||
if (typeof callback !== 'function') {
|
||||
return () => undefined;
|
||||
}
|
||||
const listener = (_event, state) => callback(state);
|
||||
ipcRenderer.on('truegrowth-window:state', listener);
|
||||
return () =>
|
||||
ipcRenderer.removeListener('truegrowth-window:state', listener);
|
||||
},
|
||||
};
|
||||
|
||||
const themeBridge = {
|
||||
getSystemTheme: () => ipcRenderer.invoke('truegrowth-theme:get-system'),
|
||||
onSystemThemeChange: (callback) => {
|
||||
if (typeof callback !== 'function') {
|
||||
return () => undefined;
|
||||
}
|
||||
const listener = (_event, state) => callback(state);
|
||||
ipcRenderer.on('truegrowth-theme:system', listener);
|
||||
return () =>
|
||||
ipcRenderer.removeListener('truegrowth-theme:system', listener);
|
||||
},
|
||||
};
|
||||
|
||||
contextBridge.exposeInMainWorld('trueGrowthRuntime', {
|
||||
app: 'TrueGrowth',
|
||||
platform: process.platform,
|
||||
assetMode: 'local',
|
||||
localApiBaseUrl: 'http://127.0.0.1:48177/local-api',
|
||||
windowControls,
|
||||
theme: themeBridge,
|
||||
});
|
||||
|
||||
contextBridge.exposeInMainWorld('__OPENTU_DESKTOP__', true);
|
||||
contextBridge.exposeInMainWorld('__OPENTU_CDN__', localCdnPreference);
|
||||
contextBridge.exposeInMainWorld('__AITU_CDN__', localCdnPreference);
|
||||
36
apps/electron/preload.ts
Normal file
36
apps/electron/preload.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { contextBridge, ipcRenderer } from 'electron';
|
||||
|
||||
const localCdnPreference = {
|
||||
cdn: 'local',
|
||||
latency: 0,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
const windowControls = {
|
||||
minimize: () => ipcRenderer.invoke('truegrowth-window:minimize'),
|
||||
toggleMaximize: () => ipcRenderer.invoke('truegrowth-window:toggle-maximize'),
|
||||
close: () => ipcRenderer.invoke('truegrowth-window:close'),
|
||||
getState: () => ipcRenderer.invoke('truegrowth-window:is-maximized'),
|
||||
onStateChange: (callback: (state: unknown) => void) => {
|
||||
if (typeof callback !== 'function') {
|
||||
return () => undefined;
|
||||
}
|
||||
const listener = (_event: Electron.IpcRendererEvent, state: unknown) =>
|
||||
callback(state);
|
||||
ipcRenderer.on('truegrowth-window:state', listener);
|
||||
return () =>
|
||||
ipcRenderer.removeListener('truegrowth-window:state', listener);
|
||||
},
|
||||
};
|
||||
|
||||
contextBridge.exposeInMainWorld('trueGrowthRuntime', {
|
||||
app: 'TrueGrowth',
|
||||
platform: process.platform,
|
||||
assetMode: 'local',
|
||||
localApiBaseUrl: 'http://127.0.0.1:48177/local-api',
|
||||
windowControls,
|
||||
});
|
||||
|
||||
contextBridge.exposeInMainWorld('__OPENTU_DESKTOP__', true);
|
||||
contextBridge.exposeInMainWorld('__OPENTU_CDN__', localCdnPreference);
|
||||
contextBridge.exposeInMainWorld('__AITU_CDN__', localCdnPreference);
|
||||
Reference in New Issue
Block a user