mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-29 19:55:20 +00:00
418 lines
12 KiB
TypeScript
418 lines
12 KiB
TypeScript
//
|
|
// Copyright © 2024 Hardcore Engineering Inc.
|
|
//
|
|
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License. You may
|
|
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
//
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
//
|
|
|
|
import { config as dotenvConfig } from 'dotenv'
|
|
import { BrowserWindow, CookiesSetDetails, Notification, app, desktopCapturer, dialog, ipcMain, nativeImage, shell, systemPreferences } from 'electron'
|
|
import contextMenu from 'electron-context-menu'
|
|
import log from 'electron-log'
|
|
import Store from 'electron-store'
|
|
import { ProgressInfo, UpdateInfo } from 'electron-updater'
|
|
import WinBadge from 'electron-windows-badge'
|
|
import * as path from 'path'
|
|
|
|
import { Config, NotificationParams } from '../ui/types'
|
|
import { getOptions } from './args'
|
|
import { cancelBackup, startBackup } from './backup'
|
|
import { addMenus } from './menu'
|
|
import { addPermissionHandlers } from './permissions'
|
|
import autoUpdater from './updater'
|
|
|
|
let mainWindow: BrowserWindow | undefined
|
|
let winBadge: any
|
|
|
|
const isMac = process.platform === 'darwin'
|
|
const isWindows = process.platform === 'win32'
|
|
const isDev = process.env.NODE_ENV === 'development'
|
|
|
|
const sessionPartition = !isDev ? 'persist:huly' : 'persist:huly_dev'
|
|
const iconKey = path.join(app.getAppPath(), 'dist', 'ui', 'public', 'AppIcon.png')
|
|
|
|
const defaultWidth = 1440
|
|
const defaultHeight = 960
|
|
|
|
const envPath = path.join(app.getAppPath(), isDev ? '.env-dev' : '.env')
|
|
console.log('do loading env from', envPath)
|
|
dotenvConfig({
|
|
path: envPath
|
|
})
|
|
const options = getOptions()
|
|
|
|
// Note: using electron-store here as local storage is not available in the main process
|
|
// before the window is created
|
|
const settings = new Store()
|
|
const oldFront = readServerUrl()
|
|
|
|
if (options.server !== undefined) {
|
|
;(settings as any).set('server', options.server)
|
|
}
|
|
|
|
const FRONT_URL = readServerUrl()
|
|
const serverChanged = oldFront !== FRONT_URL
|
|
|
|
function readServerUrl (): string {
|
|
if (isDev) {
|
|
return process.env.FRONT_URL ?? 'http://localhost:8087'
|
|
}
|
|
|
|
return ((settings as any).get('server', process.env.FRONT_URL) as string) ?? 'https://huly.app'
|
|
}
|
|
|
|
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
|
|
if (require('electron-squirrel-startup') === true) {
|
|
app.quit()
|
|
}
|
|
|
|
console.log('Running Huly', process.env.MODEL_VERSION, process.env.VERSION, isMac, isDev, process.env.NODE_ENV)
|
|
|
|
function hookOpenWindow (window: BrowserWindow): void {
|
|
window.webContents.setWindowOpenHandler(({ url }) => {
|
|
console.log('opening window', url)
|
|
|
|
/*
|
|
We need to detect if url is "our" or external. We should
|
|
open external urls in system browser
|
|
|
|
When we open local URLs it would be as file:///workbench/my-spc/tracker/TSK-2
|
|
As we load only our index.html there is no security problem to pass such URLs
|
|
to open arg as well
|
|
*/
|
|
if (url.indexOf(FRONT_URL) !== 0 && url.indexOf('file://') !== 0) {
|
|
void shell.openExternal(url)
|
|
} else {
|
|
void (async (): Promise<void> => {
|
|
const bounds = mainWindow?.getBounds()
|
|
const childWindow = new BrowserWindow({
|
|
width: bounds?.width ?? defaultWidth,
|
|
height: bounds?.height ?? defaultHeight,
|
|
x: (bounds?.x ?? 0) + 25,
|
|
y: (bounds?.y ?? 0) + 25,
|
|
titleBarStyle: isMac ? 'hidden' : 'default',
|
|
trafficLightPosition: { x: 10, y: 10 },
|
|
icon: nativeImage.createFromPath(iconKey),
|
|
webPreferences: {
|
|
devTools: true,
|
|
sandbox: false,
|
|
partition: sessionPartition,
|
|
preload: path.join(app.getAppPath(), 'dist', 'main', 'preload.js'),
|
|
additionalArguments: [
|
|
`--open=${encodeURI(
|
|
new URL(url).pathname
|
|
.split('/')
|
|
.filter((it) => it.length > 0)
|
|
.join('/')
|
|
)}`
|
|
]
|
|
}
|
|
})
|
|
await childWindow.loadFile(path.join('dist', 'ui', 'index.html'))
|
|
hookOpenWindow(childWindow)
|
|
})()
|
|
}
|
|
return { action: 'deny' }
|
|
})
|
|
}
|
|
|
|
const createWindow = async (): Promise<void> => {
|
|
mainWindow = new BrowserWindow({
|
|
width: defaultWidth,
|
|
height: defaultHeight,
|
|
titleBarStyle: isMac ? 'hidden' : 'default',
|
|
trafficLightPosition: { x: 10, y: 10 },
|
|
icon: nativeImage.createFromPath(iconKey),
|
|
webPreferences: {
|
|
devTools: true,
|
|
sandbox: false,
|
|
backgroundThrottling: false,
|
|
partition: sessionPartition,
|
|
preload: path.join(app.getAppPath(), 'dist', 'main', 'preload.js')
|
|
}
|
|
})
|
|
app.dock?.setIcon(nativeImage.createFromPath(iconKey))
|
|
// await mainWindow.webContents.openDevTools()
|
|
if (isDev) {
|
|
mainWindow.webContents.openDevTools()
|
|
}
|
|
await mainWindow.loadFile(path.join('dist', 'ui', 'index.html'))
|
|
addPermissionHandlers(mainWindow.webContents.session)
|
|
|
|
// In this example, only windows with the `about:blank` url will be created.
|
|
// All other urls will be blocked.
|
|
hookOpenWindow(mainWindow)
|
|
|
|
if (isMac) {
|
|
mainWindow.on('close', (event) => {
|
|
// Prevent the default behavior (which would quit the app)
|
|
event.preventDefault()
|
|
// Hide the window
|
|
mainWindow?.hide()
|
|
})
|
|
} else if (isWindows) {
|
|
winBadge = new WinBadge(mainWindow, {
|
|
font: '14px arial'
|
|
})
|
|
}
|
|
}
|
|
|
|
addMenus(() => mainWindow as BrowserWindow, (cmd: string, ...args: any[]) => {
|
|
mainWindow?.webContents.send(cmd, ...args)
|
|
})
|
|
|
|
contextMenu({
|
|
showSaveImageAs: false,
|
|
showInspectElement: false,
|
|
showSelectAll: false
|
|
})
|
|
|
|
ipcMain.on('set-badge', (event, badge: number) => {
|
|
app.dock?.setBadge(badge > 0 ? `${badge}` : '')
|
|
app.badgeCount = badge
|
|
|
|
if (isWindows && winBadge !== undefined) {
|
|
winBadge.update(badge)
|
|
}
|
|
})
|
|
|
|
ipcMain.on('dock-bounce', (event) => {
|
|
app.dock?.bounce('informational')
|
|
})
|
|
|
|
ipcMain.on('send-notification', (event, notificationParams: NotificationParams) => {
|
|
if (Notification.isSupported()) {
|
|
const notification = new Notification(notificationParams)
|
|
|
|
notification.on('click', () => {
|
|
mainWindow?.show()
|
|
mainWindow?.webContents.send('handle-notification-navigation')
|
|
})
|
|
|
|
notification.show()
|
|
}
|
|
})
|
|
|
|
ipcMain.on('set-title', (event, title) => {
|
|
const webContents = event.sender
|
|
const win = BrowserWindow.fromWebContents(webContents)
|
|
win?.setTitle(title)
|
|
})
|
|
|
|
ipcMain.on('set-combined-config', (event, config: Config) => {
|
|
log.info('Config set: ', config)
|
|
|
|
const updatesUrl = process.env.DESKTOP_UPDATES_URL ?? config.DESKTOP_UPDATES_URL ?? 'https://dist.huly.io'
|
|
const updatesChannel = process.env.DESKTOP_UPDATES_CHANNEL ?? config.DESKTOP_UPDATES_CHANNEL ?? 'huly'
|
|
|
|
autoUpdater.setFeedURL({
|
|
provider: 'generic',
|
|
url: updatesUrl,
|
|
channel: updatesChannel
|
|
})
|
|
void autoUpdater.checkForUpdatesAndNotify()
|
|
})
|
|
|
|
ipcMain.handle('get-main-config', (event, path) => {
|
|
const cfg = {
|
|
CONFIG_URL: process.env.CONFIG_URL ?? '',
|
|
FRONT_URL,
|
|
INITIAL_URL: process.env.INITIAL_URL ?? '',
|
|
MODEL_VERSION: process.env.MODEL_VERSION ?? '',
|
|
VERSION: process.env.VERSION ?? ''
|
|
}
|
|
return cfg
|
|
})
|
|
ipcMain.handle('get-host', (event, path) => {
|
|
return new URL(FRONT_URL).host
|
|
})
|
|
|
|
ipcMain.on('set-front-cookie', function (event, host: string, name: string, value: string) {
|
|
const webContents = event.sender
|
|
const win = BrowserWindow.fromWebContents(webContents)
|
|
const cv: CookiesSetDetails = {
|
|
url: host,
|
|
name,
|
|
value,
|
|
path: '/',
|
|
secure: true,
|
|
sameSite: 'no_restriction',
|
|
httpOnly: true,
|
|
domain: process.env.FRONT_DOMAIN
|
|
}
|
|
void win?.webContents?.session.cookies.set(cv)
|
|
})
|
|
|
|
const DEEP_LINKS_PROTOCOL = 'huly'
|
|
/*
|
|
Copy-paste from official tutorial for deep links
|
|
https://www.electronjs.org/docs/latest/tutorial/launch-app-from-url-in-another-app
|
|
*/
|
|
if (process.defaultApp) {
|
|
if (process.argv.length >= 2) {
|
|
app.setAsDefaultProtocolClient(DEEP_LINKS_PROTOCOL, process.execPath, [path.resolve(process.argv[1])])
|
|
}
|
|
} else {
|
|
app.setAsDefaultProtocolClient(DEEP_LINKS_PROTOCOL)
|
|
}
|
|
|
|
const gotTheLock = app.requestSingleInstanceLock()
|
|
|
|
if (!gotTheLock) {
|
|
app.quit()
|
|
}
|
|
|
|
let deepLink = ''
|
|
let haveDeepLinkHandler = false
|
|
function tryToOpenDeepLink (link?: string): void {
|
|
if (mainWindow == null || !haveDeepLinkHandler) {
|
|
if (link != null) {
|
|
deepLink = link
|
|
}
|
|
return
|
|
}
|
|
|
|
const url = link ?? deepLink
|
|
const httpsUrl = url.replace(`${DEEP_LINKS_PROTOCOL}://`, 'https://')
|
|
mainWindow.webContents.send('handle-deep-link', httpsUrl)
|
|
}
|
|
|
|
// Add windows / linux handler for second instance
|
|
// This is because Windows try to open second instance from the deep link
|
|
app.on('second-instance', (event, commandLine, workingDirectory) => {
|
|
if (mainWindow != null) {
|
|
if (mainWindow.isMinimized()) mainWindow.restore()
|
|
mainWindow.focus()
|
|
}
|
|
|
|
const url = commandLine.pop()
|
|
console.log('Opening url', url)
|
|
|
|
if (url != null) {
|
|
tryToOpenDeepLink(url)
|
|
}
|
|
})
|
|
|
|
ipcMain.on('on-deep-link-handler', () => {
|
|
haveDeepLinkHandler = true
|
|
tryToOpenDeepLink()
|
|
})
|
|
|
|
ipcMain.handle('get-screen-access', () => systemPreferences.getMediaAccessStatus('screen') === 'granted')
|
|
ipcMain.handle('get-screen-sources', () => {
|
|
return desktopCapturer.getSources({ types: ['window', 'screen'], fetchWindowIcons: true, thumbnailSize: { width: 225, height: 135 } }).then(async sources => {
|
|
return sources.map(source => {
|
|
return {
|
|
...source,
|
|
appIconURL: source.appIcon?.toDataURL(),
|
|
thumbnailURL: source.thumbnail.toDataURL()
|
|
}
|
|
})
|
|
})
|
|
})
|
|
|
|
// MacOS implementation
|
|
// Handle the protocol. In this case, we choose to show an Error Box.
|
|
app.on('open-url', (event, url) => {
|
|
if (url == null) {
|
|
return
|
|
}
|
|
tryToOpenDeepLink(url)
|
|
})
|
|
|
|
async function onReady (): Promise<void> {
|
|
await createWindow()
|
|
|
|
const customProtocolArg = process.argv.find((arg) => arg.startsWith(`${DEEP_LINKS_PROTOCOL}://`)) ?? ''
|
|
tryToOpenDeepLink(customProtocolArg)
|
|
|
|
if (serverChanged) {
|
|
mainWindow?.webContents.send('logout')
|
|
}
|
|
}
|
|
|
|
app.on('ready', () => {
|
|
void onReady()
|
|
})
|
|
|
|
app.on('window-all-closed', () => {
|
|
if (process.platform !== 'darwin') {
|
|
app.quit()
|
|
}
|
|
})
|
|
|
|
if (isMac) {
|
|
app.on('activate', () => {
|
|
if (mainWindow != null && (mainWindow.isMinimized() || !mainWindow.isVisible())) {
|
|
mainWindow.show()
|
|
}
|
|
})
|
|
|
|
app.on('before-quit', () => {
|
|
// Note: in case the app is exited by auto-updater all windows will be destroyed at this point
|
|
if (mainWindow === undefined || mainWindow.isDestroyed()) return
|
|
|
|
mainWindow?.removeAllListeners('close')
|
|
mainWindow?.close()
|
|
})
|
|
}
|
|
|
|
// Note: it is reset when the app is relaunched after update
|
|
let isUpdating = false
|
|
|
|
autoUpdater.on('update-available', (info: UpdateInfo) => {
|
|
if (isUpdating) return
|
|
|
|
void dialog
|
|
.showMessageBox({
|
|
type: 'info',
|
|
buttons: ['Update & Restart', 'Quit'],
|
|
defaultId: 0,
|
|
message: `A new version ${info.version} is available and it is required to continue. It will be downloaded and installed automatically.`
|
|
})
|
|
.then(({ response }) => {
|
|
log.info(`Update dialog exit code: ${response}`) // eslint-disable-line no-console
|
|
|
|
if (response !== 0) {
|
|
app.quit()
|
|
}
|
|
isUpdating = true
|
|
})
|
|
})
|
|
|
|
autoUpdater.on('download-progress', (progressObj: ProgressInfo) => {
|
|
if (mainWindow === undefined) return
|
|
|
|
mainWindow.setProgressBar(progressObj.percent / 100)
|
|
mainWindow.webContents.send('handle-update-download-progress', progressObj.percent)
|
|
})
|
|
|
|
autoUpdater.on('update-downloaded', (info) => {
|
|
// We have listeners that prevents the app from being exited on mac
|
|
app.removeAllListeners('window-all-closed')
|
|
mainWindow?.removeAllListeners('close')
|
|
|
|
autoUpdater.quitAndInstall()
|
|
})
|
|
|
|
ipcMain.on('start-backup', (event, token, endpoint, workspace) => {
|
|
console.log('start backup', token, endpoint, workspace)
|
|
if (mainWindow != null) {
|
|
startBackup(mainWindow, token, endpoint, workspace, (cmd: string, ...args: any[]) => {
|
|
mainWindow?.webContents.send(cmd, ...args)
|
|
})
|
|
}
|
|
})
|
|
|
|
ipcMain.on('cancel-backup', (event) => {
|
|
cancelBackup()
|
|
})
|