From e7dbea5bc6d7e6b121dbc94b21b759a29f16e0c0 Mon Sep 17 00:00:00 2001 From: muhamedsalih-tw <104364298+muhamedsalih-tw@users.noreply.github.com> Date: Sun, 6 Nov 2022 10:36:51 +0530 Subject: Transform tray & menu files to typescript (#740) --- src/lib/DBus.ts | 13 +- src/lib/Menu.js | 1261 ----------------------------------------------------- src/lib/Menu.ts | 1286 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib/Tray.js | 251 ----------- src/lib/Tray.ts | 265 ++++++++++++ 5 files changed, 1560 insertions(+), 1516 deletions(-) delete mode 100644 src/lib/Menu.js create mode 100644 src/lib/Menu.ts delete mode 100644 src/lib/Tray.js create mode 100644 src/lib/Tray.ts (limited to 'src/lib') diff --git a/src/lib/DBus.ts b/src/lib/DBus.ts index b1febc2d1..bbff405c4 100644 --- a/src/lib/DBus.ts +++ b/src/lib/DBus.ts @@ -1,17 +1,20 @@ import { MessageBus, sessionBus } from 'dbus-next'; import { isLinux } from '../environment'; +import TrayIcon from './Tray'; export default class DBus { bus: MessageBus | null = null; - trayIcon: any; + trayIcon: TrayIcon; - constructor(trayIcon: any) { + constructor(trayIcon: TrayIcon) { this.trayIcon = trayIcon; } start() { - if (!isLinux || this.bus) return; + if (!isLinux || this.bus) { + return; + } try { this.bus = sessionBus(); @@ -47,7 +50,9 @@ export default class DBus { } stop() { - if (!this.bus) return; + if (!this.bus) { + return; + } this.bus.disconnect(); this.bus = null; diff --git a/src/lib/Menu.js b/src/lib/Menu.js deleted file mode 100644 index 52b6be18a..000000000 --- a/src/lib/Menu.js +++ /dev/null @@ -1,1261 +0,0 @@ -import { clipboard } from 'electron'; -import { - app, - Menu, - dialog, - webContents, - systemPreferences, - getCurrentWindow, -} from '@electron/remote'; -import { autorun, action, makeObservable, observable } from 'mobx'; -import { defineMessages } from 'react-intl'; -import osName from 'os-name'; -import { fromJS } from 'immutable'; -import semver from 'semver'; -import os from 'os'; -import { - isWindows, - cmdOrCtrlShortcutKey, - altKey, - shiftKey, - settingsShortcutKey, - isLinux, - isMac, - lockFerdiumShortcutKey, - todosToggleShortcutKey, - workspaceToggleShortcutKey, - addNewServiceShortcutKey, - splitModeToggleShortcutKey, - muteFerdiumShortcutKey, - electronVersion, - chromeVersion, - nodeVersion, - osArch, - toggleFullScreenKey, -} from '../environment'; -import { CUSTOM_WEBSITE_RECIPE_ID, LIVE_API_FERDIUM_WEBSITE } from '../config'; -import { ferdiumVersion } from '../environment-remote'; -import { todoActions } from '../features/todos/actions'; -import workspaceActions from '../features/workspaces/actions'; -import { workspaceStore } from '../features/workspaces/index'; -import { importExportURL, serverBase, serverName } from '../api/apiBase'; -import { openExternalUrl } from '../helpers/url-helpers'; -import globalMessages from '../i18n/globalMessages'; -import { onAuthGoToReleaseNotes } from '../helpers/update-helpers'; - -// @ts-expect-error Cannot find module '../buildInfo.json' or its corresponding type declarations. -import * as buildInfo from '../buildInfo.json'; - -const menuItems = defineMessages({ - edit: { - id: 'menu.edit', - defaultMessage: 'Edit', - }, - undo: { - id: 'menu.edit.undo', - defaultMessage: 'Undo', - }, - redo: { - id: 'menu.edit.redo', - defaultMessage: 'Redo', - }, - cut: { - id: 'menu.edit.cut', - defaultMessage: 'Cut', - }, - copy: { - id: 'menu.edit.copy', - defaultMessage: 'Copy', - }, - paste: { - id: 'menu.edit.paste', - defaultMessage: 'Paste', - }, - pasteAndMatchStyle: { - id: 'menu.edit.pasteAndMatchStyle', - defaultMessage: 'Paste And Match Style', - }, - delete: { - id: 'menu.edit.delete', - defaultMessage: 'Delete', - }, - selectAll: { - id: 'menu.edit.selectAll', - defaultMessage: 'Select All', - }, - findInPage: { - id: 'menu.edit.findInPage', - defaultMessage: 'Find in Page', - }, - speech: { - id: 'menu.edit.speech', - defaultMessage: 'Speech', - }, - startSpeaking: { - id: 'menu.edit.startSpeaking', - defaultMessage: 'Start Speaking', - }, - stopSpeaking: { - id: 'menu.edit.stopSpeaking', - defaultMessage: 'Stop Speaking', - }, - startDictation: { - id: 'menu.edit.startDictation', - defaultMessage: 'Start Dictation', - }, - emojiSymbols: { - id: 'menu.edit.emojiSymbols', - defaultMessage: 'Emoji & Symbols', - }, - openQuickSwitch: { - id: 'menu.view.openQuickSwitch', - defaultMessage: 'Open Quick Switch', - }, - back: { - id: 'menu.view.back', - defaultMessage: 'Back', - }, - forward: { - id: 'menu.view.forward', - defaultMessage: 'Forward', - }, - resetZoom: { - id: 'menu.view.resetZoom', - defaultMessage: 'Actual Size', - }, - zoomIn: { - id: 'menu.view.zoomIn', - defaultMessage: 'Zoom In', - }, - zoomOut: { - id: 'menu.view.zoomOut', - defaultMessage: 'Zoom Out', - }, - toggleFullScreen: { - id: 'menu.view.toggleFullScreen', - defaultMessage: 'Toggle Full Screen', - }, - toggleNavigationBar: { - id: 'menu.view.toggleNavigationBar', - defaultMessage: 'Toggle Navigation Bar', - }, - splitModeToggle: { - id: 'menu.view.splitModeToggle', - defaultMessage: 'Toggle Split Mode', - }, - toggleDarkMode: { - id: 'menu.view.toggleDarkMode', - defaultMessage: 'Toggle Dark Mode', - }, - toggleDevTools: { - id: 'menu.view.toggleDevTools', - defaultMessage: 'Toggle Developer Tools', - }, - toggleTodosDevTools: { - id: 'menu.view.toggleTodosDevTools', - defaultMessage: 'Toggle Todos Developer Tools', - }, - toggleServiceDevTools: { - id: 'menu.view.toggleServiceDevTools', - defaultMessage: 'Toggle Service Developer Tools', - }, - reloadService: { - id: 'menu.view.reloadService', - defaultMessage: 'Reload Service', - }, - reloadFerdium: { - id: 'menu.view.reloadFerdium', - defaultMessage: 'Reload Ferdium', - }, - lockFerdium: { - id: 'menu.view.lockFerdium', - defaultMessage: 'Lock Ferdium', - }, - reloadTodos: { - id: 'menu.view.reloadTodos', - defaultMessage: 'Reload ToDos', - }, - minimize: { - id: 'menu.window.minimize', - defaultMessage: 'Minimize', - }, - close: { - id: 'menu.window.close', - defaultMessage: 'Close', - }, - learnMore: { - id: 'menu.help.learnMore', - defaultMessage: 'Learn More', - }, - changelog: { - id: 'menu.help.changelog', - defaultMessage: 'Changelog', - }, - importExportData: { - id: 'menu.help.importExportData', - defaultMessage: 'Import/Export Configuration Data', - }, - support: { - id: 'menu.help.support', - defaultMessage: 'Support', - }, - debugInfo: { - id: 'menu.help.debugInfo', - defaultMessage: 'Copy Debug Information', - }, - publishDebugInfo: { - id: 'menu.help.publishDebugInfo', - defaultMessage: 'Publish Debug Information', - }, - debugInfoCopiedHeadline: { - id: 'menu.help.debugInfoCopiedHeadline', - defaultMessage: 'Ferdium Debug Information', - }, - debugInfoCopiedBody: { - id: 'menu.help.debugInfoCopiedBody', - defaultMessage: 'Your Debug Information has been copied to your clipboard.', - }, - touchId: { - id: 'locked.touchId', - defaultMessage: 'Unlock with Touch ID', - }, - touchIdPrompt: { - id: 'locked.touchIdPrompt', - defaultMessage: 'unlock via Touch ID', - }, - tos: { - id: 'menu.help.tos', - defaultMessage: 'Terms of Service', - }, - privacy: { - id: 'menu.help.privacy', - defaultMessage: 'Privacy Statement', - }, - file: { - id: 'menu.file', - defaultMessage: 'File', - }, - view: { - id: 'menu.view', - defaultMessage: 'View', - }, - services: { - id: 'menu.services', - defaultMessage: 'Services', - }, - window: { - id: 'menu.window', - defaultMessage: 'Window', - }, - help: { - id: 'menu.help', - defaultMessage: 'Help', - }, - about: { - id: 'menu.app.about', - defaultMessage: 'About Ferdium', - }, - checkForUpdates: { - id: 'menu.app.checkForUpdates', - defaultMessage: 'Check for updates', - }, - hide: { - id: 'menu.app.hide', - defaultMessage: 'Hide', - }, - hideOthers: { - id: 'menu.app.hideOthers', - defaultMessage: 'Hide Others', - }, - unhide: { - id: 'menu.app.unhide', - defaultMessage: 'Unhide', - }, - autohideMenuBar: { - id: 'menu.app.autohideMenuBar', - defaultMessage: 'Auto-hide menu bar', - }, - addNewService: { - id: 'menu.services.addNewService', - defaultMessage: 'Add New Service...', - }, - addNewWorkspace: { - id: 'menu.workspaces.addNewWorkspace', - defaultMessage: 'Add New Workspace...', - }, - openWorkspaceDrawer: { - id: 'menu.workspaces.openWorkspaceDrawer', - defaultMessage: 'Open workspace drawer', - }, - closeWorkspaceDrawer: { - id: 'menu.workspaces.closeWorkspaceDrawer', - defaultMessage: 'Close workspace drawer', - }, - activateNextService: { - id: 'menu.services.setNextServiceActive', - defaultMessage: 'Activate next service', - }, - activatePreviousService: { - id: 'menu.services.activatePreviousService', - defaultMessage: 'Activate previous service', - }, - muteApp: { - id: 'sidebar.muteApp', - defaultMessage: 'Disable notifications & audio', - }, - unmuteApp: { - id: 'sidebar.unmuteApp', - defaultMessage: 'Enable notifications & audio', - }, - workspaces: { - id: 'menu.workspaces', - defaultMessage: 'Workspaces', - }, - defaultWorkspace: { - id: 'menu.workspaces.defaultWorkspace', - defaultMessage: 'All services', - }, - todos: { - id: 'menu.todos', - defaultMessage: 'Todos', - }, - openTodosDrawer: { - id: 'menu.Todoss.openTodosDrawer', - defaultMessage: 'Open Todos drawer', - }, - closeTodosDrawer: { - id: 'menu.Todoss.closeTodosDrawer', - defaultMessage: 'Close Todos drawer', - }, - enableTodos: { - id: 'menu.todos.enableTodos', - defaultMessage: 'Enable Todos', - }, - disableTodos: { - id: 'menu.todos.disableTodos', - defaultMessage: 'Disable Todos', - }, - serviceGoHome: { - id: 'menu.services.goHome', - defaultMessage: 'Home', - }, - ok: { - id: 'global.ok', - defaultMessage: 'Ok', - }, - copyToClipboard: { - id: 'menu.services.copyToClipboard', - defaultMessage: 'Copy to clipboard', - }, -}); - -function getActiveService() { - return window['ferdium'].stores.services.active; -} - -function _toggleFullScreen() { - const mainWindow = getCurrentWindow(); - - if (!mainWindow) return; - - if (mainWindow.isFullScreen()) { - mainWindow.setFullScreen(false); - } else { - mainWindow.setFullScreen(true); - } -} - -const _titleBarTemplateFactory = (intl, locked) => [ - { - label: intl.formatMessage(menuItems.edit), - accelerator: `${altKey()}+E`, - submenu: [ - { - label: intl.formatMessage(menuItems.undo), - role: 'undo', - }, - { - label: intl.formatMessage(menuItems.redo), - role: 'redo', - }, - { - type: 'separator', - }, - { - label: intl.formatMessage(menuItems.cut), - accelerator: `${cmdOrCtrlShortcutKey()}+X`, - role: 'cut', - }, - { - label: intl.formatMessage(menuItems.copy), - accelerator: `${cmdOrCtrlShortcutKey()}+C`, - role: 'copy', - }, - { - label: intl.formatMessage(menuItems.paste), - accelerator: `${cmdOrCtrlShortcutKey()}+V`, - role: 'paste', - }, - { - label: intl.formatMessage(menuItems.pasteAndMatchStyle), - accelerator: `${cmdOrCtrlShortcutKey()}+${shiftKey()}+V`, // Override the accelerator since this adds new key combo in macos - role: 'pasteAndMatchStyle', - }, - { - label: intl.formatMessage(menuItems.delete), - role: 'delete', - }, - { - label: intl.formatMessage(menuItems.selectAll), - accelerator: `${cmdOrCtrlShortcutKey()}+A`, - role: 'selectall', - }, - ], - }, - { - label: intl.formatMessage(menuItems.view), - accelerator: `${altKey()}+V`, - visible: !locked, - submenu: [ - { - type: 'separator', - }, - { - label: intl.formatMessage(menuItems.openQuickSwitch), - accelerator: `${cmdOrCtrlShortcutKey()}+S`, - click() { - window['ferdium'].features.quickSwitch.state.isModalVisible = true; - }, - }, - { - type: 'separator', - }, - { - label: intl.formatMessage(menuItems.findInPage), - accelerator: `${cmdOrCtrlShortcutKey()}+F`, - click() { - const service = getActiveService(); - // Check if there is a service active - if (service) { - // Focus webview so find in page popup gets focused - service.webview.focus(); - - window['ferdium'].actions.service.sendIPCMessage({ - serviceId: service.id, - channel: 'find-in-page', - args: {}, - }); - } - }, - }, - { - type: 'separator', - }, - { - label: intl.formatMessage(menuItems.back), - accelerator: `${!isMac ? altKey() : cmdOrCtrlShortcutKey()}+Left`, - click() { - getActiveService().webview.goBack(); - }, - }, - { - label: intl.formatMessage(menuItems.forward), - accelerator: `${!isMac ? altKey() : cmdOrCtrlShortcutKey()}+Right`, - click() { - getActiveService().webview.goForward(); - }, - }, - { - type: 'separator', - }, - { - label: intl.formatMessage(menuItems.resetZoom), - accelerator: `${cmdOrCtrlShortcutKey()}+0`, - click() { - getActiveService().webview.setZoomLevel(0); - }, - }, - { - label: intl.formatMessage(menuItems.zoomIn), - accelerator: `${cmdOrCtrlShortcutKey()}+plus`, - click() { - const activeService = getActiveService().webview; - const level = activeService.getZoomLevel(); - - activeService.setZoomLevel(level + 0.5); - }, - }, - { - label: intl.formatMessage(menuItems.zoomOut), - accelerator: `${cmdOrCtrlShortcutKey()}+-`, - click() { - const activeService = getActiveService().webview; - const level = activeService.getZoomLevel(); - - activeService.setZoomLevel(level - 0.5); - }, - }, - { - type: 'separator', - }, - { - label: intl.formatMessage(menuItems.toggleFullScreen), - click: () => { - _toggleFullScreen(); - }, - accelerator: toggleFullScreenKey(), - }, - { - label: intl.formatMessage(menuItems.toggleNavigationBar), - accelerator: `${cmdOrCtrlShortcutKey()}+B`, - role: 'toggleNavigationBar', - type: 'checkbox', - checked: - window['ferdium'].stores.settings.app.navigationBarManualActive, - click: () => { - window['ferdium'].actions.settings.update({ - type: 'app', - data: { - navigationBarManualActive: - !window['ferdium'].stores.settings.app - .navigationBarManualActive, - }, - }); - }, - }, - { - label: intl.formatMessage(menuItems.splitModeToggle), - accelerator: `${splitModeToggleShortcutKey()}`, - role: 'splitModeToggle', - type: 'checkbox', - checked: window['ferdium'].stores.settings.app.splitMode, - click: () => { - window['ferdium'].actions.settings.update({ - type: 'app', - data: { - splitMode: !window['ferdium'].stores.settings.app.splitMode, - }, - }); - }, - }, - { - label: intl.formatMessage(menuItems.toggleDarkMode), - type: 'checkbox', - accelerator: `${cmdOrCtrlShortcutKey()}+${shiftKey()}+D`, - checked: window['ferdium'].stores.settings.app.darkMode, - click: () => { - window['ferdium'].actions.settings.update({ - type: 'app', - data: { - darkMode: !window['ferdium'].stores.settings.app.darkMode, - }, - }); - }, - }, - ], - }, - { - label: intl.formatMessage(menuItems.services), - accelerator: `${altKey()}+S`, - visible: !locked, - submenu: [], - }, - { - label: intl.formatMessage(menuItems.workspaces), - accelerator: `${altKey()}+W`, - submenu: [], - visible: !locked, - }, - { - label: intl.formatMessage(menuItems.todos), - submenu: [], - visible: !locked, - }, - { - label: intl.formatMessage(menuItems.window), - role: 'window', - submenu: [ - { - label: intl.formatMessage(menuItems.minimize), - role: 'minimize', - }, - { - label: intl.formatMessage(menuItems.close), - role: 'close', - }, - ], - }, - { - label: intl.formatMessage(menuItems.help), - accelerator: `${altKey()}+H`, - role: 'help', - submenu: [ - { - label: intl.formatMessage(menuItems.learnMore), - click() { - openExternalUrl(LIVE_API_FERDIUM_WEBSITE, true); - }, - }, - { - label: intl.formatMessage(menuItems.changelog), - click() { - window.location.href = onAuthGoToReleaseNotes(window.location.href); - }, - }, - { - label: intl.formatMessage(menuItems.importExportData), - click() { - openExternalUrl(importExportURL(), true); - }, - enabled: !locked, - }, - { - type: 'separator', - }, - { - label: intl.formatMessage(menuItems.support), - click() { - openExternalUrl(`${LIVE_API_FERDIUM_WEBSITE}/contact`, true); - }, - }, - { - type: 'separator', - }, - { - label: intl.formatMessage(menuItems.tos), - click() { - openExternalUrl(`${serverBase()}/terms`, true); - }, - }, - { - label: intl.formatMessage(menuItems.privacy), - click() { - openExternalUrl(`${serverBase()}/privacy`, true); - }, - }, - ], - }, -]; - -class FranzMenu { - @observable currentTemplate = []; - - constructor(stores, actions) { - this.stores = stores; - this.actions = actions; - - makeObservable(this); - - setTimeout(() => { - autorun(this._build.bind(this)); - }, 10); - } - - @action _setCurrentTemplate(tpl) { - this.currentTemplate = tpl; - } - - rebuild() { - this._build(); - } - - get template() { - return fromJS(this.currentTemplate).toJS(); - } - - getOsName() { - let osNameParse = osName(); - const isWin11 = semver.satisfies(os.release(), '>=10.0.22000'); - - osNameParse = isWindows && isWin11 ? 'Windows 11' : osNameParse; - - return osNameParse; - } - - _build() { - // need to clone object so we don't modify computed (cached) object - const serviceTpl = Object.assign([], this.serviceTpl()); - - // Don't initialize when window['ferdium'] is undefined - if (window['ferdium'] === undefined) { - console.log('skipping menu init'); - return; - } - - const { intl } = window['ferdium']; - const locked = - this.stores.settings.app.locked && - this.stores.settings.app.lockingFeatureEnabled && - this.stores.user.isLoggedIn; - const tpl = _titleBarTemplateFactory(intl, locked); - const { actions } = this; - - if (!isMac) { - tpl[1].submenu.push({ - label: intl.formatMessage(menuItems.autohideMenuBar), - type: 'checkbox', - checked: window['ferdium'].stores.settings.app.autohideMenuBar, - click: () => { - window['ferdium'].actions.settings.update({ - type: 'app', - data: { - autohideMenuBar: - !window['ferdium'].stores.settings.app.autohideMenuBar, - }, - }); - }, - }); - } - - if (!locked) { - tpl[1].submenu.push( - { - type: 'separator', - }, - { - label: intl.formatMessage(menuItems.toggleDevTools), - accelerator: `${cmdOrCtrlShortcutKey()}+${altKey()}+I`, - click: () => { - const windowWebContents = webContents.fromId(1); - const { isDevToolsOpened, openDevTools, closeDevTools } = - windowWebContents; - - if (isDevToolsOpened()) { - closeDevTools(); - } else { - openDevTools({ mode: 'right' }); - } - }, - }, - { - label: intl.formatMessage(menuItems.toggleServiceDevTools), - accelerator: `${cmdOrCtrlShortcutKey()}+${shiftKey()}+${altKey()}+I`, - click: () => { - this.actions.service.openDevToolsForActiveService(); - }, - enabled: - this.stores.user.isLoggedIn && - this.stores.services.enabled.length > 0, - }, - ); - - if (this.stores.todos.isFeatureEnabledByUser) { - tpl[1].submenu.push({ - label: intl.formatMessage(menuItems.toggleTodosDevTools), - accelerator: `${cmdOrCtrlShortcutKey()}+${shiftKey()}+${altKey()}+O`, - click: () => { - const webview = document.querySelector('#todos-panel webview'); - if (webview) this.actions.todos.openDevTools(); - }, - }); - } - - tpl[1].submenu.unshift( - { - label: intl.formatMessage(menuItems.reloadService), - accelerator: `${cmdOrCtrlShortcutKey()}+R`, - click: () => { - if ( - this.stores.user.isLoggedIn && - this.stores.services.enabled.length > 0 - ) { - if (getActiveService().recipe.id === CUSTOM_WEBSITE_RECIPE_ID) { - getActiveService().webview.reload(); - } else { - this.actions.service.reloadActive(); - } - } else { - window.location.reload(); - } - }, - }, - { - label: intl.formatMessage(menuItems.reloadFerdium), - accelerator: `${cmdOrCtrlShortcutKey()}+${shiftKey()}+R`, - click: () => { - window.location.reload(); - }, - }, - { - label: intl.formatMessage(menuItems.reloadTodos), - accelerator: `${cmdOrCtrlShortcutKey()}+${shiftKey()}+${altKey()}+R`, - click: () => { - this.actions.todos.reload(); - }, - }, - { - type: 'separator', - }, - { - label: intl.formatMessage(menuItems.lockFerdium), - accelerator: `${lockFerdiumShortcutKey()}`, - enabled: - this.stores.user.isLoggedIn && - this.stores.settings.app.lockingFeatureEnabled, - click() { - actions.settings.update({ - type: 'app', - data: { - locked: true, - }, - }); - }, - }, - ); - - if (serviceTpl.length > 0) { - tpl[2].submenu = serviceTpl; - } - - tpl[3].submenu = this.workspacesMenu(); - - tpl[4].submenu = this.todosMenu(); - } else { - const touchIdEnabled = isMac - ? this.stores.settings.app.useTouchIdToUnlock && - systemPreferences.canPromptTouchID() - : false; - - tpl[0].submenu.unshift( - { - label: intl.formatMessage(menuItems.touchId), - accelerator: `${lockFerdiumShortcutKey()}`, - visible: touchIdEnabled, - click() { - systemPreferences - .promptTouchID(intl.formatMessage(menuItems.touchIdPrompt)) - .then(() => { - actions.settings.update({ - type: 'app', - data: { - locked: false, - }, - }); - }); - }, - }, - { - type: 'separator', - visible: touchIdEnabled, - }, - ); - } - - tpl.unshift({ - label: isMac ? app.name : intl.formatMessage(menuItems.file), - accelerator: `${altKey()}+F`, - submenu: [ - { - type: 'separator', - }, - { - label: intl.formatMessage(globalMessages.settings), - accelerator: `${settingsShortcutKey()}`, - click: () => { - this.actions.ui.openSettings({ path: 'app' }); - }, - enabled: this.stores.user.isLoggedIn, - visible: !locked, - }, - { - label: intl.formatMessage(menuItems.checkForUpdates), - visible: !locked, - click: () => { - this.actions.app.checkForUpdates(); - }, - }, - { - type: 'separator', - visible: !locked, - }, - { - label: intl.formatMessage(menuItems.services), - role: 'services', - submenu: [], - }, - { - type: 'separator', - }, - { - label: intl.formatMessage(menuItems.hide), - role: 'hide', - }, - { - label: intl.formatMessage(menuItems.hideOthers), - role: 'hideOthers', - }, - { - label: intl.formatMessage(menuItems.unhide), - role: 'unhide', - }, - { - type: 'separator', - }, - { - label: intl.formatMessage(globalMessages.quit), - accelerator: `${cmdOrCtrlShortcutKey()}+Q`, - click() { - app.quit(); - }, - }, - ], - }); - - const aboutAppDetails = [ - `Version: ${ferdiumVersion}`, - `Server: ${serverName()} Server`, - `Electron: ${electronVersion}`, - `Chrome: ${chromeVersion}`, - `Node.js: ${nodeVersion}`, - `Platform: ${this.getOsName()}`, - `Arch: ${osArch}`, - `Build date: ${new Date(Number(buildInfo.timestamp))}`, - `Git SHA: ${buildInfo.gitHashShort}`, - `Git branch: ${buildInfo.gitBranch}`, - ].join('\n'); - - const about = { - label: intl.formatMessage(menuItems.about), - click: () => { - dialog - .showMessageBox({ - type: 'info', - title: 'Ferdium', - message: 'Ferdium', - detail: aboutAppDetails, - buttons: [ - intl.formatMessage(menuItems.ok), - intl.formatMessage(menuItems.copyToClipboard), - ], - }) - .then(result => { - if (result.response === 1) { - clipboard.write({ - text: aboutAppDetails, - }); - } - }); - }, - }; - - if (isMac) { - // Edit menu. - tpl[1].submenu.push( - { - type: 'separator', - }, - { - label: intl.formatMessage(menuItems.speech), - submenu: [ - { - label: intl.formatMessage(menuItems.startSpeaking), - role: 'startspeaking', - }, - { - label: intl.formatMessage(menuItems.stopSpeaking), - role: 'stopspeaking', - }, - ], - }, - ); - - tpl[0].submenu.unshift(about, { - type: 'separator', - }); - } else { - tpl[0].submenu = [ - { - label: intl.formatMessage(globalMessages.settings), - accelerator: `${settingsShortcutKey()}`, - click: () => { - this.actions.ui.openSettings({ path: 'app' }); - }, - enabled: this.stores.user.isLoggedIn, - visible: !locked, - }, - { - type: 'separator', - }, - { - label: intl.formatMessage(globalMessages.quit), - accelerator: `${cmdOrCtrlShortcutKey()}+Q`, - click() { - app.quit(); - }, - }, - ]; - - tpl[tpl.length - 1].submenu.push( - { - type: 'separator', - }, - about, - ); - } - - if (!locked) { - if (serviceTpl.length > 0) { - tpl[3].submenu = serviceTpl; - } - - tpl[4].submenu = this.workspacesMenu(); - - tpl[5].submenu = this.todosMenu(); - - tpl[tpl.length - 1].submenu.push( - { - type: 'separator', - }, - ...this.debugMenu(), - ); - } - this._setCurrentTemplate(tpl); - const menu = Menu.buildFromTemplate(tpl); - Menu.setApplicationMenu(menu); - } - - serviceTpl() { - const { intl } = window['ferdium']; - const { user, services, settings } = this.stores; - if (!user.isLoggedIn) return []; - const menu = []; - const cmdAltShortcutsVisibile = !isLinux; - - menu.push( - { - label: intl.formatMessage(menuItems.addNewService), - accelerator: `${addNewServiceShortcutKey()}`, - click: () => { - this.actions.ui.openSettings({ path: 'recipes' }); - }, - }, - { - type: 'separator', - }, - { - label: intl.formatMessage(menuItems.activateNextService), - accelerator: `${cmdOrCtrlShortcutKey()}+tab`, - click: () => this.actions.service.setActiveNext(), - visible: !cmdAltShortcutsVisibile, - }, - { - label: intl.formatMessage(menuItems.activateNextService), - accelerator: `${cmdOrCtrlShortcutKey()}+${altKey()}+right`, - click: () => this.actions.service.setActiveNext(), - visible: cmdAltShortcutsVisibile, - }, - { - label: intl.formatMessage(menuItems.activatePreviousService), - accelerator: `${cmdOrCtrlShortcutKey()}+${shiftKey()}+tab`, - click: () => this.actions.service.setActivePrev(), - visible: !cmdAltShortcutsVisibile, - }, - { - label: intl.formatMessage(menuItems.activatePreviousService), - accelerator: `${cmdOrCtrlShortcutKey()}+${altKey()}+left`, - click: () => this.actions.service.setActivePrev(), - visible: cmdAltShortcutsVisibile, - }, - { - label: intl - .formatMessage( - settings.all.app.isAppMuted - ? menuItems.unmuteApp - : menuItems.muteApp, - ) - .replace('&', '&&'), - accelerator: `${muteFerdiumShortcutKey()}`, - click: () => this.actions.app.toggleMuteApp(), - }, - { - type: 'separator', - }, - ); - - for (const [i, service] of services.allDisplayed.entries()) { - menu.push({ - label: this._getServiceName(service), - accelerator: i < 9 ? `${cmdOrCtrlShortcutKey()}+${i + 1}` : null, - type: 'radio', - checked: service.isActive, - click: () => { - this.actions.service.setActive({ serviceId: service.id }); - - if (isMac && i === 0) { - // feat(Mac): Open Window with Cmd+1 - getCurrentWindow().restore(); - } - }, - }); - } - - if ( - services.active && - services.active.recipe.id === CUSTOM_WEBSITE_RECIPE_ID - ) { - menu.push( - { - type: 'separator', - }, - { - label: intl.formatMessage(menuItems.serviceGoHome), - accelerator: `${cmdOrCtrlShortcutKey()}+${shiftKey()}+H`, - click: () => this.actions.service.reloadActive(), - }, - ); - } - - return menu; - } - - workspacesMenu() { - const { workspaces, activeWorkspace, isWorkspaceDrawerOpen } = - workspaceStore; - const { intl } = window['ferdium']; - const menu = []; - - // Add new workspace item: - menu.push({ - label: intl.formatMessage(menuItems.addNewWorkspace), - accelerator: `${cmdOrCtrlShortcutKey()}+${shiftKey()}+N`, - click: () => { - workspaceActions.openWorkspaceSettings(); - }, - enabled: this.stores.user.isLoggedIn, - }); - - // Open workspace drawer: - if (!this.stores.settings.app.alwaysShowWorkspaces) { - const drawerLabel = isWorkspaceDrawerOpen - ? menuItems.closeWorkspaceDrawer - : menuItems.openWorkspaceDrawer; - menu.push({ - label: intl.formatMessage(drawerLabel), - accelerator: `${workspaceToggleShortcutKey()}`, - click: () => { - workspaceActions.toggleWorkspaceDrawer(); - }, - enabled: this.stores.user.isLoggedIn, - }); - } - - menu.push( - { - type: 'separator', - }, - { - label: intl.formatMessage(menuItems.defaultWorkspace), - accelerator: `${cmdOrCtrlShortcutKey()}+${altKey()}+0`, - type: 'radio', - checked: !activeWorkspace, - click: () => { - workspaceActions.deactivate(); - }, - }, - ); - - // Workspace items - for (const [i, workspace] of workspaces.entries()) { - menu.push({ - label: workspace.name, - accelerator: - i < 9 ? `${cmdOrCtrlShortcutKey()}+${altKey()}+${i + 1}` : null, - type: 'radio', - checked: activeWorkspace ? workspace.id === activeWorkspace.id : false, - click: () => { - workspaceActions.activate({ workspace }); - }, - }); - } - - return menu; - } - - todosMenu() { - const { isTodosPanelVisible, isFeatureEnabledByUser } = this.stores.todos; - const { intl } = window['ferdium']; - const menu = []; - - menu.push({ - label: intl.formatMessage( - isFeatureEnabledByUser ? menuItems.disableTodos : menuItems.enableTodos, - ), - click: () => { - todoActions.toggleTodosFeatureVisibility(); - }, - enabled: this.stores.user.isLoggedIn, - }); - - if (isFeatureEnabledByUser) { - menu.push( - { - type: 'separator', - }, - { - label: intl.formatMessage( - isTodosPanelVisible - ? menuItems.closeTodosDrawer - : menuItems.openTodosDrawer, - ), - accelerator: `${todosToggleShortcutKey()}`, - click: () => { - todoActions.toggleTodosPanel(); - }, - enabled: this.stores.user.isLoggedIn, - }, - ); - } - - return menu; - } - - debugMenu() { - const { intl } = window['ferdium']; - - return [ - { - label: intl.formatMessage(menuItems.debugInfo), - click: () => { - const { debugInfo } = this.stores.app; - - clipboard.write({ - text: JSON.stringify(debugInfo), - }); - - this.actions.app.notify({ - title: intl.formatMessage(menuItems.debugInfoCopiedHeadline), - options: { - body: intl.formatMessage(menuItems.debugInfoCopiedBody), - }, - }); - }, - }, - { - label: intl.formatMessage(menuItems.publishDebugInfo), - click: () => { - window[ - 'ferdium' - ].features.publishDebugInfo.state.isModalVisible = true; - }, - }, - ]; - } - - _getServiceName(service) { - if (service.name) { - return service.name; - } - - let { name } = service.recipe; - - if (service.team) { - name = `${name} (${service.team})`; - } else if (service.customUrl) { - name = `${name} (${service.customUrl})`; - } - - return name; - } -} - -export default FranzMenu; diff --git a/src/lib/Menu.ts b/src/lib/Menu.ts new file mode 100644 index 000000000..c206ea55d --- /dev/null +++ b/src/lib/Menu.ts @@ -0,0 +1,1286 @@ +import { clipboard, MenuItemConstructorOptions } from 'electron'; +import { + app, + Menu, + dialog, + webContents, + systemPreferences, + getCurrentWindow, +} from '@electron/remote'; +import { autorun, action, makeObservable, observable } from 'mobx'; +import { defineMessages, IntlShape } from 'react-intl'; +import osName from 'os-name'; +import { fromJS } from 'immutable'; +import semver from 'semver'; +import os from 'os'; +import { + isWindows, + cmdOrCtrlShortcutKey, + altKey, + shiftKey, + settingsShortcutKey, + isLinux, + isMac, + lockFerdiumShortcutKey, + todosToggleShortcutKey, + workspaceToggleShortcutKey, + addNewServiceShortcutKey, + splitModeToggleShortcutKey, + muteFerdiumShortcutKey, + electronVersion, + chromeVersion, + nodeVersion, + osArch, + toggleFullScreenKey, +} from '../environment'; +import { CUSTOM_WEBSITE_RECIPE_ID, LIVE_API_FERDIUM_WEBSITE } from '../config'; +import { ferdiumVersion } from '../environment-remote'; +import { todoActions } from '../features/todos/actions'; +import workspaceActions from '../features/workspaces/actions'; +import { workspaceStore } from '../features/workspaces/index'; +import { importExportURL, serverBase, serverName } from '../api/apiBase'; +import { openExternalUrl } from '../helpers/url-helpers'; +import globalMessages from '../i18n/globalMessages'; +import { onAuthGoToReleaseNotes } from '../helpers/update-helpers'; +// @ts-expect-error Cannot find module '../buildInfo.json' or its corresponding type declarations. +import { timestamp, gitHashShort, gitBranch } from '../buildInfo.json'; +import Service from '../models/Service'; +import { StoresProps } from '../@types/ferdium-components.types'; +import { RealStores } from '../stores'; + +const menuItems = defineMessages({ + edit: { + id: 'menu.edit', + defaultMessage: 'Edit', + }, + undo: { + id: 'menu.edit.undo', + defaultMessage: 'Undo', + }, + redo: { + id: 'menu.edit.redo', + defaultMessage: 'Redo', + }, + cut: { + id: 'menu.edit.cut', + defaultMessage: 'Cut', + }, + copy: { + id: 'menu.edit.copy', + defaultMessage: 'Copy', + }, + paste: { + id: 'menu.edit.paste', + defaultMessage: 'Paste', + }, + pasteAndMatchStyle: { + id: 'menu.edit.pasteAndMatchStyle', + defaultMessage: 'Paste And Match Style', + }, + delete: { + id: 'menu.edit.delete', + defaultMessage: 'Delete', + }, + selectAll: { + id: 'menu.edit.selectAll', + defaultMessage: 'Select All', + }, + findInPage: { + id: 'menu.edit.findInPage', + defaultMessage: 'Find in Page', + }, + speech: { + id: 'menu.edit.speech', + defaultMessage: 'Speech', + }, + startSpeaking: { + id: 'menu.edit.startSpeaking', + defaultMessage: 'Start Speaking', + }, + stopSpeaking: { + id: 'menu.edit.stopSpeaking', + defaultMessage: 'Stop Speaking', + }, + startDictation: { + id: 'menu.edit.startDictation', + defaultMessage: 'Start Dictation', + }, + emojiSymbols: { + id: 'menu.edit.emojiSymbols', + defaultMessage: 'Emoji & Symbols', + }, + openQuickSwitch: { + id: 'menu.view.openQuickSwitch', + defaultMessage: 'Open Quick Switch', + }, + back: { + id: 'menu.view.back', + defaultMessage: 'Back', + }, + forward: { + id: 'menu.view.forward', + defaultMessage: 'Forward', + }, + resetZoom: { + id: 'menu.view.resetZoom', + defaultMessage: 'Actual Size', + }, + zoomIn: { + id: 'menu.view.zoomIn', + defaultMessage: 'Zoom In', + }, + zoomOut: { + id: 'menu.view.zoomOut', + defaultMessage: 'Zoom Out', + }, + toggleFullScreen: { + id: 'menu.view.toggleFullScreen', + defaultMessage: 'Toggle Full Screen', + }, + toggleNavigationBar: { + id: 'menu.view.toggleNavigationBar', + defaultMessage: 'Toggle Navigation Bar', + }, + splitModeToggle: { + id: 'menu.view.splitModeToggle', + defaultMessage: 'Toggle Split Mode', + }, + toggleDarkMode: { + id: 'menu.view.toggleDarkMode', + defaultMessage: 'Toggle Dark Mode', + }, + toggleDevTools: { + id: 'menu.view.toggleDevTools', + defaultMessage: 'Toggle Developer Tools', + }, + toggleTodosDevTools: { + id: 'menu.view.toggleTodosDevTools', + defaultMessage: 'Toggle Todos Developer Tools', + }, + toggleServiceDevTools: { + id: 'menu.view.toggleServiceDevTools', + defaultMessage: 'Toggle Service Developer Tools', + }, + reloadService: { + id: 'menu.view.reloadService', + defaultMessage: 'Reload Service', + }, + reloadFerdium: { + id: 'menu.view.reloadFerdium', + defaultMessage: 'Reload Ferdium', + }, + lockFerdium: { + id: 'menu.view.lockFerdium', + defaultMessage: 'Lock Ferdium', + }, + reloadTodos: { + id: 'menu.view.reloadTodos', + defaultMessage: 'Reload ToDos', + }, + minimize: { + id: 'menu.window.minimize', + defaultMessage: 'Minimize', + }, + close: { + id: 'menu.window.close', + defaultMessage: 'Close', + }, + learnMore: { + id: 'menu.help.learnMore', + defaultMessage: 'Learn More', + }, + changelog: { + id: 'menu.help.changelog', + defaultMessage: 'Changelog', + }, + importExportData: { + id: 'menu.help.importExportData', + defaultMessage: 'Import/Export Configuration Data', + }, + support: { + id: 'menu.help.support', + defaultMessage: 'Support', + }, + debugInfo: { + id: 'menu.help.debugInfo', + defaultMessage: 'Copy Debug Information', + }, + publishDebugInfo: { + id: 'menu.help.publishDebugInfo', + defaultMessage: 'Publish Debug Information', + }, + debugInfoCopiedHeadline: { + id: 'menu.help.debugInfoCopiedHeadline', + defaultMessage: 'Ferdium Debug Information', + }, + debugInfoCopiedBody: { + id: 'menu.help.debugInfoCopiedBody', + defaultMessage: 'Your Debug Information has been copied to your clipboard.', + }, + touchId: { + id: 'locked.touchId', + defaultMessage: 'Unlock with Touch ID', + }, + touchIdPrompt: { + id: 'locked.touchIdPrompt', + defaultMessage: 'unlock via Touch ID', + }, + tos: { + id: 'menu.help.tos', + defaultMessage: 'Terms of Service', + }, + privacy: { + id: 'menu.help.privacy', + defaultMessage: 'Privacy Statement', + }, + file: { + id: 'menu.file', + defaultMessage: 'File', + }, + view: { + id: 'menu.view', + defaultMessage: 'View', + }, + services: { + id: 'menu.services', + defaultMessage: 'Services', + }, + window: { + id: 'menu.window', + defaultMessage: 'Window', + }, + help: { + id: 'menu.help', + defaultMessage: 'Help', + }, + about: { + id: 'menu.app.about', + defaultMessage: 'About Ferdium', + }, + checkForUpdates: { + id: 'menu.app.checkForUpdates', + defaultMessage: 'Check for updates', + }, + hide: { + id: 'menu.app.hide', + defaultMessage: 'Hide', + }, + hideOthers: { + id: 'menu.app.hideOthers', + defaultMessage: 'Hide Others', + }, + unhide: { + id: 'menu.app.unhide', + defaultMessage: 'Unhide', + }, + autohideMenuBar: { + id: 'menu.app.autohideMenuBar', + defaultMessage: 'Auto-hide menu bar', + }, + addNewService: { + id: 'menu.services.addNewService', + defaultMessage: 'Add New Service...', + }, + addNewWorkspace: { + id: 'menu.workspaces.addNewWorkspace', + defaultMessage: 'Add New Workspace...', + }, + openWorkspaceDrawer: { + id: 'menu.workspaces.openWorkspaceDrawer', + defaultMessage: 'Open workspace drawer', + }, + closeWorkspaceDrawer: { + id: 'menu.workspaces.closeWorkspaceDrawer', + defaultMessage: 'Close workspace drawer', + }, + activateNextService: { + id: 'menu.services.setNextServiceActive', + defaultMessage: 'Activate next service', + }, + activatePreviousService: { + id: 'menu.services.activatePreviousService', + defaultMessage: 'Activate previous service', + }, + muteApp: { + id: 'sidebar.muteApp', + defaultMessage: 'Disable notifications & audio', + }, + unmuteApp: { + id: 'sidebar.unmuteApp', + defaultMessage: 'Enable notifications & audio', + }, + workspaces: { + id: 'menu.workspaces', + defaultMessage: 'Workspaces', + }, + defaultWorkspace: { + id: 'menu.workspaces.defaultWorkspace', + defaultMessage: 'All services', + }, + todos: { + id: 'menu.todos', + defaultMessage: 'Todos', + }, + openTodosDrawer: { + id: 'menu.Todoss.openTodosDrawer', + defaultMessage: 'Open Todos drawer', + }, + closeTodosDrawer: { + id: 'menu.Todoss.closeTodosDrawer', + defaultMessage: 'Close Todos drawer', + }, + enableTodos: { + id: 'menu.todos.enableTodos', + defaultMessage: 'Enable Todos', + }, + disableTodos: { + id: 'menu.todos.disableTodos', + defaultMessage: 'Disable Todos', + }, + serviceGoHome: { + id: 'menu.services.goHome', + defaultMessage: 'Home', + }, + ok: { + id: 'global.ok', + defaultMessage: 'Ok', + }, + copyToClipboard: { + id: 'menu.services.copyToClipboard', + defaultMessage: 'Copy to clipboard', + }, +}); + +function getActiveService(): Service | undefined { + return window['ferdium'].stores.services.active; +} + +function toggleFullScreen(): void { + const mainWindow = getCurrentWindow(); + + if (!mainWindow) return; + + if (mainWindow.isFullScreen()) { + mainWindow.setFullScreen(false); + } else { + mainWindow.setFullScreen(true); + } +} + +function titleBarTemplateFactory( + intl: IntlShape, + locked: boolean, +): MenuItemConstructorOptions[] { + return [ + { + label: intl.formatMessage(menuItems.edit), + accelerator: `${altKey()}+E`, + submenu: [ + { + label: intl.formatMessage(menuItems.undo), + role: 'undo', + }, + { + label: intl.formatMessage(menuItems.redo), + role: 'redo', + }, + { + type: 'separator', + }, + { + label: intl.formatMessage(menuItems.cut), + accelerator: `${cmdOrCtrlShortcutKey()}+X`, + role: 'cut', + }, + { + label: intl.formatMessage(menuItems.copy), + accelerator: `${cmdOrCtrlShortcutKey()}+C`, + role: 'copy', + }, + { + label: intl.formatMessage(menuItems.paste), + accelerator: `${cmdOrCtrlShortcutKey()}+V`, + role: 'paste', + }, + { + label: intl.formatMessage(menuItems.pasteAndMatchStyle), + accelerator: `${cmdOrCtrlShortcutKey()}+${shiftKey()}+V`, // Override the accelerator since this adds new key combo in macos + role: 'pasteAndMatchStyle', + }, + { + label: intl.formatMessage(menuItems.delete), + role: 'delete', + }, + { + label: intl.formatMessage(menuItems.selectAll), + accelerator: `${cmdOrCtrlShortcutKey()}+A`, + role: 'selectAll', + }, + ], + }, + { + label: intl.formatMessage(menuItems.view), + accelerator: `${altKey()}+V`, + visible: !locked, + submenu: [ + { + type: 'separator', + }, + { + label: intl.formatMessage(menuItems.openQuickSwitch), + accelerator: `${cmdOrCtrlShortcutKey()}+S`, + click() { + window['ferdium'].features.quickSwitch.state.isModalVisible = true; + }, + }, + { + type: 'separator', + }, + { + label: intl.formatMessage(menuItems.findInPage), + accelerator: `${cmdOrCtrlShortcutKey()}+F`, + click() { + const activeService = getActiveService(); + if (!activeService) { + return; + } + activeService.webview.focus(); + window['ferdium'].actions.service.sendIPCMessage({ + serviceId: activeService.id, + channel: 'find-in-page', + args: {}, + }); + }, + }, + { + type: 'separator', + }, + { + label: intl.formatMessage(menuItems.back), + accelerator: `${!isMac ? altKey() : cmdOrCtrlShortcutKey()}+Left`, + click() { + const activeService = getActiveService(); + if (!activeService) { + return; + } + activeService.webview.goBack(); + }, + }, + { + label: intl.formatMessage(menuItems.forward), + accelerator: `${!isMac ? altKey() : cmdOrCtrlShortcutKey()}+Right`, + click() { + const activeService = getActiveService(); + if (!activeService) { + return; + } + activeService.webview.goForward(); + }, + }, + { + type: 'separator', + }, + { + label: intl.formatMessage(menuItems.resetZoom), + accelerator: `${cmdOrCtrlShortcutKey()}+0`, + click() { + const activeService = getActiveService(); + if (!activeService) { + return; + } + activeService.webview.setZoomLevel(0); + }, + }, + { + label: intl.formatMessage(menuItems.zoomIn), + accelerator: `${cmdOrCtrlShortcutKey()}+plus`, + click() { + const activeService = getActiveService(); + if (!activeService) { + return; + } + const { webview } = activeService; + const level = webview.getZoomLevel(); + webview.setZoomLevel(level + 0.5); + }, + }, + { + label: intl.formatMessage(menuItems.zoomOut), + accelerator: `${cmdOrCtrlShortcutKey()}+-`, + click() { + const activeService = getActiveService(); + if (!activeService) { + return; + } + const { webview } = activeService; + const level = webview.getZoomLevel(); + webview.setZoomLevel(level - 0.5); + }, + }, + { + type: 'separator', + }, + { + label: intl.formatMessage(menuItems.toggleFullScreen), + click: () => { + toggleFullScreen(); + }, + accelerator: toggleFullScreenKey(), + }, + { + label: intl.formatMessage(menuItems.toggleNavigationBar), + accelerator: `${cmdOrCtrlShortcutKey()}+B`, + // role: 'toggleNavigationBar', + type: 'checkbox', + checked: + window['ferdium'].stores.settings.app.navigationBarManualActive, + click: () => { + window['ferdium'].actions.settings.update({ + type: 'app', + data: { + navigationBarManualActive: + !window['ferdium'].stores.settings.app + .navigationBarManualActive, + }, + }); + }, + }, + { + label: intl.formatMessage(menuItems.splitModeToggle), + accelerator: `${splitModeToggleShortcutKey()}`, + // role: 'splitModeToggle', + type: 'checkbox', + checked: window['ferdium'].stores.settings.app.splitMode, + click: () => { + window['ferdium'].actions.settings.update({ + type: 'app', + data: { + splitMode: !window['ferdium'].stores.settings.app.splitMode, + }, + }); + }, + }, + { + label: intl.formatMessage(menuItems.toggleDarkMode), + type: 'checkbox', + accelerator: `${cmdOrCtrlShortcutKey()}+${shiftKey()}+D`, + checked: window['ferdium'].stores.settings.app.darkMode, + click: () => { + window['ferdium'].actions.settings.update({ + type: 'app', + data: { + darkMode: !window['ferdium'].stores.settings.app.darkMode, + }, + }); + }, + }, + ], + }, + { + label: intl.formatMessage(menuItems.services), + accelerator: `${altKey()}+S`, + visible: !locked, + submenu: [], + }, + { + label: intl.formatMessage(menuItems.workspaces), + accelerator: `${altKey()}+W`, + submenu: [], + visible: !locked, + }, + { + label: intl.formatMessage(menuItems.todos), + submenu: [], + visible: !locked, + }, + { + label: intl.formatMessage(menuItems.window), + role: 'window', + submenu: [ + { + label: intl.formatMessage(menuItems.minimize), + role: 'minimize', + }, + { + label: intl.formatMessage(menuItems.close), + role: 'close', + }, + ], + }, + { + label: intl.formatMessage(menuItems.help), + accelerator: `${altKey()}+H`, + role: 'help', + submenu: [ + { + label: intl.formatMessage(menuItems.learnMore), + click() { + openExternalUrl(LIVE_API_FERDIUM_WEBSITE, true); + }, + }, + { + label: intl.formatMessage(menuItems.changelog), + click() { + window.location.href = onAuthGoToReleaseNotes(window.location.href); + }, + }, + { + label: intl.formatMessage(menuItems.importExportData), + click() { + openExternalUrl(importExportURL(), true); + }, + enabled: !locked, + }, + { + type: 'separator', + }, + { + label: intl.formatMessage(menuItems.support), + click() { + openExternalUrl(`${LIVE_API_FERDIUM_WEBSITE}/contact`, true); + }, + }, + { + type: 'separator', + }, + { + label: intl.formatMessage(menuItems.tos), + click() { + openExternalUrl(`${serverBase()}/terms`, true); + }, + }, + { + label: intl.formatMessage(menuItems.privacy), + click() { + openExternalUrl(`${serverBase()}/privacy`, true); + }, + }, + ], + }, + ]; +} + +class FranzMenu implements StoresProps { + @observable currentTemplate: MenuItemConstructorOptions[]; + + actions: any; + + stores: RealStores; + + constructor(stores: RealStores, actions: any) { + this.stores = stores; + this.actions = actions; + this.currentTemplate = []; + + makeObservable(this); + + setTimeout(() => autorun(this._build.bind(this)), 10); + } + + @action _setCurrentTemplate(tpl: MenuItemConstructorOptions[]): void { + this.currentTemplate = tpl; + } + + rebuild(): void { + this._build(); + } + + get template(): any { + return fromJS(this.currentTemplate).toJS(); + } + + getOsName(): string { + let osNameParse = osName(); + const isWin11 = semver.satisfies(os.release(), '>=10.0.22000'); + osNameParse = isWindows && isWin11 ? 'Windows 11' : osNameParse; + + return osNameParse; + } + + _build(): void { + // need to clone object so we don't modify computed (cached) object + const serviceTpl = Object.assign([], this.serviceTpl()); + + // Don't initialize when window['ferdium'] is undefined + if (window['ferdium'] === undefined) { + console.log('skipping menu init'); + return; + } + + const { intl } = window['ferdium']; + const locked = + this.stores.settings.app.locked && + this.stores.settings.app.lockingFeatureEnabled && + this.stores.user.isLoggedIn; + const { actions } = this; + const tpl = titleBarTemplateFactory(intl, locked); + + if (!isMac) { + (tpl[1].submenu as MenuItemConstructorOptions[]).push({ + label: intl.formatMessage(menuItems.autohideMenuBar), + type: 'checkbox', + checked: window['ferdium'].stores.settings.app.autohideMenuBar, + click: () => { + window['ferdium'].actions.settings.update({ + type: 'app', + data: { + autohideMenuBar: + !window['ferdium'].stores.settings.app.autohideMenuBar, + }, + }); + }, + }); + } + + if (!locked) { + (tpl[1].submenu as MenuItemConstructorOptions[]).push( + { + type: 'separator', + }, + { + label: intl.formatMessage(menuItems.toggleDevTools), + accelerator: `${cmdOrCtrlShortcutKey()}+${altKey()}+I`, + click: () => { + const windowWebContents = webContents.fromId(1); + const { isDevToolsOpened, openDevTools, closeDevTools } = + windowWebContents; + + if (isDevToolsOpened()) { + closeDevTools(); + } else { + openDevTools({ mode: 'right' }); + } + }, + }, + { + label: intl.formatMessage(menuItems.toggleServiceDevTools), + accelerator: `${cmdOrCtrlShortcutKey()}+${shiftKey()}+${altKey()}+I`, + click: () => { + this.actions.service.openDevToolsForActiveService(); + }, + enabled: + this.stores.user.isLoggedIn && + this.stores.services.enabled.length > 0, + }, + ); + + if (this.stores.todos.isFeatureEnabledByUser) { + (tpl[1].submenu as MenuItemConstructorOptions[]).push({ + label: intl.formatMessage(menuItems.toggleTodosDevTools), + accelerator: `${cmdOrCtrlShortcutKey()}+${shiftKey()}+${altKey()}+O`, + click: () => { + const webview = document.querySelector('#todos-panel webview'); + if (webview) this.actions.todos.openDevTools(); + }, + }); + } + + (tpl[1].submenu as MenuItemConstructorOptions[]).unshift( + { + label: intl.formatMessage(menuItems.reloadService), + accelerator: `${cmdOrCtrlShortcutKey()}+R`, + click: () => { + if ( + this.stores.user.isLoggedIn && + this.stores.services.enabled.length > 0 + ) { + if (getActiveService()?.recipe.id === CUSTOM_WEBSITE_RECIPE_ID) { + getActiveService()?.webview.reload(); + } else { + this.actions.service.reloadActive(); + } + } else { + window.location.reload(); + } + }, + }, + { + label: intl.formatMessage(menuItems.reloadFerdium), + accelerator: `${cmdOrCtrlShortcutKey()}+${shiftKey()}+R`, + click: () => { + window.location.reload(); + }, + }, + { + label: intl.formatMessage(menuItems.reloadTodos), + accelerator: `${cmdOrCtrlShortcutKey()}+${shiftKey()}+${altKey()}+R`, + click: () => { + this.actions.todos.reload(); + }, + }, + { + type: 'separator', + }, + { + label: intl.formatMessage(menuItems.lockFerdium), + accelerator: `${lockFerdiumShortcutKey()}`, + enabled: + this.stores.user.isLoggedIn && + this.stores.settings.app.lockingFeatureEnabled, + click() { + actions.settings.update({ + type: 'app', + data: { + locked: true, + }, + }); + }, + }, + ); + + if (serviceTpl.length > 0) { + tpl[2].submenu = serviceTpl; + } + + tpl[3].submenu = this.workspacesMenu(); + + tpl[4].submenu = this.todosMenu(); + } else { + const touchIdEnabled = isMac + ? this.stores.settings.app.useTouchIdToUnlock && + systemPreferences.canPromptTouchID() + : false; + + (tpl[0].submenu as MenuItemConstructorOptions[]).unshift( + { + label: intl.formatMessage(menuItems.touchId), + accelerator: `${lockFerdiumShortcutKey()}`, + visible: touchIdEnabled, + click() { + systemPreferences + .promptTouchID(intl.formatMessage(menuItems.touchIdPrompt)) + .then(() => { + actions.settings.update({ + type: 'app', + data: { + locked: false, + }, + }); + }); + }, + }, + { + type: 'separator', + visible: touchIdEnabled, + }, + ); + } + + tpl.unshift({ + label: isMac ? app.name : intl.formatMessage(menuItems.file), + accelerator: `${altKey()}+F`, + submenu: [ + { + type: 'separator', + }, + { + label: intl.formatMessage(globalMessages.settings), + accelerator: `${settingsShortcutKey()}`, + click: () => { + this.actions.ui.openSettings({ path: 'app' }); + }, + enabled: this.stores.user.isLoggedIn, + visible: !locked, + }, + { + label: intl.formatMessage(menuItems.checkForUpdates), + visible: !locked, + click: () => { + this.actions.app.checkForUpdates(); + }, + }, + { + type: 'separator', + visible: !locked, + }, + { + label: intl.formatMessage(menuItems.services), + role: 'services', + submenu: [], + }, + { + type: 'separator', + }, + { + label: intl.formatMessage(menuItems.hide), + role: 'hide', + }, + { + label: intl.formatMessage(menuItems.hideOthers), + role: 'hideOthers', + }, + { + label: intl.formatMessage(menuItems.unhide), + role: 'unhide', + }, + { + type: 'separator', + }, + { + label: intl.formatMessage(globalMessages.quit), + accelerator: `${cmdOrCtrlShortcutKey()}+Q`, + click() { + app.quit(); + }, + }, + ], + }); + + const aboutAppDetails = [ + `Version: ${ferdiumVersion}`, + `Server: ${serverName()} Server`, + `Electron: ${electronVersion}`, + `Chrome: ${chromeVersion}`, + `Node.js: ${nodeVersion}`, + `Platform: ${this.getOsName()}`, + `Arch: ${osArch}`, + `Build date: ${new Date(Number(timestamp))}`, + `Git SHA: ${gitHashShort}`, + `Git branch: ${gitBranch}`, + ].join('\n'); + + const about = { + label: intl.formatMessage(menuItems.about), + click: () => { + dialog + .showMessageBox({ + type: 'info', + title: 'Ferdium', + message: 'Ferdium', + detail: aboutAppDetails, + buttons: [ + intl.formatMessage(menuItems.ok), + intl.formatMessage(menuItems.copyToClipboard), + ], + }) + .then(result => { + if (result.response === 1) { + clipboard.write({ + text: aboutAppDetails, + }); + } + }); + }, + }; + + if (isMac) { + // Edit menu. + (tpl[1].submenu as MenuItemConstructorOptions[]).push( + { + type: 'separator', + }, + { + label: intl.formatMessage(menuItems.speech), + submenu: [ + { + label: intl.formatMessage(menuItems.startSpeaking), + role: 'startSpeaking', + }, + { + label: intl.formatMessage(menuItems.stopSpeaking), + role: 'stopSpeaking', + }, + ], + }, + ); + + (tpl[0].submenu as MenuItemConstructorOptions[]).unshift(about, { + type: 'separator', + }); + } else { + tpl[0].submenu = [ + { + label: intl.formatMessage(globalMessages.settings), + accelerator: `${settingsShortcutKey()}`, + click: () => { + this.actions.ui.openSettings({ path: 'app' }); + }, + enabled: this.stores.user.isLoggedIn, + visible: !locked, + }, + { + type: 'separator', + }, + { + label: intl.formatMessage(globalMessages.quit), + accelerator: `${cmdOrCtrlShortcutKey()}+Q`, + click() { + app.quit(); + }, + }, + ]; + + (tpl[tpl.length - 1].submenu as MenuItemConstructorOptions[]).push( + { + type: 'separator', + }, + about, + ); + } + + if (!locked) { + if (serviceTpl.length > 0) { + tpl[3].submenu = serviceTpl; + } + + tpl[4].submenu = this.workspacesMenu(); + + tpl[5].submenu = this.todosMenu(); + + (tpl[tpl.length - 1].submenu as MenuItemConstructorOptions[]).push( + { + type: 'separator', + }, + ...this.debugMenu(), + ); + } + this._setCurrentTemplate(tpl); + const menu = Menu.buildFromTemplate(tpl); + Menu.setApplicationMenu(menu); + } + + serviceTpl(): MenuItemConstructorOptions[] { + const { intl } = window['ferdium']; + const { user, services, settings } = this.stores; + if (!user.isLoggedIn) { + return []; + } + + const cmdAltShortcutsVisibile = !isLinux; + const menu: MenuItemConstructorOptions[] = []; + menu.push( + { + label: intl.formatMessage(menuItems.addNewService), + accelerator: `${addNewServiceShortcutKey()}`, + click: () => { + this.actions.ui.openSettings({ path: 'recipes' }); + }, + }, + { + type: 'separator', + }, + { + label: intl.formatMessage(menuItems.activateNextService), + accelerator: `${cmdOrCtrlShortcutKey()}+tab`, + click: () => this.actions.service.setActiveNext(), + visible: !cmdAltShortcutsVisibile, + }, + { + label: intl.formatMessage(menuItems.activateNextService), + accelerator: `${cmdOrCtrlShortcutKey()}+${altKey()}+right`, + click: () => this.actions.service.setActiveNext(), + visible: cmdAltShortcutsVisibile, + }, + { + label: intl.formatMessage(menuItems.activatePreviousService), + accelerator: `${cmdOrCtrlShortcutKey()}+${shiftKey()}+tab`, + click: () => this.actions.service.setActivePrev(), + visible: !cmdAltShortcutsVisibile, + }, + { + label: intl.formatMessage(menuItems.activatePreviousService), + accelerator: `${cmdOrCtrlShortcutKey()}+${altKey()}+left`, + click: () => this.actions.service.setActivePrev(), + visible: cmdAltShortcutsVisibile, + }, + { + label: intl + .formatMessage( + settings.all.app.isAppMuted + ? menuItems.unmuteApp + : menuItems.muteApp, + ) + .replace('&', '&&'), + accelerator: `${muteFerdiumShortcutKey()}`, + click: () => this.actions.app.toggleMuteApp(), + }, + { + type: 'separator', + }, + ); + + for (const [i, service] of services.allDisplayed.entries()) { + menu.push({ + label: this._getServiceName(service), + accelerator: i < 9 ? `${cmdOrCtrlShortcutKey()}+${i + 1}` : undefined, + type: 'radio', + checked: service.isActive, + click: () => { + this.actions.service.setActive({ serviceId: service.id }); + + if (isMac && i === 0) { + // feat(Mac): Open Window with Cmd+1 + getCurrentWindow().restore(); + } + }, + }); + } + + if ( + services.active && + services.active.recipe.id === CUSTOM_WEBSITE_RECIPE_ID + ) { + menu.push( + { + type: 'separator', + }, + { + label: intl.formatMessage(menuItems.serviceGoHome), + accelerator: `${cmdOrCtrlShortcutKey()}+${shiftKey()}+H`, + click: () => this.actions.service.reloadActive(), + }, + ); + } + + return menu; + } + + workspacesMenu(): MenuItemConstructorOptions[] { + const { workspaces, activeWorkspace, isWorkspaceDrawerOpen } = + workspaceStore; + const { intl } = window['ferdium']; + + const menu: MenuItemConstructorOptions[] = []; + // Add new workspace item: + menu.push({ + label: intl.formatMessage(menuItems.addNewWorkspace), + accelerator: `${cmdOrCtrlShortcutKey()}+${shiftKey()}+N`, + click: () => { + workspaceActions.openWorkspaceSettings(); + }, + enabled: this.stores.user.isLoggedIn, + }); + + // Open workspace drawer: + if (!this.stores.settings.app.alwaysShowWorkspaces) { + const drawerLabel = isWorkspaceDrawerOpen + ? menuItems.closeWorkspaceDrawer + : menuItems.openWorkspaceDrawer; + menu.push({ + label: intl.formatMessage(drawerLabel), + accelerator: `${workspaceToggleShortcutKey()}`, + click: () => { + workspaceActions.toggleWorkspaceDrawer(); + }, + enabled: this.stores.user.isLoggedIn, + }); + } + + menu.push( + { + type: 'separator', + }, + { + label: intl.formatMessage(menuItems.defaultWorkspace), + accelerator: `${cmdOrCtrlShortcutKey()}+${altKey()}+0`, + type: 'radio', + checked: !activeWorkspace, + click: () => { + workspaceActions.deactivate(); + }, + }, + ); + + // Workspace items + for (const [i, workspace] of workspaces.entries()) { + menu.push({ + label: workspace.name, + accelerator: + i < 9 ? `${cmdOrCtrlShortcutKey()}+${altKey()}+${i + 1}` : undefined, + type: 'radio', + checked: activeWorkspace ? workspace.id === activeWorkspace.id : false, + click: () => { + workspaceActions.activate({ workspace }); + }, + }); + } + + return menu; + } + + todosMenu(): MenuItemConstructorOptions[] { + const { isTodosPanelVisible, isFeatureEnabledByUser } = this.stores.todos; + const { intl } = window['ferdium']; + + const menu: MenuItemConstructorOptions[] = []; + menu.push({ + label: intl.formatMessage( + isFeatureEnabledByUser ? menuItems.disableTodos : menuItems.enableTodos, + ), + click: () => { + todoActions.toggleTodosFeatureVisibility(); + }, + enabled: this.stores.user.isLoggedIn, + }); + + if (isFeatureEnabledByUser) { + menu.push( + { + type: 'separator', + }, + { + label: intl.formatMessage( + isTodosPanelVisible + ? menuItems.closeTodosDrawer + : menuItems.openTodosDrawer, + ), + accelerator: `${todosToggleShortcutKey()}`, + click: () => { + todoActions.toggleTodosPanel(); + }, + enabled: this.stores.user.isLoggedIn, + }, + ); + } + + return menu; + } + + debugMenu(): MenuItemConstructorOptions[] { + const { intl } = window['ferdium']; + + return [ + { + label: intl.formatMessage(menuItems.debugInfo), + click: () => { + const { debugInfo } = this.stores.app; + + clipboard.write({ + text: JSON.stringify(debugInfo), + }); + + this.actions.app.notify({ + title: intl.formatMessage(menuItems.debugInfoCopiedHeadline), + options: { + body: intl.formatMessage(menuItems.debugInfoCopiedBody), + }, + }); + }, + }, + { + label: intl.formatMessage(menuItems.publishDebugInfo), + click: () => { + window['ferdium'].features.publishDebugInfo.state.isModalVisible = + true; + }, + }, + ]; + } + + _getServiceName(service) { + if (service.name) { + return service.name; + } + + let { name: serviceName } = service.recipe; + if (service.team) { + serviceName = `${serviceName} (${service.team})`; + } else if (service.customUrl) { + serviceName = `${serviceName} (${service.customUrl})`; + } + + return serviceName; + } +} + +export default FranzMenu; diff --git a/src/lib/Tray.js b/src/lib/Tray.js deleted file mode 100644 index fffdec64d..000000000 --- a/src/lib/Tray.js +++ /dev/null @@ -1,251 +0,0 @@ -import { - app, - Menu, - nativeImage, - nativeTheme, - systemPreferences, - Tray, - ipcMain, - BrowserWindow, -} from 'electron'; -import { join } from 'path'; -import macosVersion from 'macos-version'; -import { isMac, isWindows, isLinux } from '../environment'; - -const FILE_EXTENSION = isWindows ? 'ico' : 'png'; -const INDICATOR_TRAY_PLAIN = 'tray'; -const INDICATOR_TRAY_UNREAD = 'tray-unread'; -const INDICATOR_TRAY_INDIRECT = 'tray-indirect'; - -// TODO: Need to support i18n for a lot of the hard-coded strings in this file -export default class TrayIcon { - trayIcon = null; - - indicator = 0; - - themeChangeSubscriberId = null; - - trayMenu = null; - - visible = false; - - isAppMuted = false; - - mainWindow = null; - - trayMenuTemplate = tray => [ - { - label: - tray.mainWindow.isVisible() && tray.mainWindow.isFocused() - ? 'Hide Ferdium' - : 'Show Ferdium', - click() { - tray._toggleWindow(); - }, - }, - { - label: tray.isAppMuted - ? 'Enable Notifications && Audio' - : 'Disable Notifications && Audio', - click() { - if (!tray.mainWindow) return; - tray.mainWindow.webContents.send('muteApp'); - }, - }, - { - label: 'Quit Ferdium', - click() { - app.quit(); - }, - }, - ]; - - constructor() { - ipcMain.on('initialAppSettings', (event, appSettings) => { - this._updateTrayMenu(appSettings); - }); - ipcMain.on('updateAppSettings', (event, appSettings) => { - this._updateTrayMenu(appSettings); - }); - - this.mainWindow = BrowserWindow.getAllWindows()[0]; - - // listen to window events to be able to set correct string - // to tray menu ('Hide Ferdium' / 'Show Ferdium') - this.mainWindow.on('hide', () => { - this._updateTrayMenu(null); - }); - this.mainWindow.on('restore', () => { - this._updateTrayMenu(null); - }); - this.mainWindow.on('minimize', () => { - this._updateTrayMenu(null); - }); - this.mainWindow.on('show', () => { - this._updateTrayMenu(null); - }); - this.mainWindow.on('focus', () => { - this._updateTrayMenu(null); - }); - this.mainWindow.on('blur', () => { - this._updateTrayMenu(null); - }); - } - - _updateTrayMenu(appSettings) { - if (!this.trayIcon) return; - - if (appSettings && appSettings.type === 'app') { - this.isAppMuted = appSettings.data.isAppMuted; // save current state after a change - } - - this.trayMenu = Menu.buildFromTemplate(this.trayMenuTemplate(this)); - if (isLinux) { - this.trayIcon.setContextMenu(this.trayMenu); - } - } - - show() { - this.visible = true; - this._show(); - } - - _show() { - if (this.trayIcon) return; - - this.trayIcon = new Tray(this._getAsset('tray', INDICATOR_TRAY_PLAIN)); - this.trayIcon.setToolTip('Ferdium'); - - this.trayMenu = Menu.buildFromTemplate(this.trayMenuTemplate(this)); - if (isLinux) { - this.trayIcon.setContextMenu(this.trayMenu); - } - - this.trayIcon.on('click', () => { - this._toggleWindow(); - }); - - if (isMac || isWindows) { - this.trayIcon.on('right-click', () => { - this.trayIcon.popUpContextMenu(this.trayMenu); - }); - } - - if (isMac) { - this.themeChangeSubscriberId = systemPreferences.subscribeNotification( - 'AppleInterfaceThemeChangedNotification', - () => { - this._refreshIcon(); - }, - ); - } - } - - _toggleWindow() { - const mainWindow = BrowserWindow.getAllWindows()[0]; - if (!mainWindow) return; - - if (mainWindow.isMinimized()) { - mainWindow.restore(); - } else if (mainWindow.isVisible() && mainWindow.isFocused()) { - if (isMac && mainWindow.isFullScreen()) { - mainWindow.once('show', () => mainWindow?.setFullScreen(true)); - mainWindow.once('leave-full-screen', () => mainWindow?.hide()); - mainWindow.setFullScreen(false); - } else { - mainWindow.hide(); - } - } else { - mainWindow.show(); - mainWindow.focus(); - } - } - - hide() { - this.visible = false; - this._hide(); - } - - _hide() { - if (!this.trayIcon) return; - - this.trayIcon.destroy(); - this.trayIcon = null; - - if (isMac && this.themeChangeSubscriberId) { - systemPreferences.unsubscribeNotification(this.themeChangeSubscriberId); - this.themeChangeSubscriberId = null; - } - } - - recreateIfVisible() { - if (this.visible) { - this._hide(); - setTimeout(() => { - if (this.visible) { - this._show(); - } - }, 100); - } - } - - setIndicator(indicator) { - this.indicator = indicator; - this._refreshIcon(); - } - - _getAssetFromIndicator(indicator) { - if (indicator === '•') { - return INDICATOR_TRAY_INDIRECT; - } - if (indicator !== 0) { - return INDICATOR_TRAY_UNREAD; - } - return INDICATOR_TRAY_PLAIN; - } - - _refreshIcon() { - if (!this.trayIcon) return; - - this.trayIcon.setImage( - this._getAsset('tray', this._getAssetFromIndicator(this.indicator)), - ); - - if (isMac && !macosVersion.isGreaterThanOrEqualTo('11')) { - this.trayIcon.setPressedImage( - this._getAsset( - 'tray', - `${this._getAssetFromIndicator(this.indicator)}-active`, - ), - ); - } - } - - _getAsset(type, asset) { - let { platform } = process; - - if (isMac && macosVersion.isGreaterThanOrEqualTo('11')) { - platform = `${platform}-20`; - } else if (isMac && nativeTheme.shouldUseDarkColors) { - platform = `${platform}-dark`; - } - - const trayImg = nativeImage.createFromPath( - join( - __dirname, - '..', - 'assets', - 'images', - type, - platform, - `${asset}.${FILE_EXTENSION}`, - ), - ); - - if (isMac && macosVersion.isGreaterThanOrEqualTo('11')) { - trayImg.setTemplateImage(true); - } - - return trayImg; - } -} diff --git a/src/lib/Tray.ts b/src/lib/Tray.ts new file mode 100644 index 000000000..8e489edde --- /dev/null +++ b/src/lib/Tray.ts @@ -0,0 +1,265 @@ +import { + app, + Menu, + nativeImage, + nativeTheme, + systemPreferences, + Tray, + ipcMain, + BrowserWindow, + NativeImage, +} from 'electron'; +import { join } from 'path'; +import macosVersion from 'macos-version'; +import { isMac, isWindows, isLinux } from '../environment'; + +const FILE_EXTENSION = isWindows ? 'ico' : 'png'; +const INDICATOR_TRAY_PLAIN = 'tray'; +const INDICATOR_TRAY_UNREAD = 'tray-unread'; +const INDICATOR_TRAY_INDIRECT = 'tray-indirect'; + +// TODO: Need to support i18n for a lot of the hard-coded strings in this file +export default class TrayIcon { + trayIcon: Tray | null = null; + + indicator: string | number = 0; + + themeChangeSubscriberId: number | null = null; + + trayMenu: Menu | null = null; + + visible = false; + + isAppMuted = false; + + mainWindow: BrowserWindow | null = null; + + constructor() { + ipcMain.on('initialAppSettings', (_, appSettings) => { + this._updateTrayMenu(appSettings); + }); + ipcMain.on('updateAppSettings', (_, appSettings) => { + this._updateTrayMenu(appSettings); + }); + + const [firstWindow] = BrowserWindow.getAllWindows(); + this.mainWindow = firstWindow; + + // listen to window events to be able to set correct string + // to tray menu ('Hide Ferdium' / 'Show Ferdium') + this.mainWindow.on('hide', () => { + this._updateTrayMenu(null); + }); + this.mainWindow.on('restore', () => { + this._updateTrayMenu(null); + }); + this.mainWindow.on('minimize', () => { + this._updateTrayMenu(null); + }); + this.mainWindow.on('show', () => { + this._updateTrayMenu(null); + }); + this.mainWindow.on('focus', () => { + this._updateTrayMenu(null); + }); + this.mainWindow.on('blur', () => { + this._updateTrayMenu(null); + }); + } + + trayMenuTemplate(tray) { + return [ + { + label: + tray.mainWindow.isVisible() && tray.mainWindow.isFocused() + ? 'Hide Ferdium' + : 'Show Ferdium', + click() { + tray._toggleWindow(); + }, + }, + { + label: tray.isAppMuted + ? 'Enable Notifications && Audio' + : 'Disable Notifications && Audio', + click() { + if (!tray.mainWindow) return; + tray.mainWindow.webContents.send('muteApp'); + }, + }, + { + label: 'Quit Ferdium', + click() { + app.quit(); + }, + }, + ]; + } + + _updateTrayMenu(appSettings): void { + if (!this.trayIcon) return; + + if (appSettings && appSettings.type === 'app') { + this.isAppMuted = appSettings.data.isAppMuted; // save current state after a change + } + + this.trayMenu = Menu.buildFromTemplate(this.trayMenuTemplate(this)); + if (isLinux) { + this.trayIcon.setContextMenu(this.trayMenu); + } + } + + show(): void { + this.visible = true; + this._show(); + } + + _show(): void { + if (this.trayIcon) { + return; + } + + this.trayIcon = new Tray(this._getAsset('tray', INDICATOR_TRAY_PLAIN)); + this.trayIcon.setToolTip('Ferdium'); + + this.trayMenu = Menu.buildFromTemplate(this.trayMenuTemplate(this)); + if (isLinux) { + this.trayIcon.setContextMenu(this.trayMenu); + } + + this.trayIcon.on('click', () => { + this._toggleWindow(); + }); + + if (isMac || isWindows) { + this.trayIcon.on('right-click', () => { + if (this.trayIcon && this.trayMenu) { + this.trayIcon.popUpContextMenu(this.trayMenu); + } + }); + } + + if (isMac) { + this.themeChangeSubscriberId = systemPreferences.subscribeNotification( + 'AppleInterfaceThemeChangedNotification', + () => { + this._refreshIcon(); + }, + ); + } + } + + _toggleWindow(): void { + const [mainWindow] = BrowserWindow.getAllWindows(); + if (!mainWindow) { + return; + } + + if (mainWindow.isMinimized()) { + mainWindow.restore(); + } else if (mainWindow.isVisible() && mainWindow.isFocused()) { + if (isMac && mainWindow.isFullScreen()) { + mainWindow.once('show', () => mainWindow?.setFullScreen(true)); + mainWindow.once('leave-full-screen', () => mainWindow?.hide()); + mainWindow.setFullScreen(false); + } else { + mainWindow.hide(); + } + } else { + mainWindow.show(); + mainWindow.focus(); + } + } + + hide(): void { + this.visible = false; + this._hide(); + } + + _hide(): void { + if (!this.trayIcon) return; + + this.trayIcon.destroy(); + this.trayIcon = null; + + if (isMac && this.themeChangeSubscriberId) { + systemPreferences.unsubscribeNotification(this.themeChangeSubscriberId); + this.themeChangeSubscriberId = null; + } + } + + recreateIfVisible(): void { + if (this.visible) { + this._hide(); + setTimeout(() => { + if (this.visible) { + this._show(); + } + }, 100); + } + } + + setIndicator(indicator: string | number): void { + this.indicator = indicator; + this._refreshIcon(); + } + + _getAssetFromIndicator(indicator: string | number): string { + let assetFromIndicator = INDICATOR_TRAY_PLAIN; + if (indicator === '•') { + assetFromIndicator = INDICATOR_TRAY_INDIRECT; + } + if (indicator !== 0) { + assetFromIndicator = INDICATOR_TRAY_UNREAD; + } + return assetFromIndicator; + } + + _refreshIcon(): void { + if (!this.trayIcon) { + return; + } + + this.trayIcon.setImage( + this._getAsset('tray', this._getAssetFromIndicator(this.indicator)), + ); + + if (isMac && !macosVersion.isGreaterThanOrEqualTo('11')) { + this.trayIcon.setPressedImage( + this._getAsset( + 'tray', + `${this._getAssetFromIndicator(this.indicator)}-active`, + ), + ); + } + } + + _getAsset(type, asset): NativeImage { + const { platform } = process; + let platformPath: string = platform; + + if (isMac && macosVersion.isGreaterThanOrEqualTo('11')) { + platformPath = `${platform}-20`; + } else if (isMac && nativeTheme.shouldUseDarkColors) { + platformPath = `${platform}-dark`; + } + + const trayImg = nativeImage.createFromPath( + join( + __dirname, + '..', + 'assets', + 'images', + type, + platformPath, + `${asset}.${FILE_EXTENSION}`, + ), + ); + + if (isMac && macosVersion.isGreaterThanOrEqualTo('11')) { + trayImg.setTemplateImage(true); + } + + return trayImg; + } +} -- cgit v1.2.3-70-g09d2