import { EventBusConnector } from '../EventBus'; if (process.env.CHANNEL !== 'stable'){ console.info("Loading: UI"); } const csuiVersions = { 'normal': 'csui', // csui-overlay-normal.html, maps to csui.html // 'light': 'csui-light', // csui-overlay-light.html, maps to csui-light.html // 'dark': 'csui-dark' // csui-overlay-dark.html, maps to csui-dark.html }; const MAX_IFRAME_ERROR_COUNT = 5; class UI { constructor( interfaceId, uiConfig, // {parentElement?, eventBus?, isGlobal?, playerData} ) { this.interfaceId = interfaceId; this.uiConfig = uiConfig; this.lastProbeResponseTs = null; this.isGlobal = uiConfig.isGlobal ?? false; // TODO: at some point, UI should be different for global popup and in-player UI const preferredScheme = window.getComputedStyle( document.body ,null).getPropertyValue('color-scheme'); const csuiVersion = csuiVersions[preferredScheme] ?? csuiVersions.normal; this.uiURI = chrome.runtime.getURL(`/csui/${csuiVersion}.html`); this.extensionBase = chrome.runtime.getURL('').replace(/\/$/, ""); this.eventBus = uiConfig.eventBus; this.disablePointerEvents = false; this.saveState = undefined; this.playerData = uiConfig.playerData; this.uiSettings = uiConfig.uiSettings; this.iframeErrorCount = 0; this.iframeConfirmed = false; this.iframeRejected = false; // TODO: at some point, UI should be different for global popup and in-player UI this.csuiScheme = this.getCsuiScheme(); const csuiVersion = this.getCsuiVersion(this.csuiScheme.contentScheme); this.uiURI = chrome.runtime.getURL(`/csui/${csuiVersion}.html`); this.extensionBase = chrome.runtime.getURL('').replace(/\/$/, ""); } async init() { this.initIframes(); this.initMessaging(); } /** * Returns color scheme we need to use. * * contentScheme is used to select the correct HTML template. * iframeScheme gets applied to the iframe as style * @returns {contentScheme: string, iframeScheme: string} */ getCsuiScheme() { return { contentScheme: window.getComputedStyle( document.body ,null).getPropertyValue('color-scheme'), iframeScheme: document.documentElement.style.colorScheme || document.body.style.colorScheme || undefined }; } /** * Returns correct template for given preferredScheme parameter * @param {*} preferredScheme * @returns */ getCsuiVersion(preferredScheme) { return csuiVersions[preferredScheme] ?? csuiVersions.normal; } initIframes() { const random = Math.round(Math.random() * 69420); const uwid = `uw-ultrawidify-${this.interfaceId}-root-${random}` const rootDiv = document.createElement('div'); if (this.uiConfig.additionalStyle) { rootDiv.setAttribute('style', this.uiConfig.additionalStyle); } rootDiv.setAttribute('id', uwid); rootDiv.classList.add('uw-ultrawidify-container-root'); // rootDiv.style.width = "100%"; // rootDiv.style.height = "100%"; rootDiv.style.position = this.isGlobal ? "fixed" : "absolute"; rootDiv.style.zIndex = this.isGlobal ? '90009' : '90000'; rootDiv.style.border = 0; rootDiv.style.top = 0; rootDiv.style.pointerEvents = 'none'; if (this.uiConfig?.parentElement) { this.uiConfig.parentElement.appendChild(rootDiv); } else { document.body.appendChild(rootDiv); } this.element = rootDiv; // in onMouseMove, we currently can't access this because we didn't // do things the most properly const uiURI = this.uiURI; const iframe = document.createElement('iframe'); iframe.setAttribute('src', uiURI); iframe.setAttribute("allowTransparency", 'true'); // iframe.style.width = "100%"; // iframe.style.height = "100%"; iframe.style.position = "absolute"; iframe.style.zIndex = this.isGlobal ? '90009' : '90000'; iframe.style.border = 0; iframe.style.pointerEvents = 'none'; iframe.style.opacity = 0; iframe.style.backgroundColor = 'transparent !important'; // If colorScheme is defined via CSS on the HTML or BODY elements, then we need to also // put a matching style to the iframe itself. Using the correct UI template is not enough. if (this.csuiScheme.iframeScheme) { iframe.style.colorScheme = this.csuiScheme.iframeScheme; } /* so we have a problem: we want iframe to be clickthrough everywhere except * on our actual overlay. There's no nice way of doing that, so we need some * extra javascript to deal with this. * * There's a second problem: while iframe is in clickable mode, onMouseMove * will not work (as all events will be hijacked by the iframe). This means * that iframe also needs to run its own instance of onMouseMove. */ // set uiIframe for handleMessage this.uiIframe = iframe; // set not visible by default this.setUiVisibility(false); const fn = (event) => { // remove self on fucky wuckies if (!iframe?.contentWindow ) { document.removeEventListener('mousemove', fn, true); return; } const rect = this.uiIframe.getBoundingClientRect(); const offsets = { top: window.scrollY + rect.top, left: window.scrollX + rect.left }; const coords = { x: event.pageX - offsets.left, y: event.pageY - offsets.top, frameOffset: offsets, }; const playerData = this.canShowUI(coords); // ask the iframe to check whether there's a clickable element this.uiIframe.contentWindow.postMessage( { action: 'uwui-probe', coords, playerDimensions: playerData.playerDimensions, canShowUI: playerData.canShowUI, ts: +new Date() // this should be accurate enough for our purposes, }, uiURI ); } // NOTE: you cannot communicate with UI iframe inside onload function. // onload triggers after iframe is initialized, but BEFORE vue finishes // setting up all the components. // If we need to have any data inside the vue component, we need to // request that data from vue components. iframe.onload = function() { document.addEventListener('mousemove', fn, true); } rootDiv.appendChild(iframe); } initMessaging() { // subscribe to events coming back to us. Unsubscribe if iframe vanishes. window.addEventListener('message', this.messageHandlerFn); /* set up event bus tunnel from content script to UI — necessary if we want to receive * like current zoom levels & current aspect ratio & stuff. Some of these things are * necessary for UI display in the popup. */ this.eventBus.subscribeMulti( { 'uw-config-broadcast': { function: (config, routingData) => { this.sendToIframe('uw-config-broadcast', config, routingData); } }, 'uw-set-ui-state': { function: (config, routingData) => { if (config.globalUiVisible !== undefined) { if (this.isGlobal) { this.setUiVisibility(config.globalUiVisible); } else { this.setUiVisibility(!config.globalUiVisible); } } this.sendToIframe('uw-set-ui-state', {...config, isGlobal: this.isGlobal}, routingData); } }, 'uw-get-page-stats': { function: (config, routingData) => { console.log('got get page stats!'); this.eventBus.send( 'uw-page-stats', { pcsDark: window.matchMedia('(prefers-color-scheme: dark)').matches, pcsLight: window.matchMedia('(prefers-color-scheme: light)').matches, colorScheme: window.getComputedStyle( document.body ,null).getPropertyValue('color-scheme') }, { comms: { forwardTo: 'popup' } } ); } }, 'uw-restore-ui-state': { function: (config, routingData) => { if (!this.isGlobal) { this.setUiVisibility(true); this.sendToIframe('uw-restore-ui-state', config, routingData); } } } }, this ); } messageHandlerFn = (message) => { if (!this.uiIframe?.contentWindow) { window.removeEventListener('message', this.messageHandlerFn); return; } this.handleMessage(message); } setUiVisibility(visible) { if (visible) { this.element.style.width = '100%'; this.element.style.height = '100%'; this.uiIframe.style.width = '100%'; this.uiIframe.style.height = '100%'; } else { this.element.style.width = '0px'; this.element.style.height = '0px'; this.uiIframe.style.width = '0px'; this.uiIframe.style.height = '0px'; } } async enable() { // if root element is not present, we need to init the UI. if (!this.element) { await this.init(); } // otherwise, we don't have to do anything } disable() { if (this.element) { this.destroy(); } } /** * Checks whether mouse is moving over either: * *