| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,75 +1,118 @@ | ||
| <p align="center"> | ||
| <a href="https://wexond.net"><img src="static/icons/icon.png" width="256"></a> | ||
| </p> | ||
|
|
||
| <div align="center"> | ||
| <h1>Wexond Browser Base</h1> | ||
|
|
||
| [](https://github.com/wexond/desktop/actions) | ||
| [](https://wexond.net) | ||
| [](https://app.fossa.io/projects/git%2Bgithub.com%2Fwexond%2Fwexond?ref=badge_shield) | ||
| [](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=VCPPFUAL4R6M6&source=url) | ||
| [](https://discord.gg/P7Vn4VX) | ||
|
|
||
| Wexond Base is a modern web browser, built on top of modern web technologies such as `Electron` and `React`, that can also be used as a framework to create a custom web browser (see the [License](#license) section). | ||
|
|
||
| </div> | ||
|
|
||
| # Table of Contents: | ||
| - [Motivation](#motivation) | ||
| - [Features](#features) | ||
| - [Screenshots](#screenshots) | ||
| - [Downloads](#downloads) | ||
| - [Contributing](#contributing) | ||
| - [Development](#development) | ||
| - [Running](#running) | ||
| - [Documentation](#documentation) | ||
| - [License](#license) | ||
|
|
||
| # Motivation | ||
|
|
||
| Compiling and editing Chromium directly may be challenging and time consuming, so we decided to build Wexond with modern web technologies. Hence, the development effort and time is greatly reduced. Either way Firefox is based on Web Components and Chrome implements new dialogs in WebUI (which essentially is hosted in WebContents). | ||
|
|
||
| # Features | ||
|
|
||
| - **Wexond Shield** - Browse the web without any ads and don't let websites to track you. Thanks to the Wexond Shield powered by [Cliqz](https://github.com/cliqz-oss/adblocker), websites can load even 8 times faster! | ||
| - **Chromium without Google services and low resources usage** - Since Wexond uses Electron under the hood which is based on only several and the most important Chromium components, it's not bloated with redundant Google tracking services and others. | ||
| - **Fast and fluent UI** - The animations are really smooth and their timings are perfectly balanced. | ||
| - **Highly customizable new tab page** - Customize almost an every aspect of the new tab page! | ||
| - **Customizable browser UI** - Choose whether Wexond should have compact or normal UI. | ||
| - **Tab groups** - Easily group tabs, so it's hard to get lost. | ||
| - **Scrollable tabs** | ||
| - **Partial support for Chrome extensions** - Install some extensions directly from Chrome Web Store\* (see [#110](https://github.com/wexond/wexond/issues/110)) (WIP) | ||
|
|
||
| ## Other basic features | ||
|
|
||
| - Downloads popup with currently downloaded items (download manager WebUI page is WIP) | ||
| - History manager | ||
| - Bookmarks bar & manager | ||
| - Settings | ||
| - Find in page | ||
| - Dark and light theme | ||
| - Omnibox with autocomplete algorithm similar to Chromium | ||
| - State of the art tab system | ||
|
|
||
| # Screenshots | ||
|
|
||
|  | ||
|
|
||
| UI normal variant: | ||
|  | ||
|
|
||
| UI compact variant: | ||
|  | ||
|  | ||
|
|
||
| # Downloads | ||
| - [Stable and beta versions](https://github.com/wexond/desktop/releases) | ||
| - [Nightlies](https://github.com/wexond/desktop-nightly/releases) | ||
|
|
||
| # [Roadmap](https://github.com/wexond/wexond/projects) | ||
|
|
||
| # Contributing | ||
|
|
||
| If you have found any bugs or just want to see some new features in Wexond, feel free to open an issue. Every suggestion is very valuable for us, as they help us improve the browsing experience. Also, please don't hesitate to open a pull request. This is really important to us and for the further development of this project. | ||
|
|
||
| By opening a pull request, you agree to the conditions of the [Contributor License Agreement](cla.md). | ||
|
|
||
| # Development | ||
|
|
||
| ## Running | ||
|
|
||
| Before running Wexond, please ensure you have **latest** [`Node.js`](https://nodejs.org/en/) and [`Yarn`](https://classic.yarnpkg.com/en/docs/install/#windows-stable) installed on your machine. | ||
|
|
||
| ### Windows | ||
|
|
||
| Make sure you have build tools installed. You can install them by running this command as **administrator**: | ||
|
|
||
| ```bash | ||
| $ npm i -g windows-build-tools | ||
| ``` | ||
|
|
||
| ```bash | ||
| $ yarn # Install needed depedencies. | ||
| $ yarn rebuild # Rebuild native modules using Electron headers. | ||
| $ yarn dev # Run Wexond in development mode | ||
| ``` | ||
|
|
||
| ### More commands | ||
|
|
||
| ```bash | ||
| $ yarn compile-win32 # Package Wexond for Windows | ||
| $ yarn compile-linux # Package Wexond for Linux | ||
| $ yarn compile-darwin # Package Wexond for macOS | ||
| $ yarn lint # Runs linter | ||
| $ yarn lint-fix # Runs linter and automatically applies fixes | ||
| ``` | ||
|
|
||
| More commands can be found in [`package.json`](package.json). | ||
|
|
||
| # Documentation | ||
|
|
||
| Guides and the API reference are located in [`docs`](docs) directory. | ||
|
|
||
| # License | ||
|
|
||
| Usage of this project code and assets is disallowed. | ||
|
|
||
| By sending a Pull Request, you agree that your code may be relicensed or sublicensed. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| # Development | ||
|
|
||
| ## IPC | ||
|
|
||
| Now, the preferred way to communicate between processes is to use [`@wexond/rpc-electron`](https://github.com/wexond/rpc) package. | ||
|
|
||
| Example: | ||
|
|
||
| Handling the IPC message in the main process: | ||
|
|
||
| [`src/main/network/network-service-handler.ts`](../src/main/network/network-service-handler.ts) | ||
|
|
||
|
|
||
| Sending the IPC message to the main process: | ||
|
|
||
| ```ts | ||
| const { data } = await networkMainChannel.getInvoker().request('http://localhost'); | ||
| ``` | ||
|
|
||
| Common RPC interface | ||
|
|
||
| [`src/common/rpc/network.ts`](../src/common/rpc/network.ts) | ||
|
|
||
| ## Remote module | ||
|
|
||
| As Electron will be deprecating the `remote` module, we are migrating to our RPC solution. | ||
|
|
||
| ## Node integration | ||
|
|
||
| We are going to turn off `nodeIntegration`, enable `contextIsolation` and `sandbox` in the UI webContents, | ||
| therefore we prefer not having requires to node.js built-in modules in renderers. | ||
|
|
||
| ## Project structure | ||
|
|
||
| Common interfaces, constants etc. should land into the `common` directory. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| const package = require('../package.json'); | ||
| const electronBuilder = require('../electron-builder.json'); | ||
| const { promises } = require('fs'); | ||
| const { resolve } = require('path'); | ||
| const { run } = require('./utils'); | ||
|
|
||
| const isNightly = package.version.indexOf('nightly') !== -1; | ||
|
|
||
| const getPlatform = () => { | ||
| if (process.platform === 'win32') return 'windows'; | ||
| else if (process.platform === 'darwin') return 'mac'; | ||
| return process.platform; | ||
| }; | ||
|
|
||
| const getEnv = (name) => process.env[name.toUpperCase()] || null; | ||
|
|
||
| const setEnv = (name, value) => { | ||
| if (value) { | ||
| process.env[name.toUpperCase()] = value.toString(); | ||
| } | ||
| }; | ||
|
|
||
| const getInput = (name) => { | ||
| return getEnv(`INPUT_${name}`); | ||
| }; | ||
|
|
||
| (async () => { | ||
| try { | ||
| if (isNightly) { | ||
| await promises.copyFile( | ||
| resolve(__dirname, '../package.json'), | ||
| resolve(__dirname, '../temp-package.json'), | ||
| ); | ||
| await promises.copyFile( | ||
| resolve(__dirname, '../electron-builder.json'), | ||
| resolve(__dirname, '../temp-electron-builder.json'), | ||
| ); | ||
| const newPkg = { | ||
| ...package, | ||
| name: 'wexond-nightly', | ||
| repository: { | ||
| type: 'git', | ||
| url: 'git+https://github.com/wexond/desktop-nightly.git', | ||
| }, | ||
| }; | ||
| await promises.writeFile( | ||
| resolve(__dirname, '../package.json'), | ||
| JSON.stringify(newPkg), | ||
| ); | ||
|
|
||
| const newEBConfig = { | ||
| ...electronBuilder, | ||
| appId: 'org.wexond.wexond-nightly', | ||
| productName: 'Wexond Nightly', | ||
| directories: { | ||
| output: 'dist', | ||
| buildResources: 'static/nightly-icons', | ||
| }, | ||
| }; | ||
|
|
||
| await promises.writeFile( | ||
| resolve(__dirname, '../electron-builder.json'), | ||
| JSON.stringify(newEBConfig), | ||
| ); | ||
| } | ||
|
|
||
| const release = | ||
| (getEnv('release') === 'true' || getEnv('release') === true) && | ||
| getEnv('GH_TOKEN'); | ||
| const platform = getPlatform(); | ||
|
|
||
| if (platform === 'mac') { | ||
| setEnv('CSC_LINK', getEnv('mac_certs')); | ||
| setEnv('CSC_KEY_PASSWORD', getEnv('mac_certs_password')); | ||
| } else if (platform === 'windows') { | ||
| setEnv('CSC_LINK', getEnv('windows_certs')); | ||
| setEnv('CSC_KEY_PASSWORD', getEnv('windows_certs_password')); | ||
| } | ||
|
|
||
| run('yarn run build'); | ||
| run( | ||
| `npx --no-install electron-builder --${platform} ${ | ||
| release ? '-p always' : '' | ||
| }`, | ||
| ); | ||
| } catch (e) { | ||
| console.error(e); | ||
| process.exit(1); | ||
| } | ||
| })(); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| const { execSync } = require('child_process'); | ||
| const { resolve } = require('path'); | ||
|
|
||
| const run = cmd => { | ||
| execSync(cmd, { | ||
| encoding: 'utf8', | ||
| cwd: resolve(__dirname, '..'), | ||
| stdio: 'inherit', | ||
| }); | ||
| }; | ||
|
|
||
| module.exports = { | ||
| run, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| import { configure } from 'mobx'; | ||
| import { setIpcRenderer } from '@wexond/rpc-electron'; | ||
| import { ipcRenderer } from 'electron'; | ||
|
|
||
| export const configureUI = () => { | ||
| configure({ enforceActions: 'never' }); | ||
| }; | ||
|
|
||
| export const configureRenderer = () => { | ||
| setIpcRenderer(ipcRenderer); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| import { RendererToMainChannel } from '@wexond/rpc-electron'; | ||
|
|
||
| export interface ExtensionMainService { | ||
| uninstall(id: string): void; | ||
| inspectBackgroundPage(id: string): void; | ||
| } | ||
|
|
||
| export const extensionMainChannel = new RendererToMainChannel<ExtensionMainService>( | ||
| 'ExtensionMainService', | ||
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| import { RendererToMainChannel } from '@wexond/rpc-electron'; | ||
|
|
||
| export interface ResponseDetails { | ||
| statusCode: number; | ||
| data: string; | ||
| } | ||
|
|
||
| export interface NetworkService { | ||
| request(url: string): Promise<ResponseDetails>; | ||
| } | ||
|
|
||
| export const networkMainChannel = new RendererToMainChannel<NetworkService>( | ||
| 'NetworkService', | ||
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| import { WEBUI_BASE_URL, WEBUI_URL_SUFFIX } from '~/constants/files'; | ||
|
|
||
| export const getWebUIURL = (hostname: string) => | ||
| `${WEBUI_BASE_URL}${hostname}${WEBUI_URL_SUFFIX}`; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,31 @@ | ||
| export const DEFAULT_TAB_MARGIN_TOP = 4; | ||
| export const COMPACT_TAB_MARGIN_TOP = 3; | ||
|
|
||
| export const DEFAULT_TAB_HEIGHT = 32; | ||
| export const COMPACT_TAB_HEIGHT = 32; | ||
|
|
||
| // Toolbar | ||
| export const TOOLBAR_HEIGHT = 42; | ||
|
|
||
| export const TOOLBAR_BUTTON_WIDTH = 36; | ||
| export const TOOLBAR_BUTTON_HEIGHT = 32; | ||
|
|
||
| export const ADD_TAB_BUTTON_WIDTH = 28; | ||
| export const ADD_TAB_BUTTON_HEIGHT = 28; | ||
|
|
||
| export const DEFAULT_TITLEBAR_HEIGHT = | ||
| DEFAULT_TAB_MARGIN_TOP + DEFAULT_TAB_HEIGHT; | ||
| export const COMPACT_TITLEBAR_HEIGHT = | ||
| 2 * COMPACT_TAB_MARGIN_TOP + COMPACT_TAB_HEIGHT; | ||
|
|
||
| export const VIEW_Y_OFFSET = TOOLBAR_HEIGHT + DEFAULT_TITLEBAR_HEIGHT; | ||
|
|
||
| // Widths | ||
| export const WINDOWS_BUTTON_WIDTH = 45; | ||
| export const MENU_WIDTH = 330; | ||
|
|
||
| // Dialogs | ||
| export const DIALOG_MIN_HEIGHT = 130; | ||
| export const DIALOG_MARGIN = 16; | ||
| export const DIALOG_TOP = 34; | ||
| export const DIALOG_MARGIN_TOP = 3; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,73 +1,79 @@ | ||
| import { ISettings } from '~/interfaces'; | ||
| import { remote, app } from 'electron'; | ||
|
|
||
| export const DEFAULT_SEARCH_ENGINES = [ | ||
| { | ||
| name: 'DuckDuckGo', | ||
| url: 'https://duckduckgo.com/?q=%s', | ||
| keywordsUrl: '', | ||
| keyword: 'duckduckgo.com', | ||
| icon: | ||
| 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAACT1BMVEUAAAD/gFXfWjbeWTPfWDTeWDTeWDPfYDXfWDPeWTT/bUngWDPfWTPfWDPjWTfeWTTjWzPeWTTeWDPkelzrnojvr53ywLLibEvxuKj77en66OPyvq/urJrrm4XgYj/ws6P88/HvsqHjdFbfYT/jclP65uHywrThaUjmgmf88e/rmoTlfmLxuKnxvK7tpZLibk/mhWruqZf88e7////3187haEfpk3v43NX88u/rmoPgZELywbPgY0H1zsP88/DmgWX43NT87+zgZ0XvsaDoj3bf5e7Gz+D2+PvW3em2wtjks6nwt6fN1eT9+ffgZUP0yr/5+vzv8vehsM3H0OHrnonywLNkfaxRbqL3+ftVcaSKncDxuqvurZuntdB+k7r+/v/eWjPmdinumx3wnxvlcyrojXT//vb/99P/87z2v1r0rRb8ywz90gr6xQ7ha0v//vj+5nP90w/90Qr1shXpgyXeWzP+42X90AvxphnzqRjypxnsjSLhZi/+42T3uRLuq5n//O/2uyL0rxbriSLsjiHwoRr7yg3iaC7niW/icFHiZy7zrBf5whD3uhLumR3jdlf66uXqlH3zxrrYXDTdWTPso4/d8da136bq9uX99fPeXDjcWTSkhzxquEa4dznlf2PB5LRlvEZmvUeHyXNuvlVouUh7qkKohTzmg2ffXjv+/fzG5rpas0ZkvEajiDz54NrQ68dftU1atEZit06npl+ApUKrgjvzx7vp9uV3xFy24Kj4/Pb55N/eWTXdeVfus6H77+v44Nr44Nn54tz76uYtKC1GAAAAEnRSTlMABkeVv9nzGJbxB4L0xy7jLfK6UobZAAACRklEQVQ4y4VT9XsTQRC9aKMEJsnFrb0kTbDSoUWKF5eixa24HO4Eihco7i6Lu7u35Q/jbu/2uLQfH/PTzs7bmdk3bzhOM4PRZLZYrRazyWjgOlqJzQ5en5/n/T4v2G0l7cIOpysQDIUj0VgsGgmHggGX01H03B1PJFOloFppKpkoc+uSdPII6UwWdJbNpAVPZ+29pzyXh67duvfo+ReSz5V71BwOt5CrAOhViYi9BQ1RkRPcSh/OeDovXVRVVfdB7NuvP0PE0nEnLeBK1Mj+gIFIbdBghsgkXHIRWyBJ+xuCqg0dxjpNBmwSf/ZgrewNxxEjR8nx0WPGshSpoN3AGSFE/z8Ox7MUE+oYHyEwciZvmDoTESdNpvEpU6tZirDXxJmnRei5HnH6jJkyYNbsOQwQ8Zk5iz9Kz3MR582nGRZgPSxsWLR4CUDUb+GsfIwClrIOli1fsXLValFc0yBRwVs1wFolvG79BlGyjZs2b5G54rtoJaCSAraK27bv2FnYpdzJJcw+pUlQiNwtNuqGKjfJvgl7KGDvPnF/4cDBQ41Nh9VvMqLgiNLE0ebCseNNJ042M6IkqlMUUKcATukqUKq1YcFpCjijkxUdljzuDPXPUoA8+nPn9eNmgoELePHSZbxy9dr1Gzep6NJlTr3k4BbeJnfu3rv/gJCHRZKTRPtIEi08fvKUkGeEPCfkRZFoFdnXZOHlK0Jev3n77j35UCx7dXFqP376/OUr4rfvP362Wxy2er9aWtvaWlt+d1y9/y/vv9f/DzYRmYvARwKBAAAAAElFTkSuQmCC', | ||
| }, | ||
| { | ||
| name: 'Google', | ||
| url: 'https://www.google.com/search?q=%s', | ||
| keywordsUrl: 'http://google.com/complete/search?client=chrome&q=%s', | ||
| keyword: 'google.com', | ||
| icon: | ||
| 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAACDVBMVEUAAAD+/v7+/v79/f39/f39/f39/f39/f3+/v7+/v79/f39/f39/f39/f3+/v7////9/f39/f39/f39/f3+/v7+/v79/f39/f3+/v7+/v79/f39/f3////+/v79/f3////9/f3+/v7+/v79/f39/f39/f3+/v7////9/f3+/v79/f39/f39/f3////9/f3////+/v7////85uT1pZ7wdGrsVUjqRjjqRTfsU0bvcmj1oZr85OL85+XxgXjqRDbqQzXxf3b3urXrSz7rTD/4v7r1p6DyioHuZVn0m5P3tK73ta/0m5TuZlryjob//v7rSj3tXFD5zMj5y8fsWEzzlIz0kG3vbGH+8vL+9fT+9t37uQfxdyD98O7934b7vAX1kxXrSTT3uLP8zUb5riH/+/v7whz801lChfS/1fv7vQf93H2lxfqfwfn80liqyPr8zkfkwi76/fve6v3B1/v934iztSE8qFCw3byox/rn8P7+9t/2vAmGsDM0qFNGsGLs9+/z9/5UkPVgmfaPx4JcuXXw+fL6/P93p/epyPrm9Oo+rFtMsmfC5cza7+BjsahChfH2+f634MJUtm6Nzp+q2rer27iV0qVkvHs1qFQ3oHdBiePD2Pyj2LE1pWA+jsq54cQ+rFw3qVab0rHn9et7xo82qVRnvn7X7t2h169swIJJsWU4qlZGsGNlvX3Y7t5rzgcKAAAAMXRSTlMADlqcy+367Fknn/f2nSUKjfz7iwkm1dIkMuvqMAuKD/UNV5nI+VYMmiOH09AI6CIvZ7+SkQAAAe5JREFUOMtjYIADRiZmFlY2NnYWZg5GBkzAycVtCAc8vHxo0vwCgoYoQEhYBFleVMwQA4hLIOQlpcBCRsYmpmbmFpZW1mCutAxcP1jextbOHgrsHGzAKqBmyILNd3SyRwLOLmBbIO4QALFd7VGBG9gWObD/QO53hAi7e3h6efsAGb5+EL+ALOECOc8fJB0QCHF/UHAIzCvMDAzyoPAJBcmHhcOEI+B+VVBkYAJSkVHR9vamMYZYgBIDM5CMjYuLT0jEJm+ozMACJJPi4uKSU7AqUGFgBZKpQAVpUJF0BMgABRYDG5DMBCrIwlSQDeSq4lOQA1bADrUiF1NBHtgKkCPzgQoKCpHdVgRSUAx2JMibJXFxpWXlyAoqQAoqwd7kAJJV1TW1tXX1CPmGRqB8E4ilxsDIA6Saa4GgpRUm39YOMqADFNTqDAy8QLqzC6Siuwci39vXD5RvnABkagBjk08IyJhYCwaTJk+ZOm36jNqZs9LTZwOF2TRBCUIYpGtOLQqYOw8UCIZakCSvDWLPX4CsYOEikJiOLiRRSkiDeIuXLIXLL1sOEtHThyVrGbAKw84VK1etXrhm7bR1YK6eASJjSIhjRrSOPnLWEpETQpVm09JFy50SzAoIaQUNTSz5W1FJWUVaVVVaRVlNHSEKACBb24XRQm7rAAAAAElFTkSuQmCC', | ||
| }, | ||
| { | ||
| name: 'Bing', | ||
| url: 'https://www.bing.com/search?q=%s', | ||
| keywordsUrl: '', | ||
| keyword: 'bing.com', | ||
| icon: | ||
| 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAA3NCSVQICAjb4U/gAAAANlBMVEUAfX3k8/MAiop7trYAgoL6//8AdXULhIT///+exMQ4l5ew0dFLoaG929v0+/vQ5+eVwsIcjo4kKiufAAAAZElEQVQYlY2QyQ6AMAhEpy0Fuqr//7NWo108OQcSHsMSICJEMoSWR8sLQNCCD/A/AEALYFMy3+wFQdWkg0aLNaoaMM3gbQaV3eVww9HKajL60KbqmJ61QrvXBPQ7WrCRp0vXB5xZ6wZZjwmZbQAAAABJRU5ErkJggg==', | ||
| }, | ||
| { | ||
| name: 'Yahoo!', | ||
| url: 'https://search.yahoo.com/search?p=%s', | ||
| keywordsUrl: '', | ||
| keyword: 'yahoo.com', | ||
| icon: | ||
| 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAA3NCSVQICAjb4U/gAAABnklEQVQokZWSPUyTYRSFn/t+rTX9EFp/SICwqIumMUwaJxdlYHAgBJwMCa5sLMaITizGARYSNTGROOAAokMHYggLBBIGCIHESBggwfJTQGgLL/3e61CxVWOa3ukO59xz7z1HOmWBSspUhAZCZREZdSfoGSQqprzCgQa945dHT5qef7mSVVdUCFQtapCICGBVAzSE1MUjibvngOSrrTDyixCoXm+P3u6Ipb7akcdbBlp6LzQ2nZ14nW7uugjsb+Qnh/fOSwiQTllQRZC3mgAeyWIO915vAA9l8Z0mgKEn68m+dEE8BIiQ0WD20+7N+/HWwUu5Hw6YGdu99zSmeeyh+9i3U316rRR8cKqx2nB/6lomHdicizeEu2X5zoua/e/5Dy+36ynsX/JWI7K2efxtJnP1lu/jrc5lE93Rtp56YH36eGUq550yim/1McPPNgr9WH9q6XPWHuj2ql2ZOjK/5/9lXN4qkE0HE0N7MbwH1fMGqvCkiC8hHKFdA41A8s2mjzEiNXj/Wiml4TtUZ3E+JiL/TcAfK1WJKRuWitP6EzoWnEvW4UekAAAAAElFTkSuQmCC', | ||
| }, | ||
| { | ||
| name: 'Ecosia', | ||
| url: 'https://www.ecosia.org/search?q=%s', | ||
| keywordsUrl: '', | ||
| keyword: 'ecosia.org', | ||
| icon: | ||
| 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAylJREFUOI1Nk01sVFUYhp9z7r1zO1NK25ShtkxmSqE0VisWf0KbatkQ0USnEhPiwh+SGgkkLowQ6EKKWGgTNi4ICxtQMNFEMG1iMBqURKNMgtYwtKkUq20tFFoYxjudmc7ce+5xgdZ+2zfPu3m/R7Ds9P44oGOoYg9uoQ2IACaaG1jWMDJwDNNMiCODS4xYgg90gqFP4nk7/a1dyOZWCFffD527MDYMX5+CRecCwtr6X4lYgnEv6ujGLeKNwzjeON/+keTjiTBKCxrLVxBfH+Op2jD68+OIy0NTWKV14sgghu7uBJMPdHXjDrGnj/6fL1NMvc71tM25+QaigRlGnRzvX5tjau4W255/CSPtVIjZa+097U1nhD4Qh0VH03uexO2zdP+imSpGsKTHm2suEK89Q8EvI6dWEJJTmIEXWBs7idnzMixmn5Yo9yjtO/hLC1p/qCCjwqw0XJ4ovcGW8DdkvBqUH8IWPp6Oks0P8fdCAv+ZLvAKb0vcQhvNm/ny+gwNpkQIjRQeu+pPYwnNWKYJV5sAGMJDimrS6S/QjY+DKrZItFrtV0boqBxgQ/Aeed8iaqeIhS4hhKZ5ZRJLuBS1yW+ZjdhGDuVeRVeWAyIi0WitIWQoDj/4Hg+HZpnIV7MnOcBsPgZA0EzRO/4Wxye3c2pyF8l0C4YGwJdI4yZz09glLVhinptuGbZUjOVqmMnHsKXDuZmdjGZrcVSQT+ceQ5mPIu7eBpiWBEouyl+/Y1XVc7wz2o/jBZFoTOFTai6QV2Us+JqQ9JBocn6OhppnYeRHMAOJ+zPm04p3z8rTsw77hq8SswMoLVgXvIMpFFeyayiVLvOuxyt1EQ5tfgS644C5VuJrKK/az4l9vNpUz7FNDzFRcMn6HqPZKq4srEbrAr8XinSti3KorQX94UEwjU+QYvLfV34RDP8jQlWvsbufW8EKvhr/k5FUGuVrNlSUsW19HfW2RA/0IKaTCQi0ir7BZTJ1d4L291LI9PFkp2RTB6yqASnh3h0YuQTffwYloRN47BZHl8n0v87bQShwCwdRbgdaPQBCIEQKI/ATpr0XA0Tv0BLzDxefVnicNn0wAAAAAElFTkSuQmCC', | ||
| }, | ||
| { | ||
| name: 'Ekoru', | ||
| url: 'https://www.ekoru.org/?ext=wexond&q=%s', | ||
| keywordsUrl: 'http://ac.ekoru.org/?ext=wexond&q=%s', | ||
| keyword: 'ekoru.org', | ||
| icon: | ||
| 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAA3NCSVQICAjb4U/gAAABTVBMVEVHcEwGoc4fq68HodEirbQ/v78RmcxNsqIupbJOsVAZocIHntchqb0KoNgZqL0LoNk2r38HoNYaq71Ss0VVt1BZtEJUsj+fyjtuwGUXms8Poc0HnNMFpM8JotISoMgRn9UQn9EInNVdulhyuUIYqLMHp8p4vUAZo8mKxD4Gn9YNpcsGoNSMxj8Wq751uj8Fpdo0r3sFpNwsrotUtEiTyk1SsjtSskVQtWJItGmMxTyHxUgmsLAsqaI0q4kOmNQ9sG4yr4Jvu0krsakcpbRjuk6Dwz0KqMtBsm4Upr6AwkBCsW4NqMcurpM9sHaUyFhwvEdIsVSSylUwsKWizkqkzkBdt0lct0Iqrq5fuER/wT6Bwj8lrbNwwHC21UNPt4cPpMRIt49Zu38Up7szspglrKdEtHc8solGt5BavH5OuIB3xFqAxVMFp9hAsn9lv3FSuK/uAAAAaHRSTlMA/P6dPgQPCxUwIcYubZRWYLpdQthwTGn6GlGl9O6AOefX9SDzzpbvtqz54uCdRvjxyfuV2Thg9+166sJzeVmu3Mrw2tqi27Xz0W+p6aJdu4mYolKqrX+WyVqKgrEqx/bYvP///////rhNcOkAAAD4SURBVBiVJY9VQ8IAGAC/jSUrxpKUkO4uu7u7XQPq/3904r3dvR0AACGUKghSKQkkLFgS3612s9lG47HgwtPosKPSPfViw2D8QomoLFFhvpbE6HUjQAJXHqrsxPU8d4un9w8EaFm3wcnPuFiszs2adCJCw1HCXrWOQ3Z3hoZPy/Bl9+9nT+T1GZk9dI+PDD8oV/PHV+3mEs9Pc6FlaHx3nvURgNal8tM9P7TMB+xjPNK6L9S5mQttA2dk+tibrn+y9YwRWOUhwpgyTWAYQW+a8egKCxBNOLLSk+52HCRAEv4LziUsezCwLSQW+b/F15hUoZBKJ6k/+wXyrSm5N9MsrwAAAABJRU5ErkJggg==', | ||
| }, | ||
| ]; | ||
|
|
||
| export const DEFAULT_SETTINGS: ISettings = { | ||
| theme: 'wexond-light', | ||
| darkContents: false, | ||
| shield: true, | ||
| multrin: true, | ||
| animations: true, | ||
| bookmarksBar: false, | ||
| suggestions: true, | ||
| themeAuto: true, | ||
| searchEngines: DEFAULT_SEARCH_ENGINES, | ||
| searchEngine: 0, | ||
| startupBehavior: { | ||
| type: 'empty', | ||
| }, | ||
| warnOnQuit: false, | ||
| version: 2, | ||
| downloadsDialog: false, | ||
| downloadsPath: remote | ||
| ? remote.app.getPath('downloads') | ||
| : app | ||
| ? app.getPath('downloads') | ||
| : '', | ||
| doNotTrack: true, | ||
| topBarVariant: 'default', | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| export const ZOOM_FACTOR_INCREMENT = 0.2; | ||
| export const ZOOM_FACTOR_MAX = 5; | ||
| export const ZOOM_FACTOR_MIN = 0.2; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| export type BrowserActionChangeType = | ||
| | 'setPopup' | ||
| | 'setBadgeText' | ||
| | 'setTitle' | ||
| | 'setIcon' | ||
| | 'setBadgeBackgroundColor'; | ||
|
|
||
| export const BROWSER_ACTION_METHODS: BrowserActionChangeType[] = [ | ||
| 'setPopup', | ||
| 'setBadgeText', | ||
| 'setTitle', | ||
| 'setIcon', | ||
| 'setBadgeBackgroundColor', | ||
| ]; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,10 @@ | ||
| export interface ISuggestion { | ||
| primaryText?: string; | ||
| secondaryText?: string; | ||
| id?: number; | ||
| favicon?: string; | ||
| canSuggest?: boolean; | ||
| isSearch?: boolean; | ||
| hovered?: boolean; | ||
| url?: string; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| export type TabEvent = | ||
| | 'load-commit' | ||
| | 'url-updated' | ||
| | 'title-updated' | ||
| | 'favicon-updated' | ||
| | 'did-navigate' | ||
| | 'loading' | ||
| | 'pinned' | ||
| | 'credentials' | ||
| | 'blocked-ad' | ||
| | 'zoom-updated' | ||
| | 'media-playing' | ||
| | 'media-paused'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| export interface IURLSegment { | ||
| value: string; | ||
| grayOut: boolean; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| import { app, ipcMain, Menu } from 'electron'; | ||
| import { isAbsolute, extname } from 'path'; | ||
| import { existsSync } from 'fs'; | ||
| import { SessionsService } from './sessions-service'; | ||
| import { checkFiles } from '~/utils/files'; | ||
| import { Settings } from './models/settings'; | ||
| import { isURL, prefixHttp } from '~/utils'; | ||
| import { WindowsService } from './windows-service'; | ||
| import { StorageService } from './services/storage'; | ||
| import { getMainMenu } from './menus/main'; | ||
| import { runAutoUpdaterService } from './services'; | ||
| import { DialogsService } from './services/dialogs-service'; | ||
| import { requestAuth } from './dialogs/auth'; | ||
| import { NetworkServiceHandler } from './network/network-service-handler'; | ||
| import { ExtensionServiceHandler } from './extension-service-handler'; | ||
|
|
||
| export class Application { | ||
| public static instance = new Application(); | ||
|
|
||
| public sessions: SessionsService; | ||
|
|
||
| public settings = new Settings(); | ||
|
|
||
| public storage = new StorageService(); | ||
|
|
||
| public windows = new WindowsService(); | ||
|
|
||
| public dialogs = new DialogsService(); | ||
|
|
||
| public start() { | ||
| const gotTheLock = app.requestSingleInstanceLock(); | ||
|
|
||
| if (!gotTheLock) { | ||
| app.quit(); | ||
| return; | ||
| } else { | ||
| app.on('second-instance', async (e, argv) => { | ||
| const path = argv[argv.length - 1]; | ||
|
|
||
| if (isAbsolute(path) && existsSync(path)) { | ||
| if (process.env.NODE_ENV !== 'development') { | ||
| const path = argv[argv.length - 1]; | ||
| const ext = extname(path); | ||
|
|
||
| if (ext === '.html') { | ||
| this.windows.current.win.focus(); | ||
| this.windows.current.viewManager.create({ | ||
| url: `file:///${path}`, | ||
| active: true, | ||
| }); | ||
| } | ||
| } | ||
| return; | ||
| } else if (isURL(path)) { | ||
| this.windows.current.win.focus(); | ||
| this.windows.current.viewManager.create({ | ||
| url: prefixHttp(path), | ||
| active: true, | ||
| }); | ||
| return; | ||
| } | ||
|
|
||
| this.windows.open(); | ||
| }); | ||
| } | ||
|
|
||
| app.on('login', async (e, webContents, request, authInfo, callback) => { | ||
| e.preventDefault(); | ||
|
|
||
| const window = this.windows.findByBrowserView(webContents.id); | ||
| const credentials = await requestAuth( | ||
| window.win, | ||
| request.url, | ||
| webContents.id, | ||
| ); | ||
|
|
||
| if (credentials) { | ||
| callback(credentials.username, credentials.password); | ||
| } | ||
| }); | ||
|
|
||
| ipcMain.on('create-window', (e, incognito = false) => { | ||
| this.windows.open(incognito); | ||
| }); | ||
|
|
||
| this.onReady(); | ||
| } | ||
|
|
||
| private async onReady() { | ||
| await app.whenReady(); | ||
|
|
||
| new ExtensionServiceHandler(); | ||
|
|
||
| NetworkServiceHandler.get(); | ||
|
|
||
| checkFiles(); | ||
|
|
||
| this.storage.run(); | ||
| this.dialogs.run(); | ||
|
|
||
| this.windows.open(); | ||
|
|
||
| this.sessions = new SessionsService(); | ||
|
|
||
| Menu.setApplicationMenu(getMainMenu()); | ||
| runAutoUpdaterService(); | ||
|
|
||
| app.on('activate', () => { | ||
| if (this.windows.list.filter((x) => x !== null).length === 0) { | ||
| this.windows.open(); | ||
| } | ||
| }); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,42 +1,49 @@ | ||
| import { BrowserWindow } from 'electron'; | ||
| import { Application } from '../application'; | ||
| import { DIALOG_MARGIN_TOP, DIALOG_MARGIN } from '~/constants/design'; | ||
| import { IBookmark } from '~/interfaces'; | ||
|
|
||
| export const showAddBookmarkDialog = ( | ||
| browserWindow: BrowserWindow, | ||
| x: number, | ||
| y: number, | ||
| data?: { | ||
| url: string; | ||
| title: string; | ||
| bookmark?: IBookmark; | ||
| favicon?: string; | ||
| }, | ||
| ) => { | ||
| if (!data) { | ||
| const { | ||
| url, | ||
| title, | ||
| bookmark, | ||
| favicon, | ||
| } = Application.instance.windows.current.viewManager.selected; | ||
| data = { | ||
| url, | ||
| title, | ||
| bookmark, | ||
| favicon, | ||
| }; | ||
| } | ||
|
|
||
| const dialog = Application.instance.dialogs.show({ | ||
| name: 'add-bookmark', | ||
| browserWindow, | ||
| getBounds: () => ({ | ||
| width: 366, | ||
| height: 240, | ||
| x: x - 366 + DIALOG_MARGIN, | ||
| y: y - DIALOG_MARGIN_TOP, | ||
| }), | ||
| onWindowBoundsUpdate: () => dialog.hide(), | ||
| }); | ||
|
|
||
| if (!dialog) return; | ||
|
|
||
| dialog.on('loaded', (e) => { | ||
| e.reply('data', data); | ||
| }); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,44 +1,49 @@ | ||
| import { VIEW_Y_OFFSET } from '~/constants/design'; | ||
| import { BrowserWindow } from 'electron'; | ||
| import { Application } from '../application'; | ||
|
|
||
| export const requestAuth = ( | ||
| browserWindow: BrowserWindow, | ||
| url: string, | ||
| tabId: number, | ||
| ): Promise<{ username: string; password: string }> => { | ||
| return new Promise((resolve, reject) => { | ||
| const appWindow = Application.instance.windows.fromBrowserWindow( | ||
| browserWindow, | ||
| ); | ||
|
|
||
| const tab = appWindow.viewManager.views.get(tabId); | ||
| tab.requestedAuth = { url }; | ||
|
|
||
| const dialog = Application.instance.dialogs.show({ | ||
| name: 'auth', | ||
| browserWindow, | ||
| getBounds: () => { | ||
| const { width } = browserWindow.getContentBounds(); | ||
| return { | ||
| width: 400, | ||
| height: 500, | ||
| x: width / 2 - 400 / 2, | ||
| y: VIEW_Y_OFFSET, | ||
| }; | ||
| }, | ||
| tabAssociation: { | ||
| tabId, | ||
| getTabInfo: (tabId) => { | ||
| const tab = appWindow.viewManager.views.get(tabId); | ||
| return tab.requestedAuth; | ||
| }, | ||
| }, | ||
| onWindowBoundsUpdate: (disposition) => { | ||
| if (disposition === 'resize') dialog.rearrange(); | ||
| }, | ||
| }); | ||
|
|
||
| if (!dialog) return; | ||
|
|
||
| dialog.on('result', (e, result) => { | ||
| resolve(result); | ||
| dialog.hide(); | ||
| }); | ||
| }); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,47 +1,46 @@ | ||
| import { BrowserWindow } from 'electron'; | ||
| import { Application } from '../application'; | ||
| import { | ||
| DIALOG_MARGIN_TOP, | ||
| DIALOG_MARGIN, | ||
| DIALOG_TOP, | ||
| } from '~/constants/design'; | ||
|
|
||
| export const showDownloadsDialog = ( | ||
| browserWindow: BrowserWindow, | ||
| x: number, | ||
| y: number, | ||
| ) => { | ||
| let height = 0; | ||
|
|
||
| const dialog = Application.instance.dialogs.show({ | ||
| name: 'downloads-dialog', | ||
| browserWindow, | ||
| getBounds: () => { | ||
| const winBounds = browserWindow.getContentBounds(); | ||
| const maxHeight = winBounds.height - DIALOG_TOP - 16; | ||
|
|
||
| height = Math.round(Math.min(winBounds.height, height + 28)); | ||
|
|
||
| dialog.browserView.webContents.send( | ||
| `max-height`, | ||
| Math.min(maxHeight, height), | ||
| ); | ||
|
|
||
| return { | ||
| x: x - 350 + DIALOG_MARGIN, | ||
| y: y - DIALOG_MARGIN_TOP, | ||
| width: 350, | ||
| height, | ||
| }; | ||
| }, | ||
| onWindowBoundsUpdate: () => dialog.hide(), | ||
| }); | ||
|
|
||
| if (!dialog) return; | ||
|
|
||
| dialog.on('height', (e, h) => { | ||
| height = h; | ||
| dialog.rearrange(); | ||
| }); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,58 +1,51 @@ | ||
| import { BrowserWindow } from 'electron'; | ||
| import { Application } from '../application'; | ||
| import { DIALOG_MARGIN_TOP, DIALOG_MARGIN } from '~/constants/design'; | ||
|
|
||
| export const showExtensionDialog = ( | ||
| browserWindow: BrowserWindow, | ||
| x: number, | ||
| y: number, | ||
| url: string, | ||
| inspect = false, | ||
| ) => { | ||
| if (!process.env.ENABLE_EXTENSIONS) return; | ||
|
|
||
| let height = 512; | ||
| let width = 512; | ||
|
|
||
| const dialog = Application.instance.dialogs.show({ | ||
| name: 'extension-popup', | ||
| browserWindow, | ||
| getBounds: () => { | ||
| return { | ||
| x: x - width + DIALOG_MARGIN, | ||
| y: y - DIALOG_MARGIN_TOP, | ||
| height: Math.min(1024, height), | ||
| width: Math.min(1024, width), | ||
| }; | ||
| }, | ||
| onWindowBoundsUpdate: () => dialog.hide(), | ||
| }); | ||
|
|
||
| if (!dialog) return; | ||
|
|
||
| dialog.on('bounds', (e, w, h) => { | ||
| width = w; | ||
| height = h; | ||
| dialog.rearrange(); | ||
| }); | ||
|
|
||
| dialog.browserView.webContents.on( | ||
| 'will-attach-webview', | ||
| (e, webPreferences, params) => { | ||
| webPreferences.sandbox = true; | ||
| webPreferences.nodeIntegration = false; | ||
| webPreferences.contextIsolation = true; | ||
| }, | ||
| ); | ||
|
|
||
| dialog.on('loaded', (e) => { | ||
| e.reply('data', { url, inspect }); | ||
| }); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,45 +1,37 @@ | ||
| import { VIEW_Y_OFFSET } from '~/constants/design'; | ||
| import { BrowserWindow } from 'electron'; | ||
| import { Application } from '../application'; | ||
|
|
||
| export const showFindDialog = (browserWindow: BrowserWindow) => { | ||
| const appWindow = | ||
| Application.instance.windows.fromBrowserWindow(browserWindow); | ||
|
|
||
| const dialog = Application.instance.dialogs.show({ | ||
| name: 'find', | ||
| browserWindow, | ||
| devtools: false, | ||
| getBounds: () => { | ||
| const { width } = browserWindow.getContentBounds(); | ||
| return { | ||
| width: 416, | ||
| height: 70, | ||
| x: width - 416, | ||
| y: VIEW_Y_OFFSET, | ||
| }; | ||
| }, | ||
| tabAssociation: { | ||
| tabId: appWindow.viewManager.selectedId, | ||
| getTabInfo: (tabId) => { | ||
| const tab = appWindow.viewManager.views.get(tabId); | ||
| return tab.findInfo; | ||
| }, | ||
| setTabInfo: (tabId, info) => { | ||
| const tab = appWindow.viewManager.views.get(tabId); | ||
| tab.findInfo = info; | ||
| }, | ||
| }, | ||
| onWindowBoundsUpdate: (disposition) => { | ||
| if (disposition === 'resize') dialog.rearrange(); | ||
| }, | ||
| }); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,32 +1,24 @@ | ||
| import { BrowserWindow } from 'electron'; | ||
| import { Application } from '../application'; | ||
| import { DIALOG_MARGIN_TOP, DIALOG_MARGIN } from '~/constants/design'; | ||
|
|
||
| export const showMenuDialog = ( | ||
| browserWindow: BrowserWindow, | ||
| x: number, | ||
| y: number, | ||
| ) => { | ||
| const menuWidth = 330; | ||
| const dialog = Application.instance.dialogs.show({ | ||
| name: 'menu', | ||
| browserWindow, | ||
| getBounds: () => ({ | ||
| width: menuWidth, | ||
| height: 510, | ||
| x: x - menuWidth + DIALOG_MARGIN, | ||
| y: y - DIALOG_MARGIN_TOP, | ||
| }), | ||
| onWindowBoundsUpdate: () => { | ||
| dialog.hide(); | ||
| }, | ||
| }); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,49 +1,55 @@ | ||
| import { VIEW_Y_OFFSET } from '~/constants/design'; | ||
| import { BrowserWindow } from 'electron'; | ||
| import { Application } from '../application'; | ||
|
|
||
| export const requestPermission = ( | ||
| browserWindow: BrowserWindow, | ||
| name: string, | ||
| url: string, | ||
| details: any, | ||
| tabId: number, | ||
| ): Promise<boolean> => { | ||
| return new Promise((resolve, reject) => { | ||
| if ( | ||
| name === 'unknown' || | ||
| (name === 'media' && details.mediaTypes.length === 0) || | ||
| name === 'midiSysex' | ||
| ) { | ||
| return reject('Unknown permission'); | ||
| } | ||
|
|
||
| const appWindow = Application.instance.windows.fromBrowserWindow( | ||
| browserWindow, | ||
| ); | ||
|
|
||
| appWindow.viewManager.selected.requestedPermission = { name, url, details }; | ||
|
|
||
| const dialog = Application.instance.dialogs.show({ | ||
| name: 'permissions', | ||
| browserWindow, | ||
| getBounds: () => ({ | ||
| width: 366, | ||
| height: 165, | ||
| x: 0, | ||
| y: VIEW_Y_OFFSET, | ||
| }), | ||
| tabAssociation: { | ||
| tabId, | ||
| getTabInfo: (tabId) => { | ||
| const tab = appWindow.viewManager.views.get(tabId); | ||
| return tab.requestedPermission; | ||
| }, | ||
| }, | ||
| onWindowBoundsUpdate: (disposition) => { | ||
| if (disposition === 'resize') dialog.rearrange(); | ||
| }, | ||
| }); | ||
|
|
||
| if (!dialog) return; | ||
|
|
||
| dialog.on('result', (e, result) => { | ||
| resolve(result); | ||
| dialog.hide(); | ||
| }); | ||
| }); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,58 +1,51 @@ | ||
| import { BrowserWindow } from 'electron'; | ||
| import { Application } from '../application'; | ||
| import { DIALOG_MARGIN_TOP, DIALOG_MARGIN } from '~/constants/design'; | ||
| import { PersistentDialog } from './dialog'; | ||
| import { ERROR_PROTOCOL } from '~/constants/files'; | ||
|
|
||
| const HEIGHT = 256; | ||
|
|
||
| export class PreviewDialog extends PersistentDialog { | ||
| public visible = false; | ||
| public tab: { id?: number; x?: number; y?: number } = {}; | ||
|
|
||
| constructor() { | ||
| super({ | ||
| name: 'preview', | ||
| bounds: { | ||
| height: HEIGHT, | ||
| }, | ||
| hideTimeout: 150, | ||
| }); | ||
| } | ||
|
|
||
| public rearrange() { | ||
| const { width } = this.browserWindow.getContentBounds(); | ||
| super.rearrange({ width, y: this.tab.y }); | ||
| } | ||
|
|
||
| public async show(browserWindow: BrowserWindow) { | ||
| super.show(browserWindow, false); | ||
|
|
||
| const { | ||
| id, | ||
| url, | ||
| title, | ||
| errorURL, | ||
| } = Application.instance.windows | ||
| .fromBrowserWindow(browserWindow) | ||
| .viewManager.views.get(this.tab.id); | ||
|
|
||
| this.send('visible', true, { | ||
| id, | ||
| url: url.startsWith(`${ERROR_PROTOCOL}://`) ? errorURL : url, | ||
| title, | ||
| x: this.tab.x - 8, | ||
| }); | ||
| } | ||
|
|
||
| public hide(bringToTop = true) { | ||
| super.hide(bringToTop); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,92 +1,81 @@ | ||
| import { ipcMain, BrowserWindow } from 'electron'; | ||
| import { | ||
| DIALOG_MIN_HEIGHT, | ||
| DIALOG_MARGIN_TOP, | ||
| DIALOG_MARGIN, | ||
| } from '~/constants/design'; | ||
| import { PersistentDialog } from './dialog'; | ||
| import { Application } from '../application'; | ||
|
|
||
| const WIDTH = 800; | ||
| const HEIGHT = 80; | ||
|
|
||
| export class SearchDialog extends PersistentDialog { | ||
| private isPreviewVisible = false; | ||
|
|
||
| public data = { | ||
| text: '', | ||
| x: 0, | ||
| y: 0, | ||
| width: 200, | ||
| }; | ||
|
|
||
| public constructor() { | ||
| super({ | ||
| name: 'search', | ||
| bounds: { | ||
| width: WIDTH, | ||
| height: HEIGHT, | ||
| y: 48, | ||
| }, | ||
|
|
||
| devtools: false, | ||
| }); | ||
|
|
||
| ipcMain.on(`height-${this.id}`, (e, height) => { | ||
| super.rearrange({ | ||
| height: this.isPreviewVisible | ||
| ? Math.max(DIALOG_MIN_HEIGHT, HEIGHT + height) | ||
| : HEIGHT + height, | ||
| }); | ||
| }); | ||
|
|
||
| ipcMain.on(`addressbar-update-input-${this.id}`, (e, data) => { | ||
| this.browserWindow.webContents.send('addressbar-update-input', data); | ||
| }); | ||
| } | ||
|
|
||
| public rearrange() { | ||
| super.rearrange({ | ||
| x: this.data.x - DIALOG_MARGIN, | ||
| y: this.data.y - DIALOG_MARGIN_TOP, | ||
| width: this.data.width + 2 * DIALOG_MARGIN, | ||
| }); | ||
| } | ||
|
|
||
| private onResize = () => { | ||
| this.hide(); | ||
| }; | ||
|
|
||
| public async show(browserWindow: BrowserWindow) { | ||
| super.show(browserWindow, true, false); | ||
|
|
||
| browserWindow.once('resize', this.onResize); | ||
|
|
||
| this.send('visible', true, { | ||
| id: Application.instance.windows.current.viewManager.selectedId, | ||
| ...this.data, | ||
| }); | ||
|
|
||
| ipcMain.once('get-search-tabs', (e, tabs) => { | ||
| this.send('search-tabs', tabs); | ||
| }); | ||
|
|
||
| browserWindow.webContents.send('get-search-tabs'); | ||
| } | ||
|
|
||
| public hide(bringToTop = false) { | ||
| super.hide(bringToTop); | ||
| this.browserWindow.removeListener('resize', this.onResize); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,31 +1,24 @@ | ||
| import { BrowserWindow } from 'electron'; | ||
| import { Application } from '../application'; | ||
| import { DIALOG_MARGIN_TOP, DIALOG_MARGIN } from '~/constants/design'; | ||
|
|
||
| export const showTabGroupDialog = ( | ||
| browserWindow: BrowserWindow, | ||
| tabGroup: any, | ||
| ) => { | ||
| const dialog = Application.instance.dialogs.show({ | ||
| name: 'tabgroup', | ||
| browserWindow, | ||
| getBounds: () => ({ | ||
| width: 266, | ||
| height: 180, | ||
| x: tabGroup.x - DIALOG_MARGIN, | ||
| y: tabGroup.y - DIALOG_MARGIN_TOP, | ||
| }), | ||
| onWindowBoundsUpdate: () => dialog.hide(), | ||
| }); | ||
|
|
||
| if (!dialog) return; | ||
|
|
||
| dialog.handle('tabgroup', () => tabGroup); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| import { BrowserWindow } from 'electron'; | ||
| import { Application } from '../application'; | ||
| import { DIALOG_MARGIN_TOP, DIALOG_MARGIN } from '~/constants/design'; | ||
|
|
||
| export const showZoomDialog = ( | ||
| browserWindow: BrowserWindow, | ||
| x: number, | ||
| y: number, | ||
| ) => { | ||
| const tabId = Application.instance.windows.fromBrowserWindow(browserWindow) | ||
| .viewManager.selectedId; | ||
|
|
||
| const dialog = Application.instance.dialogs.show({ | ||
| name: 'zoom', | ||
| browserWindow, | ||
| getBounds: () => ({ | ||
| width: 280, | ||
| height: 100, | ||
| x: x - 280 + DIALOG_MARGIN, | ||
| y: y - DIALOG_MARGIN_TOP, | ||
| }), | ||
| onWindowBoundsUpdate: () => dialog.hide(), | ||
| }); | ||
|
|
||
| if (!dialog) return; | ||
|
|
||
| dialog.handle('tab-id', () => tabId); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import { RpcMainEvent, RpcMainHandler } from '@wexond/rpc-electron'; | ||
| import { session, webContents } from 'electron'; | ||
| import { | ||
| extensionMainChannel, | ||
| ExtensionMainService, | ||
| } from '~/common/rpc/extensions'; | ||
| import { Application } from './application'; | ||
|
|
||
| export class ExtensionServiceHandler | ||
| implements RpcMainHandler<ExtensionMainService> { | ||
| constructor() { | ||
| extensionMainChannel.getReceiver().handler = this; | ||
| } | ||
|
|
||
| inspectBackgroundPage(e: RpcMainEvent, id: string): void { | ||
| webContents | ||
| .getAllWebContents() | ||
| .find( | ||
| (x) => | ||
| x.session === Application.instance.sessions.view && | ||
| new URL(x.getURL()).hostname === id, | ||
| ) | ||
| .openDevTools({ mode: 'detach' }); | ||
| } | ||
|
|
||
| uninstall(e: RpcMainEvent, id: string): void { | ||
| Application.instance.sessions.uninstallExtension(id); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,131 @@ | ||
| import { | ||
| Menu, | ||
| nativeImage, | ||
| NativeImage, | ||
| MenuItemConstructorOptions, | ||
| app, | ||
| } from 'electron'; | ||
| import { join } from 'path'; | ||
| import { IBookmark } from '~/interfaces'; | ||
| import { Application } from '../application'; | ||
| import { AppWindow } from '../windows/app'; | ||
| import { showAddBookmarkDialog } from '../dialogs/add-bookmark'; | ||
|
|
||
| function getPath(file: string) { | ||
| if (process.env.NODE_ENV === 'development') { | ||
| return join( | ||
| app.getAppPath(), | ||
| 'src', | ||
| 'renderer', | ||
| 'resources', | ||
| 'icons', | ||
| `${file}.png`, | ||
| ); | ||
| } else { | ||
| const path = require(`~/renderer/resources/icons/${file}.png`); | ||
| return join(app.getAppPath(), `build`, path); | ||
| } | ||
| } | ||
|
|
||
| function getIcon( | ||
| favicon: string | undefined, | ||
| isFolder: boolean, | ||
| ): NativeImage | string { | ||
| if (favicon) { | ||
| let dataURL = Application.instance.storage.favicons.get(favicon); | ||
| if (dataURL) { | ||
| // some favicon data urls have a corrupted base 64 file type descriptor | ||
| // prefixed with data:png;base64, instead of data:image/png;base64, | ||
| // see: https://github.com/electron/electron/issues/23369 | ||
| if (!dataURL.split(',')[0].includes('image')) { | ||
| const split = dataURL.split(':'); | ||
| dataURL = split.join(':image/'); | ||
| } | ||
|
|
||
| const image = nativeImage | ||
| .createFromDataURL(dataURL) | ||
| .resize({ width: 16, height: 16 }); | ||
| return image; | ||
| } | ||
| } | ||
|
|
||
| if (Application.instance.settings.object.theme === 'wexond-dark') { | ||
| if (isFolder) { | ||
| return getPath('folder_light'); | ||
| } else { | ||
| return getPath('page_light'); | ||
| } | ||
| } else { | ||
| if (isFolder) { | ||
| return getPath('folder_dark'); | ||
| } else { | ||
| return getPath('page_dark'); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| export function createDropdown( | ||
| appWindow: AppWindow, | ||
| parentID: string, | ||
| bookmarks: IBookmark[], | ||
| ): Electron.Menu { | ||
| const folderBookmarks = bookmarks.filter( | ||
| ({ static: staticName, parent }) => !staticName && parent === parentID, | ||
| ); | ||
| const template = folderBookmarks.map<MenuItemConstructorOptions>( | ||
| ({ title, url, favicon, isFolder, _id }) => ({ | ||
| click: url | ||
| ? () => { | ||
| appWindow.viewManager.create({ url, active: true }); | ||
| } | ||
| : undefined, | ||
| label: title, | ||
| icon: getIcon(favicon, isFolder), | ||
| submenu: isFolder ? createDropdown(appWindow, _id, bookmarks) : undefined, | ||
| id: _id, | ||
| }), | ||
| ); | ||
|
|
||
| return template.length > 0 | ||
| ? Menu.buildFromTemplate(template) | ||
| : Menu.buildFromTemplate([{ label: '(empty)', enabled: false }]); | ||
| } | ||
|
|
||
| export function createMenu(appWindow: AppWindow, item: IBookmark) { | ||
| const { isFolder, url } = item; | ||
| const folderItems: MenuItemConstructorOptions[] = [ | ||
| { | ||
| label: 'Open in New Tab', | ||
| click: () => { | ||
| appWindow.viewManager.create({ url, active: true }); | ||
| }, | ||
| }, | ||
| { | ||
| type: 'separator', | ||
| }, | ||
| ]; | ||
|
|
||
| const template: MenuItemConstructorOptions[] = [ | ||
| ...(!isFolder ? folderItems : []), | ||
| { | ||
| label: 'Edit', | ||
| click: () => { | ||
| const windowBounds = appWindow.win.getBounds(); | ||
| showAddBookmarkDialog(appWindow.win, windowBounds.width - 20, 72, { | ||
| url: item.url, | ||
| title: item.title, | ||
| bookmark: item, | ||
| favicon: item.favicon, | ||
| }); | ||
| }, | ||
| }, | ||
| { | ||
| label: 'Delete', | ||
| click: () => { | ||
| Application.instance.storage.removeBookmark(item._id); | ||
| }, | ||
| }, | ||
| ]; | ||
|
|
||
| return Menu.buildFromTemplate(template); | ||
| } |