diff --git a/desktop-package/installer.nsh b/desktop-package/installer.nsh new file mode 100644 index 0000000000..a2ba319efe --- /dev/null +++ b/desktop-package/installer.nsh @@ -0,0 +1,3 @@ +!macro customInit + StrCpy $INSTDIR "$LOCALAPPDATA\Programs\HulyDesktop" +!macroend \ No newline at end of file diff --git a/desktop-package/package.json b/desktop-package/package.json index 9405473941..63ce4c4e22 100644 --- a/desktop-package/package.json +++ b/desktop-package/package.json @@ -64,12 +64,26 @@ ], "artifactName": "Huly-windows-${version}.${ext}", "verifyUpdateCodeSignature": false, - "icon": "./src/AppIcon.png", + "icon": "./src/AppIcon.ico", "publish": { "provider": "generic", "url": "https://dist.huly.io", "channel": "latest" - } + }, + "extraResources": [ + { + "from": "./dist/ui/public/icons/", + "to": "./icons/", + "filter": [ + "**/*.ico" + ] + } + ] + }, + "nsis": { + "include": "installer.nsh", + "installerIcon": "./src/AppIcon.ico", + "uninstallerIcon": "./src/AppIcon.ico" }, "linux": { "artifactName": "Huly-linux-${version}.${ext}", diff --git a/desktop/public/AppIcon.ico b/desktop/public/AppIcon.ico new file mode 100644 index 0000000000..e5c1f32f44 Binary files /dev/null and b/desktop/public/AppIcon.ico differ diff --git a/desktop/public/icons/InboxIcon.ico b/desktop/public/icons/InboxIcon.ico new file mode 100644 index 0000000000..dd9e35a75f Binary files /dev/null and b/desktop/public/icons/InboxIcon.ico differ diff --git a/desktop/public/icons/OfficeIcon.ico b/desktop/public/icons/OfficeIcon.ico new file mode 100644 index 0000000000..f2a6e18ad9 Binary files /dev/null and b/desktop/public/icons/OfficeIcon.ico differ diff --git a/desktop/public/icons/PlannerIcon.ico b/desktop/public/icons/PlannerIcon.ico new file mode 100644 index 0000000000..c212c70dc9 Binary files /dev/null and b/desktop/public/icons/PlannerIcon.ico differ diff --git a/desktop/public/icons/SettingsIcon.ico b/desktop/public/icons/SettingsIcon.ico new file mode 100644 index 0000000000..7431da75e9 Binary files /dev/null and b/desktop/public/icons/SettingsIcon.ico differ diff --git a/desktop/src/main/customMenu.ts b/desktop/src/main/customMenu.ts index 3e0339c22e..b3c1c93917 100644 --- a/desktop/src/main/customMenu.ts +++ b/desktop/src/main/customMenu.ts @@ -14,7 +14,7 @@ // import { app, BrowserWindow } from 'electron' -import { MenuBarAction, StandardMenuCommandLogout, StandardMenuCommandSelectWorkspace, StandardMenuCommandOpenSettings } from '../ui/types' +import { MenuBarAction, CommandLogout, CommandSelectWorkspace, CommandOpenSettings } from '../ui/types' export function dipatchMenuBarAction(mainWindow: BrowserWindow | undefined, action: MenuBarAction) { if (mainWindow == null) { @@ -33,13 +33,13 @@ export function dipatchMenuBarAction(mainWindow: BrowserWindow | undefined, acti switch (action) { case 'settings': - mainWindow.webContents.send(StandardMenuCommandOpenSettings) + mainWindow.webContents.send(CommandOpenSettings) break; case 'select-workspace': - mainWindow.webContents.send(StandardMenuCommandSelectWorkspace) + mainWindow.webContents.send(CommandSelectWorkspace) break; case 'logout': - mainWindow.webContents.send(StandardMenuCommandLogout) + mainWindow.webContents.send(CommandLogout) break; case 'exit': app.quit(); diff --git a/desktop/src/main/standardMenu.ts b/desktop/src/main/standardMenu.ts index 0fa82745a4..9ae500ee5e 100644 --- a/desktop/src/main/standardMenu.ts +++ b/desktop/src/main/standardMenu.ts @@ -14,12 +14,12 @@ // import { Menu, MenuItemConstructorOptions } from 'electron' -import { StandardMenuCommand, StandardMenuCommandOpenSettings, StandardMenuCommandSelectWorkspace, StandardMenuCommandLogout, } from '../ui/types' +import { Command, CommandOpenSettings, CommandSelectWorkspace, CommandLogout, } from '../ui/types' const isMac = process.platform === 'darwin' const isLinux = process.platform === 'linux' -export const addMenus = (sendCommand: (cmd: StandardMenuCommand, ...args: any[]) => void): void => { +export const addMenus = (sendCommand: (cmd: Command, ...args: any[]) => void): void => { const template: MenuItemConstructorOptions[] = [ { label: 'File', @@ -27,15 +27,15 @@ export const addMenus = (sendCommand: (cmd: StandardMenuCommand, ...args: any[]) { label: 'Settings', accelerator: isLinux ? 'Ctrl+,' : 'Meta+,', - click: () => { sendCommand(StandardMenuCommandOpenSettings) } + click: () => { sendCommand(CommandOpenSettings) } }, { label: 'Select workspace', - click: () => { sendCommand(StandardMenuCommandSelectWorkspace) } + click: () => { sendCommand(CommandSelectWorkspace) } }, { label: 'Logout', - click: () => { sendCommand(StandardMenuCommandLogout) } + click: () => { sendCommand(CommandLogout) } }, { role: isMac ? 'close' : 'quit' } ] diff --git a/desktop/src/main/start.ts b/desktop/src/main/start.ts index c3cf1e0110..0e5b4320b7 100644 --- a/desktop/src/main/start.ts +++ b/desktop/src/main/start.ts @@ -22,7 +22,7 @@ import { ProgressInfo, UpdateInfo } from 'electron-updater' import WinBadge from 'electron-windows-badge' import * as path from 'path' -import { Config, MenuBarAction, NotificationParams, StandardMenuCommandLogout, StandardMenuCommandSelectWorkspace, StandardMenuCommandOpenSettings } from '../ui/types' +import { Config, MenuBarAction, NotificationParams } from '../ui/types' import { getOptions } from './args' import { addMenus } from './standardMenu' import { dipatchMenuBarAction } from './customMenu' @@ -30,6 +30,8 @@ import { addPermissionHandlers } from './permissions' import autoUpdater from './updater' import { generateId } from '@hcengineering/core' import { DownloadItem } from '@hcengineering/desktop-downloads' +import { setupWindowsSpecific } from './windowsSpecificSetup' + let mainWindow: BrowserWindow | undefined let winBadge: any @@ -39,7 +41,7 @@ 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 iconKey = path.join(app.getAppPath(), 'dist', 'ui', 'public', isWindows ? 'AppIcon.ico' : 'AppIcon.png') const preloadScriptPath = path.join(app.getAppPath(), 'dist', 'main', 'preload.js') const defaultWidth = 1440 @@ -200,7 +202,7 @@ function handleAuthRedirects (window: BrowserWindow): void { } function handleWillDownload (window: BrowserWindow): void { - window.webContents.session.on('will-download', (event, item) => { + window.webContents.session.on('will-download', (_event, item) => { const key = generateId() const notifyDownloadUpdated = (): void => { @@ -303,10 +305,14 @@ const createWindow = async (): Promise => { } } -if (false == isWindows) { - addMenus((cmd: string, ...args: any[]) => { - mainWindow?.webContents.send(cmd, ...args) - }) +function sendCommand(cmd: string, ...args: any[]): void { + mainWindow?.webContents.send(cmd, ...args) +} + +if (isWindows) { + setupWindowsSpecific(sendCommand) +} else { + addMenus(sendCommand) } contextMenu({ @@ -315,7 +321,7 @@ contextMenu({ showSelectAll: false }) -ipcMain.on('set-badge', (event, badge: number) => { +ipcMain.on('set-badge', (_event: any, badge: number) => { app.dock?.setBadge(badge > 0 ? `${badge}` : '') app.badgeCount = badge @@ -324,11 +330,11 @@ ipcMain.on('set-badge', (event, badge: number) => { } }) -ipcMain.on('dock-bounce', (event) => { +ipcMain.on('dock-bounce', (_event: any) => { app.dock?.bounce('informational') }) -ipcMain.on('send-notification', (event, notificationParams: NotificationParams) => { +ipcMain.on('send-notification', (_event: any, notificationParams: NotificationParams) => { if (Notification.isSupported()) { const notification = new Notification(notificationParams) @@ -341,13 +347,13 @@ ipcMain.on('send-notification', (event, notificationParams: NotificationParams) } }) -ipcMain.on('set-title', (event, title) => { +ipcMain.on('set-title', (event: any, title: string) => { const webContents = event.sender const window = BrowserWindow.fromWebContents(webContents) window?.setTitle(title) }) -ipcMain.on('set-combined-config', (event, config: Config) => { +ipcMain.on('set-combined-config', (_event: any, config: Config) => { log.info('Config set: ', config) setupCookieHandler(config) @@ -363,7 +369,7 @@ ipcMain.on('set-combined-config', (event, config: Config) => { void autoUpdater.checkForUpdatesAndNotify() }) -ipcMain.handle('get-main-config', (event, path) => { +ipcMain.handle('get-main-config', (_event: any, _path: any) => { const cfg = { CONFIG_URL: process.env.CONFIG_URL ?? '', FRONT_URL, @@ -373,11 +379,11 @@ ipcMain.handle('get-main-config', (event, path) => { } return cfg }) -ipcMain.handle('get-host', (event, path) => { +ipcMain.handle('get-host', (_event: any, _path: any) => { return new URL(FRONT_URL).host }) -ipcMain.on('set-front-cookie', function (event, host: string, name: string, value: string) { +ipcMain.on('set-front-cookie', function (event: any, host: string, name: string, value: string) { const webContents = event.sender const win = BrowserWindow.fromWebContents(webContents) const cv: CookiesSetDetails = { @@ -428,7 +434,7 @@ if (!gotTheLock) { 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 sources.map((source: any) => { return { ...source, appIconURL: source.appIcon?.toDataURL(), @@ -490,7 +496,7 @@ autoUpdater.on('update-available', (info: UpdateInfo) => { 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 }) => { + .then(({ response }: any) => { log.info(`Update dialog exit code: ${response}`) // eslint-disable-line no-console if (response !== 0) { @@ -513,7 +519,7 @@ function setDownloadProgress (percent: number): void { mainWindow.webContents.send('handle-update-download-progress', percent) } -autoUpdater.on('update-downloaded', (info) => { +autoUpdater.on('update-downloaded', (_info: any) => { // We have listeners that prevents the app from being exited on mac app.removeAllListeners('window-all-closed') mainWindow?.removeAllListeners('close') diff --git a/desktop/src/main/windowsSpecificSetup.ts b/desktop/src/main/windowsSpecificSetup.ts new file mode 100644 index 0000000000..a09ca1f24a --- /dev/null +++ b/desktop/src/main/windowsSpecificSetup.ts @@ -0,0 +1,102 @@ +// +// Copyright © 2025 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 { app } from 'electron' +import { Command, CommandOpenSettings, CommandOpenInbox, CommandOpenOffice, CommandOpenPlanner } from '../ui/types' +import * as path from 'path' + +const JUMP_COMMANDS = { + INBOX: '--jump-to-inbox', + SETTINGS: '--jump-to-settings', + PLANNER: '--jump-to-planner', + OFFICE: '--jump-to-office' +} as const + +type JumpCommand = typeof JUMP_COMMANDS[keyof typeof JUMP_COMMANDS]; + +export function setupWindowsSpecific(sendCommand: (cmd: Command, ...args: any[]) => void): void { + + app.setAppUserModelId(app.getName()); + + app.on('second-instance', (_event: any, commandLine: any, _workingDirectory: any) => { + const jumpCommand = commandLine[1] as JumpCommand; + + let command: Command | undefined; + switch (jumpCommand) { + case JUMP_COMMANDS.INBOX: + command = CommandOpenInbox; + break; + case JUMP_COMMANDS.OFFICE: + command = CommandOpenOffice; + break; + case JUMP_COMMANDS.PLANNER: + command = CommandOpenPlanner; + break; + case JUMP_COMMANDS.SETTINGS: + command = CommandOpenSettings; + break; + default: { + // compile-time check: if jumpCommand is not a known value, this line errors + const _exhaustive: never = jumpCommand; + return; // or handle error + } + } + + if (command) { + sendCommand(command); + } + }) + + function getIconPath(iconName: string): string { + return path.join(process.resourcesPath, 'icons', iconName); + } + + app.setUserTasks + ([ + { + program: process.execPath, + arguments: JUMP_COMMANDS.SETTINGS, + iconPath: getIconPath('SettingsIcon.ico'), + iconIndex: 0, + title: 'Settings', + description: 'Open Settings Screen' + }, + { + program: process.execPath, + arguments: JUMP_COMMANDS.INBOX, + iconPath: getIconPath('InboxIcon.ico'), + iconIndex: 0, + title: 'Inbox', + description: 'Open Inbox' + }, + { + program: process.execPath, + arguments: JUMP_COMMANDS.PLANNER, + iconPath: getIconPath('PlannerIcon.ico'), + iconIndex: 0, + title: 'Planner', + description: 'Open Planner' + }, + { + program: process.execPath, + arguments: JUMP_COMMANDS.OFFICE, + iconPath: getIconPath('OfficeIcon.ico'), + iconIndex: 0, + title: 'Office', + description: 'Open Office' + } + ]) + +} \ No newline at end of file diff --git a/desktop/src/ui/index.ts b/desktop/src/ui/index.ts index a9221191b7..1226baa6f9 100644 --- a/desktop/src/ui/index.ts +++ b/desktop/src/ui/index.ts @@ -1,4 +1,7 @@ import { loginId } from '@hcengineering/login' +import { loveId } from '@hcengineering/love' +import { timeId } from '@hcengineering/time' + import { getEmbeddedLabel, getMetadata } from '@hcengineering/platform' import presentation, { MessageBox, setDownloadProgress } from '@hcengineering/presentation' import settings, { settingId } from '@hcengineering/setting' @@ -21,7 +24,7 @@ import { isOwnerOrMaintainer } from '@hcengineering/core' import { configurePlatform } from './platform' import { setupTitleBarMenu } from './titleBarMenu' import { defineScreenShare, defineGetDisplayMedia } from './screenShare' -import { StandardMenuCommandLogout, StandardMenuCommandSelectWorkspace, StandardMenuCommandOpenSettings } from './types' +import { CommandLogout, CommandSelectWorkspace, CommandOpenSettings, CommandOpenInbox, CommandOpenPlanner, CommandOpenOffice } from './types' import { ipcMainExposed } from './typesUtils' import { themeStore } from '@hcengineering/theme' @@ -61,18 +64,34 @@ window.addEventListener('DOMContentLoaded', () => { createApp(document.body) }) - ipcMain.on(StandardMenuCommandOpenSettings, () => { + function openScreen(screenId: any): void { closePopup() closePanel() const loc = getCurrentResolvedLocation() loc.fragment = undefined loc.query = undefined - loc.path[2] = settingId + loc.path[2] = screenId loc.path.length = 3 navigate(loc) + } + + ipcMain.on(CommandOpenSettings, () => { + openScreen(settingId) }) - ipcMain.on(StandardMenuCommandSelectWorkspace, () => { + ipcMain.on(CommandOpenInbox, () => { + openScreen(notificationId) + }) + + ipcMain.on(CommandOpenOffice, () => { + openScreen(loveId) + }) + + ipcMain.on(CommandOpenPlanner, () => { + openScreen(timeId) + }) + + ipcMain.on(CommandSelectWorkspace, () => { closePopup() closePanel() const loc = getCurrentResolvedLocation() @@ -84,7 +103,7 @@ window.addEventListener('DOMContentLoaded', () => { navigate(loc) }) - ipcMain.on(StandardMenuCommandLogout, () => { + ipcMain.on(CommandLogout, () => { void logOut().then(() => { navigate({ path: [loginId] }) }) diff --git a/desktop/src/ui/types.ts b/desktop/src/ui/types.ts index db1bf257c2..08ec72a706 100644 --- a/desktop/src/ui/types.ts +++ b/desktop/src/ui/types.ts @@ -73,10 +73,20 @@ export interface Branding { export type BrandingMap = Record -export const StandardMenuCommandOpenSettings = 'open-settings' as const -export const StandardMenuCommandSelectWorkspace = 'select-workspace' as const -export const StandardMenuCommandLogout = 'logout' as const -export type StandardMenuCommand = typeof StandardMenuCommandOpenSettings | typeof StandardMenuCommandSelectWorkspace | typeof StandardMenuCommandLogout +export const CommandOpenSettings = 'open-settings' as const +export const CommandOpenInbox = 'open-inbox' as const +export const CommandOpenOffice = 'open-office' as const +export const CommandOpenPlanner = 'open-planner' as const +export const CommandSelectWorkspace = 'select-workspace' as const +export const CommandLogout = 'logout' as const + +export type Command = + typeof CommandOpenSettings | + typeof CommandOpenInbox | + typeof CommandOpenOffice | + typeof CommandOpenPlanner | + typeof CommandSelectWorkspace | + typeof CommandLogout export interface NotificationParams { title: string