Skip to content

Commit 5a4d6d8

Browse files
committed
init
0 parents  commit 5a4d6d8

36 files changed

+7157
-0
lines changed

.github/pull_request_template.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
✅ By sending this pull request, I agree to the [Contributor License Agreement](https://github.com/samuelmaddock/electron-browser-shell#contributor-license-agreement) of this project.

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
extensions/*
2+
!extensions/.gitkeep
3+
!extensions/README.md

LICENSE

+674
Large diffs are not rendered by default.

README.md

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# electron-browser-shell
2+
3+
A bare-bones, tabbed web browser with support for Chrome extensions—built on Electron.
4+
5+
This is a WIP testbed for development of Chrome extension support in Electron. Minimal dependencies are used as a means to allow developers to take what they need for their own projects.
6+
7+
![browser preview image showing 3 tabs and a youtube video](./screenshot.png)
8+
9+
## Usage
10+
11+
```bash
12+
# Get the code
13+
git clone [email protected]:samuelmaddock/electron-browser-shell.git
14+
cd electron-browser-shell
15+
16+
# Install and launch the browser
17+
yarn
18+
yarn start
19+
```
20+
21+
### Install extensions
22+
23+
Load unpacked extensions into `./extensions` then launch the browser.
24+
25+
## Roadmap
26+
27+
- [x] Browser tabs
28+
- [x] Unpacked extension loader
29+
- [x] Initial [`chrome.tabs` extensions API](https://developer.chrome.com/extensions/tabs)
30+
- [x] Initial [extension popup](https://developer.chrome.com/extensions/browserAction) support
31+
- [ ] Full support of [`chrome.*` extensions APIs](https://developer.chrome.com/extensions/devguide)
32+
- [ ] Robust extension popup support
33+
- [ ] Chrome webstore extension installer?
34+
35+
## Contributor license agreement
36+
37+
By sending a pull request, you hereby grant to owners and users of the
38+
electron-browser-shell project a perpetual, worldwide, non-exclusive,
39+
no-charge, royalty-free, irrevocable copyright license to reproduce, prepare
40+
derivative works of, publicly display, publicly perform, sublicense, and
41+
distribute your contributions and such derivative works.
42+
43+
The owners of the electron-browser-shell project will also be granted the right to relicense the
44+
contributed source code and its derivative works.

extensions/.gitkeep

Whitespace-only changes.

extensions/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Place unpacked extensions (not .crx archives) here to have them automatically loaded by the browser.

package.json

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "electron-browser-shell",
3+
"version": "1.0.0",
4+
"description": "A minimal browser shell built on Electron.",
5+
"private": true,
6+
"workspaces": [
7+
"packages/shell",
8+
"packages/electron-chrome-extensions"
9+
],
10+
"scripts": {
11+
"start": "cd packages/shell && npm start"
12+
},
13+
"license": "GPL-3.0",
14+
"author": "Samuel Maddock <[email protected]>",
15+
"dependencies": {},
16+
"repository": "[email protected]:samuelmaddock/electron-browser-shell.git",
17+
"prettier": {
18+
"printWidth": 100,
19+
"singleQuote": true,
20+
"jsonEnable": false,
21+
"semi": false
22+
}
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
dist
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{
2+
"name": "electron-chrome-extensions",
3+
"version": "1.0.0",
4+
"description": "Chrome extension support for Electron—built with tabbed browsers in mind.",
5+
"main": "dist/index.js",
6+
"scripts": {
7+
"build": "webpack"
8+
},
9+
"repository": "https://github.com/samuelmaddock/electron-browser-shell",
10+
"author": "Samuel Maddock <[email protected]>",
11+
"license": "GPL-3.0",
12+
"devDependencies": {
13+
"@babel/core": "^7.11.6",
14+
"@babel/plugin-proposal-class-properties": "^7.10.4",
15+
"@babel/plugin-proposal-optional-chaining": "^7.11.0",
16+
"@babel/preset-env": "^7.11.5",
17+
"@babel/preset-typescript": "^7.10.4",
18+
"@types/chrome": "^0.0.122",
19+
"babel-loader": "^8.1.0",
20+
"electron": "^10.1.1",
21+
"typescript": "^4.0.2",
22+
"webpack": "^4.44.1",
23+
"webpack-cli": "^3.3.12"
24+
},
25+
"babel": {
26+
"presets": [
27+
[
28+
"@babel/preset-env",
29+
{
30+
"targets": {
31+
"electron": "10.1.0"
32+
}
33+
}
34+
],
35+
"@babel/preset-typescript"
36+
],
37+
"plugins": [
38+
"@babel/plugin-proposal-class-properties",
39+
"@babel/plugin-proposal-optional-chaining"
40+
]
41+
}
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
export class ExtensionAPIState {
2+
tabs = new Set<Electron.WebContents>()
3+
extensionHosts = new Set<Electron.WebContents>()
4+
5+
constructor(public session: Electron.Session) {}
6+
7+
sendToHosts(eventName: string, ...args: any[]) {
8+
this.extensionHosts.forEach((host) => {
9+
if (host.isDestroyed()) return
10+
host.send(eventName, ...args)
11+
})
12+
}
13+
14+
getTabById(tabId: number) {
15+
return Array.from(this.tabs).find((tab) => tab.id === tabId)
16+
}
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { session, ipcMain, nativeImage } from 'electron'
2+
import { EventEmitter } from 'events'
3+
import * as path from 'path'
4+
5+
interface ExtensionAction {
6+
backgroundColor?: string
7+
text?: string
8+
title?: string
9+
icon?:
10+
| string
11+
| {
12+
path: string
13+
}
14+
popup?: {
15+
path: string
16+
}
17+
}
18+
19+
interface ExtensionActionStore extends Partial<ExtensionAction> {
20+
tabs: { [key: string]: ExtensionAction }
21+
}
22+
23+
export class BrowserActionAPI extends EventEmitter {
24+
sessionActionMap = new Map<Electron.Session, Map<string, ExtensionActionStore>>()
25+
26+
constructor() {
27+
super()
28+
29+
const setter = (propName: string) => (
30+
event: Electron.IpcMainInvokeEvent,
31+
extensionId: string,
32+
details: chrome.browserAction.TabDetails
33+
) => {
34+
const senderSession = event.sender.session
35+
const action = this.getAction(senderSession, extensionId)
36+
const { tabId, ...rest } = details
37+
38+
if (details.tabId) {
39+
const tabAction = action.tabs[details.tabId] || (action.tabs[details.tabId] = {})
40+
Object.assign(tabAction, rest)
41+
} else {
42+
Object.assign(action, rest)
43+
}
44+
}
45+
46+
ipcMain.handle('browserAction.setBadgeBackgroundColor', setter('backgroundColor'))
47+
ipcMain.handle('browserAction.setBadgeText', setter('text'))
48+
ipcMain.handle('browserAction.setTitle', setter('title'))
49+
ipcMain.handle('browserAction.setIcon', setter('icon'))
50+
ipcMain.handle('browserAction.setPopup', setter('popup'))
51+
52+
// extended methods for webui
53+
ipcMain.handle('browserAction.getAll', this.getAll.bind(this))
54+
55+
ipcMain.handle('click-action', this.onClicked.bind(this))
56+
}
57+
58+
private getAction(session: Electron.Session, extensionId: string) {
59+
let sessionActions = this.sessionActionMap.get(session)
60+
if (!sessionActions) {
61+
sessionActions = new Map()
62+
this.sessionActionMap.set(session, sessionActions)
63+
}
64+
65+
let action = sessionActions.get(extensionId)
66+
if (!action) {
67+
action = { tabs: {} }
68+
sessionActions.set(extensionId, action)
69+
}
70+
71+
return action
72+
}
73+
74+
private processIcon(extension: Electron.Extension) {
75+
const { browser_action } = extension.manifest
76+
const { default_icon } = browser_action
77+
78+
if (typeof default_icon === 'string') {
79+
const iconPath = path.join(extension.path, default_icon)
80+
const image = nativeImage.createFromPath(iconPath)
81+
return image.toDataURL()
82+
} else if (typeof default_icon === 'object') {
83+
const key = Object.keys(default_icon).pop() as any
84+
const iconPath = path.join(extension.path, default_icon[key])
85+
const image = nativeImage.createFromPath(iconPath)
86+
return image.toDataURL()
87+
}
88+
}
89+
90+
getPopupPath(session: Electron.Session, extensionId: string, tabId: string) {
91+
const action = this.getAction(session, extensionId)
92+
return action.tabs[tabId]?.popup?.path
93+
}
94+
95+
processExtensions(session: Electron.Session, extensions: Electron.Extension[]) {
96+
const populate = (extension: Electron.Extension) => {
97+
const manifest = extension.manifest as chrome.runtime.Manifest
98+
const { browser_action } = manifest
99+
if (browser_action) {
100+
const action = this.getAction(session, extension.id)
101+
102+
action.title = browser_action.default_title || manifest.name
103+
104+
const icon = this.processIcon(extension)
105+
if (icon) action.icon = icon
106+
}
107+
}
108+
109+
extensions.forEach(populate)
110+
}
111+
112+
private getAll(event: Electron.IpcMainInvokeEvent) {
113+
const senderSession = event.sender.session || session.defaultSession
114+
let sessionActions = this.sessionActionMap.get(senderSession)
115+
if (!sessionActions) return []
116+
117+
return Array.from(sessionActions.entries()).map((val: any) => ({ id: val[0], ...val[1] }))
118+
}
119+
120+
private onClicked(event: Electron.IpcMainInvokeEvent, extensionId: string) {
121+
this.emit('clicked', event, extensionId)
122+
}
123+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { BrowserWindow } from 'electron'
2+
3+
export interface TabContents extends Electron.WebContents {
4+
favicon?: string
5+
}
6+
7+
export const getParentWindowOfTab = (tab: TabContents): BrowserWindow | null => {
8+
switch (tab.getType()) {
9+
case 'window':
10+
return BrowserWindow.fromWebContents(tab)
11+
case 'browserView':
12+
case 'webview':
13+
return (tab as any).getOwnerBrowserWindow()
14+
}
15+
return null
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { ipcMain } from 'electron'
2+
import { EventEmitter } from 'events'
3+
4+
export class ContextMenusAPI extends EventEmitter {
5+
private menus = new Map</* extensionId */ string, any>()
6+
7+
constructor() {
8+
super()
9+
10+
ipcMain.handle('contextMenus.create', this.create)
11+
}
12+
13+
private addContextItem(extensionId: string, item: any) {
14+
let contextItems = this.menus.get(extensionId)
15+
if (!contextItems) {
16+
contextItems = []
17+
this.menus.set(extensionId, contextItems)
18+
}
19+
contextItems.push(item)
20+
}
21+
22+
private create = (
23+
event: Electron.IpcMainInvokeEvent,
24+
extensionId: string,
25+
createProperties: chrome.contextMenus.CreateProperties
26+
) => {
27+
const { id, type, title } = createProperties
28+
29+
if (this.menus.has(id!)) {
30+
// TODO: duplicate error
31+
return
32+
}
33+
34+
if (!title && type !== 'separator') {
35+
// TODO: error
36+
return
37+
}
38+
39+
if (createProperties.parentId) {
40+
// TODO
41+
} else {
42+
this.addContextItem(extensionId, createProperties)
43+
}
44+
}
45+
}

0 commit comments

Comments
 (0)