diff --git a/package.json b/package.json index 32418e0d3..9a7aa843b 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,8 @@ "dev": "yarn compile && concurrently --kill-others \"yarn compile -w\" \"yarn workspace @devhub/web start\" \"yarn workspace @devhub/mobile start\"", "dev:web": "yarn compile && concurrently --kill-others \"yarn compile -w\" \"yarn workspace @devhub/web start\"", "dev:mobile": "yarn compile && concurrently --kill-others \"yarn compile -w\" \"yarn workspace @devhub/mobile start\"", + "dev:desktop": "yarn compile && concurrently --kill-others \"yarn compile -w\" \"yarn workspace @devhub/desktop start\"", + "build:desktop": "yarn compile && concurrently --kill-others \"yarn compile -w\" \"yarn workspace @devhub/desktop build\"", "format": "yarn workspaces run format", "lint": "yarn workspaces run lint", "now": "npx now", diff --git a/packages/desktop/.gitignore b/packages/desktop/.gitignore new file mode 100644 index 000000000..dd9baee6c --- /dev/null +++ b/packages/desktop/.gitignore @@ -0,0 +1,15 @@ +.DS_Store +.env.development.local +.env.local +.env.production.local +.env.test.local +.history +.jest +.vscode +build +coverage +dist +node_modules +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/packages/desktop/package.json b/packages/desktop/package.json new file mode 100644 index 000000000..658c16f1e --- /dev/null +++ b/packages/desktop/package.json @@ -0,0 +1,80 @@ +{ + "name": "@devhub/desktop", + "version": "0.0.1", + "private": false, + "main": "dist/electron.js", + "scripts": { + "build": "tsc -b && electron-builder -mw", + "clean": "rm -rf dist/*", + "format": "prettier --write '{.,src/**}/*.{js,jsx,ts,tsx}'", + "lint": "tslint -p .", + "predeploy": "yarn run build", + "start": "rm -rf dist && tsc && electron .", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "build": { + "productName": "devhub", + "appId": "com.devhubapp.devhub", + "dmg": { + "contents": [ + { + "x": 410, + "y": 150, + "type": "link", + "path": "/Applications" + }, + { + "x": 130, + "y": 150, + "type": "file" + } + ] + }, + "directories": { + "output": "build" + }, + "files": [ + "dist/**/*", + "node_modules/**/*", + "package.json" + ], + "mac": { + "icon": "public/static/icns/logo.icns" + }, + "win": { + "target": "nsis", + "icon": "public/static/icns/logo.ico" + }, + "nsis": { + "oneClick": true + } + }, + "dependencies": { + "@babel/polyfill": "^7.0.0", + "@devhub/components": "0.39.2", + "react": "^16.7.0-alpha.2", + "react-app-polyfill": "^0.1.3", + "react-art": "^16.7.0-alpha.2", + "react-dom": "^16.7.0-alpha.2", + "react-native-web": "^0.9.8", + "react-scripts": "2.1.1", + "resize-observer-polyfill": "^1.5.0" + }, + "devDependencies": { + "electron": "^4.0.0", + "electron-builder": "^20.38.3", + "tslint": "^5.11.0", + "tslint-config-airbnb": "^5.11.1", + "tslint-config-prettier": "^1.17.0", + "typescript": "^3.2.1" + }, + "resolutions": { + "scheduler": "0.12.0-alpha.3" + }, + "browserslist": [ + ">0.2%", + "not dead", + "not ie <= 9", + "not op_mini all" + ] +} diff --git a/packages/desktop/public/favicon.ico b/packages/desktop/public/favicon.ico new file mode 100644 index 000000000..da4c18b30 Binary files /dev/null and b/packages/desktop/public/favicon.ico differ diff --git a/packages/desktop/public/index.html b/packages/desktop/public/index.html new file mode 100644 index 000000000..e1c50ddaa --- /dev/null +++ b/packages/desktop/public/index.html @@ -0,0 +1,103 @@ + + + + + + + + DevHub - TweetDeck for GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + \ No newline at end of file diff --git a/packages/desktop/public/manifest.json b/packages/desktop/public/manifest.json new file mode 100644 index 000000000..68610dc2c --- /dev/null +++ b/packages/desktop/public/manifest.json @@ -0,0 +1,39 @@ +{ + "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 new file mode 100644 index 000000000..7f772a1fd --- /dev/null +++ b/packages/desktop/public/static/css/arrow.css @@ -0,0 +1,14 @@ +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 new file mode 100644 index 000000000..bced294f2 --- /dev/null +++ b/packages/desktop/public/static/css/error.css @@ -0,0 +1,25 @@ +* { + 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 new file mode 100644 index 000000000..92491eedf Binary files /dev/null and b/packages/desktop/public/static/icns/logo.icns differ diff --git a/packages/desktop/public/static/icns/logo.ico b/packages/desktop/public/static/icns/logo.ico new file mode 100644 index 000000000..cd76e6c02 Binary files /dev/null and b/packages/desktop/public/static/icns/logo.ico differ diff --git a/packages/desktop/public/static/media/logo-transparent@2x.png b/packages/desktop/public/static/media/logo-transparent@2x.png new file mode 100644 index 000000000..9aaa6d9d2 Binary files /dev/null and b/packages/desktop/public/static/media/logo-transparent@2x.png differ diff --git a/packages/desktop/public/static/media/logo.png b/packages/desktop/public/static/media/logo.png new file mode 100644 index 000000000..71330d5fd Binary files /dev/null and b/packages/desktop/public/static/media/logo.png differ diff --git a/packages/desktop/public/static/media/logo@2x.png b/packages/desktop/public/static/media/logo@2x.png new file mode 100644 index 000000000..17400f328 Binary files /dev/null and b/packages/desktop/public/static/media/logo@2x.png differ diff --git a/packages/desktop/public/static/media/logo@3x.png b/packages/desktop/public/static/media/logo@3x.png new file mode 100644 index 000000000..bfcd83a45 Binary files /dev/null and b/packages/desktop/public/static/media/logo@3x.png differ diff --git a/packages/desktop/src/electron.ts b/packages/desktop/src/electron.ts new file mode 100644 index 000000000..373234d51 --- /dev/null +++ b/packages/desktop/src/electron.ts @@ -0,0 +1,373 @@ +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/tsconfig.json b/packages/desktop/tsconfig.json new file mode 100644 index 000000000..efbde400d --- /dev/null +++ b/packages/desktop/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "target": "es2015", + "module": "commonjs", + "allowJs": false, + "composite": true, + "declaration": true, + "noEmit": false, + "outDir": "dist/", + "rootDir": "src", + "typeRoots": [ + "../../@types", + "../../node_modules/@types" + ] + } +} diff --git a/packages/desktop/tslint.json b/packages/desktop/tslint.json new file mode 100644 index 000000000..b520bae6b --- /dev/null +++ b/packages/desktop/tslint.json @@ -0,0 +1,3 @@ +{ + "extends": ["../../tslint.json"] +} \ No newline at end of file diff --git a/packages/web/public/index.html b/packages/web/public/index.html index e1c50ddaa..986d5af5b 100644 --- a/packages/web/public/index.html +++ b/packages/web/public/index.html @@ -53,8 +53,15 @@ +
-