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

[Bug]: incorrect behavior of the WebContentsView when the electron app window is inactive #42059

Closed
3 tasks done
HyrepBall opened this issue May 7, 2024 · 0 comments
Closed
3 tasks done

Comments

@HyrepBall
Copy link

HyrepBall commented May 7, 2024

Preflight Checklist

Electron Version

30.0.1

What operating system are you using?

Windows

Operating System Version

Windows 11 Pro version 23H2 Build 22631.3447

What arch are you using?

x64

Last Known Working Electron version

No response

Expected Behavior

the content in the WebContentsView should load in any state of the application, even when it is minimized

Actual Behavior

the content in the WebContentsView becomes visible only when the application becomes active (you can see this at 55 seconds in the video)

20240507132712.online-video-cutter.com.mp4

Testcase Gist URL

No response

Additional Information

In the video, Google Meet was used to reproduce the bug to clearly show the behavior of the application window in inactive mode

There was a bug with both BrowserView and WebContentsView

backgroundThrottling: false doesn't solve my problem.

In my application there are 2 types of files: local ones which i download like video or pdf and remote (simple URLs) that renders with WebContentsView. Just in the video I compare the behavior of a regular vue.js component consisting of and the WebContentsView component in which the content begins to be displayed only if the application is made active (55 seconds in the video)

main.js

import { app, BrowserWindow, ipcMain } from 'electron'
import path from 'path'

import icon from '../assets/icon.png'
import { download } from './download'
import { createBrowserView } from './browserView'
import { createBrowserViewCustomEvents } from '../helpers/browserView'
import { getAppPaths } from './getAppPaths'

process.env.ROOT = path.join(__dirname, '..')
process.env.DIST = path.join(process.env.ROOT, 'dist-electron')
process.env.VITE_PUBLIC = process.env.VITE_DEV_SERVER_URL
  ? path.join(process.env.ROOT, 'public')
  : path.join(process.env.ROOT, '.output/public')
process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true'

const { VITE_DEV_SERVER_URL, VITE_PUBLIC, NODE_ENV } = process.env

let win: BrowserWindow
const preload = path.join(process.env.DIST, 'preload.js')

async function bootstrap() {
  win = new BrowserWindow({
    autoHideMenuBar: false,
    ...(process.platform === 'linux' ? { icon } : {}),
    webPreferences: {
      backgroundThrottling: false,
      preload,
      nodeIntegrationInWorker: true,
      contextIsolation: false,
      nodeIntegration: true,
      webSecurity: false,
    },
  })

  ipcMain.on('download', download(win))
  ipcMain.on(createBrowserViewCustomEvents('').create, createBrowserView(win))
  ipcMain.on('get_app_paths', getAppPaths(win))

  win.maximize()

  win.on('ready-to-show', () => {
    win.show()
  })

  if (VITE_DEV_SERVER_URL) {
    await win.loadURL(VITE_DEV_SERVER_URL)
    win.webContents.openDevTools()
  } else {
    win.setFullScreen(true)
    win.setMenu(null)
    await win.loadFile(path.join(VITE_PUBLIC!, 'index.html'))
  }
}

app.whenReady().then(bootstrap)

createBrowserView function

import {
  BrowserView,
  WebContentsView,
  BrowserWindow,
  ipcMain,
  type IpcMainEvent,
  type Rectangle,
} from 'electron'

import { BrowserViewEventsEnum } from '../types/browserView'
import { createBrowserViewCustomEvents } from '../helpers/browserView'

interface EventData {
  url: string
  id: string
  bounds: Rectangle
}

export const createBrowserView =
  (win: BrowserWindow) =>
  async (event: IpcMainEvent, { url, id, bounds }: EventData) => {
    const view = new WebContentsView({
      webPreferences: {
        backgroundThrottling: false,
      },
    })

    Object.values(BrowserViewEventsEnum).forEach((name) =>
      // @ts-ignore
      view.webContents.on(name, () => win.webContents.send(`${name}_${id}`)),
    )

    const customEvents = createBrowserViewCustomEvents(id)
    ipcMain.on(customEvents.reload, () => view.webContents.reload())
    ipcMain.on(customEvents.pageBack, () => {
      if (!view.webContents.canGoBack()) return
      view.webContents.goBack()
    })
    ipcMain.on(customEvents.pageForward, () => {
      if (!view.webContents.canGoForward()) return
      view.webContents.goForward()
    })
    ipcMain.on(customEvents.zoomOut, () => {
      const factor = view.webContents.getZoomFactor()
      view.webContents.setZoomFactor(factor - 0.1)
    })
    ipcMain.on(customEvents.zoomIn, () => {
      const factor = view.webContents.getZoomFactor()
      view.webContents.setZoomFactor(factor + 0.1)
    })
    ipcMain.on(customEvents.close, () => {
      view.webContents?.close()
      win.contentView.removeChildView(view)
    })
    ipcMain.on(customEvents.execute, async (_, { script }) => {
      try {
        await view.webContents.executeJavaScript(script)
      } catch (e) {
        console.log(e)
      }
    })

    win.contentView.addChildView(view)
    view.setBounds(bounds)
    await view.webContents.loadURL(url)
  }

useBrowserView hook that uses in vue.js component for intercation with electron processes

import { useIpcRenderer } from '@vueuse/electron'
import { nanoid } from 'nanoid'
import type { Ref } from '@vue/reactivity'
import { z } from 'zod'
import { BrowserViewEventsEnum } from '~/types/browserView'
import { createBrowserViewCustomEvents } from '~/helpers/browserView'
import mitt from 'mitt'
import path from 'path'

