Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Early Draft Discussion: Delegate popup creation + action handling #50

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions packages/electron-chrome-extensions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,9 @@
"typescript": "^4.0.2",
"uuid": "^8.3.1",
"walkdir": "^0.4.1",
"webpack": "^4.44.1",
"webpack": "^4.46.0",
"webpack-cli": "^3.3.12"
},
"peerDependencies": {},
"babel": {
"presets": [
[
Expand Down
121 changes: 76 additions & 45 deletions packages/electron-chrome-extensions/src/browser/api/browser-action.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { Menu, MenuItem, protocol, nativeImage, app } from 'electron'
import { ExtensionContext } from '../context'
import { PopupView } from '../popup'
import { ExtensionEvent } from '../router'
import {app, Menu, MenuItem, nativeImage, NativeImage, protocol, WebContents} from 'electron'
import {ExtensionContext} from '../context'
import {PopupView} from '../popup'
import {ExtensionEvent} from '../router'
import {
getExtensionUrl,
getExtensionManifest,
getExtensionUrl,
getIconPath,
resolveExtensionPath,
matchSize,
ResizeType,
resolveExtensionPath,
} from './common'

const debug = require('debug')('electron-chrome-extensions:browserAction')
Expand All @@ -29,13 +29,27 @@ interface ExtensionAction {

type ExtensionActionKey = keyof ExtensionAction

interface ActivateDetails {
eventType: string
export interface ActivateDetails {
eventType: 'click' | 'contextmenu'
extensionId: string
tabId: number
anchorRect: { x: number; y: number; width: number; height: number }
}

interface BrowserActionItem {
id: string,
tabs?: { [key: string]: any },
title?: string,
iconModified?: number,
popup?: string
iconDataURI?: string
}

export interface BrowserActionState {
activeTabId?: number
actions: BrowserActionItem[]
}

const getBrowserActionDefaults = (extension: Electron.Extension): ExtensionAction | undefined => {
const manifest = getExtensionManifest(extension)
const { browser_action } = manifest
Expand Down Expand Up @@ -194,8 +208,39 @@ export class BrowserActionAPI {
session.protocol.registerBufferProtocol('crx', this.handleCrxRequest)
}

private makeIconImage = (extensionId: string, tabId?:string | null, imageSize: number = 32, resizeType: ResizeType = ResizeType.Up) => {
const extension = this.ctx.session.getExtension(extensionId)
if(!extension){
return null
}
let iconDetails: chrome.browserAction.TabIconDetails | undefined
const action = this.actionMap.get(extensionId)
if (action) {
iconDetails = (tabId && action.tabs[tabId]?.icon) || action.icon
}
if(!iconDetails){
return null
}

let iconImage: NativeImage | null = null
if (typeof iconDetails.path === 'string') {
const iconAbsPath = resolveExtensionPath(extension, iconDetails.path)
if (iconAbsPath) iconImage = nativeImage.createFromPath(iconAbsPath)
} else if (typeof iconDetails.path === 'object') {
const imagePath = matchSize(iconDetails.path, imageSize, resizeType)
const iconAbsPath = imagePath && resolveExtensionPath(extension, imagePath)
if (iconAbsPath) iconImage = nativeImage.createFromPath(iconAbsPath)
} else if (typeof iconDetails.imageData === 'string') {
iconImage = nativeImage.createFromDataURL(iconDetails.imageData)
} else if (typeof iconDetails.imageData === 'object') {
const imageData = matchSize(iconDetails.imageData as any, imageSize, resizeType)
iconImage = imageData ? nativeImage.createFromDataURL(imageData) : null
}
return iconImage
}

private handleCrxRequest = (
request: Electron.ProtocolRequest,
request: {url: string } | Electron.ProtocolRequest,
callback: (response: Electron.ProtocolResponse) => void
) => {
debug('%s', request.url)
Expand All @@ -215,33 +260,7 @@ export class BrowserActionAPI {
const imageSize = parseInt(fragments[2], 10)
const resizeType = parseInt(fragments[3], 10) || ResizeType.Up

const extension = this.ctx.session.getExtension(extensionId)

let iconDetails: chrome.browserAction.TabIconDetails | undefined

const action = this.actionMap.get(extensionId)
if (action) {
iconDetails = (tabId && action.tabs[tabId]?.icon) || action.icon
}

let iconImage

if (extension && iconDetails) {
if (typeof iconDetails.path === 'string') {
const iconAbsPath = resolveExtensionPath(extension, iconDetails.path)
if (iconAbsPath) iconImage = nativeImage.createFromPath(iconAbsPath)
} else if (typeof iconDetails.path === 'object') {
const imagePath = matchSize(iconDetails.path, imageSize, resizeType)
const iconAbsPath = imagePath && resolveExtensionPath(extension, imagePath)
if (iconAbsPath) iconImage = nativeImage.createFromPath(iconAbsPath)
} else if (typeof iconDetails.imageData === 'string') {
iconImage = nativeImage.createFromDataURL(iconDetails.imageData)
} else if (typeof iconDetails.imageData === 'object') {
const imageData = matchSize(iconDetails.imageData as any, imageSize, resizeType)
iconImage = imageData ? nativeImage.createFromDataURL(imageData) : undefined
}
}

const iconImage = this.makeIconImage(extensionId, tabId, imageSize, resizeType)
if (iconImage) {
response = {
statusCode: 200,
Expand Down Expand Up @@ -304,7 +323,7 @@ export class BrowserActionAPI {
}
}

private getState() {
private getState(): BrowserActionState {
// Get state without icon data.
const actions = Array.from(this.actionMap.entries()).map(([id, details]) => {
const { icon, tabs, ...rest } = details
Expand All @@ -327,7 +346,16 @@ export class BrowserActionAPI {
return { activeTabId: activeTab?.id, actions }
}

private activate({ sender }: ExtensionEvent, details: ActivateDetails) {
private getStateWithIcons(): BrowserActionState {
const state = this.getState()
state.actions = state.actions.map(s => ({
...s,
iconDataURI: this.makeIconImage(s.id)?.toDataURL() || "",
}))
return state
}

public activate({ sender }: {sender: WebContents}, details: ActivateDetails) {
const { eventType, extensionId, tabId } = details

debug(
Expand Down Expand Up @@ -373,17 +401,19 @@ export class BrowserActionAPI {
throw new Error('Unable to get BrowserWindow from active tab')
}

this.popup = new PopupView({
this.ctx.store.createPopup({
extensionId,
session: this.ctx.session,
parent: win,
url: popupUrl,
anchorRect,
})
session: this.ctx.session,
parent: win,
url: popupUrl,
anchorRect,
tabId: tab.id,
})

debug(`opened popup: ${popupUrl}`)

this.ctx.emit('browser-action-popup-created', this.popup)
// TODO: what are the key parts of this message? Seems to only be used in tests.
// this.ctx.emit('browser-action-popup-created', this.popup)
} else {
debug(`dispatching onClicked for ${extensionId}`)

Expand Down Expand Up @@ -448,6 +478,7 @@ export class BrowserActionAPI {
queueMicrotask(() => {
this.queuedUpdate = false
debug(`dispatching update to ${this.observers.size} observer(s)`)
this.ctx.store.onBrowserActionUpdate(this.getStateWithIcons())
Array.from(this.observers).forEach((observer) => {
if (!observer.isDestroyed()) {
observer.send('browserAction.update')
Expand Down
7 changes: 7 additions & 0 deletions packages/electron-chrome-extensions/src/browser/impl.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import {BrowserActionState} from "./api/browser-action"
import {PopupViewOptions} from "./popup"

/** App-specific implementation details for extensions. */
export interface ChromeExtensionImpl {
createTab?(
Expand All @@ -14,4 +17,8 @@ export interface ChromeExtensionImpl {

createWindow?(details: chrome.windows.CreateData): Promise<Electron.BrowserWindow>
removeWindow?(window: Electron.BrowserWindow): void

createPopup?(details: PopupViewOptions): void

onBrowserActionUpdate?(details: BrowserActionState): void
}
6 changes: 5 additions & 1 deletion packages/electron-chrome-extensions/src/browser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { EventEmitter } from 'events'
import path from 'path'
import { promises as fs } from 'fs'

import { BrowserActionAPI } from './api/browser-action'
import {ActivateDetails, BrowserActionAPI} from './api/browser-action'
import { TabsAPI } from './api/tabs'
import { WindowsAPI } from './api/windows'
import { WebNavigationAPI } from './api/web-navigation'
Expand Down Expand Up @@ -130,6 +130,10 @@ export class ElectronChromeExtensions extends EventEmitter {
}
}

activateExtension(tab: Electron.WebContents, details: ActivateDetails) {
this.api.browserAction.activate({sender: tab}, details)
}

/**
* Add webContents to be tracked as an extension host which will receive
* extension events when a chrome-extension:// resource is loaded.
Expand Down
3 changes: 2 additions & 1 deletion packages/electron-chrome-extensions/src/browser/popup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ export interface PopupAnchorRect {
height: number
}

interface PopupViewOptions {
export interface PopupViewOptions {
extensionId: string
session: Session
parent: BrowserWindow
url: string
anchorRect: PopupAnchorRect
tabId: number
}

export class PopupView {
Expand Down
19 changes: 19 additions & 0 deletions packages/electron-chrome-extensions/src/browser/store.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { BrowserWindow, webContents } from 'electron'
import { EventEmitter } from 'events'
import {BrowserActionState} from "./api/browser-action"
import { ContextMenuType } from './api/common'
import { ChromeExtensionImpl } from './impl'
import { ExtensionEvent } from './router'
import {PopupView, PopupViewOptions} from "./popup"

const debug = require('debug')('electron-chrome-extensions:store')

Expand Down Expand Up @@ -189,6 +191,23 @@ export class ExtensionStore extends EventEmitter {
}
}

// TODO: this probably doesn't belong in store.ts
createPopup(opts: PopupViewOptions){
if(this.impl.createPopup){
return this.impl.createPopup(opts)
} else {
return new PopupView(opts)
}

}

// TODO: this probably doesn't belong in store.ts
onBrowserActionUpdate(opts: BrowserActionState){
if(this.impl.onBrowserActionUpdate){
this.impl.onBrowserActionUpdate(opts)
}
}

buildMenuItems(extensionId: string, menuType: ContextMenuType): Electron.MenuItem[] {
// This function is overwritten by ContextMenusAPI
return []
Expand Down
2 changes: 1 addition & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4658,7 +4658,7 @@ webpack-sources@^1.4.0, webpack-sources@^1.4.1:
source-list-map "^2.0.0"
source-map "~0.6.1"

webpack@^4.44.1:
webpack@^4.46.0:
version "4.46.0"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.46.0.tgz#bf9b4404ea20a073605e0a011d188d77cb6ad542"
integrity sha512-6jJuJjg8znb/xRItk7bkT0+Q7AHCYjjFnvKIWQPkNIOyRqoCGvkOs0ipeQzrqz4l5FtN5ZI/ukEHroeX/o1/5Q==
Expand Down