diff --git a/.npmrc b/.npmrc deleted file mode 100644 index de0241178..000000000 --- a/.npmrc +++ /dev/null @@ -1,4 +0,0 @@ -runtime=electron -target=1.6.5 -target_arch=x64 -disturl=https://atom.io/download/atom-shell \ No newline at end of file diff --git a/README.md b/README.md index 9ece87562..49161b2f4 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,9 @@ Development on Insomnia can be done on Windows, Mac, and Linux with few requirem requires [NodeJS 7.4](https://nodejs.org) and [Git](https://git-scm.com/) to get started. ```bash -# Install dependencies +# Install dependencies and build addons for Electron npm install +npm run rebuild # Start app npm run dev diff --git a/app/__tests__/package.test.js b/app/__tests__/package.test.js new file mode 100644 index 000000000..e7cc15352 --- /dev/null +++ b/app/__tests__/package.test.js @@ -0,0 +1,12 @@ +import * as appPackage from '../package.json'; +import * as globalPackage from '../../package.json'; + +describe('package.json', () => { + it('all app dependencies should be same in global', () => { + for (const name of Object.keys(appPackage.dependencies)) { + const expected = globalPackage.dependencies[name]; + const actual = appPackage.dependencies[name]; + expect(`${name}::${actual}`).toBe(`${name}::${expected}`); + } + }); +}); diff --git a/app/common/constants.js b/app/common/constants.js index 0e0e6f3f2..4079346ae 100644 --- a/app/common/constants.js +++ b/app/common/constants.js @@ -26,6 +26,14 @@ export function isMac () { return getAppPlatform() === 'darwin'; } +export function isLinux () { + return getAppPlatform() === 'linux'; +} + +export function isWindows () { + return getAppPlatform() === 'win32'; +} + export function isDevelopment () { return getAppEnvironment() === 'development'; } @@ -48,6 +56,7 @@ export const CHANGELOG_PAGE = 'https://insomnia.rest/changelog/'; export const STATUS_CODE_RENDER_FAILED = -333; export const LARGE_RESPONSE_MB = 5; export const FLEXIBLE_URL_REGEX = /^(http|https):\/\/[0-9a-zA-Z\-_.]+[/\w.\-+=:\][@%^*&!#?;]*/; +export const CHECK_FOR_UPDATES_INTERVAL = 1000 * 60 * 60 * 3; // 3 hours // Hotkeys export const MOD_SYM = isMac() ? '⌘' : 'ctrl'; diff --git a/app/common/import.js b/app/common/import.js index 981241c16..16e993e2a 100644 --- a/app/common/import.js +++ b/app/common/import.js @@ -3,6 +3,11 @@ import * as db from './database'; import * as models from '../models'; import {getAppVersion} from './constants'; import * as misc from './misc'; +import {showModal} from '../ui/components/modals/index'; +import AlertModal from '../ui/components/modals/alert-modal'; +import * as fetch from './fetch'; +import fs from 'fs'; +import {trackEvent} from '../analytics/index'; const EXPORT_FORMAT = 3; @@ -23,6 +28,43 @@ const MODELS = { [EXPORT_TYPE_ENVIRONMENT]: models.environment }; +export async function importUri (workspaceId, uri) { + let rawText; + if (uri.match(/^(http|https):\/\//)) { + const response = await fetch.rawFetch(uri); + rawText = await response.text(); + } else if (uri.match(/^(file):\/\//)) { + const path = uri.replace(/^(file):\/\//, ''); + rawText = fs.readFileSync(path, 'utf8'); + } else { + throw new Error(`Invalid import URI ${uri}`); + } + + const workspace = await models.workspace.getById(workspaceId); + const result = await importRaw(workspace, rawText); + const {summary, source, error} = result; + + if (error) { + showModal(AlertModal, {title: 'Import Failed', message: error}); + return; + } + + let statements = Object.keys(summary).map(type => { + const count = summary[type].length; + const name = models.getModelName(type, count); + return count === 0 ? null : `${count} ${name}`; + }).filter(s => s !== null); + + let message; + if (statements.length === 0) { + message = 'Nothing was found to import.'; + } else { + message = `You imported ${statements.join(', ')}!`; + } + showModal(AlertModal, {title: 'Import Succeeded', message}); + trackEvent('Import', 'Success', source); +} + export async function importRaw (workspace, rawContent, generateNewIds = false) { let results; try { diff --git a/app/common/prettify.js b/app/common/prettify.js index c4041aab3..4fc72dd2b 100644 --- a/app/common/prettify.js +++ b/app/common/prettify.js @@ -92,6 +92,9 @@ export function prettifyJson (json, indentChars) { } newJson += currentChar; break; + case '\r': + // Skip windows return characters + break; default: newJson += currentChar; break; diff --git a/app/main.development.js b/app/main.development.js index 63545e903..e8845c1a9 100644 --- a/app/main.development.js +++ b/app/main.development.js @@ -1,192 +1,39 @@ -import reboot from 'electron-squirrel-startup'; -if (reboot) { +import needsRestart from 'electron-squirrel-startup'; +import electron from 'electron'; +import {isDevelopment, isMac} from './common/constants'; +import * as errorHandling from './main/error-handling'; +import * as updates from './main/updates'; +import {createWindow} from './main/window-utils'; + +// Handle potential auto-update +if (needsRestart) { process.exit(0); } -import fs from 'fs'; -import Raven from 'raven'; -import path from 'path'; -import electron from 'electron'; -import * as packageJSON from './package.json'; -import LocalStorage from './common/local-storage'; +// Initialize some things +errorHandling.init(); +updates.init(); -// Some useful helpers -const IS_DEV = process.env.INSOMNIA_ENV === 'development'; -const IS_MAC = process.platform === 'darwin'; -const IS_WINDOWS = process.platform === 'win32'; -const IS_LINUX = !IS_MAC && !IS_WINDOWS; - -const ravenClient = Raven.config('https://786e43ae199c4757a9ea4a48a9abd17d@sentry.io/109702', { - environment: process.env.INSOMNIA_ENV || 'production', - release: packageJSON.version, - logger: 'electron.main' -}); - -if (!IS_DEV) { - ravenClient.install(); +function addUrlToOpen (e, url) { + e.preventDefault(); + args.push(url); } -const {app, dialog, shell, ipcMain, autoUpdater, Menu, BrowserWindow} = electron; -const {version: appVersion, productName: appName} = packageJSON; +const {app, ipcMain} = electron; -const UPDATE_URLS = { - // Add `r` param to help cache bust - darwin: `https://updates.insomnia.rest/builds/check/mac?v=${appVersion}`, - linux: `https://updates.insomnia.rest/builds/check/linux?v=${appVersion}`, - win32: `https://downloads.insomnia.rest/win` -}; +const args = process.argv.slice(1); -let localStorage = null; +// Set as default protocol +app.setAsDefaultProtocolClient(`insomnia${isDevelopment() ? 'dev' : ''}`); -let mainWindow = null; -let hasPromptedForUpdates = false; +app.on('open-url', addUrlToOpen); // Enable this for CSS grid layout :) app.commandLine.appendSwitch('enable-experimental-web-platform-features'); -process.on('uncaughtException', e => { - if (IS_DEV) { - console.error(e); - } else { - ravenClient.captureError(e, {}); - } -}); - -autoUpdater.on('error', e => { - if (IS_DEV) { - console.error(e); - } else { - // Don't report autoUpdater error. They are way too noisy - } -}); - -autoUpdater.on('update-not-available', () => { - console.log('-- Update Not Available --'); -}); - -autoUpdater.on('update-available', () => { - console.log('-- Update Available --'); -}); - -autoUpdater.on('update-downloaded', (e, releaseNotes, releaseName, releaseDate, updateUrl) => { - console.log(`-- Update Downloaded ${releaseName} --`); - showUpdateNotification(); -}); - -function checkForUpdates () { - if (hasPromptedForUpdates) { - // We've already prompted for updates. Don't bug the user anymore - return; - } - - if (IS_DEV) { - console.log('-- Skipping update check in Development --'); - return; - } - - if (!IS_LINUX) { - try { - autoUpdater.setFeedURL(UPDATE_URLS[process.platform]); - autoUpdater.checkForUpdates(); - } catch (e) { - // This will fail in development - } - } -} - -function showUnresponsiveModal () { - dialog.showMessageBox({ - type: 'info', - buttons: ['Cancel', 'Reload'], - defaultId: 1, - cancelId: 0, - title: 'Unresponsive', - message: 'Insomnia has become unresponsive. Do you want to reload?' - }, id => { - if (id === 1) { - mainWindow.destroy(); - createWindow(); - } - }); -} - -function showUpdateNotification () { - if (hasPromptedForUpdates) { - return; - } - - const window = BrowserWindow.getFocusedWindow(); - if (!window || !window.webContents) { - return; - } - - window.webContents.send('update-available'); - hasPromptedForUpdates = true; -} - -function trackEvent (...args) { - const window = BrowserWindow.getFocusedWindow(); - if (!window || !window.webContents) { - return; - } - - window.webContents.send('analytics-track-event', args); -} - -ipcMain.on('check-for-updates', () => { - console.log('-- Checking for Updates --'); - checkForUpdates(); -}); - -function saveBounds () { - if (!mainWindow) { - return; - } - - const fullscreen = mainWindow.isFullScreen(); - - // Only save the size if we're not in fullscreen - if (!fullscreen) { - localStorage.setItem('bounds', mainWindow.getBounds()); - localStorage.setItem('fullscreen', false); - } else { - localStorage.setItem('fullscreen', true); - } -} - -function getBounds () { - let bounds = {}; - let fullscreen = false; - try { - bounds = localStorage.getItem('bounds', {}); - fullscreen = localStorage.getItem('fullscreen', false); - } catch (e) { - // This should never happen, but if it does...! - console.error('Failed to parse window bounds', e); - } - - return {bounds, fullscreen}; -} - -function saveZoomFactor (zoomFactor) { - localStorage.setItem('zoomFactor', zoomFactor); -} - -function getZoomFactor () { - let zoomFactor = 1; - try { - zoomFactor = localStorage.getItem('zoomFactor', 1); - } catch (e) { - // This should never happen, but if it does...! - console.error('Failed to parse zoomFactor', e); - } - - return zoomFactor; -} - // Quit when all windows are closed (except on Mac). app.on('window-all-closed', () => { - if (!IS_MAC) { + if (!isMac()) { app.quit(); } }); @@ -207,307 +54,26 @@ app.on('activate', (e, hasVisibleWindows) => { // When the app is first launched app.on('ready', () => { - initLocalStorage(); - createWindow(); - checkForUpdates(); + app.removeListener('open-url', addUrlToOpen); + const window = createWindow(); + + // Handle URLs sent via command line args + ipcMain.once('app-ready', () => { + args.length && window.send('run-command', args[0]); + }); + + // Called when second instance launched with args (Windows) + app.makeSingleInstance(args => { + args.length && window.send('run-command', args[0]); + }); + + // Handle URLs when app already open + app.addListener('open-url', (e, url) => { + window.send('run-command', url); + // Apparently a timeout is needed because Chrome steals back focus immediately + // after opening the URL. + setTimeout(() => { + window.focus(); + }, 100); + }); }); - -function initLocalStorage () { - const localStoragePath = path.join(app.getPath('userData'), 'localStorage'); - localStorage = new LocalStorage(localStoragePath); -} - -function createWindow () { - const zoomFactor = getZoomFactor(); - const {bounds, fullscreen} = getBounds(); - const {x, y, width, height} = bounds; - - // Make sure we don't place the window outside of the visible space - let maxX = 0; - let maxY = 0; - for (const d of electron.screen.getAllDisplays()) { - // Set the maximum placement location to 50 pixels short of the end - maxX = Math.max(maxX, d.bounds.x + d.bounds.width - 50); - maxY = Math.max(maxY, d.bounds.y + d.bounds.height - 50); - } - const finalX = Math.min(maxX, x); - const finalY = Math.min(maxX, y); - - mainWindow = new BrowserWindow({ - // Make sure we don't initialize the window outside the bounds - x: finalX, - y: finalY, - fullscreen: fullscreen, - fullscreenable: true, - title: appName, - width: width || 1200, - height: height || 600, - minHeight: 500, - minWidth: 500, - acceptFirstMouse: true, - icon: path.resolve(__dirname, 'static/icon.png'), - webPreferences: { - zoomFactor: zoomFactor - } - }); - - let _resizeTimeout = null; - mainWindow.on('resize', e => { - saveBounds(); - - clearTimeout(_resizeTimeout); - _resizeTimeout = setTimeout(() => { - trackEvent('Window', 'Resize'); - }, 1000); - }); - - let _moveTimeout = null; - mainWindow.on('move', e => { - saveBounds(); - - clearTimeout(_moveTimeout); - _moveTimeout = setTimeout(() => { - trackEvent('Window', 'Move'); - }, 1000); - }); - mainWindow.on('unresponsive', e => { - showUnresponsiveModal(); - trackEvent('Window', 'Unresponsive'); - }); - - // and load the app.html of the app. - // TODO: Use path.join for this - mainWindow.loadURL(`file://${__dirname}/renderer.html`); - - // 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 = null; - trackEvent('Window', 'Close'); - }); - - require('electron-context-menu')({}); - - let template = [ - { - label: 'Application', - submenu: [ - { - label: `About ${appName}`, - role: 'about', - visible: IS_MAC - }, - { - type: 'separator', - visible: IS_MAC - }, - { - label: 'Preferences', - accelerator: 'CmdOrCtrl+,', - click: function (menuItem, window, e) { - if (!window || !window.webContents) { - return; - } - - window.webContents.send('toggle-preferences'); - trackEvent('App Menu', 'Preferences'); - } - }, - { - label: 'Changelog', - click: function (menuItem, window, e) { - if (!window || !window.webContents) { - return; - } - - window.webContents.send('toggle-changelog'); - trackEvent('App Menu', 'Changelog'); - } - }, - { - type: 'separator', - visible: IS_MAC - }, - { - role: 'hide', - visible: IS_MAC - }, - { - role: 'hideothers', - visible: IS_MAC - }, - {type: 'separator'}, - { - label: 'Quit', - accelerator: 'Command+Q', - click: function () { - app.quit(); - } - } - ] - }, - { - label: 'Edit', - submenu: [{ - label: 'Undo', - accelerator: 'CmdOrCtrl+Z', - selector: 'undo:' - }, { - label: 'Redo', - accelerator: 'Shift+CmdOrCtrl+Z', - selector: 'redo:' - }, { - type: 'separator' - }, { - label: 'Cut', - accelerator: 'CmdOrCtrl+X', - selector: 'cut:' - }, { - label: 'Copy', - accelerator: 'CmdOrCtrl+C', - selector: 'copy:' - }, { - label: 'Paste', - accelerator: 'CmdOrCtrl+V', - selector: 'paste:' - }, { - label: 'Select All', - accelerator: 'CmdOrCtrl+A', - selector: 'selectAll:' - }] - }, - { - label: 'View', - submenu: [ - { - role: 'togglefullscreen' - }, - { - label: 'Actual Size', - accelerator: 'CmdOrCtrl+0', - click: () => { - const window = BrowserWindow.getFocusedWindow(); - if (!window || !window.webContents) { - return; - } - - const zoomFactor = 1; - window.webContents.setZoomFactor(zoomFactor); - saveZoomFactor(zoomFactor); - trackEvent('App Menu', 'Zoom Reset'); - } - }, - { - label: 'Zoom In', - accelerator: IS_MAC ? 'CmdOrCtrl+Plus' : 'CmdOrCtrl+=', - click: () => { - const window = BrowserWindow.getFocusedWindow(); - if (!window || !window.webContents) { - return; - } - - const zoomFactor = Math.min(1.8, getZoomFactor() + 0.05); - window.webContents.setZoomFactor(zoomFactor); - - saveZoomFactor(zoomFactor); - trackEvent('App Menu', 'Zoom In'); - } - }, - { - label: 'Zoom Out', - accelerator: 'CmdOrCtrl+-', - click: () => { - const window = BrowserWindow.getFocusedWindow(); - if (!window || !window.webContents) { - return; - } - - const zoomFactor = Math.max(0.5, getZoomFactor() - 0.05); - window.webContents.setZoomFactor(zoomFactor); - saveZoomFactor(zoomFactor); - trackEvent('App Menu', 'Zoom Out'); - } - }, - { - label: 'Toggle Sidebar', - accelerator: 'CmdOrCtrl+\\', - click: () => { - const window = BrowserWindow.getFocusedWindow(); - if (!window || !window.webContents) { - return; - } - - window.webContents.send('toggle-sidebar'); - trackEvent('App Menu', 'Toggle Sidebar'); - } - } - ] - }, { - label: 'Window', - role: 'window', - submenu: [ - {role: 'minimize'}, - ...(IS_MAC ? [{role: 'close'}] : []) - ] - }, { - label: 'Help', - role: 'help', - id: 'help', - submenu: [ - { - label: 'Contact Support', - click: () => { - trackEvent('App Menu', 'Contact'); - shell.openExternal('https://insomnia.rest/documentation/support-and-feedback/'); - } - }, - { - label: 'Insomnia Help', - accelerator: 'CmdOrCtrl+?', - click: () => { - trackEvent('App Menu', 'Help'); - shell.openExternal('https://insomnia.rest/documentation/'); - } - } - ] - } - ]; - - if (IS_DEV || process.env.INSOMNIA_FORCE_DEBUG) { - template.push({ - label: 'Developer', - position: 'before=help', - submenu: [{ - label: 'Reload', - accelerator: 'CmdOrCtrl+R', - click: function () { - mainWindow.reload(); - } - }, { - label: 'Toggle DevTools', - accelerator: 'Alt+CmdOrCtrl+I', - click: function () { - mainWindow.toggleDevTools(); - } - }, { - label: 'Resize to Default', - click: function () { - mainWindow.setBounds({x: 100, y: 100, width: 1000, height: 480}); - } - }, { - label: 'Take Screenshot', - click: function () { - mainWindow.capturePage(image => { - const buffer = image.toPNG(); - const dir = app.getPath('desktop'); - fs.writeFileSync(path.join(dir, `Screenshot-${new Date()}.png`), buffer); - }); - } - }] - }); - } - - Menu.setApplicationMenu(Menu.buildFromTemplate(template)); -} diff --git a/app/main/error-handling.js b/app/main/error-handling.js new file mode 100644 index 000000000..ec5eae3ab --- /dev/null +++ b/app/main/error-handling.js @@ -0,0 +1,28 @@ +import Raven from 'raven'; +import {getAppVersion, isDevelopment} from '../common/constants'; + +export function init () { + _initSentry(); +} + +function _initSentry () { + let ravenClient = null; + + if (!isDevelopment()) { + ravenClient = Raven.config('https://786e43ae199c4757a9ea4a48a9abd17d@sentry.io/109702', { + environment: isDevelopment() ? 'development' : 'production', + release: getAppVersion(), + logger: 'electron.main' + }); + + ravenClient.install(); + } + + process.on('uncaughtException', e => { + if (ravenClient) { + ravenClient.captureException(e, {}); + } else { + console.error(e); + } + }); +} diff --git a/app/main/updates.js b/app/main/updates.js new file mode 100644 index 000000000..ca6cd2832 --- /dev/null +++ b/app/main/updates.js @@ -0,0 +1,77 @@ +import electron from 'electron'; +import {CHECK_FOR_UPDATES_INTERVAL, getAppVersion, isDevelopment, isLinux} from '../common/constants'; + +const {autoUpdater, BrowserWindow} = electron; + +const UPDATE_URLS = { + darwin: `https://updates.insomnia.rest/builds/check/mac?v=${getAppVersion()}`, + linux: `https://updates.insomnia.rest/builds/check/linux?v=${getAppVersion()}`, + win32: `https://downloads.insomnia.rest/win` +}; + +let hasPromptedForUpdates = false; + +export function init () { + // Check for updates immediately + _checkForUpdates(); + + // Check for updates on an interval + setInterval(_checkForUpdates, CHECK_FOR_UPDATES_INTERVAL); + + autoUpdater.on('error', e => { + // NOTE: Don't report autoUpdater errors to Sentry. They are way too noisy. + if (isDevelopment()) { + console.error(e); + } + }); + + autoUpdater.on('update-not-available', () => { + console.log('-- Update Not Available --'); + }); + + autoUpdater.on('update-available', () => { + console.log('-- Update Available --'); + }); + + autoUpdater.on('update-downloaded', (e, releaseNotes, releaseName, releaseDate, updateUrl) => { + console.log(`-- Update Downloaded ${releaseName} --`); + _showUpdateNotification(); + }); +} + +function _showUpdateNotification () { + if (hasPromptedForUpdates) { + return; + } + + const window = BrowserWindow.getFocusedWindow(); + if (!window || !window.webContents) { + return; + } + + window.webContents.send('update-available'); + hasPromptedForUpdates = true; +} + +function _checkForUpdates () { + if (hasPromptedForUpdates) { + // We've already prompted for updates. Don't bug the user anymore + return; + } + + if (isDevelopment()) { + console.log('-- Skipping update check in Development --'); + return; + } else { + console.log('-- Checking for Updates --'); + } + + if (!isLinux()) { + try { + autoUpdater.setFeedURL(UPDATE_URLS[process.platform]); + autoUpdater.checkForUpdates(); + } catch (e) { + // This will fail in development + } + } +} diff --git a/app/main/window-utils.js b/app/main/window-utils.js new file mode 100644 index 000000000..b4d242007 --- /dev/null +++ b/app/main/window-utils.js @@ -0,0 +1,354 @@ +import electron from 'electron'; +import path from 'path'; +import fs from 'fs'; +import LocalStorage from '../common/local-storage'; +import {getAppName, isDevelopment, isMac} from '../common/constants'; + +const {app, Menu, BrowserWindow, shell, dialog} = electron; + +let mainWindow = null; +let localStorage = null; + +export function createWindow () { + const zoomFactor = getZoomFactor(); + const {bounds, fullscreen} = getBounds(); + const {x, y, width, height} = bounds; + + // Make sure we don't place the window outside of the visible space + let maxX = 0; + let maxY = 0; + for (const d of electron.screen.getAllDisplays()) { + // Set the maximum placement location to 50 pixels short of the end + maxX = Math.max(maxX, d.bounds.x + d.bounds.width - 50); + maxY = Math.max(maxY, d.bounds.y + d.bounds.height - 50); + } + const finalX = Math.min(maxX, x); + const finalY = Math.min(maxX, y); + + mainWindow = new BrowserWindow({ + // Make sure we don't initialize the window outside the bounds + x: finalX, + y: finalY, + fullscreen: fullscreen, + fullscreenable: true, + title: getAppName(), + width: width || 1200, + height: height || 600, + minHeight: 500, + minWidth: 500, + acceptFirstMouse: true, + icon: path.resolve(__dirname, 'static/icon.png'), + webPreferences: { + zoomFactor: zoomFactor + } + }); + + let _resizeTimeout = null; + mainWindow.on('resize', e => { + saveBounds(); + + clearTimeout(_resizeTimeout); + _resizeTimeout = setTimeout(() => { + trackEvent('Window', 'Resize'); + }, 1000); + }); + + let _moveTimeout = null; + mainWindow.on('move', e => { + saveBounds(); + + clearTimeout(_moveTimeout); + _moveTimeout = setTimeout(() => { + trackEvent('Window', 'Move'); + }, 1000); + }); + + mainWindow.on('unresponsive', e => { + showUnresponsiveModal(); + trackEvent('Window', 'Unresponsive'); + }); + + // and load the app.html of the app. + // TODO: Use path.join for this + mainWindow.loadURL(`file://${__dirname}/renderer.html`); + + // 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 = null; + trackEvent('Window', 'Close'); + }); + + const applicationMenu = { + label: 'Application', + submenu: [ + ...(isMac() ? [ + {label: `About ${getAppName()}`, role: 'about'}, + {type: 'separator'} + ] : []), + { + label: 'Preferences', + accelerator: 'CmdOrCtrl+,', + click: function (menuItem, window, e) { + if (!window || !window.webContents) { + return; + } + window.webContents.send('toggle-preferences'); + trackEvent('App Menu', 'Preferences'); + } + }, + { + label: 'Changelog', + click: function (menuItem, window, e) { + if (!window || !window.webContents) { + return; + } + window.webContents.send('toggle-changelog'); + trackEvent('App Menu', 'Changelog'); + } + }, + ...(isMac() ? [ + {type: 'separator'}, + {role: 'hide'}, + {role: 'hideothers'} + ] : []), + {type: 'separator'}, + {label: 'Quit', accelerator: 'Command+Q', click: () => app.quit()} + ] + }; + + const editMenu = { + label: 'Edit', + submenu: [ + {label: 'Undo', accelerator: 'CmdOrCtrl+Z', selector: 'undo:'}, + {label: 'Redo', accelerator: 'Shift+CmdOrCtrl+Z', selector: 'redo:'}, + {type: 'separator'}, + {label: 'Cut', accelerator: 'CmdOrCtrl+X', selector: 'cut:'}, + {label: 'Copy', accelerator: 'CmdOrCtrl+C', selector: 'copy:'}, + {label: 'Paste', accelerator: 'CmdOrCtrl+V', selector: 'paste:'}, + {label: 'Select All', accelerator: 'CmdOrCtrl+A', selector: 'selectAll:'} + ] + }; + + const viewMenu = { + label: 'View', + submenu: [ + {role: 'togglefullscreen'}, + { + label: 'Actual Size', + accelerator: 'CmdOrCtrl+0', + click: () => { + const window = BrowserWindow.getFocusedWindow(); + if (!window || !window.webContents) { + return; + } + + const zoomFactor = 1; + window.webContents.setZoomFactor(zoomFactor); + saveZoomFactor(zoomFactor); + trackEvent('App Menu', 'Zoom Reset'); + } + }, + { + label: 'Zoom In', + accelerator: isMac() ? 'CmdOrCtrl+Plus' : 'CmdOrCtrl+=', + click: () => { + const window = BrowserWindow.getFocusedWindow(); + if (!window || !window.webContents) { + return; + } + + const zoomFactor = Math.min(1.8, getZoomFactor() + 0.05); + window.webContents.setZoomFactor(zoomFactor); + + saveZoomFactor(zoomFactor); + trackEvent('App Menu', 'Zoom In'); + } + }, + { + label: 'Zoom Out', + accelerator: 'CmdOrCtrl+-', + click: () => { + const window = BrowserWindow.getFocusedWindow(); + if (!window || !window.webContents) { + return; + } + + const zoomFactor = Math.max(0.5, getZoomFactor() - 0.05); + window.webContents.setZoomFactor(zoomFactor); + saveZoomFactor(zoomFactor); + trackEvent('App Menu', 'Zoom Out'); + } + }, + { + label: 'Toggle Sidebar', + accelerator: 'CmdOrCtrl+\\', + click: () => { + const window = BrowserWindow.getFocusedWindow(); + if (!window || !window.webContents) { + return; + } + + window.webContents.send('toggle-sidebar'); + trackEvent('App Menu', 'Toggle Sidebar'); + } + } + ] + }; + + const windowMenu = { + label: 'Window', + role: 'window', + submenu: [ + {role: 'minimize'}, + ...(isMac() ? [{role: 'close'}] : []) + ] + }; + + const helpMenu = { + label: 'Help', + role: 'help', + id: 'help', + submenu: [ + { + label: 'Contact Support', + click: () => { + trackEvent('App Menu', 'Contact'); + shell.openExternal('https://insomnia.rest/documentation/support-and-feedback/'); + } + }, + { + label: 'Insomnia Help', + accelerator: 'CmdOrCtrl+?', + click: () => { + trackEvent('App Menu', 'Help'); + shell.openExternal('https://insomnia.rest/documentation/'); + } + } + ] + }; + + const developerMenu = { + label: 'Developer', + position: 'before=help', + submenu: [{ + label: 'Reload', + accelerator: 'CmdOrCtrl+R', + click: () => mainWindow.reload() + }, { + label: 'Toggle DevTools', + accelerator: 'Alt+CmdOrCtrl+I', + click: () => mainWindow.toggleDevTools() + }, { + label: 'Resize to Default', + click: () => mainWindow.setBounds({x: 100, y: 100, width: 1000, height: 480}) + }, { + label: 'Take Screenshot', + click: function () { + mainWindow.capturePage(image => { + const buffer = image.toPNG(); + const dir = app.getPath('desktop'); + fs.writeFileSync(path.join(dir, `Screenshot-${new Date()}.png`), buffer); + }); + } + }] + }; + + let template = []; + + template.push(applicationMenu); + template.push(editMenu); + template.push(viewMenu); + template.push(windowMenu); + template.push(helpMenu); + + if (isDevelopment() || process.env.INSOMNIA_FORCE_DEBUG) { + template.push(developerMenu); + } + + Menu.setApplicationMenu(Menu.buildFromTemplate(template)); + + require('electron-context-menu')({}); + + return mainWindow; +} + +function showUnresponsiveModal () { + dialog.showMessageBox({ + type: 'info', + buttons: ['Cancel', 'Reload'], + defaultId: 1, + cancelId: 0, + title: 'Unresponsive', + message: 'Insomnia has become unresponsive. Do you want to reload?' + }, id => { + if (id === 1) { + mainWindow.destroy(); + createWindow(); + } + }); +} + +function trackEvent (...args) { + const window = BrowserWindow.getFocusedWindow(); + if (!window || !window.webContents) { + return; + } + + window.webContents.send('analytics-track-event', args); +} + +function saveBounds () { + if (!mainWindow) { + return; + } + + const fullscreen = mainWindow.isFullScreen(); + + // Only save the size if we're not in fullscreen + if (!fullscreen) { + localStorage.setItem('bounds', mainWindow.getBounds()); + localStorage.setItem('fullscreen', false); + } else { + localStorage.setItem('fullscreen', true); + } +} + +function getBounds () { + let bounds = {}; + let fullscreen = false; + try { + bounds = localStorage.getItem('bounds', {}); + fullscreen = localStorage.getItem('fullscreen', false); + } catch (e) { + // This should never happen, but if it does...! + console.error('Failed to parse window bounds', e); + } + + return {bounds, fullscreen}; +} + +function saveZoomFactor (zoomFactor) { + localStorage.setItem('zoomFactor', zoomFactor); +} + +function getZoomFactor () { + let zoomFactor = 1; + try { + zoomFactor = localStorage.getItem('zoomFactor', 1); + } catch (e) { + // This should never happen, but if it does...! + console.error('Failed to parse zoomFactor', e); + } + + return zoomFactor; +} + +function initLocalStorage () { + const localStoragePath = path.join(app.getPath('userData'), 'localStorage'); + localStorage = new LocalStorage(localStoragePath); +} + +initLocalStorage(); diff --git a/app/package.json b/app/package.json index 0bd972df6..10f0e96ef 100644 --- a/app/package.json +++ b/app/package.json @@ -1,28 +1,27 @@ { "private": true, "name": "insomnia", - "version": "5.0.10", + "version": "5.0.12", "productName": "Insomnia", "longName": "Insomnia REST Client", "description": "Debug APIs like a human, not a robot", "homepage": "https://insomnia.rest", "author": "Insomnia ", - "main": "main.js", + "main": "main.min.js", "dependencies": { - "electron-context-menu": "~0.8.0", - "electron-squirrel-startup": "~1.0.0", + "electron-context-menu": "0.9.0", + "electron-squirrel-startup": "1.0.0", "hkdf": "0.0.2", "httpsnippet": "1.16.5", - "insomnia-importers": "~1.3.1", - "jsonpath": "~0.2.9", - "mkdirp": "~0.5.1", - "nedb": "~1.8.0", - "node-forge": "~0.7.0", + "insomnia-importers": "1.3.3", + "jsonpath": "0.2.11", + "mkdirp": "0.5.1", + "nedb": "1.8.0", + "node-forge": "0.7.0", "node-libcurl": "git://github.com/getinsomnia/node-libcurl.git#3fc2afea435f3548eedbef9c68d3fee642dfcddb", - "raven": "~1.1.2", - "request": "~2.71.0", + "raven": "1.1.2", "srp-js": "0.2.0", - "tough-cookie": "~2.3.1", - "vkbeautify": "~0.99.1" + "tough-cookie": "2.3.1", + "vkbeautify": "0.99.1" } } diff --git a/app/renderer.html b/app/renderer.html index 909c10437..dd592327d 100644 --- a/app/renderer.html +++ b/app/renderer.html @@ -18,8 +18,6 @@