-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/packages/desktop/public/manifest.json b/packages/desktop/public/manifest.json
deleted file mode 100644
index 68610dc2c..000000000
--- a/packages/desktop/public/manifest.json
+++ /dev/null
@@ -1,39 +0,0 @@
-{
- "name": "DevHub",
- "short_name": "DevHub",
- "description": "TweetDeck for GitHub",
- "lang": "en-US",
- "start_url": "./?utm_source=web_app_manifest",
- "display": "standalone",
- "orientation": "any",
- "theme_color": "#292c33",
- "icons": [
- {
- "src": "static/media/logo.png",
- "sizes": "100x100",
- "type": "image/png"
- },
- {
- "src": "static/media/logo.png",
- "sizes": "200x200",
- "type": "image/png"
- },
- {
- "src": "static/media/logo.png",
- "sizes": "300x300",
- "type": "image/png"
- },
- {
- "src": "static/media/logo@2x.png",
- "sizes": "600x600",
- "type": "image/png"
- },
- {
- "src": "static/media/logo@3x.png",
- "sizes": "900x900",
- "type": "image/png"
- }
- ],
- "background_color": "#1c1c1c",
- "prefer_related_applications": true
-}
\ No newline at end of file
diff --git a/packages/desktop/public/static/css/arrow.css b/packages/desktop/public/static/css/arrow.css
deleted file mode 100644
index 7f772a1fd..000000000
--- a/packages/desktop/public/static/css/arrow.css
+++ /dev/null
@@ -1,14 +0,0 @@
-body {
- background: transparent !important;
- -webkit-app-region: drag !important;
-}
-
-.header-arrow {
- width: 0;
- height: 0;
- border-right: 10px solid transparent;
- border-bottom: 10px solid #fff;
- border-left: 10px solid transparent;
- margin: auto;
- transition: border-bottom-width .2s ease,border-bottom-color .3s ease;
-}
\ No newline at end of file
diff --git a/packages/desktop/public/static/css/error.css b/packages/desktop/public/static/css/error.css
deleted file mode 100644
index bced294f2..000000000
--- a/packages/desktop/public/static/css/error.css
+++ /dev/null
@@ -1,25 +0,0 @@
-* {
- box-sizing: border-box;
-}
-
-body,
-html {
- width: 100%;
- height: 100%;
- margin: 0;
- padding: 0;
-}
-
-body {
- display: flex;
- align-content: center;
- align-items: center;
- background-color: #1c1c1c;
- text-align: center;
- color: #fff;
- font-family: sans-serif;
-}
-
-#error-container {
- margin: auto;
-}
\ No newline at end of file
diff --git a/packages/desktop/public/static/icns/logo.icns b/packages/desktop/public/static/icns/logo.icns
deleted file mode 100644
index 92491eedf..000000000
Binary files a/packages/desktop/public/static/icns/logo.icns and /dev/null differ
diff --git a/packages/desktop/public/static/icns/logo.ico b/packages/desktop/public/static/icns/logo.ico
deleted file mode 100644
index cd76e6c02..000000000
Binary files a/packages/desktop/public/static/icns/logo.ico and /dev/null differ
diff --git a/packages/desktop/public/static/media/logo-transparent@2x.png b/packages/desktop/public/static/media/logo-transparent@2x.png
deleted file mode 100644
index 9aaa6d9d2..000000000
Binary files a/packages/desktop/public/static/media/logo-transparent@2x.png and /dev/null differ
diff --git a/packages/desktop/public/static/media/logo.png b/packages/desktop/public/static/media/logo.png
deleted file mode 100644
index 71330d5fd..000000000
Binary files a/packages/desktop/public/static/media/logo.png and /dev/null differ
diff --git a/packages/desktop/public/static/media/logo@2x.png b/packages/desktop/public/static/media/logo@2x.png
deleted file mode 100644
index 17400f328..000000000
Binary files a/packages/desktop/public/static/media/logo@2x.png and /dev/null differ
diff --git a/packages/desktop/public/static/media/logo@3x.png b/packages/desktop/public/static/media/logo@3x.png
deleted file mode 100644
index bfcd83a45..000000000
Binary files a/packages/desktop/public/static/media/logo@3x.png and /dev/null differ
diff --git a/packages/desktop/src/electron.ts b/packages/desktop/src/electron.ts
deleted file mode 100644
index 373234d51..000000000
--- a/packages/desktop/src/electron.ts
+++ /dev/null
@@ -1,373 +0,0 @@
-import { constants } from '@devhub/core'
-import {
- app,
- BrowserWindow,
- Menu,
- nativeImage,
- shell,
- TouchBar,
- Tray,
-} from 'electron'
-import fs from 'fs'
-import path from 'path'
-
-const URL = 'https://devhubapp.com'
-const WIDTH = 350
-const HEIGHT = 450
-
-let mainWindow: Electron.BrowserWindow
-let menubarWindow: Electron.BrowserWindow
-let tray: Electron.Tray
-
-const template: Electron.MenuItemConstructorOptions[] = [
- {
- label: 'Edit',
- submenu: [
- {
- label: 'Undo',
- accelerator: 'CmdOrCtrl+Z',
- role: 'undo',
- },
- {
- label: 'Redo',
- accelerator: 'Shift+CmdOrCtrl+Z',
- role: 'redo',
- },
- {
- type: 'separator',
- },
- {
- label: 'Cut',
- accelerator: 'CmdOrCtrl+X',
- role: 'cut',
- },
- {
- label: 'Copy',
- accelerator: 'CmdOrCtrl+C',
- role: 'copy',
- },
- {
- label: 'Paste',
- accelerator: 'CmdOrCtrl+V',
- role: 'paste',
- },
- {
- label: 'Select All',
- accelerator: 'CmdOrCtrl+A',
- role: 'selectall',
- },
- ],
- },
- {
- label: 'View',
- submenu: [
- {
- label: 'Reload',
- accelerator: 'CmdOrCtrl+R',
- },
- ],
- },
- {
- label: 'Window',
- role: 'window',
- submenu: [
- {
- label: 'Minimize',
- accelerator: 'CmdOrCtrl+M',
- role: 'minimize',
- },
- {
- label: 'Close',
- accelerator: 'CmdOrCtrl+W',
- role: 'close',
- },
- ],
- },
- {
- type: 'separator',
- },
- {
- label: 'Bring All to Front',
- role: 'front',
- },
- {
- label: 'Help',
- role: 'help',
- submenu: [
- {
- label: 'Learn More',
- click: () => {
- shell.openExternal('https://devhubapp.com')
- },
- },
- ],
- },
-]
-
-if (process.platform === 'darwin') {
- const name = app.getName()
- template.unshift({
- label: name,
- submenu: [
- {
- label: `About ${name}`,
- role: 'about',
- },
- {
- type: 'separator',
- },
- {
- label: `Hide ${name}`,
- accelerator: 'Command+H',
- role: 'hide',
- },
- {
- label: 'Hide Others',
- accelerator: 'Command+Alt+H',
- role: 'hideothers',
- },
- {
- type: 'separator',
- },
- {
- label: 'Quit',
- accelerator: 'Command+Q',
- click: () => {
- app.quit()
- },
- },
- ],
- })
-}
-
-function createWindow() {
- // Create a new window
- mainWindow = new BrowserWindow({
- minWidth: constants.MIN_COLUMN_WIDTH,
- width: WIDTH,
- height: HEIGHT,
- // Don't show the window until it ready, this prevents any white flickering
- show: false,
- })
-
- mainWindow.loadURL(URL)
-
- // Emitted when the window is closed.
- mainWindow.on('closed', () => {
- // Dereference the window object, usually you would store windows
- // in an array if your app supports multi windows, this is the time
- // when you should delete the corresponding element.
- mainWindow.destroy()
- })
-
- // Show window when page is ready
- mainWindow.once('ready-to-show', () => {
- mainWindow.show()
- const menu = Menu.buildFromTemplate(template)
- Menu.setApplicationMenu(menu)
- if (process.platform === 'darwin') {
- const spin = new TouchBar.TouchBarButton({
- label: 'devhub',
- })
-
- const touchBar = new TouchBar({
- items: [spin],
- })
- mainWindow.setTouchBar(touchBar)
- }
- })
-
- mainWindow.on('move', () => {
- const [x, y] = mainWindow.getPosition()
- if (y <= 100) {
- mainWindow.hide()
- showMenubarWindow()
- }
- })
-}
-
-function createMenubarWindow() {
- menubarWindow = new BrowserWindow({
- minWidth: constants.MIN_COLUMN_WIDTH,
- width: WIDTH,
- height: HEIGHT,
- show: false,
- frame: false,
- fullscreenable: false,
- movable: true,
- hasShadow: false,
- transparent: true,
- webPreferences: {
- backgroundThrottling: false,
- },
- })
- menubarWindow.loadURL(URL)
- const webContents = menubarWindow.webContents
-
- webContents.on('dom-ready', () => {
- const position = getWindowPosition()
- setWindowPosition(menubarWindow, position.x, position.y)
- webContents.insertCSS(
- fs.readFileSync(
- path.join(__dirname, '../public/static/css/arrow.css'),
- 'utf8',
- ),
- )
- menubarWindow.show()
- menubarWindow.focus()
- })
-
- // Hide the window when it loses focus
- menubarWindow.on('blur', () => {
- menubarWindow.hide()
- })
-
- menubarWindow.on('move', () => {
- const [x, y] = menubarWindow.getPosition()
- if (y <= 100) {
- const position = getWindowPosition()
- menubarWindow.setPosition(position.x, position.y, false)
- } else {
- menubarWindow.hide()
- showMainWindow(x, y)
- }
- })
-}
-
-function createTray() {
- const trayIcon = nativeImage.createFromPath(
- `${path.join(__dirname, '../public/static/media/logo-transparent@2x.png')}`,
- )
- tray = new Tray(
- trayIcon.resize({
- width: 22,
- height: 22,
- }),
- )
-
- const trayMenuTemplate = [
- {
- label: 'Devhub',
- click() {
- menubarWindow.hide()
- toggleMainWindow()
- },
- },
- {
- label: 'Quit',
- click() {
- app.quit()
- },
- },
- ]
- tray.setToolTip(app.getName())
-
- tray.on('click', () => {
- toggleMenubarWindow()
- })
-
- tray.on('right-click', () => {
- const contextMenu = Menu.buildFromTemplate(trayMenuTemplate)
- tray.popUpContextMenu(contextMenu)
- })
-}
-
-// Wait until the app is ready
-app.on('ready', () => {
- app.setAsDefaultProtocolClient('x-devhub-client')
- createTray()
- createMenubarWindow()
- if (process.platform === 'darwin') {
- app.setAboutPanelOptions({
- applicationName: 'devhub',
- applicationVersion: app.getVersion(),
- copyright: 'Copyright 2018',
- credits: 'devhub',
- })
- }
-})
-
-// window-all-closed
-app.on('window-all-closed', () => {
- if (process.platform !== 'darwin') {
- app.quit()
- }
-})
-
-// activate
-app.on('activate', () => {
- if (menubarWindow === null) {
- createTray()
- }
-})
-
-// web-contents-created
-app.on('web-contents-created', (event, webContents) => {
- webContents.on('new-window', (e, url) => {
- if (!url.includes('https://api.devhubapp.com/oauth')) {
- e.preventDefault()
- shell.openExternal(url)
- }
- })
-})
-
-function getWindowPosition() {
- const windowBounds = menubarWindow.getBounds()
- const trayBounds = tray.getBounds()
-
- // Center window horizontally below the tray icon
- const x = Math.round(
- trayBounds.x + trayBounds.width / 2 - windowBounds.width / 2,
- )
-
- // Position window 4 pixels vertically below the tray icon
- const y = Math.round(trayBounds.y + trayBounds.height + 4)
-
- return { x, y }
-}
-
-// toggle menubar window
-function toggleMenubarWindow() {
- if (menubarWindow.isVisible()) {
- menubarWindow.hide()
- } else {
- showMenubarWindow()
- }
-}
-
-// toggle main window
-function toggleMainWindow() {
- if (mainWindow === undefined) {
- createWindow()
- } else if (mainWindow.isVisible()) {
- mainWindow.hide()
- } else {
- mainWindow.show()
- }
-}
-
-function showMainWindow(x: number, y: number) {
- if (mainWindow === undefined) {
- createWindow()
- } else {
- mainWindow.show()
- mainWindow.focus()
- }
- setWindowPosition(mainWindow, x, y)
-}
-
-function showMenubarWindow() {
- const position = getWindowPosition()
- menubarWindow.setPosition(position.x, position.y, false)
- setWindowPosition(menubarWindow, position.x, position.y)
- menubarWindow.show()
- menubarWindow.focus()
-}
-
-function setWindowPosition(
- window: Electron.BrowserWindow,
- x: number,
- y: number,
-): void {
- window.setPosition(x, y)
-}
diff --git a/packages/desktop/src/index.ts b/packages/desktop/src/index.ts
new file mode 100644
index 000000000..bf1360619
--- /dev/null
+++ b/packages/desktop/src/index.ts
@@ -0,0 +1,815 @@
+import {
+ app,
+ BrowserWindow,
+ Menu,
+ nativeImage,
+ screen,
+ shell,
+ TouchBar,
+ Tray,
+} from 'electron'
+import Store from 'electron-store'
+import { autoUpdater } from 'electron-updater'
+import path from 'path'
+
+import { __DEV__ } from './libs/electron-is-dev'
+import { WindowState, windowStateKeeper } from './libs/electron-window-state'
+
+const config = new Store({
+ defaults: {
+ isMenuBarMode: false,
+ launchCount: 0,
+ lockOnCenter: false,
+ },
+})
+
+const frameIsDifferentBetweenModes = process.platform !== 'darwin'
+
+const dock: Electron.Dock | null = app.dock || null
+let mainWindow: Electron.BrowserWindow
+let tray: Electron.Tray | null = null
+
+let mainWindowState: WindowState
+let menubarWindowState: WindowState
+
+const startURL = __DEV__
+ ? 'http://localhost:3000'
+ : `file://${path.join(__dirname, 'web/index.html')}`
+
+function setupBrowserExtensions() {
+ const {
+ default: installExtension,
+ REACT_DEVELOPER_TOOLS,
+ REDUX_DEVTOOLS,
+ } = require('electron-devtools-installer') // tslint:disable-line no-var-requires
+
+ installExtension(REACT_DEVELOPER_TOOLS).catch(console.error)
+ installExtension(REDUX_DEVTOOLS).catch(console.error)
+}
+
+function getBrowserWindowOptions() {
+ if (!mainWindowState) {
+ mainWindowState = windowStateKeeper({
+ defaultWidth: screen.getPrimaryDisplay().workAreaSize.width,
+ defaultHeight: screen.getPrimaryDisplay().workAreaSize.height,
+ file: 'main-window.json',
+ })
+ }
+
+ if (!menubarWindowState) {
+ menubarWindowState = windowStateKeeper({
+ defaultWidth: 340,
+ defaultHeight: 550,
+ file: 'menubar-window.json',
+ })
+ }
+
+ const options: Electron.BrowserWindowConstructorOptions = {
+ minWidth: 320,
+ minHeight: 450,
+ backgroundColor: '#292c33',
+ darkTheme: true,
+ fullscreenable: true,
+ resizable: true,
+ show: true,
+ title: 'DevHub',
+ webPreferences: {
+ affinity: 'main-window',
+ backgroundThrottling: false,
+ contextIsolation: false,
+ nativeWindowOpen: true,
+ nodeIntegration: false,
+ preload: path.join(__dirname, 'preload.js'),
+ },
+ ...(config.get('isMenuBarMode')
+ ? {
+ x: menubarWindowState.x,
+ y: menubarWindowState.y,
+ width: menubarWindowState.width,
+ height: menubarWindowState.height,
+ alwaysOnTop: true,
+ center: false,
+ frame: false,
+ maxWidth: screen.getPrimaryDisplay().workAreaSize.width * 0.8,
+ maxHeight: screen.getPrimaryDisplay().workAreaSize.height * 0.8,
+ movable: false,
+ skipTaskbar: true,
+ }
+ : {
+ x: mainWindowState.x,
+ y: mainWindowState.y,
+ width: mainWindowState.width,
+ height: mainWindowState.height,
+ alwaysOnTop: false,
+ center: true,
+ frame: frameIsDifferentBetweenModes,
+ maxWidth: undefined,
+ maxHeight: undefined,
+ movable: !config.get('lockOnCenter'),
+ skipTaskbar: false,
+ }),
+ }
+
+ return options
+}
+
+function showWindow() {
+ if (mainWindow.isMinimized()) mainWindow.restore()
+ if (mainWindow.isVisible()) mainWindow.focus()
+ else mainWindow.show()
+}
+
+function createWindow() {
+ const win = new BrowserWindow(getBrowserWindowOptions())
+
+ win.loadURL(startURL)
+
+ win.once('ready-to-show', () => {
+ win.show()
+ })
+
+ win.on('show', () => {
+ updateTrayHightlightMode()
+ updateBrowserWindowOptions()
+ })
+
+ win.on('hide', () => {
+ if (tray) tray.setHighlightMode('selection')
+ })
+
+ win.on('closed', () => {
+ win.destroy()
+ })
+
+ win.on('resize', () => {
+ if (config.get('isMenuBarMode')) {
+ alignWindowWithTray()
+ } else if (config.get('lockOnCenter')) {
+ win.center()
+ }
+ })
+
+ win.on('blur', () => {
+ setTimeout(() => {
+ if (config.get('isMenuBarMode')) win.hide()
+ }, 200)
+ })
+
+ win.on('enter-full-screen', () => {
+ if (dock) dock.show()
+ })
+
+ win.on('leave-full-screen', () => {
+ update()
+ })
+
+ return win
+}
+
+function showTrayContextPopup() {
+ tray!.popUpContextMenu(getTrayContextMenu())
+}
+
+function createTray() {
+ const trayIcon = nativeImage.createFromPath(
+ path.join(
+ __dirname,
+ `../assets/icons/${
+ process.platform === 'win32' ? 'trayIconWhite' : 'trayIconTemplate'
+ }.png`,
+ ),
+ )
+
+ if (tray && !tray.isDestroyed()) tray.destroy()
+
+ tray = new Tray(trayIcon)
+
+ tray.on('click', () => {
+ if (mainWindow.isFullScreen()) {
+ showTrayContextPopup()
+ return
+ }
+
+ if (mainWindow.isVisible() && !mainWindow.isMinimized()) {
+ if (mainWindow.isFocused() || process.platform !== 'darwin') {
+ if (config.get('isMenuBarMode')) {
+ mainWindow.hide()
+ } else {
+ showTrayContextPopup()
+ }
+
+ return
+ }
+ }
+
+ showWindow()
+ })
+
+ tray.on('right-click', () => {
+ showTrayContextPopup()
+ })
+}
+
+function init() {
+ app.setName('DevHub')
+
+ const gotTheLock = app.requestSingleInstanceLock()
+ if (!gotTheLock) {
+ app.quit()
+ return
+ }
+
+ app.on('second-instance', (event, argv, _workingDirectory) => {
+ if (!mainWindow) return
+
+ showWindow()
+
+ app.emit('open-url', event, __DEV__ ? argv[2] : argv[1])
+ })
+
+ app.on('ready', () => {
+ config.set('launchCount', config.get('launchCount', 0) + 1)
+
+ app.removeAsDefaultProtocolClient('devhub')
+
+ if (__DEV__ && process.platform === 'win32') {
+ app.setAsDefaultProtocolClient('devhub', process.execPath, [
+ path.resolve(process.argv[1]),
+ ])
+ } else {
+ app.setAsDefaultProtocolClient('devhub')
+ }
+
+ createTray()
+ if (!mainWindow) mainWindow = createWindow()
+ update()
+
+ if (process.platform === 'darwin') {
+ app.setAboutPanelOptions({
+ applicationName: 'DevHub',
+ applicationVersion: app.getVersion(),
+ copyright: 'Copyright 2018',
+ credits: 'devhub',
+ })
+ }
+
+ if (__DEV__) setupBrowserExtensions()
+
+ autoUpdater.checkForUpdatesAndNotify()
+ })
+
+ app.on('window-all-closed', () => {
+ if (process.platform !== 'darwin') {
+ app.quit()
+ }
+ })
+
+ app.on('activate', () => {
+ if (!mainWindow) mainWindow = createWindow()
+
+ mainWindow.show()
+ })
+
+ app.on('web-contents-created', (_event, webContents) => {
+ webContents.on(
+ 'new-window',
+ (event, uri, _frameName, _disposition, _options) => {
+ event.preventDefault()
+ shell.openExternal(uri)
+ },
+ )
+ })
+
+ app.on('open-url', (_event, url) => {
+ if (!mainWindow) return
+
+ mainWindow.webContents.send('open-url', url)
+ showWindow()
+ })
+}
+
+function getCenterPosition(obj: Electron.BrowserWindow | Electron.Tray) {
+ const bounds = obj.getBounds()
+
+ const x = Math.round(bounds.x + bounds.width / 2)
+ const y = Math.round(bounds.y + bounds.height / 2)
+
+ return { x, y }
+}
+
+function alignWindowWithTray() {
+ if (!(tray && !tray.isDestroyed())) return
+
+ const screenSize = screen.getPrimaryDisplay().size
+ const workArea = screen.getPrimaryDisplay().workArea
+ const windowBounds = mainWindow.getBounds()
+ const trayBounds = tray.getBounds()
+ const trayCenter = getCenterPosition(tray)
+
+ const top = trayBounds.y < screenSize.height / 3
+ const bottom = screenSize.height - trayBounds.y < screenSize.height / 3
+ const left = trayBounds.x < screenSize.width / 3
+ const right = screenSize.width - trayBounds.x < screenSize.width / 3
+
+ let x: number
+ let y: number
+ const spacing = 8
+
+ if (top) {
+ y = Math.round(trayCenter.y)
+ } else if (bottom) {
+ y = Math.round(trayCenter.y - windowBounds.height / 2)
+ } else {
+ y = Math.round(trayCenter.y - windowBounds.height / 2)
+ }
+
+ if (left) {
+ x = Math.round(trayCenter.x)
+ } else if (right) {
+ x = Math.round(trayCenter.x - windowBounds.width / 2)
+ } else {
+ x = Math.round(trayCenter.x - windowBounds.width / 2)
+ }
+
+ const fixedX = Math.max(
+ workArea.x + spacing,
+ Math.min(x, workArea.x + workArea.width - windowBounds.width - spacing),
+ )
+ const fixedY = Math.max(
+ workArea.y + spacing,
+ Math.min(y, workArea.y + workArea.height - windowBounds.height - spacing),
+ )
+
+ mainWindow.setPosition(fixedX, fixedY)
+}
+
+function getAboutMenuItems() {
+ const menuItems: Electron.MenuItemConstructorOptions[] = [
+ ...(process.platform === 'darwin'
+ ? [
+ {
+ label: `About ${app.getName()}`,
+ role: 'about',
+ },
+ ]
+ : []),
+ {
+ label: 'View on GitHub',
+ click: () => {
+ shell.openExternal('https://github.com/devhubapp/devhub')
+ },
+ },
+ {
+ type: 'separator',
+ },
+ {
+ label: 'Check for Updates...',
+ enabled: !__DEV__,
+ click: () => {
+ autoUpdater.checkForUpdatesAndNotify()
+ },
+ },
+ ]
+
+ return menuItems
+}
+
+function getModeMenuItems() {
+ const isCurrentWindow = mainWindow.isVisible() && !mainWindow.isMinimized()
+ const enabled = isCurrentWindow || config.get('isMenuBarMode')
+
+ const menuItems: Electron.MenuItemConstructorOptions[] = [
+ {
+ type: 'radio',
+ label: 'Desktop mode',
+ checked: !config.get('isMenuBarMode'),
+ enabled,
+ click() {
+ enableDesktopMode()
+ },
+ },
+ {
+ type: 'radio',
+ label: 'Menubar mode',
+ checked: !!config.get('isMenuBarMode'),
+ enabled,
+ click() {
+ enableMenuBarMode()
+ },
+ },
+ ]
+
+ return menuItems
+}
+
+function getOptionsMenuItems() {
+ const isCurrentWindow = mainWindow.isVisible() && !mainWindow.isMinimized()
+ const enabled = isCurrentWindow || config.get('isMenuBarMode')
+
+ const menuItems: Electron.MenuItemConstructorOptions[] = [
+ {
+ type: 'checkbox',
+ label: 'Lock on center',
+ checked: config.get('lockOnCenter'),
+ enabled,
+ visible: !config.get('isMenuBarMode'),
+ click(item) {
+ config.set('lockOnCenter', item.checked)
+
+ if (item.checked) {
+ if (!config.get('isMenuBarMode')) {
+ mainWindow.setMovable(false)
+ }
+
+ mainWindow.center()
+ } else {
+ if (!config.get('isMenuBarMode')) {
+ mainWindow.setMovable(getBrowserWindowOptions().movable !== false)
+ }
+ }
+ },
+ },
+ ]
+
+ return menuItems
+}
+
+function getMainMenuItems() {
+ const isCurrentWindow = mainWindow.isVisible() && !mainWindow.isMinimized()
+ const enabled = isCurrentWindow || config.get('isMenuBarMode')
+
+ const menuItems: Electron.MenuItemConstructorOptions[] = [
+ ...(process.platform === 'darwin'
+ ? ([
+ {
+ label: app.getName(),
+ submenu: [
+ ...getAboutMenuItems(),
+ {
+ type: 'separator',
+ },
+ {
+ label: `Hide ${app.getName()}`,
+ accelerator: 'Command+H',
+ role: 'hide',
+ },
+ {
+ label: 'Hide Others',
+ accelerator: 'Command+Alt+H',
+ role: 'hideothers',
+ },
+ {
+ type: 'separator',
+ },
+ {
+ label: 'Quit',
+ role: 'quit',
+ click: () => {
+ app.quit()
+ },
+ },
+ ],
+ },
+ ] as Electron.MenuItemConstructorOptions[])
+ : []),
+ {
+ label: 'Edit',
+ submenu: [
+ {
+ label: 'Undo',
+ accelerator: 'CmdOrCtrl+Z',
+ role: 'undo',
+ },
+ {
+ label: 'Redo',
+ accelerator: 'Shift+CmdOrCtrl+Z',
+ role: 'redo',
+ },
+ {
+ type: 'separator',
+ },
+ {
+ label: 'Cut',
+ accelerator: 'CmdOrCtrl+X',
+ role: 'cut',
+ },
+ {
+ label: 'Copy',
+ accelerator: 'CmdOrCtrl+C',
+ role: 'copy',
+ },
+ {
+ label: 'Paste',
+ accelerator: 'CmdOrCtrl+V',
+ role: 'paste',
+ },
+ {
+ label: 'Select All',
+ accelerator: 'CmdOrCtrl+A',
+ role: 'selectall',
+ },
+ ],
+ },
+ {
+ label: 'View',
+ submenu: [
+ {
+ label: 'Reload',
+ accelerator: 'CmdOrCtrl+R',
+ click(_, focusedWindow) {
+ if (focusedWindow) focusedWindow.reload()
+ },
+ },
+ {
+ label: 'Toggle Developer Tools',
+ accelerator: __DEV__
+ ? process.platform === 'darwin'
+ ? 'Alt+Command+I'
+ : 'Ctrl+Shift+I'
+ : undefined,
+ visible: __DEV__,
+ click(_, focusedWindow) {
+ if (focusedWindow) focusedWindow.webContents.toggleDevTools()
+ },
+ },
+ {
+ type: 'separator',
+ enabled,
+ },
+ ...getModeMenuItems(),
+ {
+ type: 'separator',
+ enabled,
+ },
+ ...getOptionsMenuItems(),
+ ],
+ },
+ {
+ label: 'Window',
+ role: 'window',
+ submenu: getWindowMenuItems(),
+ },
+ {
+ label: 'Help',
+ role: 'help',
+ submenu: [
+ ...(process.platform === 'darwin'
+ ? []
+ : ([
+ ...getAboutMenuItems(),
+ {
+ type: 'separator',
+ },
+ ] as Electron.MenuItemConstructorOptions[])),
+ {
+ label: 'Report bug',
+ click: () => {
+ shell.openExternal('https://github.com/devhubapp/devhub/issues/new')
+ },
+ },
+ {
+ label: 'Send feedback',
+ click: () => {
+ shell.openExternal('https://github.com/devhubapp/devhub/issues/new')
+ },
+ },
+ ],
+ },
+ ]
+
+ return menuItems
+}
+
+function getDockMenuItems() {
+ return getModeMenuItems()
+}
+
+function getWindowMenuItems() {
+ const isCurrentWindow = mainWindow.isVisible() && !mainWindow.isMinimized()
+ const enabled = isCurrentWindow || config.get('isMenuBarMode')
+
+ const menuItems: Electron.MenuItemConstructorOptions[] = [
+ {
+ label: 'Close',
+ accelerator: 'CmdOrCtrl+W',
+ click() {
+ mainWindow.hide()
+ },
+ },
+ {
+ label: 'Minimize',
+ accelerator: config.get('isMenuBarMode') ? undefined : 'CmdOrCtrl+M',
+ role: config.get('isMenuBarMode') ? undefined : 'minimize',
+ enabled: enabled && !mainWindow.isMinimized(),
+ visible: !config.get('isMenuBarMode'), // && mainWindow.isMinimizable(),
+ },
+ {
+ label: 'Maximize',
+ visible: !config.get('isMenuBarMode'), // && mainWindow.isMaximizable(),
+ enabled, // && !mainWindow.isMaximized(),
+ click() {
+ showWindow()
+ mainWindow.maximize()
+ },
+ },
+ {
+ type: 'checkbox',
+ label: 'Full Screen',
+ accelerator: process.platform === 'darwin' ? 'Ctrl+Command+F' : 'F11',
+ checked: mainWindow.isFullScreen(),
+ enabled: enabled || mainWindow.isFullScreen(),
+ click(item) {
+ mainWindow.setFullScreen(item.checked)
+ },
+ },
+ ]
+
+ return menuItems
+}
+
+function getTrayMenuItems() {
+ const isCurrentWindow = mainWindow.isVisible() && !mainWindow.isMinimized()
+ const enabled = isCurrentWindow || config.get('isMenuBarMode')
+
+ const menuItems: Electron.MenuItemConstructorOptions[] = [
+ {
+ label: 'Open',
+ visible: !isCurrentWindow || config.get('isMenuBarMode'),
+ click() {
+ showWindow()
+ },
+ },
+ {
+ type: 'separator',
+ visible: !isCurrentWindow || config.get('isMenuBarMode'),
+ },
+ ...getModeMenuItems(),
+ {
+ type: 'separator',
+ enabled,
+ },
+ ...getOptionsMenuItems(),
+ {
+ type: 'separator',
+ enabled,
+ visible: !config.get('isMenuBarMode'),
+ },
+ ...getWindowMenuItems().filter(item => item.label !== 'Close'),
+ {
+ type: 'separator',
+ enabled,
+ },
+ {
+ label: 'Quit',
+ accelerator: 'CmdOrCtrl+Q',
+ role: 'quit',
+ },
+ ]
+
+ return menuItems
+}
+
+function getTrayContextMenu() {
+ return Menu.buildFromTemplate(getTrayMenuItems())
+}
+
+function updateTrayHightlightMode() {
+ if (!(tray && !tray.isDestroyed())) return
+
+ tray.setHighlightMode(
+ config.get('isMenuBarMode') &&
+ mainWindow.isVisible() &&
+ mainWindow.isFocused() &&
+ !mainWindow.isFullScreen()
+ ? 'always'
+ : 'selection',
+ )
+}
+
+function updateBrowserWindowOptions() {
+ const options = getBrowserWindowOptions()
+
+ const maximize =
+ !config.get('isMenuBarMode') &&
+ (mainWindow.isMaximized() ||
+ mainWindowState.isMaximized ||
+ config.get('launchCount') === 1)
+
+ mainWindow.setAlwaysOnTop(options.alwaysOnTop === true)
+
+ mainWindow.setMinimumSize(
+ Math.floor(options.minWidth || 0),
+ Math.floor(options.minHeight || 0),
+ )
+
+ mainWindow.setMaximumSize(
+ Math.ceil(
+ options.maxWidth ||
+ (process.platform === 'darwin'
+ ? screen.getPrimaryDisplay().workAreaSize.width
+ : 0),
+ ),
+ Math.ceil(
+ options.maxHeight ||
+ (process.platform === 'darwin'
+ ? screen.getPrimaryDisplay().workAreaSize.height
+ : 0),
+ ),
+ )
+
+ mainWindow.setMovable(options.movable !== false)
+
+ mainWindow.setPosition(
+ maximize ? screen.getPrimaryDisplay().workArea.x || 0 : options.x || 0,
+ maximize ? screen.getPrimaryDisplay().workArea.y || 0 : options.y || 0,
+ false,
+ )
+
+ mainWindow.setSkipTaskbar(options.skipTaskbar === true)
+
+ mainWindow.setSize(
+ maximize
+ ? screen.getPrimaryDisplay().workAreaSize.width
+ : options.width || 500,
+ maximize
+ ? screen.getPrimaryDisplay().workAreaSize.height
+ : options.height || 500,
+ false,
+ )
+
+ if (dock) {
+ if (options.skipTaskbar === true) {
+ dock.hide()
+ } else {
+ dock.show()
+ }
+ }
+
+ mainWindowState.unmanage()
+ menubarWindowState.unmanage()
+ if (config.get('isMenuBarMode')) {
+ menubarWindowState.manage(mainWindow)
+ } else {
+ mainWindowState.manage(mainWindow)
+ }
+
+ if (config.get('isMenuBarMode')) {
+ alignWindowWithTray()
+ } else {
+ if (config.get('lockOnCenter')) {
+ mainWindow.center()
+ }
+
+ if (maximize) {
+ mainWindow.maximize()
+ }
+ }
+}
+
+function updateMenu() {
+ Menu.setApplicationMenu(Menu.buildFromTemplate(getMainMenuItems()))
+
+ if (process.platform === 'darwin') {
+ const touchBar = new TouchBar({
+ items: [],
+ })
+
+ mainWindow.setTouchBar(touchBar)
+ }
+
+ if (dock) dock.setMenu(Menu.buildFromTemplate(getDockMenuItems()))
+}
+
+function update() {
+ if (mainWindow.isFullScreen()) {
+ mainWindow.setFullScreen(false)
+ return
+ }
+
+ showWindow()
+ updateMenu()
+ updateTrayHightlightMode()
+ updateBrowserWindowOptions()
+}
+
+function updateOrRecreateWindow() {
+ if (frameIsDifferentBetweenModes) {
+ const oldWindow = mainWindow
+ mainWindow = createWindow()
+ oldWindow.close()
+ }
+
+ update()
+}
+
+function enableDesktopMode() {
+ config.set('isMenuBarMode', false)
+ updateOrRecreateWindow()
+}
+
+function enableMenuBarMode() {
+ config.set('isMenuBarMode', true)
+ updateOrRecreateWindow()
+}
+
+init()
diff --git a/packages/desktop/src/libs/electron-is-dev/index.ts b/packages/desktop/src/libs/electron-is-dev/index.ts
new file mode 100644
index 000000000..9267d0e3a
--- /dev/null
+++ b/packages/desktop/src/libs/electron-is-dev/index.ts
@@ -0,0 +1,12 @@
+// Source: https://github.com/sindresorhus/electron-is-dev
+
+import electron from 'electron'
+
+const app = electron.app || electron.remote.app
+
+const isEnvSet = 'ELECTRON_IS_DEV' in process.env
+const getFromEnv =
+ !!process.env.ELECTRON_IS_DEV &&
+ parseInt(process.env.ELECTRON_IS_DEV, 10) === 1
+
+export const __DEV__ = isEnvSet ? getFromEnv : !app.isPackaged
diff --git a/packages/desktop/src/libs/electron-window-state/index.ts b/packages/desktop/src/libs/electron-window-state/index.ts
new file mode 100644
index 000000000..356bf12c6
--- /dev/null
+++ b/packages/desktop/src/libs/electron-window-state/index.ts
@@ -0,0 +1,271 @@
+// Modified version of: https://github.com/mawie81/electron-window-state
+
+import electron from 'electron'
+import jsonfile from 'jsonfile'
+import mkdirp from 'mkdirp'
+import path from 'path'
+
+export interface WindowStateOptions {
+ /** The height that should be returned if no file exists yet. Defaults to `600`. */
+ defaultHeight?: number
+ /** The width that should be returned if no file exists yet. Defaults to `800`. */
+ defaultWidth?: number
+ /** Should we automatically restore the window to full screen, if it was last closed full screen. Defaults to `true`. */
+ fullScreen?: boolean
+ /** The path where the state file should be written to. Defaults to `app.getPath('userData')`. */
+ path?: string
+ /** The name of file. Defaults to `window-state.json`. */
+ file?: string
+ /** Should we automatically maximize the window, if it was last closed maximized. Defaults to `true`. */
+ maximize?: boolean
+}
+
+export interface WindowInternalState {
+ displayBounds: {
+ height: number
+ width: number
+ }
+ /** The saved height of loaded state. `defaultHeight` if the state has not been saved yet. */
+ height: number
+ /** true if the window state was saved while the window was in full screen mode. `undefined` if the state has not been saved yet. */
+ isFullScreen: boolean
+ /** `true` if the window state was saved while the window was maximized. `undefined` if the state has not been saved yet. */
+ isMaximized: boolean
+ /** The saved width of loaded state. `defaultWidth` if the state has not been saved yet. */
+ width: number
+ /** The saved x coordinate of the loaded state. `undefined` if the state has not been saved yet. */
+ x: number
+ /** The saved y coordinate of the loaded state. `undefined` if the state has not been saved yet. */
+ y: number
+}
+
+export interface WindowState extends WindowInternalState {
+ /** Register listeners on the given `BrowserWindow` for events that are related to size or position changes (resize, move). It will also restore the window's maximized or full screen state. When the window is closed we automatically remove the listeners and save the state. */
+ manage: (window: Electron.BrowserWindow) => void
+ /** Removes all listeners of the managed `BrowserWindow` in case it does not need to be managed anymore. */
+ unmanage: () => void
+}
+
+export function windowStateKeeper(options: WindowStateOptions): WindowState {
+ const app = electron.app || electron.remote.app
+ const screen = electron.screen || electron.remote.screen
+
+ let state: WindowInternalState
+ let winRef: Electron.BrowserWindow | null = null
+ let updateStateTimer: number
+ let saveStateTimer: number
+
+ const updateStateDelay = 100
+ const saveStateDelay = 500
+
+ const config = Object.assign(
+ {
+ file: 'window-state.json',
+ path: app.getPath('userData'),
+ maximize: true,
+ fullScreen: true,
+ },
+ options,
+ )
+
+ const fullStoreFileName = path.join(config.path, config.file)
+
+ function isNormal(win: Electron.BrowserWindow) {
+ return !win.isMaximized() && !win.isMinimized() && !win.isFullScreen()
+ }
+
+ function hasBounds() {
+ return (
+ state &&
+ Number.isInteger(state.x) &&
+ Number.isInteger(state.y) &&
+ Number.isInteger(state.width) &&
+ state.width > 0 &&
+ Number.isInteger(state.height) &&
+ state.height > 0
+ )
+ }
+
+ function resetStateToDefault() {
+ const displayBounds = screen.getPrimaryDisplay().bounds
+
+ // Reset state to default values on the primary display
+ state = {
+ displayBounds,
+ x: 0,
+ y: 0,
+ width: config.defaultWidth || 800,
+ height: config.defaultHeight || 600,
+ isFullScreen: false,
+ isMaximized: false,
+ }
+ }
+
+ function windowWithinBounds(bounds: Electron.Rectangle) {
+ return (
+ state.x >= bounds.x &&
+ state.y >= bounds.y &&
+ state.x + state.width <= bounds.x + bounds.width &&
+ state.y + state.height <= bounds.y + bounds.height
+ )
+ }
+
+ function ensureWindowVisibleOnSomeDisplay() {
+ const visible = screen.getAllDisplays().some(display => {
+ return windowWithinBounds(display.bounds)
+ })
+
+ if (!visible) {
+ // Window is partially or fully not visible now.
+ // Reset it to safe defaults.
+ return resetStateToDefault()
+ }
+ }
+
+ function validateState() {
+ const isValid =
+ state && (hasBounds() || state.isMaximized || state.isFullScreen)
+
+ if (!isValid) {
+ resetStateToDefault()
+ return
+ }
+
+ if (hasBounds() && state.displayBounds) {
+ ensureWindowVisibleOnSomeDisplay()
+ }
+ }
+
+ function updateState(_win?: Electron.BrowserWindow) {
+ const win = _win || winRef
+
+ if (!win) {
+ return
+ }
+
+ // Don't throw an error when window was closed
+ try {
+ const winBounds = win.getBounds()
+
+ if (isNormal(win)) {
+ state.x = winBounds.x
+ state.y = winBounds.y
+ state.width = winBounds.width
+ state.height = winBounds.height
+ }
+
+ state.isMaximized = win.isMaximized()
+ state.isFullScreen = win.isFullScreen()
+ state.displayBounds = screen.getDisplayMatching(winBounds).bounds
+ } catch (err) {
+ //
+ }
+ }
+
+ function saveState(win?: Electron.BrowserWindow) {
+ // Update window state only if it was provided
+ if (win) {
+ updateState(win)
+ }
+
+ // Save state
+ try {
+ mkdirp.sync(path.dirname(fullStoreFileName))
+ jsonfile.writeFileSync(fullStoreFileName, state)
+ } catch (err) {
+ //
+ }
+ }
+
+ function stateChangeHandler() {
+ // Handles both 'resize' and 'move'
+ clearTimeout(updateStateTimer)
+ clearTimeout(saveStateTimer)
+
+ updateStateTimer = setTimeout(updateState, updateStateDelay)
+ saveStateTimer = setTimeout(saveState, saveStateDelay)
+ }
+
+ function closeHandler() {
+ updateState()
+ }
+
+ function closedHandler() {
+ // Unregister listeners and save state
+ unmanage()
+ saveState()
+ }
+
+ function manage(win: Electron.BrowserWindow) {
+ winRef = win
+
+ if (config.maximize && state.isMaximized) {
+ win.maximize()
+ }
+
+ if (config.fullScreen && state.isFullScreen) {
+ win.setFullScreen(true)
+ }
+
+ win.on('resize', stateChangeHandler)
+ win.on('move', stateChangeHandler)
+ win.on('close', closeHandler)
+ win.on('closed', closedHandler)
+ }
+
+ function unmanage() {
+ if (winRef) {
+ clearTimeout(updateStateTimer)
+ winRef.removeListener('resize', stateChangeHandler)
+ winRef.removeListener('move', stateChangeHandler)
+ winRef.removeListener('close', closeHandler)
+ winRef.removeListener('closed', closedHandler)
+ winRef = null
+ }
+ }
+
+ // Load previous state
+ try {
+ state = jsonfile.readFileSync(fullStoreFileName)
+ } catch (err) {
+ //
+ }
+
+ // Check state validity
+ validateState()
+
+ // Set state fallback values
+ state = Object.assign(
+ {
+ width: config.defaultWidth || 800,
+ height: config.defaultHeight || 600,
+ },
+ state!,
+ )
+
+ return {
+ get x() {
+ return state.x
+ },
+ get y() {
+ return state.y
+ },
+ get width() {
+ return state.width
+ },
+ get height() {
+ return state.height
+ },
+ get displayBounds() {
+ return state.displayBounds
+ },
+ get isMaximized() {
+ return state.isMaximized
+ },
+ get isFullScreen() {
+ return state.isFullScreen
+ },
+ manage,
+ unmanage,
+ }
+}
diff --git a/packages/desktop/src/preload.ts b/packages/desktop/src/preload.ts
new file mode 100644
index 000000000..e2ff13faa
--- /dev/null
+++ b/packages/desktop/src/preload.ts
@@ -0,0 +1,5 @@
+import electron from 'electron'
+
+// Communication between webapp and electron main process
+// Used on oauth flow
+window.ipc = electron.ipcRenderer
diff --git a/packages/desktop/tsconfig.json b/packages/desktop/tsconfig.json
index efbde400d..c71475f3b 100644
--- a/packages/desktop/tsconfig.json
+++ b/packages/desktop/tsconfig.json
@@ -1,17 +1,27 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
- "target": "es2015",
- "module": "commonjs",
"allowJs": false,
- "composite": true,
- "declaration": true,
+ "isolatedModules": true,
+ "module": "commonjs",
"noEmit": false,
- "outDir": "dist/",
+ "outDir": "dist",
"rootDir": "src",
+ "target": "es2015",
"typeRoots": [
"../../@types",
"../../node_modules/@types"
]
- }
+ },
+ "include": [
+ "src"
+ ],
+ "references": [
+ {
+ "path": "../core"
+ },
+ {
+ "path": "../components"
+ }
+ ]
}
diff --git a/packages/mobile/android/app/src/main/ic_launcher-web.png b/packages/mobile/android/app/src/main/ic_launcher-web.png
index 619b3d98f..45f50f257 100644
Binary files a/packages/mobile/android/app/src/main/ic_launcher-web.png and b/packages/mobile/android/app/src/main/ic_launcher-web.png differ
diff --git a/packages/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/packages/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
index 036d09bc5..4ae7d1237 100644
--- a/packages/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ b/packages/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -1,5 +1,5 @@