const fs = window.require('fs-extra')

interface Options {
  container: Ref<HTMLDivElement | undefined>
  url: string
}
interface BrowserViewHandler {
  action: BrowserViewEventsEnum
  once: boolean
}

type ScriptFileName =
  | 'zoomInExcel'
  | 'zoomOutExcel'
  | 'pageUpExcel'
  | 'pageDownExcel'
  | 'slideNextExcel'
  | 'slidePrevExcel'
  | 'pageLeftExcel'
  | 'pageRightExcel'
  | 'zoomInWord'
  | 'zoomOutWord'
  | 'listDownWord'
  | 'listUpWord'
  | 'pageUpMiro'
  | 'pageDownMiro'
  | 'slideNextMiro'
  | 'slidePrevMiro'
  | 'pageLeftMiro'
  | 'pageRightMiro'
  | 'slideNextFigma'
  | 'slidePrevFigma'
  | 'slideNextPptx'
  | 'slidePrevPptx'
  | 'removeTargetBlancAttribute'

const urlValidationScheme = z.string().trim().url()

export function useBrowserView({ url, container }: Options) {
  console.log('on useBrowserView')

  const ipcRenderer = useIpcRenderer()
  const id = nanoid()
  const customEvents = createBrowserViewCustomEvents(id)
  const { GLOBAL_DIRS } = useAppPaths()
  const bus = mitt<Record<BrowserViewEventsEnum, undefined>>()

  const executeViewScript = (script: string) => {
    ipcRenderer.send(customEvents.execute, { script })
  }

  const executeViewScriptFromFile = async (name: ScriptFileName) => {
    const scriptPath = path.join(
      GLOBAL_DIRS.PUBLIC_DIR,
      'web-view-scripts',
      `${name}.js`,
    )
    const script = await fs.readFile(scriptPath, 'utf8')
    executeViewScript(script)
  }

  const reloadView = () => ipcRenderer.send(customEvents.reload)

  const pageBackView = () => ipcRenderer.send(customEvents.pageBack)

  const pageForwardView = () => ipcRenderer.send(customEvents.pageForward)
  const zoomOutView = () => ipcRenderer.send(customEvents.zoomOut)

  const zoomInView = () => ipcRenderer.send(customEvents.zoomIn)

  const handlers: BrowserViewHandler[] = [
    {
      action: BrowserViewEventsEnum.DidFinishLoad,
      once: false,
    },
    {
      action: BrowserViewEventsEnum.DomReady,
      once: false,
    },
  ]

  handlers.forEach((h) => {
    const name = `${h.action}_${id}`
    if (h.once) {
      ipcRenderer.once(name, () => bus.emit(h.action))
    } else {
      ipcRenderer.on(name, () => bus.emit(h.action))
    }
  })

  const onViewEvent = bus.on

  onViewEvent(BrowserViewEventsEnum.DidFinishLoad, async () => {
    await executeViewScriptFromFile('removeTargetBlancAttribute')
  })

  onActivated(() => {
    try {
      urlValidationScheme.parse(url)

      const bounds = container.value?.getBoundingClientRect()

      ipcRenderer.send(customEvents.create, {
        url,
        id,
        bounds: JSON.parse(JSON.stringify(bounds)),
      })
    } catch (e) {
      console.log(e)
    }
  })

  onDeactivated(() => {
    ipcRenderer.send(customEvents.close)
    bus.all.clear()
  })

  return {
    reloadView,
    pageBackView,
    pageForwardView,
    zoomInView,
    zoomOutView,
    executeViewScript,
    executeViewScriptFromFile,
    onViewEvent,
  }
}

vue.js component

<template>
  <div ref="container" class="relative size-full" />
</template>

<script lang="ts" setup>
import { ActionEnum } from '~/types/actions'
import {
  MaterialAuthTypeEnum,
  type MaterialComponentProps,
  MaterialSubTypeEnum,
  MaterialType,
  PictureMaterialState,
} from '~/types/material'
import { BrowserViewEventsEnum } from '~/types/browserView'
import { wait } from '~/utils'

const props = defineProps<MaterialComponentProps>()
const { areaId, statusContentArea, material } = toRefs(props)
const materialId = ref(material.value.id)

const container = ref<HTMLDivElement>()
const url = material.value.url || ''

console.log('browser.vue before useBrowserView')

const {
  reloadView,
  pageBackView,
  pageForwardView,
  zoomInView,
  zoomOutView,
  executeViewScript,
  executeViewScriptFromFile,
  onViewEvent,
} = useBrowserView({
  container,
  url,
})

const { updateState, state } = useMaterialState<PictureMaterialState>(
  MaterialType.Picture,
  {
    material: material.value,
    areaId: areaId.value,
    statusContentId: statusContentArea.value.statusContentId,
  },
)

onViewEvent(BrowserViewEventsEnum.DidFinishLoad, async () => {
  const { authData } = material.value
  if (authData?.authType !== MaterialAuthTypeEnum.Script) return
  if (!authData?.authScript) return

  let script = authData.authScript
  if (authData.password) {
    script = script.replaceAll('{{password}}', authData.password)
  }
  if (authData.username) {
    script = script.replaceAll('{{username}}', authData.username)
  }

  await wait(3000)

  executeViewScript(script)
})
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: 🛠️ Fixed for Next Release
Status: 🛠️ Fixed for Next Release
Development

No branches or pull requests

2 participants