Skip to content
This repository has been archived by the owner on Oct 10, 2023. It is now read-only.

Commit

Permalink
Add Electron menu (#274)
Browse files Browse the repository at this point in the history
- [x] Create menu items
- [x] Support i18n for menu
- [x] Messaging `main` <-> `render` process via ipc (needed to support `lang` changes in menu)
- [x] Share `common` `i18n` sources
- [x] Build `electron` sources w/ `webpack`
  • Loading branch information
veado committed Jul 14, 2020
1 parent 9a8d7f8 commit 2f1dc77
Show file tree
Hide file tree
Showing 24 changed files with 561 additions and 47 deletions.
37 changes: 30 additions & 7 deletions electron/electron.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import { join } from 'path'

import { BrowserWindow, app, remote } from 'electron'
import { BrowserWindow, app, remote, ipcMain } from 'electron'
import electronDebug from 'electron-debug'
import isDev from 'electron-is-dev'
import log from 'electron-log'
import { warn } from 'electron-log'
import { fromEvent } from 'rxjs'

import { Locale } from '../src/shared/i18n/types'
import IPCMessages from '../src/shared/ipc/messages'
import { setMenu } from './menu'

export const IS_DEV = isDev && process.env.NODE_ENV !== 'production'

export const APP_ROOT = join(__dirname, '..', '..')

const BASE_URL_DEV = 'http://localhost:3000'
Expand All @@ -17,8 +23,9 @@ export const BASE_URL = IS_DEV ? BASE_URL_DEV : BASE_URL_PROD
const initLogger = () => {
log.transports.file.resolvePath = (variables: log.PathVariables) => {
const ap = app || remote.app
// Logs go into ~/.{appName}/logs/ dir
return join(ap.getPath('userData'), app.getName(), 'logs', variables.fileName as string)
// Logs go into ~/.config/{appName}/logs/ dir
const path = join(ap.getPath('userData'), 'logs', variables.fileName as string)
return path
}
}

Expand Down Expand Up @@ -52,11 +59,11 @@ const closeHandler = () => {
}

const setupDevEnv = async () => {
// install react & redux chrome dev tools
const { default: install, REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS } = require('electron-devtools-installer') // eslint-disable-line @typescript-eslint/no-var-requires
// install react chrome dev tools
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { default: install, REACT_DEVELOPER_TOOLS } = require('electron-devtools-installer')
try {
await install(REACT_DEVELOPER_TOOLS)
await install(REDUX_DEVTOOLS)
} catch (e) {
warn('unable to install devtools', e)
}
Expand All @@ -71,14 +78,28 @@ const initMainWindow = async () => {
nodeIntegration: true
}
})
mainWindow.setMenuBarVisibility(false)

if (IS_DEV) {
await setupDevEnv()
}

mainWindow.on('closed', closeHandler)
mainWindow.loadURL(BASE_URL)
// hide menu at start, we need to wait for locale sent by `ipcRenderer`
mainWindow.setMenuBarVisibility(false)
}

const langChangeHandler = (locale: Locale) => {
setMenu(locale, IS_DEV)
// show menu, which is hided at start
if (mainWindow && !mainWindow.isMenuBarVisible()) {
mainWindow.setMenuBarVisibility(true)
}
}

const initIPC = () => {
const source$ = fromEvent<Locale>(ipcMain, IPCMessages.UPDATE_LANG, (_, locale) => locale)
source$.subscribe(langChangeHandler)
}

