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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
/**
 * 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 vote down vote up
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 vote down vote up
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 vote down vote up
webFrame: typeof webFrame;
Example #10
Source File: wikiOperation.ts    From TidGi-Desktop with Mozilla Public License 2.0 5 votes vote down vote up
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 vote down vote up
// 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 vote down vote up
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 vote down vote up
webFrame: typeof webFrame;
Example #14
Source File: electron.service.ts    From blockcore-hub with MIT License 5 votes vote down vote up
webFrame: typeof webFrame;
Example #15
Source File: electron.service.ts    From VIR with MIT License 5 votes vote down vote up
// @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 vote down vote up
webFrame: typeof webFrame;
Example #17
Source File: index.ts    From electron-browser-shell with GNU General Public License v3.0 4 votes vote down vote up
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 vote down vote up
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 vote down vote up
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>
  );
}