diff --git a/app/actions.js b/app/actions.js index bbe327a..967ea86 100644 --- a/app/actions.js +++ b/app/actions.js @@ -1,7 +1,5 @@ const { app } = require('electron') -const { createWindow } = require('./windows') - const { accelerators } = require('./config') const FOCUS_URL_BAR_SCRIPT = ` @@ -12,118 +10,123 @@ const OPEN_FIND_BAR_SCRIPT = ` document.getElementById('find').show() ` -module.exports = { - OpenDevTools: { - label: 'Open Dev Tools', - accelerator: accelerators.OpenDevTools, - click: onOpenDevTools - }, - NewWindow: { - label: 'New Window', - click: onNewWindow, - accelerator: accelerators.NewWindow - }, - Forward: { - label: 'Forward', - accelerator: accelerators.Forward, - click: onGoForward - }, - Back: { - label: 'Back', - accelerator: accelerators.Back, - click: onGoBack - }, - FocusURLBar: { - label: 'Focus URL Bar', - click: onFocusURlBar, - accelerator: accelerators.FocusURLBar - }, - FindInPage: { - label: 'Find in Page', - click: onFindInPage, - accelerator: accelerators.FindInPage - }, - Reload: { - label: 'Reload', - accelerator: accelerators.Reload, - click: onReload - }, - HardReload: { - label: 'Hard Reload', - accelerator: accelerators.HardReload, - click: onHardReload - }, - LearnMore: { - label: 'Learn More', - accelerator: accelerators.LearnMore, - click: onLearMore - }, - SetAsDefault: { - label: 'Set as default browser', - accelerator: accelerators.SetAsDefault, - click: onSetAsDefault +module.exports = { createActions } + +function createActions ({ + createWindow +}) { + return { + OpenDevTools: { + label: 'Open Dev Tools', + accelerator: accelerators.OpenDevTools, + click: onOpenDevTools + }, + NewWindow: { + label: 'New Window', + click: onNewWindow, + accelerator: accelerators.NewWindow + }, + Forward: { + label: 'Forward', + accelerator: accelerators.Forward, + click: onGoForward + }, + Back: { + label: 'Back', + accelerator: accelerators.Back, + click: onGoBack + }, + FocusURLBar: { + label: 'Focus URL Bar', + click: onFocusURlBar, + accelerator: accelerators.FocusURLBar + }, + FindInPage: { + label: 'Find in Page', + click: onFindInPage, + accelerator: accelerators.FindInPage + }, + Reload: { + label: 'Reload', + accelerator: accelerators.Reload, + click: onReload + }, + HardReload: { + label: 'Hard Reload', + accelerator: accelerators.HardReload, + click: onHardReload + }, + LearnMore: { + label: 'Learn More', + accelerator: accelerators.LearnMore, + click: onLearMore + }, + SetAsDefault: { + label: 'Set as default browser', + accelerator: accelerators.SetAsDefault, + click: onSetAsDefault + } + } + async function onSetAsDefault () { + app.setAsDefaultProtocolClient('http') + app.setAsDefaultProtocolClient('https') } -} - -async function onSetAsDefault () { - app.setAsDefaultProtocolClient('http') - app.setAsDefaultProtocolClient('https') -} -async function onLearMore () { - const { shell } = require('electron') - await shell.openExternal('https://github.com/RangerMauve/agregore-browser') -} + async function onLearMore () { + const { shell } = require('electron') + await shell.openExternal('https://github.com/RangerMauve/agregore-browser') + } -function onOpenDevTools (event, focusedWindow, focusedWebContents) { - const contents = getContents(focusedWindow) - for (const webContents of contents) { - webContents.openDevTools() + function onOpenDevTools (event, focusedWindow, focusedWebContents) { + const contents = getContents(focusedWindow) + for (const webContents of contents) { + webContents.openDevTools() + } } -} -function onNewWindow (event, focusedWindow, focusedWebContents) { - createWindow() -} + function onNewWindow (event, focusedWindow, focusedWebContents) { + createWindow() + } -function onFocusURlBar (event, focusedWindow) { - focusedWindow.webContents.focus() - focusedWindow.webContents.executeJavaScript(FOCUS_URL_BAR_SCRIPT, true) -} + function onFocusURlBar (event, focusedWindow) { + focusedWindow.webContents.focus() + focusedWindow.webContents.executeJavaScript(FOCUS_URL_BAR_SCRIPT, true) + } -function onFindInPage (event, focusedWindow) { - focusedWindow.webContents.focus() - focusedWindow.webContents.executeJavaScript(OPEN_FIND_BAR_SCRIPT, true) -} + function onFindInPage (event, focusedWindow) { + focusedWindow.webContents.focus() + focusedWindow.webContents.executeJavaScript(OPEN_FIND_BAR_SCRIPT, true) + } -function onReload (event, focusedWindow, focusedWebContents) { + function onReload (event, focusedWindow, focusedWebContents) { // Reload - for (const webContents of getContents(focusedWindow)) { - webContents.reload() + for (const webContents of getContents(focusedWindow)) { + webContents.reload() + } } -} -function onHardReload (event, focusedWindow, focusedWebContents) { + function onHardReload (event, focusedWindow, focusedWebContents) { // Hard reload - for (const webContents of getContents(focusedWindow)) { - webContents.reloadIgnoringCache() + for (const webContents of getContents(focusedWindow)) { + webContents.reloadIgnoringCache() + } } -} -function onGoForward (event, focusedWindow) { - for (const webContents of getContents(focusedWindow)) { - webContents.goForward() + function onGoForward (event, focusedWindow) { + for (const webContents of getContents(focusedWindow)) { + webContents.goForward() + } } -} -function onGoBack (event, focusedWindow) { - for (const webContents of getContents(focusedWindow)) { - webContents.goBack() + function onGoBack (event, focusedWindow) { + for (const webContents of getContents(focusedWindow)) { + webContents.goBack() + } } -} -function getContents (focusedWindow) { - const views = focusedWindow.getBrowserViews() - if (!views.length) return [focusedWindow.webContents] - return views.map(({ webContents }) => webContents) + function getContents (focusedWindow) { + const views = focusedWindow.getBrowserViews() + if (!views.length) return [focusedWindow.webContents] + return views.map(({ webContents }) => webContents) + } } diff --git a/app/context-menus.js b/app/context-menus.js new file mode 100644 index 0000000..4df3b59 --- /dev/null +++ b/app/context-menus.js @@ -0,0 +1,207 @@ +const { + Menu, + MenuItem, + dialog, + app, + clipboard +} = require('electron') + +const path = require('path').posix + +module.exports = { + attachContextMenus +} + +function attachContextMenus ({ window, createWindow }) { + window.webContents.on('context-menu', headerContextMenu) + window.web.on('context-menu', pageContextMenu) + + function headerContextMenu (event, params) { + if (params.inputFieldType === 'plainText') { + showContextMenu([ + historyBufferGroup(params, false), + editGroup(params, true) + ]) + } + } + + function pageContextMenu (event, params) { + showContextMenu([ + navigationGroup(window.web, params), + historyBufferGroup(params), + linkGroup(params), + saveGroup(params), + editGroup(params), + developmentGroup(window.web, params) + ]) + } + + function showContextMenu (groups) { + const menu = new Menu() + groups + .filter(group => group != null) + .flatMap((group, index, array) => { + if (index + 1 < array.length) { + const seperator = new MenuItem({ type: 'separator' }) + group.push(seperator) + } + return group + }) + .forEach(item => menu.append(item)) + menu.popup(window.window) + } + + function historyBufferGroup ({ editFlags, isEditable }, showRedo = true) { + return !isEditable ? null : [ + new MenuItem({ + label: 'Undo', + enabled: editFlags.canUndo, + accelerator: 'CommandOrControl+Z', + role: 'undo' + }), + new MenuItem({ + label: 'Redo', + enabled: editFlags.canRedo, + visible: showRedo, + accelerator: 'CommandOrControl+Y', + role: 'redo' + }) + ] + } + + function editGroup ({ editFlags, isEditable, selectionText }) { + return !isEditable && !selectionText ? null : [ + new MenuItem({ + label: 'Cut', + enabled: editFlags.canCut, + accelerator: 'CommandOrControl+X', + role: 'cut' + }), + new MenuItem({ + label: 'Copy', + enabled: editFlags.canCopy, + accelerator: 'CommandOrControl+C', + role: 'copy' + }), + new MenuItem({ + label: 'Paste', + enabled: editFlags.canPaste, + accelerator: 'CommandOrControl+P', + role: 'paste' + }), + new MenuItem({ + label: 'Delete', + enabled: editFlags.canDelete, + role: 'delete' + }), + new MenuItem({ + type: 'separator' + }), + new MenuItem({ + label: 'Select All', + enabled: editFlags.canSelectAll, + accelerator: 'CommandOrControl+A', + role: 'selectAll' + }) + ] + } + + function navigationGroup (wc, { mediaType, isEditable }) { + return mediaType !== 'none' || isEditable ? null : [ + new MenuItem({ + label: 'Back', + enabled: wc.canGoBack(), + click: wc.goBack + }), + new MenuItem({ + label: 'Forward', + enabled: wc.canGoForward(), + click: wc.goForward + }), + new MenuItem({ + label: 'Reload', + click: wc.reload + }), + new MenuItem({ + label: 'Hard Reload', + click: wc.reloadIgnoringCache + }) + ] + } + + function developmentGroup (wc, { x, y }) { + return [ + new MenuItem({ + label: 'Inspect', + click () { + wc.inspectElement(x, y) + if (wc.isDevToolsOpened()) wc.devToolsWebContents.focus() + } + }) + ] + } + + function linkGroup ({ linkURL }) { + return !linkURL.length ? null : [ + new MenuItem({ + label: 'Open link in new window', + click: () => createWindow(linkURL) + }), + new MenuItem({ + label: 'Copy link address', + click: () => clipboard.writeText(linkURL) + }) + ] + } + + function saveGroup ({ srcURL }) { + return !srcURL.length ? null : [ + new MenuItem({ + label: 'Save As', + click: (_, browserWindow) => saveAs(srcURL, browserWindow) + }) + ] + } + + async function saveAs (link, browserWindow) { + const downloads = app.getPath('downloads') + const name = path.basename(link) + const defaultPath = path.join(downloads, name) + const { filePath } = await dialog.showSaveDialog(browserWindow, { + defaultPath + }) + + if (!filePath) return + + await window.webContents.executeJavaScript(` + (async () => { + const fs = require('fs') + const pump = require('pump') + const { Readable } = require('stream') + const link = ${JSON.stringify(link)} + const filePath = ${JSON.stringify(filePath)} + + const response = await window.fetch(link) + + pump( + Readable.from(consumeBody(response.body)), + fs.createWriteStream(filePath) + ) + + async function * consumeBody (body) { + const reader = body.getReader() + + try { + const { done, value } = await reader.read() + + if (done) return + + yield value + } finally { + reader.releaseLock() + } + } + })() + `) + } +} diff --git a/app/extensions/index.js b/app/extensions/index.js index e2f1af6..71c1d87 100644 --- a/app/extensions/index.js +++ b/app/extensions/index.js @@ -1,11 +1,13 @@ const path = require('path') const fs = require('fs-extra') -const { ExtensibleSession } = require('electron-extensions/main') -const { createWindow } = require('../windows') +const { ExtensibleSession } = require('../../node_modules/electron-extensions/main') const { webContents } = require('electron') +const DEFAULT_PARTITION = 'persist:web-content' + let extensions = null +let createWindow = null module.exports = { init, @@ -43,9 +45,11 @@ function getExtension (name) { return extensions.extensions[name] } -async function init () { +async function init ({ partition = DEFAULT_PARTITION, createWindow: _createWindow } = {}) { + createWindow = _createWindow + extensions = new ExtensibleSession({ - partition: 'persist:web-content', + partition, blacklist: ['agregore-browser://*/*', 'file://*/*'] }) diff --git a/app/main.js b/app/main.js index 3a9de18..de26322 100644 --- a/app/main.js +++ b/app/main.js @@ -1,8 +1,10 @@ const { app, BrowserWindow, session } = require('electron') const protocols = require('./protocols') +const { createActions } = require('./actions') const { registerMenu } = require('./menu') -const { createWindow, saveOpen, loadFromHistory } = require('./windows') +const { attachContextMenus } = require('./context-menus') +const { WindowManager } = require('./window') const Extensions = require('./extensions') const history = require('./history') @@ -15,12 +17,22 @@ if (!gotTheLock) { } else { app.on('second-instance', (event, argv) => { const urls = argv.filter((arg) => arg.includes('://')) - urls.map((url) => createWindow(url)) + urls.map((url) => windowManager.open({ url })) }) } +const windowManager = new WindowManager({ + onSearch: (...args) => history.search(...args) +}) + protocols.registerPriviledges() +const actions = createActions({ + createWindow +}) + +windowManager.on('open', (window) => attachContextMenus({ window, createWindow })) + // This method will be called when Electron has finished // initialization and is ready to create browser windows. // Some APIs can only be used after this event occurs. @@ -39,34 +51,38 @@ app.on('activate', () => { // On macOS it's common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. if (BrowserWindow.getAllWindows().length === 0) { - createWindow() + windowManager.open() } }) app.on('before-quit', () => { - saveOpen() + windowManager.saveOpened() }) async function onready () { const webSession = session.fromPartition(WEB_PARTITION) await protocols.setupProtocols(webSession) - await registerMenu() + await registerMenu(actions) - await Extensions.init(webSession) + await Extensions.init({ partition: WEB_PARTITION, createWindow }) const historyExtension = await Extensions.getExtension('agregore-history') history.setExtension(historyExtension) const rootURL = new URL(process.cwd(), 'file://') + const opened = await windowManager.openSaved() + const urls = process.argv .slice(2) .filter((arg) => arg.includes('/')) .map((arg) => arg.includes('://') ? arg : (new URL(arg, rootURL)).href) - if (urls.length) urls.map(createWindow) - else { - const opened = await loadFromHistory() - if (!opened.length) createWindow() - } + if (urls.length) { + urls.map((url) => { + windowManager.open({ url }) + }) + } else if (!opened.length) windowManager.open() } + +function createWindow (url, options = {}) { windowManager.open({ url, ...options }) } diff --git a/app/menu.js b/app/menu.js index 3ee2652..6127e45 100644 --- a/app/menu.js +++ b/app/menu.js @@ -2,24 +2,24 @@ const { Menu, app } = require('electron') const isMac = process.platform === 'darwin' -const { - OpenDevTools, - NewWindow, - Forward, - Back, - FocusURLBar, - FindInPage, - Reload, - HardReload, - LearnMore, - SetAsDefault -} = require('./actions') - module.exports = { registerMenu } -function registerMenu () { +function registerMenu (actions) { + const { + OpenDevTools, + NewWindow, + Forward, + Back, + FocusURLBar, + FindInPage, + Reload, + HardReload, + LearnMore, + SetAsDefault + } = actions + const template = [ // { role: 'appMenu' } ...(isMac ? [{ diff --git a/app/protocols/index.js b/app/protocols/index.js index 2ade4ab..21eee56 100644 --- a/app/protocols/index.js +++ b/app/protocols/index.js @@ -6,7 +6,8 @@ const P2P_PRIVILEDGES = { allowServiceWorkers: true, supportFetchAPI: true, bypassCSP: false, - corsEnabled: true + corsEnabled: true, + stream: true } const BROWSER_PRIVILEDGES = { diff --git a/app/ui/browser-actions.js b/app/ui/browser-actions.js index 9b10af1..8bd6920 100644 --- a/app/ui/browser-actions.js +++ b/app/ui/browser-actions.js @@ -2,7 +2,8 @@ class BrowserActions extends HTMLElement { async connectedCallback () { - const { remote } = require('electron') + /* + const remote = require('@electron/remote') const Extensions = remote.require('./extensions') const actions = await Extensions.listActions() @@ -18,7 +19,7 @@ class BrowserActions extends HTMLElement { ` this.appendChild(button) - } + } */ } get tabId () { diff --git a/app/ui/context-menus.js b/app/ui/context-menus.js deleted file mode 100644 index 628433f..0000000 --- a/app/ui/context-menus.js +++ /dev/null @@ -1,190 +0,0 @@ -const electron = require('electron') -const { remote, clipboard } = electron -const { Menu, MenuItem } = remote || electron - -exports.headerContextMenu = function (event, params) { - if (params.inputFieldType === 'plainText') { - showContextMenu(this, [ - historyBufferGroup(params, false), - editGroup(params, true) - ]) - } -} - -exports.pageContextMenu = function (event, params) { - showContextMenu(this, [ - navigationGroup(this.webContents, params), - historyBufferGroup(params), - linkGroup(params), - saveGroup(params), - editGroup(params), - developmentGroup(this.webContents, params) - ]) -} - -function showContextMenu (browserWindow, groups) { - const menu = new Menu() - groups - .filter(group => group != null) - .flatMap((group, index, array) => { - if (index + 1 < array.length) { - const seperator = new MenuItem({ type: 'separator' }) - group.push(seperator) - } - return group - }) - .forEach(item => menu.append(item)) - menu.popup(browserWindow) -} - -function historyBufferGroup ({ editFlags, isEditable }, showRedo = true) { - return !isEditable ? null : [ - new MenuItem({ - label: 'Undo', - enabled: editFlags.canUndo, - accelerator: 'CommandOrControl+Z', - role: 'undo' - }), - new MenuItem({ - label: 'Redo', - enabled: editFlags.canRedo, - visible: showRedo, - accelerator: 'CommandOrControl+Y', - role: 'redo' - }) - ] -} - -function editGroup ({ editFlags, isEditable, selectionText }) { - return !isEditable && !selectionText ? null : [ - new MenuItem({ - label: 'Cut', - enabled: editFlags.canCut, - accelerator: 'CommandOrControl+X', - role: 'cut' - }), - new MenuItem({ - label: 'Copy', - enabled: editFlags.canCopy, - accelerator: 'CommandOrControl+C', - role: 'copy' - }), - new MenuItem({ - label: 'Paste', - enabled: editFlags.canPaste, - accelerator: 'CommandOrControl+P', - role: 'paste' - }), - new MenuItem({ - label: 'Delete', - enabled: editFlags.canDelete, - role: 'delete' - }), - new MenuItem({ - type: 'separator' - }), - new MenuItem({ - label: 'Select All', - enabled: editFlags.canSelectAll, - accelerator: 'CommandOrControl+A', - role: 'selectAll' - }) - ] -} - -function navigationGroup (wc, { mediaType, isEditable }) { - return mediaType !== 'none' || isEditable ? null : [ - new MenuItem({ - label: 'Back', - enabled: wc.canGoBack(), - click: wc.goBack - }), - new MenuItem({ - label: 'Forward', - enabled: wc.canGoForward(), - click: wc.goForward - }), - new MenuItem({ - label: 'Reload', - click: wc.reload - }), - new MenuItem({ - label: 'Hard Reload', - click: wc.reloadIgnoringCache - }) - ] -} - -function developmentGroup (wc, { x, y }) { - return [ - new MenuItem({ - label: 'Inspect', - click () { - wc.inspectElement(x, y) - if (wc.isDevToolsOpened()) wc.devToolsWebContents.focus() - } - }) - ] -} - -function linkGroup ({ linkURL }) { - return !linkURL.length ? null : [ - new MenuItem({ - label: 'Open link in new window', - click: () => remote.require('./windows').createWindow(linkURL) - }), - new MenuItem({ - label: 'Copy link address', - click: () => clipboard.writeText(linkURL) - }) - ] -} - -function saveGroup ({ srcURL }) { - return !srcURL.length ? null : [ - new MenuItem({ - label: 'Save As', - click: (_, browserWindow) => saveAs(srcURL, browserWindow) - }) - ] -} - -async function saveAs (link, browserWindow) { - const fs = remote.require('fs') - const path = remote.require('path').posix - const pump = require('pump') - const { dialog, app } = remote - const { Readable } = require('stream') - const downloads = app.getPath('downloads') - - const name = path.basename(link) - - const defaultPath = path.join(downloads, name) - - const response = await window.fetch(link) - - const { filePath } = await dialog.showSaveDialog(browserWindow, { - defaultPath - }) - - if (!filePath) return - - pump( - Readable.from(consumeBody(response.body)), - fs.createWriteStream(filePath) - ) -} - -async function * consumeBody (body) { - const reader = body.getReader() - - try { - const { done, value } = await reader.read() - - if (done) return - - yield value - } finally { - reader.releaseLock() - } -} diff --git a/app/ui/current-window.js b/app/ui/current-window.js new file mode 100644 index 0000000..ac037be --- /dev/null +++ b/app/ui/current-window.js @@ -0,0 +1,67 @@ +window.getCurrentWindow = function getCurrentWindow () { + const { ipcRenderer } = require('electron') + const EventEmitter = require('events') + + const EVENTS = [ + 'navigating', + 'history-buttons-change', + 'page-title-updated', + 'close' + ] + + class CurrentWindow extends EventEmitter { + constructor () { + super() + + for (const name of EVENTS) { + ipcRenderer.on(`agregore-window-${name}`, (event, ...args) => this.emit(name, ...args)) + } + } + + async goBack () { + return this.invoke('goBack') + } + + async goForward () { + return this.invoke('goForward') + } + + async reload () { + return this.invoke('reload') + } + + async focus () { + return this.invoke('focus') + } + + async loadURL (url) { + return this.invoke('loadURL', url) + } + + async getURL () { + return this.invoke('getURL') + } + + async findInPage (value, opts) { + return this.invoke('findInPage', value, opts) + } + + async stopFindInPage () { + return this.invoke('stopFindInPage') + } + + async setBounds (rect) { + return this.invoke('setBounds', rect) + } + + async searchHistory (query, limit = 8) { + return this.invoke('searchHistory', query, limit) + } + + async invoke (name, ...args) { + return ipcRenderer.invoke(`agregore-window-${name}`, ...args) + } + } + + return new CurrentWindow() +} diff --git a/app/ui/electron-browser-view.js b/app/ui/electron-browser-view.js deleted file mode 100644 index ff4a6ac..0000000 --- a/app/ui/electron-browser-view.js +++ /dev/null @@ -1,266 +0,0 @@ -/* global HTMLElement, ResizeObserver, CustomEvent, customElements */ - -class BrowserViewElement extends HTMLElement { - static EVENTS () { - return [ - 'did-finish-load', - 'did-fail-load', - 'did-fail-provisional-load', - 'did-frame-finish-load', - 'did-start-loading', - 'did-stop-loading', - 'dom-ready', - 'page-title-updated', - 'page-favicon-updated', - 'new-window', - 'will-navigate', - 'did-start-navigation', - 'will-redirect', - 'did-redirect-navigation', - 'did-navigate', - 'did-frame-navigate', - 'did-navigate-in-page', - 'will-prevent-unload', - 'crashed', - 'unresponsive', - 'responsive', - 'plugin-crashed', - 'destroyed', - 'before-input-event', - 'enter-html-full-screen', - 'leave-html-full-screen', - 'zoom-changed', - 'devtools-opened', - 'devtools-closed', - 'devtools-focused', - 'certificate-error', - 'select-client-certificate', - 'login', - 'found-in-page', - 'media-started-playing', - 'media-paused', - 'did-change-theme-color', - 'update-target-url', - 'cursor-changed', - 'context-menu', - 'select-bluetooth-device', - 'devtools-reload-page', - 'will-attach-webview', - 'did-attach-webview', - 'console-message', - 'preload-error' - ] - } - - static METHODS () { - return [ - 'loadURL', - 'loadFile', - 'downloadURL', - 'getURL', - 'getTitle', - 'isDestroyed', - 'focus', - 'isFocused', - 'isLoading', - 'isLoadingMainFrame', - 'isWaitingForResponse', - 'stop', - 'reload', - 'reloadIgnoringCache', - 'canGoBack', - 'canGoForward', - 'canGoToOffset', - 'clearHistory', - 'goBack', - 'goForward', - 'goToIndex', - 'goToOffset', - 'isCrashed', - 'setUserAgent', - 'getUserAgent', - 'insertCSS', - 'removeInsertedCSS', - 'executeJavaScript', - 'executeJavaScriptInIsolatedWorld', - 'setIgnoreMenuShortcuts', - 'setAudioMuted', - 'isAudioMuted', - 'isCurrentlyAudible', - 'setZoomFactor', - 'getZoomFactor', - 'setZoomLevel', - 'getZoomLevel', - 'setVisualZoomLevelLimits', - 'undo', - 'redo', - 'cut', - 'copy', - 'copyImageAt', - 'paste', - 'pasteAndMatchStyle', - 'delete', - 'selectAll', - 'unselect', - 'replace', - 'replaceMisspelling', - 'insertText', - 'findInPage', - 'stopFindInPage', - 'capturePage', - 'isBeingCaptured', - 'incrementCapturerCount', - 'decrementCapturerCount', - 'getPrinters', - 'print', - 'printToPDF', - 'addWorkSpace', - 'removeWorkSpace', - 'setDevToolsWebContents', - 'openDevTools', - 'closeDevTools', - 'isDevToolsOpened', - 'isDevToolsFocused', - 'toggleDevTools', - 'inspectElement', - 'inspectSharedWorker', - 'inspectSharedWorkerById', - 'getAllSharedWorkers', - 'inspectServiceWorker', - 'send', - 'sendToFrame', - 'enableDeviceEmulation', - 'disableDeviceEmulation', - 'sendInputEvent', - 'beginFrameSubscription', - 'endFrameSubscription', - 'startDrag', - 'savePage', - 'showDefinitionForSelection', - 'isOffscreen', - 'startPainting', - 'stopPainting', - 'isPainting', - 'setFrameRate', - 'getFrameRate', - 'invalidate', - 'getWebRTCIPHandlingPolicy', - 'setWebRTCIPHandlingPolicy', - 'getOSProcessId', - 'getProcessId', - 'takeHeapSnapshot', - 'setBackgroundThrottling', - 'getType' - ] - } - - constructor () { - super() - - this.view = null - - this.observer = new ResizeObserver(() => this.resizeView()) - - for (const name of BrowserViewElement.METHODS()) { - this[name] = (...args) => this.view.webContents[name](...args) - } - - this.addEventListener('focus', () => { - if (this.view) this.view.webContents.focus() - }) - - window.addEventListener('beforeunload', () => { - if (this.view) this.view.destroy() - }) - } - - connectedCallback () { - this.observer.observe(this) - - const remote = require('electron').remote - const currentWindow = remote.getCurrentWindow() - - const { BrowserView } = remote - this.view = new BrowserView({ - webPreferences: { - sandbox: true, - safeDialogs: true, - navigateOnDragDrop: true, - enableRemoteModule: false, - partition: this.getAttribute('partition') - } - }) - - currentWindow.setBrowserView(this.view) - this.resizeView() - - for (const event of BrowserViewElement.EVENTS()) { - this.view.webContents.on(event, (...detail) => { - this.dispatchEvent(new CustomEvent(event, { detail })) - }) - } - - const src = this.getAttribute('src') - if (src) this.loadURL(src) - } - - disconnectedCallback () { - this.observer.unobserve(this) - this.view.destroy() - this.view = null - } - - static get observedAttributes () { - return ['src'] - } - - attributeChangedCallback (name, oldValue, newValue) { - if (!this.view) return - this.loadURL(newValue) - } - - resizeView () { - if (!this.view) return - - const { x, y, width, height } = this.getBoundingClientRect() - - const rect = { - x: Math.trunc(x), - y: Math.trunc(y), - width: Math.trunc(width), - height: Math.trunc(height) - } - - this.view.setBounds(rect) - } - - get src () { return this.getAttribute('src') } - set src (url) { this.setAttribute('src', url) } - - get audioMuted () { return this.view.webContents.audioMuted } - set audioMuted (audioMuted) { this.view.webContents.audioMuted = audioMuted } - - get userAgent () { return this.view.webContents.userAgent } - set userAgent (userAgent) { this.view.webContents.userAgent = userAgent } - - get zoomLevel () { return this.view.webContents.zoomLevel } - set zoomLevel (zoomLevel) { this.view.webContents.zoomLevel = zoomLevel } - - get zoomFactor () { return this.view.webContents.zoomFactor } - set zoomFactor (zoomFactor) { this.view.webContents.zoomFactor = zoomFactor } - - get frameRate () { return this.view.webContents.frameRate } - set frameRate (frameRate) { this.view.webContents.frameRate = frameRate } - - get id () { return this.view.webContents.id } - - get session () { return this.view.webContents.session } - - get hostWebContents () { return this.view.webContents.hostWebContents } - - get devToolsWebContents () { return this.view.webContents.devToolsWebContents } - - get debugger () { return this.view.webContents.debugger } -} - -customElements.define('browser-view', BrowserViewElement) diff --git a/app/ui/index.html b/app/ui/index.html index a4ee9fd..cacb1dd 100644 --- a/app/ui/index.html +++ b/app/ui/index.html @@ -11,18 +11,16 @@ @import url('style.css'); - - + +
-
- - + diff --git a/app/ui/omni-box.js b/app/ui/omni-box.js index d9338c3..4046b4f 100644 --- a/app/ui/omni-box.js +++ b/app/ui/omni-box.js @@ -4,10 +4,6 @@ class OmniBox extends HTMLElement { constructor () { super() this.firstLoad = true - - const { remote } = require('electron') - - this.history = remote.require('./history') this.lastSearch = 0 } @@ -32,6 +28,7 @@ class OmniBox extends HTMLElement { this.input.addEventListener('focus', () => { this.input.select() }) + this.form.addEventListener('submit', (e) => { e.preventDefault(true) @@ -138,10 +135,12 @@ class OmniBox extends HTMLElement { return } - const results = await this.history.search(query) + this.dispatchEvent(new CustomEvent('search', { detail: { query, searchID } })) + } + async setSearchResults (results, query, searchID) { if (this.lastSearch !== searchID) { - return console.debug('Urlbar changed since query finished', this.input.value, query) + return console.debug('Urlbar changed since query finished', this.lastSearch, searchID, query) } const finalItems = [] diff --git a/app/ui/script.js b/app/ui/script.js index 3e75390..2222df6 100644 --- a/app/ui/script.js +++ b/app/ui/script.js @@ -1,16 +1,10 @@ -const { pageContextMenu } = require('./context-menus') - const DEFAULT_PAGE = 'agregore://welcome' const webview = $('#view') const search = $('#search') const find = $('#find') -webview.addEventListener('dom-ready', () => { - if (process.env.MODE === 'debug') { - webview.openDevTools() - } -}) +const currentWindow = window.getCurrentWindow() const pageTitle = $('title') @@ -22,14 +16,18 @@ const rawFrame = searchParams.get('rawFrame') === 'true' if (rawFrame) $('#top').classList.toggle('hidden', true) -webview.src = toNavigate +window.addEventListener('load', () => { + console.log('toNavigate', toNavigate) + currentWindow.loadURL(toNavigate) + webview.emitResize() +}) search.addEventListener('back', () => { - webview.goBack() + currentWindow.goBack() }) search.addEventListener('forward', () => { - webview.goForward() + currentWindow.goForward() }) search.addEventListener('navigate', ({ detail }) => { @@ -38,66 +36,69 @@ search.addEventListener('navigate', ({ detail }) => { navigateTo(url) }) -search.addEventListener('unfocus', () => { - webview.focus() - search.src = webview.getURL() +search.addEventListener('unfocus', async () => { + await currentWindow.focus() + search.src = await webview.getURL() }) -webview.addEventListener('did-start-navigation', ({ detail }) => { - const url = detail[1] - const isMainFrame = detail[3] - if (!isMainFrame) return - search.src = url +search.addEventListener('search', async ({ detail }) => { + const { query, searchID } = detail + + const results = await currentWindow.searchHistory(query, searchID) + + search.setSearchResults(results, query, searchID) }) -webview.addEventListener('did-navigate', updateButtons) +webview.addEventListener('focus', () => { + currentWindow.focus() +}) -webview.view.webContents.on('context-menu', pageContextMenu.bind(webview.view)) +webview.addEventListener('resize', ({ detail: rect }) => { + currentWindow.setBounds(rect) +}) -webview.addEventListener('page-title-updated', ({ detail }) => { - const title = detail[1] - pageTitle.innerText = title + ' - Agregore Browser' +currentWindow.on('navigating', (url) => { + search.src = url }) -webview.addEventListener('new-window', ({ detail }) => { - const options = detail[4] +currentWindow.on('history-buttons-change', updateButtons) - if (options && options.webContents) { - options.webContents.on('context-menu', pageContextMenu.bind(webview.view)) - } +currentWindow.on('page-title-updated', (title) => { + pageTitle.innerText = title + ' - Agregore Browser' }) find.addEventListener('next', ({ detail }) => { const { value, findNext } = detail - webview.findInPage(value, { findNext }) + currentWindow.findInPage(value, { findNext }) }) find.addEventListener('previous', ({ detail }) => { const { value, findNext } = detail - webview.findInPage(value, { forward: false, findNext }) + currentWindow.findInPage(value, { forward: false, findNext }) }) find.addEventListener('hide', () => { - webview.stopFindInPage('clearSelection') + currentWindow.stopFindInPage('clearSelection') }) -function updateButtons () { - search.setAttribute('back', webview.canGoBack() ? 'visible' : 'hidden') - search.setAttribute('forward', webview.canGoForward() ? 'visible' : 'hidden') +function updateButtons ({ canGoBack, canGoForward }) { + search.setAttribute('back', canGoBack ? 'visible' : 'hidden') + search.setAttribute('forward', canGoForward ? 'visible' : 'hidden') } function $ (query) { return document.querySelector(query) } -function navigateTo (url) { - if (webview.getURL() === url) { +async function navigateTo (url) { + const currentURL = await currentWindow.getURL() + if (currentURL === url) { console.log('Reloading') - webview.reload() + currentWindow.reload() } else { - webview.src = url - webview.focus() + currentWindow.loadURL(url) + currentWindow.focus() } } diff --git a/app/ui/style.css b/app/ui/style.css index 12ab1cb..abaefb2 100644 --- a/app/ui/style.css +++ b/app/ui/style.css @@ -41,7 +41,7 @@ omni-box { .omni-box-header { display: flex; flex-direction: row; - border: 2px solid var(--ag-color-purple); + border-bottom: 2px solid var(--ag-color-purple); background: var(--ag-color-black); color: var(--ag-color-white); } @@ -101,6 +101,7 @@ omni-box { find-menu { border: 2px solid var(--ag-color-purple); + border-top: none; display: flex; flex-direction: row; background: var(--ag-color-black); diff --git a/app/ui/tracked-box.js b/app/ui/tracked-box.js new file mode 100644 index 0000000..72f20b3 --- /dev/null +++ b/app/ui/tracked-box.js @@ -0,0 +1,27 @@ +/* global HTMLElement, ResizeObserver, CustomEvent, customElements */ + +class TrackedBox extends HTMLElement { + constructor () { + super() + + this.observer = new ResizeObserver(() => this.emitResize()) + } + + connectedCallback () { + this.observer.observe(this) + + this.emitResize() + } + + disconnectedCallback () { + this.observer.unobserve(this) + } + + emitResize () { + const { x, y, width, height } = this.getBoundingClientRect() + + this.dispatchEvent(new CustomEvent('resize', { detail: { x, y, width, height } })) + } +} + +customElements.define('tracked-box', TrackedBox) diff --git a/app/window/index.js b/app/window/index.js new file mode 100644 index 0000000..259de3f --- /dev/null +++ b/app/window/index.js @@ -0,0 +1,277 @@ +const { + BrowserWindow, + BrowserView, + ipcMain, + app +} = require('electron') +const path = require('path') +const EventEmitter = require('events') +const fs = require('fs-extra') + +const MAIN_PAGE = path.resolve(__dirname, '../ui/index.html') +const LOGO_FILE = path.join(__dirname, '../../build/icon.png') +const PERSIST_FILE = path.join(app.getPath('userData'), 'lastOpened.json') + +const DEFAULT_PAGE = 'agregore://welcome' + +const IS_DEBUG = process.env.NODE_ENV === 'debug' + +const WINDOW_METHODS = [ + 'goBack', + 'goForward', + 'reload', + 'focus', + 'loadURL', + 'getURL', + 'findInPage', + 'stopFindInPage', + 'setBounds', + 'searchHistory' +] + +async function DEFAULT_SEARCH () { + return [] +} + +class WindowManager extends EventEmitter { + constructor ({ + onSearch = DEFAULT_SEARCH, + persistTo = PERSIST_FILE + } = {}) { + super() + this.windows = new Set() + this.onSearch = onSearch + this.persistTo = persistTo + + for (const method of WINDOW_METHODS) { + this.relayMethod(method) + } + } + + open (opts = {}) { + const { onSearch } = this + const window = new Window({ ...opts, onSearch }) + + console.log('created window', window.id) + this.windows.add(window) + window.once('close', () => { + this.windows.delete(window) + this.emit('close', window) + }) + this.emit('open', window) + + window.load() + + return window + } + + relayMethod (name) { + ipcMain.handle(`agregore-window-${name}`, ({ sender }, ...args) => { + const { id } = sender + console.log('<-', id, name, '(', args, ')') + const window = this.get(id) + if (!window) return console.warn(`Got method ${name} from invalid frame ${id}`) + + return window[name](...args) + }) + } + + get (id) { + for (const window of this.windows) { + if (window.id === id) return window + } + return null + } + + get all () { + return [...this.windows.values()] + } + + async saveOpened () { + let urls = await Promise.all(this.all.map(async (window) => { + const url = window.web.getURL() + const position = window.window.getPosition() + const size = window.window.getSize() + + return { url, position, size } + })) + + if (urls.length === 1) urls = [] + + fs.outputJsonSync(this.persistTo, urls) + } + + async openSaved () { + const saved = await this.loadSaved() + + return Promise.all(saved.map((info) => { + console.log('About to open', info) + const options = {} + + if (typeof info === 'string') { + options.url = info + } else { + const { url, position, size } = info + + options.url = url + + if (position) { + const [x, y] = position + options.x = x + options.y = y + } + + if (size) { + const [width, height] = size + options.width = width + options.height = height + } + } + + return this.open(options) + })) + } + + async loadSaved () { + try { + const infos = await fs.readJson(this.persistTo) + return infos + } catch (e) { + console.error('Error loading saved windows', e.stack) + return [] + } + } +} + +class Window extends EventEmitter { + constructor ({ + url = DEFAULT_PAGE, + rawFrame = false, + onSearch, + ...opts + } = {}) { + super() + + this.onSearch = onSearch + + this.window = new BrowserWindow({ + autoHideMenuBar: true, + webPreferences: { + // partition: 'persist:web-content', + nodeIntegration: true, + webviewTag: false, + contextIsolation: false + }, + show: false, + icon: LOGO_FILE, + ...opts + }) + + this.view = new BrowserView({ + webPreferences: { + partition: 'persist:web-content', + nodeIntegration: false, + sandbox: true, + webviewTag: false, + contextIsolation: true + } + }) + this.window.setBrowserView(this.view) + + this.web.on('did-navigate', (event, url, isMainFrame) => { + console.log('Navigating', url, isMainFrame) + if (!isMainFrame) return + this.send('navigating', url) + }) + this.web.on('did-navigate', () => { + const canGoBack = this.web.canGoBack() + const canGoForward = this.web.canGoForward() + + this.send('history-buttons-change', { canGoBack, canGoForward }) + }) + this.web.on('page-title-updated', (event, title) => { + this.send('page-title-updated', title) + }) + this.window.once('ready-to-show', () => this.window.show()) + this.window.on('close', () => { + if (this.view.destroy) this.view.destroy() + this.emit('close') + }) + + const toLoad = new URL(MAIN_PAGE, 'file:') + + if (url) toLoad.searchParams.set('url', url) + if (rawFrame) toLoad.searchParams.set('rawFrame', 'true') + + this.toLoad = toLoad.href + + if (IS_DEBUG) { + // this.web.openDevTools() + this.window.webContents.openDevTools() + } + } + + load () { + return this.window.loadURL(this.toLoad) + } + + async goBack () { + return this.web.goBack() + } + + async goForward () { + return this.web.goForward() + } + + async reload () { + return this.web.reload() + } + + async focus () { + return this.web.focus() + } + + async loadURL (url) { + return this.web.loadURL(url) + } + + async getURL () { + return this.web.getURL() + } + + async findInPage (value, opts) { + return this.web.findInPage(value, opts) + } + + async stopFindInPage () { + return this.web.stopFindInPage('clearSelection') + } + + async searchHistory (...args) { + return this.onSearch(...args) + } + + async setBounds (rect) { + return this.view.setBounds(rect) + } + + send (name, ...args) { + this.emit(name, ...args) + console.log('->', this.id, name, '(', args, ')') + this.window.webContents.send(`agregore-window-${name}`, ...args) + } + + get web () { + return this.view.webContents + } + + get webContents () { + return this.window.webContents + } + + get id () { + return this.window.webContents.id + } +} + +module.exports = { WindowManager, Window } diff --git a/app/windows.js b/app/windows.js deleted file mode 100644 index 8b5ccd4..0000000 --- a/app/windows.js +++ /dev/null @@ -1,96 +0,0 @@ -const { app } = require('electron') -const { BrowserWindow } = require('electron') -const { resolve } = require('path') -const { headerContextMenu } = require('./ui/context-menus') -const fs = require('fs-extra') -const path = require('path') - -const MAIN_PAGE = resolve(__dirname, './ui/index.html') -const PERSIST_FILE = path.join(app.getPath('userData'), 'lastOpened.json') -const LOGO_FILE = path.join(__dirname, '../build/icon.png') - -module.exports = { - createWindow, - loadFromHistory, - saveOpen -} - -const openWindows = new Set() - -function createWindow (url, options = {}) { - // Create the browser window. - const win = new BrowserWindow({ - autoHideMenuBar: true, - webPreferences: { - session: 'persist:web-content', - nodeIntegration: true, - webviewTag: false - }, - icon: LOGO_FILE, - show: false, - ...options - }) - - win.once('ready-to-show', () => win.show()) - - win.webContents.on('context-menu', headerContextMenu.bind(win)) - - const { rawFrame } = options - - const toLoad = new URL(MAIN_PAGE, 'file:') - - if (url) toLoad.searchParams.set('url', url) - if (rawFrame) toLoad.searchParams.set('rawFrame', 'true') - - // and load the index.html of the app. - win.loadURL(toLoad.href) - - // Open the DevTools. - if (process.env.MODE === 'debug') { - win.webContents.openDevTools() - } - - openWindows.add(win) - - win.once('closed', () => { - openWindows.delete(win) - }) -} - -function saveOpen (file = PERSIST_FILE) { - const urls = getToSave() - - fs.outputJsonSync(file, urls) -} - -function getToSave () { - const currentlyOpen = [...openWindows] - - const urls = currentlyOpen.map((win) => { - const view = win.getBrowserView() || win - return view.webContents.getURL() - }) - - if (urls.length === 1) return [] - return urls -} - -async function loadFromHistory (file = PERSIST_FILE) { - const urls = await getHistory(file) - - for (const url of urls) { - createWindow(url) - } - - return urls -} - -async function getHistory (file = PERSIST_FILE) { - try { - const urls = await fs.readJson(file) - return urls - } catch (e) { - console.error(e.stack) - return [] - } -} diff --git a/package.json b/package.json index eed509e..0915c03 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "test": "npm run lint", "start": "electron .", - "debug": "env MODE=debug electron .", + "debug": "env NODE_ENV=debug electron --trace-uncaught .", "build": "electron-builder build --publish never", "build-all": "electron-builder build -mwl", "lint": "standard --fix", @@ -89,14 +89,14 @@ }, "homepage": "https://github.com/RangerMauve/agregore-browser#readme", "devDependencies": { - "electron": "^9.1.0", + "electron": "11.0.0-beta.1", "electron-builder": "^21", "electron-rebuild": "^1.11.0", "standard": "^14.3.4" }, "dependencies": { "@geut/hyperdrive-promise": "^3.0.1", - "dat-fetch": "^5.0.0", + "dat-fetch": "^5.0.1", "dat-sdk": "^2.1.0", "dat-sdk-old": "^1.0.0", "electron-extensions": "^6.0.4", @@ -105,8 +105,8 @@ "gemini-to-html": "^1.0.0", "ipfs": "^0.46.0", "mime": "^2.4.6", - "whatwg-mimetype": "https://github.com/jsdom/whatwg-mimetype#v2.3.0", "rc": "^1.2.8", - "scoped-fs": "^1.4.1" + "scoped-fs": "^1.4.1", + "whatwg-mimetype": "https://github.com/jsdom/whatwg-mimetype#v2.3.0" } } diff --git a/yarn.lock b/yarn.lock index d45c68a..317b9eb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2320,10 +2320,10 @@ dat-encoding@^5.0.1: dependencies: safe-buffer "^5.0.1" -dat-fetch@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/dat-fetch/-/dat-fetch-5.0.0.tgz#81a890d835d23ebcb9bc1284ea745db3f68b4901" - integrity sha512-ir0hVJwRnebsolTWlKDGFJytcgxcrMPXlqdeWNLFNe9sI7Lv7JQOcGrczCcqHpHRjIs2wF8ZNk6dwt/lWTDV2A== +dat-fetch@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/dat-fetch/-/dat-fetch-5.0.1.tgz#b9fe6f47bd35002219c4de2b8a4939fa2370cf5e" + integrity sha512-T5+LY9nE/if3dCBEHVebomw+QS4dlD7wdUdpIi21ka0W24vDnbfWUOwIoKtTSFRBBDNelDBWZlxAKw65VpA+bA== dependencies: concat-stream "^2.0.0" end-of-stream-promise "^1.0.0" @@ -3046,10 +3046,10 @@ electron-rebuild@^1.11.0: spawn-rx "^3.0.0" yargs "^14.2.0" -electron@^9.1.0: - version "9.2.0" - resolved "https://registry.yarnpkg.com/electron/-/electron-9.2.0.tgz#d9fc8c8c9e5109669c366bd7b9ba83b06095d7a4" - integrity sha512-4ecZ3rcGg//Gk4fAK3Jo61T+uh36JhU6HHR/PTujQqQiBw1g4tNPd4R2hGGth2d+7FkRIs5GdRNef7h64fQEMw== +electron@11.0.0-beta.1: + version "11.0.0-beta.1" + resolved "https://registry.yarnpkg.com/electron/-/electron-11.0.0-beta.1.tgz#f4a3b3e0783c7ef2c70a4e5232e9cc0ef8f98974" + integrity sha512-+dYLXqFmLWOCSlwfzMec4CpcpHenpB9cjWRiiKc2CDwc3Esp5fZ+dU/nX/kIbdrlj7FaOBn0Wf6RjCSif4ebmg== dependencies: "@electron/get" "^1.0.1" "@types/node" "^12.0.12"