const init = async () => {
Expand All @@ -88,6 +109,8 @@ const init = async () => {

app.on('window-all-closed', allClosedHandler)
app.on('activate', activateHandler)

initIPC()
}

try {
Expand Down
36 changes: 36 additions & 0 deletions electron/i18n/de/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { EditMenuMessages, HelpMenuMessages, ViewMenuMessages, AppMenuMessages } from '../types'

const appMenu: AppMenuMessages = {
'menu.app.about': 'Über {name}',
'menu.app.hideApp': '{name} ausblenden',
'menu.app.hideOthers': 'Andere ausblenden',
'menu.app.unhide': 'Zeige alle',
'menu.app.quit': '{name} beenden'
}

const editMenu: EditMenuMessages = {
'menu.edit.title': 'Bearbeiten',
'menu.edit.undo': 'Rückgängig',
'menu.edit.redo': 'Wiederherstellen',
'menu.edit.cut': 'Ausschneiden',
'menu.edit.copy': 'Kopieren',
'menu.edit.paste': 'Einfügen',
'menu.edit.selectAll': 'Alles auswählen'
}

const helpMenu: HelpMenuMessages = {
'menu.help.title': 'Hilfe',
'menu.help.learn': 'Mehr erfahren ...',
'menu.help.docs': 'Dokumentation',
'menu.help.telegram': 'Telegram',
'menu.help.issues': 'Probleme melden'
}

const viewMenu: ViewMenuMessages = {
'menu.view.title': 'Ansicht',
'menu.view.reload': 'Neuladen',
'menu.view.toggleFullscreen': 'Vollbild ein/aus',
'menu.view.toggleDevTools': 'Dev Tools ein/aus'
}

export default { ...appMenu, ...editMenu, ...viewMenu, ...helpMenu }
36 changes: 36 additions & 0 deletions electron/i18n/en/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { EditMenuMessages, HelpMenuMessages, ViewMenuMessages, AppMenuMessages } from '../types'

const appMenu: AppMenuMessages = {
'menu.app.about': 'About {name}',
'menu.app.hideApp': 'Hide {name}',
'menu.app.hideOthers': 'Hide Others',
'menu.app.unhide': 'Show All',
'menu.app.quit': 'Quit {name}'
}

const editMenu: EditMenuMessages = {
'menu.edit.title': 'Edit',
'menu.edit.undo': 'Undo',
'menu.edit.redo': 'Redo',
'menu.edit.cut': 'Cut',
'menu.edit.copy': 'Copy',
'menu.edit.paste': 'Paste',
'menu.edit.selectAll': 'Select all'
}

const helpMenu: HelpMenuMessages = {
'menu.help.title': 'Help',
'menu.help.learn': 'Learn more ...',
'menu.help.docs': 'Documentation',
'menu.help.telegram': 'Telegram',
'menu.help.issues': 'Report issues'
}

const viewMenu: ViewMenuMessages = {
'menu.view.title': 'View',
'menu.view.reload': 'Reload',
'menu.view.toggleFullscreen': 'Toggle Fullscreen',
'menu.view.toggleDevTools': 'Toggle Dev Tools'
}

export default { ...appMenu, ...editMenu, ...viewMenu, ...helpMenu }
36 changes: 36 additions & 0 deletions electron/i18n/fr/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { EditMenuMessages, HelpMenuMessages, ViewMenuMessages, AppMenuMessages } from '../types'

const appMenu: AppMenuMessages = {
'menu.app.about': 'About {name} - FR',
'menu.app.hideApp': 'Hide {name} - FR',
'menu.app.hideOthers': 'Hide Others - FR',
'menu.app.unhide': 'Show All - FR',
'menu.app.quit': 'Quit {name} - FR'
}

const editMenu: EditMenuMessages = {
'menu.edit.title': 'Edit - FR',
'menu.edit.undo': 'Undo - FR',
'menu.edit.redo': 'Redo - FR',
'menu.edit.cut': 'Cut - FR',
'menu.edit.copy': 'Copy - FR',
'menu.edit.paste': 'Paste - FR',
'menu.edit.selectAll': 'Select all - FR'
}

const helpMenu: HelpMenuMessages = {
'menu.help.title': 'Help - FR',
'menu.help.learn': 'Learn more ... - FR',
'menu.help.docs': 'Documentation - FR',
'menu.help.telegram': 'Telegram - FR',
'menu.help.issues': 'Issues - FR'
}

const viewMenu: ViewMenuMessages = {
'menu.view.title': 'View - FR',
'menu.view.reload': 'Reload - FR',
'menu.view.toggleFullscreen': 'Toggle Fullscreen - FR',
'menu.view.toggleDevTools': 'Toggle Dev Tools - FR'
}

export default { ...appMenu, ...editMenu, ...viewMenu, ...helpMenu }
22 changes: 22 additions & 0 deletions electron/i18n/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { createIntlCache } from 'react-intl'

import { Locale } from '../../src/shared/i18n/types'
import de from './de'
import en from './en'
import fr from './fr'
import { Messages } from './types'

export const getMessagesByLocale = (l: Locale): Messages => {
switch (l) {
case Locale.EN:
return en
case Locale.FR:
return fr
case Locale.DE:
return de
default:
return en
}
}

export const cache = createIntlCache()
34 changes: 34 additions & 0 deletions electron/i18n/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
export type AppMenuMessages = {
'menu.app.about': string
'menu.app.hideApp': string
'menu.app.hideOthers': string
'menu.app.unhide': string
'menu.app.quit': string
}

export type EditMenuMessages = {
'menu.edit.title': string
'menu.edit.undo': string
'menu.edit.redo': string
'menu.edit.cut': string
'menu.edit.copy': string
'menu.edit.paste': string
'menu.edit.selectAll': string
}

export type HelpMenuMessages = {
'menu.help.title': string
'menu.help.learn': string
'menu.help.docs': string
'menu.help.telegram': string
'menu.help.issues': string
}

export type ViewMenuMessages = {
'menu.view.title': string
'menu.view.reload': string
'menu.view.toggleFullscreen': string
'menu.view.toggleDevTools': string
}

export type Messages = AppMenuMessages & HelpMenuMessages & HelpMenuMessages & ViewMenuMessages
36 changes: 36 additions & 0 deletions electron/menu/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { MenuItemConstructorOptions, app } from 'electron'
import { IntlShape } from 'react-intl'

const menu = (intl: IntlShape): MenuItemConstructorOptions => {
const name = app.getName()

return {
label: name,
submenu: [
{
label: intl.formatMessage({ id: 'menu.app.about' }, { name }),
role: 'about'
},
{ type: 'separator' },
{
label: intl.formatMessage({ id: 'menu.app.hideApp' }, { name }),
role: 'hide'
},
{
label: intl.formatMessage({ id: 'menu.app.hideOthers' }),
role: 'hideOthers'
},
{
label: intl.formatMessage({ id: 'menu.app.unhide' }),
role: 'unhide'
},
{ type: 'separator' },
{
label: intl.formatMessage({ id: 'menu.app.quit' }, { name }),
role: 'quit'
}
]
}
}

export default menu
37 changes: 37 additions & 0 deletions electron/menu/edit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { MenuItemConstructorOptions } from 'electron'
import { IntlShape } from 'react-intl'

const menu = (intl: IntlShape): MenuItemConstructorOptions => ({
label: intl.formatMessage({ id: 'menu.edit.title' }),
submenu: [
{
label: intl.formatMessage({ id: 'menu.edit.undo' }),
role: 'undo'
},
{
label: intl.formatMessage({ id: 'menu.edit.redo' }),
role: 'redo'
},
{
type: 'separator'
},
{
label: intl.formatMessage({ id: 'menu.edit.cut' }),
role: 'cut'
},
{
label: intl.formatMessage({ id: 'menu.edit.copy' }),
role: 'copy'
},
{
label: intl.formatMessage({ id: 'menu.edit.paste' }),
role: 'paste'
},
{
label: intl.formatMessage({ id: 'menu.edit.selectAll' }),
role: 'selectAll'
}
]
})

export default menu
36 changes: 36 additions & 0 deletions electron/menu/help.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { MenuItemConstructorOptions, shell } from 'electron'
import { IntlShape } from 'react-intl'

import { ExternalUrl } from '../../src/shared/const'

const menu = (intl: IntlShape): MenuItemConstructorOptions => ({
label: intl.formatMessage({ id: 'menu.help.title' }),
submenu: [
{
label: intl.formatMessage({ id: 'menu.help.learn' }),
click() {
shell.openExternal(ExternalUrl.WEBSITE)
}
},
{
label: intl.formatMessage({ id: 'menu.help.docs' }),
click() {
shell.openExternal(ExternalUrl.DOCS)
}
},
{
label: intl.formatMessage({ id: 'menu.help.telegram' }),
click() {
shell.openExternal(ExternalUrl.TELEGRAM)
}
},
{
label: intl.formatMessage({ id: 'menu.help.issues' }),
click() {
shell.openExternal(`${ExternalUrl.GITHUB}/issues`)
}
}
]
})

export default menu
17 changes: 17 additions & 0 deletions electron/menu/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Menu } from 'electron'
import { createIntl } from 'react-intl'

import { Locale } from '../../src/shared/i18n/types'
import { getMessagesByLocale, cache } from '../i18n'
import appMenu from './app'
import editMenu from './edit'
import helpMenu from './help'
import viewMenu from './view'

export const setMenu = (locale: Locale, isDev: boolean) => {
const intl = createIntl({ locale, messages: getMessagesByLocale(locale) }, cache)
const defaultTemplate = [editMenu(intl), viewMenu(intl, isDev), helpMenu(intl)]
const template = process.platform === 'darwin' ? [appMenu(intl), ...defaultTemplate] : defaultTemplate
const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
}
Loading

0 comments on commit 2f1dc77

Please sign in to comment.