electron#webFrame TypeScript Examples
The following examples show how to use
electron#webFrame.
You can vote up the ones you like or vote down the ones you don't like,
and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: electron.service.ts From league-profile-tool with MIT License | 6 votes |
constructor() {
if (this.isElectron) {
this.ipcRenderer = window.require('electron').ipcRenderer;
this.webFrame = window.require('electron').webFrame;
this.shell = window.require('electron').shell;
this.request = window.require('request-promise');
this.dialog = window.require('electron').remote.dialog;
this.childProcess = window.require('child_process');
this.fs = window.require('fs');
this.LCUConnector = window.require('lcu-connector');
}
}
Example #2
Source File: electron.service.ts From msfs-community-downloader with GNU Affero General Public License v3.0 | 6 votes |
constructor() {
// Conditional imports
if (this.isElectron) {
this.ipcRenderer = window.require('electron').ipcRenderer;
this.webFrame = window.require('electron').webFrame;
// If you want to use remote object in renderer process, please set enableRemoteModule to true in main.ts
this.remote = window.require('@electron/remote');
this.childProcess = window.require('child_process');
this.fs = window.require('fs');
}
}
Example #3
Source File: wikiOperation.ts From TidGi-Desktop with Mozilla Public License 2.0 | 6 votes |
ipcRenderer.on(WikiChannel.printTiddler, async (event, tiddlerName?: string) => {
const printer = await import('../services/libs/printer');
if (typeof tiddlerName !== 'string' || tiddlerName.length === 0) {
tiddlerName = await (webFrame.executeJavaScript(`
$tw.wiki.getTiddlerText('$:/temp/focussedTiddler');
`) as Promise<string>);
}
await executeTWJavaScriptWhenIdle(`
var page = (${printer.printTiddler.toString()})('${tiddlerName}');
page?.print?.();
page?.close?.();
`);
});
Example #4
Source File: electron.service.ts From VIR with MIT License | 6 votes |
constructor() {
// Conditional imports
if (this.isElectron) {
this.ipcRenderer = window.require('electron').ipcRenderer
this.webFrame = window.require('electron').webFrame
// If you wan to use remote object, pleanse set enableRemoteModule to
// true in main.ts
this.remote = window.require('electron').remote
this.childProcess = window.require('child_process')
this.fs = window.require('fs')
this.os = window.require('os')
this.path = window.require('path')
}
}
Example #5
Source File: electron.service.ts From blockcore-hub with MIT License | 6 votes |
constructor() {
if (this.isElectron) {
// const { BrowserWindow } = require('electron').remote
this.ipcRenderer = window.require('electron').ipcRenderer;
this.webFrame = window.require('electron').webFrame;
this.shell = window.require('electron').shell;
this.childProcess = window.require('child_process');
this.fs = window.require('fs');
}
}
Example #6
Source File: wikiOperation.ts From TidGi-Desktop with Mozilla Public License 2.0 | 6 votes |
/**
* Execute statement with $tw when idle, so there won't be significant lagging.
* Will retry till $tw is not undefined.
* @param script js statement to be executed, nothing will be returned
*/
async function executeTWJavaScriptWhenIdle(script: string, options?: { onlyWhenVisible?: boolean }): Promise<void> {
const executeHandlerCode =
options?.onlyWhenVisible === true
? `
if (document.visibilityState === 'visible') {
handler();
}`
: `handler();`;
await webFrame.executeJavaScript(`
new Promise((resolve, reject) => {
const handler = () => {
requestIdleCallback(() => {
if (typeof $tw !== 'undefined') {
try {
${script}
resolve();
} catch (error) {
reject(error);
}
} else {
// wait till $tw is not undefined.
setTimeout(handler, 500);
}
});
};
${executeHandlerCode}
})
`);
}
Example #7
Source File: remote.ts From TidGi-Desktop with Mozilla Public License 2.0 | 6 votes |
remoteMethods = {
buildContextMenuAndPopup: async (menus: MenuItemConstructorOptions[], parameters: IOnContextMenuInfo): Promise<() => void> => {
const [ipcSafeMenus, unregister] = rendererMenuItemProxy(menus);
await service.menu.buildContextMenuAndPopup(ipcSafeMenus, parameters, windowName);
return unregister;
},
closeCurrentWindow: async (): Promise<void> => {
await service.window.close(windowName);
},
/** call NodeJS.path */
getBaseName: (pathString?: string): string | undefined => {
if (typeof pathString === 'string') return path.basename(pathString);
},
getDirectoryName: (pathString?: string): string | undefined => {
if (typeof pathString === 'string') return path.dirname(pathString);
},
joinPath: (...paths: string[]): string => {
return path.join(...paths);
},
getLocalHostUrlWithActualIP,
/**
* an wrapper around setVisualZoomLevelLimits
*/
setVisualZoomLevelLimits: (minimumLevel: number, maximumLevel: number): void => {
webFrame.setVisualZoomLevelLimits(minimumLevel, maximumLevel);
},
registerOpenFindInPage: (handleOpenFindInPage: () => void): void => void ipcRenderer.on(WindowChannel.openFindInPage, handleOpenFindInPage),
unregisterOpenFindInPage: (handleOpenFindInPage: () => void): void => void ipcRenderer.removeListener(WindowChannel.openFindInPage, handleOpenFindInPage),
registerCloseFindInPage: (handleCloseFindInPage: () => void): void => void ipcRenderer.on(WindowChannel.closeFindInPage, handleCloseFindInPage),
unregisterCloseFindInPage: (handleCloseFindInPage: () => void): void => void ipcRenderer.removeListener(WindowChannel.closeFindInPage, handleCloseFindInPage),
registerUpdateFindInPageMatches: (updateFindInPageMatches: (event: Electron.IpcRendererEvent, activeMatchOrdinal: number, matches: number) => void): void =>
void ipcRenderer.on(ViewChannel.updateFindInPageMatches, updateFindInPageMatches),
unregisterUpdateFindInPageMatches: (updateFindInPageMatches: (event: Electron.IpcRendererEvent, activeMatchOrdinal: number, matches: number) => void): void =>
void ipcRenderer.removeListener(ViewChannel.updateFindInPageMatches, updateFindInPageMatches),
}
Example #8
Source File: electron.service.ts From StraxUI with MIT License | 6 votes |
constructor() {
// Conditional imports
if (this.isElectron) {
this.ipcRenderer = window.require('electron').ipcRenderer;
this.webFrame = window.require('electron').webFrame;
this.shell = window.require('electron').shell;
// If you want to use remote object in renderer process, please set enableRemoteModule to true in main.ts
this.remote = window.require('@electron/remote');
this.childProcess = window.require('child_process');
this.fs = window.require('fs');
}
}
Example #9
Source File: electron.service.ts From league-profile-tool with MIT License | 5 votes |
webFrame: typeof webFrame;
Example #10
Source File: wikiOperation.ts From TidGi-Desktop with Mozilla Public License 2.0 | 5 votes |
ipcRenderer.on(WikiChannel.runFilter, async (event, nonceReceived: number, filter: string) => {
const filterResult: string[] = await (webFrame.executeJavaScript(`
$tw.wiki.compileFilter('${filter}')()
`) as Promise<string[]>);
ipcRenderer.send(WikiChannel.runFilterDone, nonceReceived, filterResult);
});
Example #11
Source File: wikiOperation.ts From TidGi-Desktop with Mozilla Public License 2.0 | 5 votes |
// get tiddler text
ipcRenderer.on(WikiChannel.getTiddlerText, async (event, nonceReceived: number, title: string) => {
const tiddlerText: string = await (webFrame.executeJavaScript(`
$tw.wiki.getTiddlerText('${title}');
`) as Promise<string>);
ipcRenderer.send(WikiChannel.getTiddlerTextDone, nonceReceived, tiddlerText);
});
Example #12
Source File: view.ts From TidGi-Desktop with Mozilla Public License 2.0 | 5 votes |
async function executeJavaScriptInBrowserView(): Promise<void> {
// Fix Can't show file list of Google Drive
// https://github.com/electron/electron/issues/16587
// Fix chrome.runtime.sendMessage is undefined for FastMail
// https://github.com/atomery/singlebox/issues/21
const initialShouldPauseNotifications = await preference.get('pauseNotifications');
const { workspaceID } = browserViewMetaData as IPossibleWindowMeta<WindowMeta[WindowNames.view]>;
try {
await webFrame.executeJavaScript(`
(function() {
// Customize Notification behavior
// https://stackoverflow.com/questions/53390156/how-to-override-javascript-web-api-notification-object
// TODO: fix logic here, get latest pauseNotifications from preference, and focusWorkspace
const oldNotification = window.Notification;
let shouldPauseNotifications = ${
typeof initialShouldPauseNotifications === 'string' && initialShouldPauseNotifications.length > 0 ? `"${initialShouldPauseNotifications}"` : 'undefined'
};
window.Notification = function() {
if (!shouldPauseNotifications) {
const notification = new oldNotification(...arguments);
notification.addEventListener('click', () => {
window.postMessage({ type: '${WorkspaceChannel.focusWorkspace}', workspaceID: "${workspaceID ?? '-'}" });
});
return notification;
}
return null;
}
window.Notification.requestPermission = oldNotification.requestPermission;
Object.defineProperty(Notification, 'permission', {
get() {
return oldNotification.permission;
}
});
})();
`);
} catch (error) {
console.error(error);
}
}
Example #13
Source File: electron.service.ts From StraxUI with MIT License | 5 votes |
webFrame: typeof webFrame;
Example #14
Source File: electron.service.ts From blockcore-hub with MIT License | 5 votes |
webFrame: typeof webFrame;
Example #15
Source File: electron.service.ts From VIR with MIT License | 5 votes |
// @ts-ignore
webFrame: typeof webFrame
Example #16
Source File: electron.service.ts From msfs-community-downloader with GNU Affero General Public License v3.0 | 5 votes |
webFrame: typeof webFrame;
Example #17
Source File: index.ts From electron-browser-shell with GNU General Public License v3.0 | 4 votes |
injectExtensionAPIs = () => {
interface ExtensionMessageOptions {
noop?: boolean
serialize?: (...args: any[]) => any[]
}
const invokeExtension = async function (
extensionId: string,
fnName: string,
options: ExtensionMessageOptions = {},
...args: any[]
) {
const callback = typeof args[args.length - 1] === 'function' ? args.pop() : undefined
if (process.env.NODE_ENV === 'development') {
console.log(fnName, args)
}
if (options.noop) {
console.warn(`${fnName} is not yet implemented.`)
if (callback) callback()
return
}
if (options.serialize) {
args = options.serialize(...args)
}
let result
try {
result = await ipcRenderer.invoke('crx-msg', extensionId, fnName, ...args)
} catch (e) {
// TODO: Set chrome.runtime.lastError?
console.error(e)
result = undefined
}
if (process.env.NODE_ENV === 'development') {
console.log(fnName, '(result)', result)
}
if (callback) {
callback(result)
} else {
return result
}
}
const electronContext = {
invokeExtension,
addExtensionListener,
removeExtensionListener,
}
// Function body to run in the main world.
// IMPORTANT: This must be self-contained, no closure variable will be included!
function mainWorldScript() {
// Use context bridge API or closure variable when context isolation is disabled.
const electron = ((window as any).electron as typeof electronContext) || electronContext
const chrome = window.chrome || {}
const extensionId = chrome.runtime?.id
// NOTE: This uses a synchronous IPC to get the extension manifest.
// To avoid this, JS bindings for RendererExtensionRegistry would be
// required.
const manifest: chrome.runtime.Manifest =
(extensionId && chrome.runtime.getManifest()) || ({} as any)
const invokeExtension =
(fnName: string, opts: ExtensionMessageOptions = {}) =>
(...args: any[]) =>
electron.invokeExtension(extensionId, fnName, opts, ...args)
function imageData2base64(imageData: ImageData) {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
if (!ctx) return null
canvas.width = imageData.width
canvas.height = imageData.height
ctx.putImageData(imageData, 0, 0)
return canvas.toDataURL()
}
class ExtensionEvent<T extends Function> implements chrome.events.Event<T> {
constructor(private name: string) {}
addListener(callback: T) {
electron.addExtensionListener(extensionId, this.name, callback)
}
removeListener(callback: T) {
electron.removeExtensionListener(extensionId, this.name, callback)
}
getRules(callback: (rules: chrome.events.Rule[]) => void): void
getRules(ruleIdentifiers: string[], callback: (rules: chrome.events.Rule[]) => void): void
getRules(ruleIdentifiers: any, callback?: any) {
throw new Error('Method not implemented.')
}
hasListener(callback: T): boolean {
throw new Error('Method not implemented.')
}
removeRules(ruleIdentifiers?: string[] | undefined, callback?: (() => void) | undefined): void
removeRules(callback?: (() => void) | undefined): void
removeRules(ruleIdentifiers?: any, callback?: any) {
throw new Error('Method not implemented.')
}
addRules(
rules: chrome.events.Rule[],
callback?: ((rules: chrome.events.Rule[]) => void) | undefined
): void {
throw new Error('Method not implemented.')
}
hasListeners(): boolean {
throw new Error('Method not implemented.')
}
}
class ChromeSetting implements Partial<chrome.types.ChromeSetting> {
set() {}
get() {}
clear() {}
// onChange: chrome.types.ChromeSettingChangedEvent
}
type DeepPartial<T> = {
[P in keyof T]?: DeepPartial<T[P]>
}
type APIFactoryMap = {
[apiName in keyof typeof chrome]: {
shouldInject?: () => boolean
factory: (base: DeepPartial<typeof chrome[apiName]>) => DeepPartial<typeof chrome[apiName]>
}
}
/**
* Factories for each additional chrome.* API.
*/
const apiDefinitions: Partial<APIFactoryMap> = {
browserAction: {
shouldInject: () => !!manifest.browser_action,
factory: (base) => {
const api = {
...base,
setTitle: invokeExtension('browserAction.setTitle'),
getTitle: invokeExtension('browserAction.getTitle'),
setIcon: invokeExtension('browserAction.setIcon', {
serialize: (details: any) => {
if (details.imageData) {
if (details.imageData instanceof ImageData) {
details.imageData = imageData2base64(details.imageData)
} else {
details.imageData = Object.entries(details.imageData).reduce(
(obj: any, pair: any[]) => {
obj[pair[0]] = imageData2base64(pair[1])
return obj
},
{}
)
}
}
return [details]
},
}),
setPopup: invokeExtension('browserAction.setPopup'),
getPopup: invokeExtension('browserAction.getPopup'),
setBadgeText: invokeExtension('browserAction.setBadgeText'),
getBadgeText: invokeExtension('browserAction.getBadgeText'),
setBadgeBackgroundColor: invokeExtension('browserAction.setBadgeBackgroundColor'),
getBadgeBackgroundColor: invokeExtension('browserAction.getBadgeBackgroundColor'),
enable: invokeExtension('browserAction.enable', { noop: true }),
disable: invokeExtension('browserAction.disable', { noop: true }),
onClicked: new ExtensionEvent('browserAction.onClicked'),
}
return api
},
},
commands: {
factory: (base) => {
return {
...base,
getAll: invokeExtension('commands.getAll'),
onCommand: new ExtensionEvent('commands.onCommand'),
}
},
},
contextMenus: {
factory: (base) => {
let menuCounter = 0
const menuCallbacks: {
[key: string]: chrome.contextMenus.CreateProperties['onclick']
} = {}
const menuCreate = invokeExtension('contextMenus.create')
let hasInternalListener = false
const addInternalListener = () => {
api.onClicked.addListener((info, tab) => {
const callback = menuCallbacks[info.menuItemId]
if (callback && tab) callback(info, tab)
})
hasInternalListener = true
}
const api = {
...base,
create: function (
createProperties: chrome.contextMenus.CreateProperties,
callback?: Function
) {
if (typeof createProperties.id === 'undefined') {
createProperties.id = `${++menuCounter}`
}
if (createProperties.onclick) {
if (!hasInternalListener) addInternalListener()
menuCallbacks[createProperties.id] = createProperties.onclick
delete createProperties.onclick
}
menuCreate(createProperties, callback)
return createProperties.id
},
update: invokeExtension('contextMenus.update', { noop: true }),
remove: invokeExtension('contextMenus.remove'),
removeAll: invokeExtension('contextMenus.removeAll'),
onClicked: new ExtensionEvent<
(info: chrome.contextMenus.OnClickData, tab: chrome.tabs.Tab) => void
>('contextMenus.onClicked'),
}
return api
},
},
cookies: {
factory: (base) => {
return {
...base,
get: invokeExtension('cookies.get'),
getAll: invokeExtension('cookies.getAll'),
set: invokeExtension('cookies.set'),
remove: invokeExtension('cookies.remove'),
getAllCookieStores: invokeExtension('cookies.getAllCookieStores'),
onChanged: new ExtensionEvent('cookies.onChanged'),
}
},
},
extension: {
factory: (base) => {
return {
...base,
isAllowedIncognitoAccess: () => false,
// TODO: Add native implementation
getViews: () => [],
}
},
},
notifications: {
factory: (base) => {
return {
...base,
clear: invokeExtension('notifications.clear'),
create: invokeExtension('notifications.create'),
getAll: invokeExtension('notifications.getAll'),
getPermissionLevel: invokeExtension('notifications.getPermissionLevel'),
update: invokeExtension('notifications.update'),
onClicked: new ExtensionEvent('notifications.onClicked'),
onButtonClicked: new ExtensionEvent('notifications.onButtonClicked'),
onClosed: new ExtensionEvent('notifications.onClosed'),
}
},
},
privacy: {
factory: (base) => {
return {
...base,
network: {
networkPredictionEnabled: new ChromeSetting(),
webRTCIPHandlingPolicy: new ChromeSetting(),
},
websites: {
hyperlinkAuditingEnabled: new ChromeSetting(),
},
}
},
},
runtime: {
factory: (base) => {
return {
...base,
openOptionsPage: invokeExtension('runtime.openOptionsPage'),
}
},
},
storage: {
factory: (base) => {
const local = base && base.local
return {
...base,
// TODO: provide a backend for browsers to opt-in to
managed: local,
sync: local,
}
},
},
tabs: {
factory: (base) => {
const api = {
...base,
create: invokeExtension('tabs.create'),
executeScript: function (arg1: unknown, arg2: unknown, arg3: unknown) {
// Electron's implementation of chrome.tabs.executeScript is in
// C++, but it doesn't support implicit execution in the active
// tab. To handle this, we need to get the active tab ID and
// pass it into the C++ implementation ourselves.
if (typeof arg1 === 'object') {
api.query(
{ active: true, windowId: chrome.windows.WINDOW_ID_CURRENT },
([activeTab]: chrome.tabs.Tab[]) => {
api.executeScript(activeTab.id, arg1, arg2)
}
)
} else {
;(base.executeScript as typeof chrome.tabs.executeScript)(
arg1 as number,
arg2 as chrome.tabs.InjectDetails,
arg3 as () => {}
)
}
},
get: invokeExtension('tabs.get'),
getCurrent: invokeExtension('tabs.getCurrent'),
getAllInWindow: invokeExtension('tabs.getAllInWindow'),
insertCSS: invokeExtension('tabs.insertCSS'),
query: invokeExtension('tabs.query'),
reload: invokeExtension('tabs.reload'),
update: invokeExtension('tabs.update'),
remove: invokeExtension('tabs.remove'),
goBack: invokeExtension('tabs.goBack'),
goForward: invokeExtension('tabs.goForward'),
onCreated: new ExtensionEvent('tabs.onCreated'),
onRemoved: new ExtensionEvent('tabs.onRemoved'),
onUpdated: new ExtensionEvent('tabs.onUpdated'),
onActivated: new ExtensionEvent('tabs.onActivated'),
onReplaced: new ExtensionEvent('tabs.onReplaced'),
}
return api
},
},
webNavigation: {
factory: (base) => {
return {
...base,
getFrame: invokeExtension('webNavigation.getFrame'),
getAllFrames: invokeExtension('webNavigation.getAllFrames'),
onBeforeNavigate: new ExtensionEvent('webNavigation.onBeforeNavigate'),
onCommitted: new ExtensionEvent('webNavigation.onCommitted'),
onCompleted: new ExtensionEvent('webNavigation.onCompleted'),
onCreatedNavigationTarget: new ExtensionEvent(
'webNavigation.onCreatedNavigationTarget'
),
onDOMContentLoaded: new ExtensionEvent('webNavigation.onDOMContentLoaded'),
onErrorOccurred: new ExtensionEvent('webNavigation.onErrorOccurred'),
onHistoryStateUpdated: new ExtensionEvent('webNavigation.onHistoryStateUpdated'),
onReferenceFragmentUpdated: new ExtensionEvent(
'webNavigation.onReferenceFragmentUpdated'
),
onTabReplaced: new ExtensionEvent('webNavigation.onTabReplaced'),
}
},
},
webRequest: {
factory: (base) => {
return {
...base,
onHeadersReceived: new ExtensionEvent('webRequest.onHeadersReceived'),
}
},
},
windows: {
factory: (base) => {
return {
...base,
WINDOW_ID_NONE: -1,
WINDOW_ID_CURRENT: -2,
get: invokeExtension('windows.get'),
getLastFocused: invokeExtension('windows.getLastFocused'),
getAll: invokeExtension('windows.getAll'),
create: invokeExtension('windows.create'),
update: invokeExtension('windows.update'),
remove: invokeExtension('windows.remove'),
onCreated: new ExtensionEvent('windows.onCreated'),
onRemoved: new ExtensionEvent('windows.onRemoved'),
onFocusChanged: new ExtensionEvent('windows.onFocusChanged'),
}
},
},
}
// Initialize APIs
Object.keys(apiDefinitions).forEach((key: any) => {
const apiName: keyof typeof chrome = key
const baseApi = chrome[apiName] as any
const api = apiDefinitions[apiName]!
// Allow APIs to opt-out of being available in this context.
if (api.shouldInject && !api.shouldInject()) return
Object.defineProperty(chrome, apiName, {
value: api.factory(baseApi),
enumerable: true,
configurable: true,
})
})
// Remove access to internals
delete (window as any).electron
Object.freeze(chrome)
void 0 // no return
}
try {
// Expose extension IPC to main world
contextBridge.exposeInMainWorld('electron', electronContext)
// Mutate global 'chrome' object with additional APIs in the main world.
webFrame.executeJavaScript(`(${mainWorldScript}());`)
} catch {
// contextBridge threw an error which means we're in the main world so we
// can just execute our function.
mainWorldScript()
}
}
Example #18
Source File: browser-action.ts From electron-browser-shell with GNU General Public License v3.0 | 4 votes |
injectBrowserAction = () => {
const actionMap = new Map<string, any>()
const internalEmitter = new EventEmitter()
const observerCounts = new Map<string, number>()
const invoke = <T>(name: string, partition: string, ...args: any[]): Promise<T> => {
return ipcRenderer.invoke('crx-msg-remote', partition, name, ...args)
}
interface ActivateDetails {
eventType: string
extensionId: string
tabId: number
anchorRect: { x: number; y: number; width: number; height: number }
}
const browserAction = {
addEventListener(name: string, listener: (...args: any[]) => void) {
internalEmitter.addListener(name, listener)
},
removeEventListener(name: string, listener: (...args: any[]) => void) {
internalEmitter.removeListener(name, listener)
},
getAction(extensionId: string) {
return actionMap.get(extensionId)
},
async getState(partition: string): Promise<{ activeTabId?: number; actions: any[] }> {
const state = await invoke<any>('browserAction.getState', partition)
for (const action of state.actions) {
actionMap.set(action.id, action)
}
queueMicrotask(() => internalEmitter.emit('update', state))
return state
},
activate: (partition: string, details: ActivateDetails) => {
return invoke('browserAction.activate', partition, details)
},
addObserver(partition: string) {
let count = observerCounts.has(partition) ? observerCounts.get(partition)! : 0
count = count + 1
observerCounts.set(partition, count)
if (count === 1) {
invoke('browserAction.addObserver', partition)
}
},
removeObserver(partition: string) {
let count = observerCounts.has(partition) ? observerCounts.get(partition)! : 0
count = Math.max(count - 1, 0)
observerCounts.set(partition, count)
if (count === 0) {
invoke('browserAction.removeObserver', partition)
}
},
}
ipcRenderer.on('browserAction.update', () => {
for (const partition of observerCounts.keys()) {
browserAction.getState(partition)
}
})
// Function body to run in the main world.
// IMPORTANT: This must be self-contained, no closure variables can be used!
function mainWorldScript() {
const DEFAULT_PARTITION = '_self'
class BrowserActionElement extends HTMLButtonElement {
private updateId?: number
private badge?: HTMLDivElement
private pendingIcon?: HTMLImageElement
get id(): string {
return this.getAttribute('id') || ''
}
set id(id: string) {
this.setAttribute('id', id)
}
get tab(): number {
const tabId = parseInt(this.getAttribute('tab') || '', 10)
return typeof tabId === 'number' && !isNaN(tabId) ? tabId : -1
}
set tab(tab: number) {
this.setAttribute('tab', `${tab}`)
}
get partition(): string | null {
return this.getAttribute('partition')
}
set partition(partition: string | null) {
if (partition) {
this.setAttribute('partition', partition)
} else {
this.removeAttribute('partition')
}
}
static get observedAttributes() {
return ['id', 'tab', 'partition']
}
constructor() {
super()
// TODO: event delegation
this.addEventListener('click', this.onClick.bind(this))
this.addEventListener('contextmenu', this.onContextMenu.bind(this))
}
connectedCallback() {
if (this.isConnected) {
this.update()
}
}
disconnectedCallback() {
if (this.updateId) {
cancelAnimationFrame(this.updateId)
this.updateId = undefined
}
if (this.pendingIcon) {
this.pendingIcon = undefined
}
}
attributeChangedCallback() {
if (this.isConnected) {
this.update()
}
}
private activate(event: Event) {
const rect = this.getBoundingClientRect()
browserAction.activate(this.partition || DEFAULT_PARTITION, {
eventType: event.type,
extensionId: this.id,
tabId: this.tab,
anchorRect: {
x: rect.left,
y: rect.top,
width: rect.width,
height: rect.height,
},
})
}
private onClick(event: MouseEvent) {
this.activate(event)
}
private onContextMenu(event: MouseEvent) {
event.stopImmediatePropagation()
event.preventDefault()
this.activate(event)
}
private getBadge() {
let badge = this.badge
if (!badge) {
this.badge = badge = document.createElement('div')
badge.className = 'badge'
;(badge as any).part = 'badge'
this.appendChild(badge)
}
return badge
}
private update() {
if (this.updateId) return
this.updateId = requestAnimationFrame(this.updateCallback.bind(this))
}
private updateIcon(info: any) {
const iconSize = 32
const resizeType = 2
const timeParam = info.iconModified ? `&t=${info.iconModified}` : ''
const iconUrl = `crx://extension-icon/${this.id}/${iconSize}/${resizeType}?tabId=${this.tab}${timeParam}`
const bgImage = `url(${iconUrl})`
if (this.pendingIcon) {
this.pendingIcon = undefined
}
// Preload icon to prevent it from blinking
const img = (this.pendingIcon = new Image())
img.onload = () => {
if (this.isConnected) {
this.style.backgroundImage = bgImage
this.pendingIcon = undefined
}
}
img.src = iconUrl
}
private updateCallback() {
this.updateId = undefined
const action = browserAction.getAction(this.id)
const activeTabId = this.tab
const tabInfo = activeTabId > -1 ? action.tabs[activeTabId] : {}
const info = { ...tabInfo, ...action }
this.title = typeof info.title === 'string' ? info.title : ''
this.updateIcon(info)
if (info.text) {
const badge = this.getBadge()
badge.textContent = info.text
badge.style.color = '#fff' // TODO: determine bg lightness?
badge.style.backgroundColor = info.color
} else if (this.badge) {
this.badge.remove()
this.badge = undefined
}
}
}
customElements.define('browser-action', BrowserActionElement, { extends: 'button' })
class BrowserActionListElement extends HTMLElement {
private observing: boolean = false
get tab(): number | null {
const tabId = parseInt(this.getAttribute('tab') || '', 10)
return typeof tabId === 'number' && !isNaN(tabId) ? tabId : null
}
set tab(tab: number | null) {
if (typeof tab === 'number') {
this.setAttribute('tab', `${tab}`)
} else {
this.removeAttribute('tab')
}
}
get partition(): string | null {
return this.getAttribute('partition')
}
set partition(partition: string | null) {
if (partition) {
this.setAttribute('partition', partition)
} else {
this.removeAttribute('partition')
}
}
static get observedAttributes() {
return ['tab', 'partition']
}
constructor() {
super()
const shadowRoot = this.attachShadow({ mode: 'open' })
const style = document.createElement('style')
style.textContent = `
:host {
display: flex;
flex-direction: row;
gap: 5px;
}
.action {
width: 28px;
height: 28px;
background-color: transparent;
background-position: center;
background-repeat: no-repeat;
background-size: 70%;
border: none;
border-radius: 4px;
padding: 0;
position: relative;
outline: none;
}
.action:hover {
background-color: var(--browser-action-hover-bg, rgba(255, 255, 255, 0.3));
}
.badge {
box-shadow: 0px 0px 1px 1px var(--browser-action-badge-outline, #444);
box-sizing: border-box;
max-width: 100%;
height: 12px;
padding: 0 2px;
border-radius: 2px;
position: absolute;
bottom: 1px;
right: 0;
pointer-events: none;
line-height: 1.5;
font-size: 9px;
font-weight: 400;
overflow: hidden;
white-space: nowrap;
}`
shadowRoot.appendChild(style)
}
connectedCallback() {
if (this.isConnected) {
this.startObserving()
this.fetchState()
}
}
disconnectedCallback() {
this.stopObserving()
}
attributeChangedCallback(name: string, oldValue: any, newValue: any) {
if (oldValue === newValue) return
if (this.isConnected) {
this.fetchState()
}
}
private startObserving() {
if (this.observing) return
browserAction.addEventListener('update', this.update)
browserAction.addObserver(this.partition || DEFAULT_PARTITION)
this.observing = true
}
private stopObserving() {
if (!this.observing) return
browserAction.removeEventListener('update', this.update)
browserAction.removeObserver(this.partition || DEFAULT_PARTITION)
this.observing = false
}
private fetchState = async () => {
try {
await browserAction.getState(this.partition || DEFAULT_PARTITION)
} catch {
console.error(
`browser-action-list failed to update [tab: ${this.tab}, partition: '${this.partition}']`
)
}
}
private update = (state: any) => {
const tabId =
typeof this.tab === 'number' && this.tab >= 0 ? this.tab : state.activeTabId || -1
for (const action of state.actions) {
let browserActionNode = this.shadowRoot?.querySelector(
`[id=${action.id}]`
) as BrowserActionElement
if (!browserActionNode) {
const node = document.createElement('button', {
is: 'browser-action',
}) as BrowserActionElement
node.id = action.id
node.className = 'action'
;(node as any).part = 'action'
browserActionNode = node
this.shadowRoot?.appendChild(browserActionNode)
}
if (this.partition) browserActionNode.partition = this.partition
browserActionNode.tab = tabId
}
}
}
customElements.define('browser-action-list', BrowserActionListElement)
}
try {
contextBridge.exposeInMainWorld('browserAction', browserAction)
// Must execute script in main world to modify custom component registry.
webFrame.executeJavaScript(`(${mainWorldScript}());`)
} catch {
// When contextIsolation is disabled, contextBridge will throw an error.
// If that's the case, we're in the main world so we can just execute our
// function.
mainWorldScript()
}
}
Example #19
Source File: AppDataProvider.tsx From yana with MIT License | 4 votes |
AppDataProvider: React.FC = props => {
const [isCreatingWorkspace, setIsCreatingWorkspace] = useState(false);
const [appData, setAppData] = useState<AppData>({ workspaces: [], settings: defaultSettings, telemetryId: '_' });
const [currentWorkspace, setCurrentWorkspace] = useState<WorkSpace>(appData.workspaces[0]);
const [autoBackup, setAutoBackup] = useState<undefined | AutoBackupService>();
const [lastAutoBackup, setLastAutoBackup] = useState(0);
const isInInitialCreationScreen = !appData.workspaces[0];
useAsyncEffect(async () => {
if (!fsLib.existsSync(userDataFolder)) {
await fs.mkdir(userDataFolder);
}
let appData: AppData = {
workspaces: [],
settings: defaultSettings,
telemetryId: uuid(),
};
if (!fsLib.existsSync(appDataFile)) {
await fs.writeFile(appDataFile, JSON.stringify(appData));
} else {
appData = {
...appData,
...JSON.parse(await fs.readFile(appDataFile, { encoding: 'utf8' })),
};
}
appData.settings = { ...defaultSettings, ...appData.settings };
setAppData(appData);
setCurrentWorkspace(appData.workspaces[0]);
const autoBackupService = new AutoBackupService(appData.workspaces, appData.settings, setLastAutoBackup);
await autoBackupService.load();
setAutoBackup(autoBackupService);
webFrame.setZoomFactor(appData.settings.zoomFactor);
LogService.applySettings(appData.settings);
}, []);
const ctx: AppDataContextValue = {
...appData,
lastAutoBackup,
currentWorkspace: currentWorkspace,
setWorkSpace: ws => {
setCurrentWorkspace(ws);
TelemetryService?.trackEvent(...TelemetryEvents.Workspaces.switch);
},
openWorkspaceCreationWindow: () => setIsCreatingWorkspace(true),
addWorkSpace: async (name, path) => {
const workspace: WorkSpace = {
name,
dataSourceType: 'sqlite3', // TODO
dataSourceOptions: {
sourcePath: path,
},
};
if (appData.workspaces.find(w => w.name === name)) {
throw Error(`A workspace with the name ${name} already exists.`);
}
const newAppData: AppData = {
...appData,
workspaces: [...appData.workspaces, workspace],
};
await fs.writeFile(appDataFile, JSON.stringify(newAppData));
setAppData(newAppData);
autoBackup?.addWorkspace(workspace);
setCurrentWorkspace(workspace);
TelemetryService?.trackEvent(...TelemetryEvents.Workspaces.addExisting);
},
createWorkSpace: async (name, path, dataSourceType, empty?: boolean) => {
if (appData.workspaces.find(ws => ws.name === name)) {
throw Error('A workspace with that name already exists.');
}
const workspace = await initializeWorkspace(name, path, dataSourceType, empty);
const newAppData: AppData = {
...appData,
workspaces: [...appData.workspaces, workspace],
};
await fs.writeFile(appDataFile, JSON.stringify(newAppData));
setAppData(newAppData);
autoBackup?.addWorkspace(workspace);
TelemetryService?.trackEvent(...TelemetryEvents.Workspaces.create);
return workspace;
},
deleteWorkspace: async (workspace, deleteData) => {
if (currentWorkspace.dataSourceOptions.sourcePath === workspace.dataSourceOptions.sourcePath) {
return Alerter.Instance.alert({
content: `Cannot delete the workspace that is currently opened. Please open a different workspace and retry deletion.`,
intent: 'danger',
canEscapeKeyCancel: true,
canOutsideClickCancel: true,
icon: 'warning-sign',
});
}
if (deleteData) {
await new Promise((res, rev) => {
rimraf(workspace.dataSourceOptions.sourcePath, error => {
if (error) {
Alerter.Instance.alert({ content: 'Error: ' + error.message });
} else {
res();
}
});
});
TelemetryService?.trackEvent(...TelemetryEvents.Workspaces.deleteFromDisk);
} else {
TelemetryService?.trackEvent(...TelemetryEvents.Workspaces.deleteFromYana);
}
const newAppData: AppData = {
...appData,
workspaces: appData.workspaces.filter(w => w.name !== workspace.name),
};
await fs.writeFile(appDataFile, JSON.stringify(newAppData));
setAppData(newAppData);
autoBackup?.removeWorkspace(workspace);
setCurrentWorkspace(newAppData.workspaces[0]);
},
moveWorkspace: async (workspace, direction) => {
const oldIndex = appData.workspaces.findIndex(w => w.name === workspace.name);
if (
(oldIndex === 0 && direction === 'up') ||
(oldIndex === appData.workspaces.length - 1 && direction === 'down')
) {
return;
}
const newAppData: AppData = {
...appData,
workspaces: moveItem(appData.workspaces, oldIndex, direction === 'up' ? oldIndex - 1 : oldIndex + 1),
};
await fs.writeFile(appDataFile, JSON.stringify(newAppData));
setAppData(newAppData);
},
renameWorkspace: async (workspace, newName) => {
if (appData.workspaces.find(ws => ws.name === newName)) {
throw Error('A workspace with that name already exists.');
}
const oldWorkspace = appData.workspaces.find(ws => ws.name === workspace.name);
if (!oldWorkspace) {
throw Error(`The old workspace ${workspace.name} was not found.`);
}
const newWorkspace = {
...oldWorkspace,
name: newName,
};
const newAppData: AppData = {
...appData,
workspaces: appData.workspaces.map(ws => (ws.name === workspace.name ? newWorkspace : ws)),
};
await fs.writeFile(appDataFile, JSON.stringify(newAppData));
setAppData(newAppData);
if (currentWorkspace.name === workspace.name) {
ctx.setWorkSpace(newWorkspace);
}
},
saveSettings: async (settings: Partial<SettingsObject>) => {
const newAppData: AppData = {
...appData,
settings: { ...defaultSettings, ...appData.settings, ...settings },
};
await fs.writeFile(appDataFile, JSON.stringify(newAppData));
setAppData(newAppData);
webFrame.setZoomFactor(newAppData.settings.zoomFactor);
},
};
return (
<AppDataContext.Provider value={ctx}>
<div key={currentWorkspace?.dataSourceOptions?.sourcePath || '__'} style={{ height: '100%' }}>
{isCreatingWorkspace || isInInitialCreationScreen ? (
<CreateWorkspaceWindow
isInitialCreationScreen={isInInitialCreationScreen}
defaultWorkspaceName={getNewWorkspaceName(ctx)}
onClose={() =>
isInInitialCreationScreen ? remote.getCurrentWindow().close() : setIsCreatingWorkspace(false)
}
onCreate={async (name, wsPath) => {
try {
const workspace = await ctx.createWorkSpace(name, wsPath, 'sqlite3'); // TODO
setCurrentWorkspace(workspace);
setIsCreatingWorkspace(false);
} catch (e) {
Alerter.Instance.alert({
content: `Error: ${e.message}`,
intent: 'danger',
canEscapeKeyCancel: true,
canOutsideClickCancel: true,
icon: 'warning-sign',
});
}
}}
onImported={() => {
setCurrentWorkspace(appData.workspaces[0]);
}}
/>
) : (
props.children
)}
</div>
</AppDataContext.Provider>
);
}