diff --git a/src/ext/UWContent.ts b/src/ext/UWContent.ts index 6bcf72e..ef15702 100644 --- a/src/ext/UWContent.ts +++ b/src/ext/UWContent.ts @@ -111,7 +111,7 @@ export default class UWContent { this.logger.log('info', 'debug', "[uw.js::setup] KeyboardHandler initiated."); - this.globalUi = new UI('ultrawidify-global-ui', {eventBus: this.eventBus, isGlobal: true}); + this.globalUi = new UI('ultrawidify-global-ui', {eventBus: this.eventBus, isGlobal: true, siteSettings: this.siteSettings}); this.globalUi.enable(); this.globalUi.setUiVisibility(false); diff --git a/src/ext/UWServer.ts b/src/ext/UWServer.ts index 59c4c43..63a2af8 100644 --- a/src/ext/UWServer.ts +++ b/src/ext/UWServer.ts @@ -379,7 +379,7 @@ export default class UWServer { } async verifyUiTransparency(verificationData: IframeVerificationPlayerData, sender: MessageSender, telemetryData?: any) { - const hasTransparency = this.IframeTransparencyVerifier.verifyUiTransparency( + const transparencyVerificationResult = await this.IframeTransparencyVerifier.verifyUiTransparency( sender.tab.windowId, { player: verificationData, @@ -390,8 +390,13 @@ export default class UWServer { } ); + console.log('Transparency confirmed.'); + this.eventBus.send( 'iframe-transparency-results', + { + transparencyVerificationResult + }, { comms: { forwardTo: 'active' @@ -399,12 +404,12 @@ export default class UWServer { } ); - axios.post( - 'https://uw-telemetry.tamius.net/iframe-transparency', - { - ...telemetryData, - transparencyCheckResult: hasTransparency, - } - ); + // axios.post( + // 'https://uw-telemetry.tamius.net/iframe-transparency', + // { + // ...telemetryData, + // transparencyCheckResult: hasTransparency, + // } + // ); } } diff --git a/src/ext/lib/IframeTransparencyVerifier.ts b/src/ext/lib/IframeTransparencyVerifier.ts index f1add8b..d2431b7 100644 --- a/src/ext/lib/IframeTransparencyVerifier.ts +++ b/src/ext/lib/IframeTransparencyVerifier.ts @@ -34,16 +34,18 @@ interface IframeCheckPosition { export class IframeTransparencyVerifier { - async verifyUiTransparency(windowId: number, tabDimensions: IframeVerificationData): Promise { + async verifyUiTransparency(windowId: number, tabDimensions: IframeVerificationData): Promise<{result: TransparencyVerificationResult, dataUrl?: string}> { console.info('Verifying UI transparency:', tabDimensions); + const {visibleX, visibleY} = this.getVisibleMarkers(tabDimensions); - if (!visibleX.length || visibleY.length) { - return TransparencyVerificationResult.NoVisibleElements; + if (!visibleX.length || !visibleY.length) { + console.warn('[transparency check] No visible elements.'); + return {result: TransparencyVerificationResult.NoVisibleElements}; } const checkPositions = this.processMarkers(visibleX, visibleY); const dataUrl = await chrome.tabs.captureVisibleTab( - windowId, + undefined, // windowId, { format: "png" } @@ -53,21 +55,31 @@ export class IframeTransparencyVerifier { const canvas = new OffscreenCanvas(tabDimensions.tab.width, tabDimensions.tab.height); const ctx = canvas.getContext('2d')!; - const image = new Image(); - const imageLoadPromise = new Promise(r => image.onload = r); - image.src = dataUrl; - await imageLoadPromise; - (ctx as any).drawImage(image, 0, 0); + const res = await fetch(dataUrl); + const blob = await res.blob(); + + const bitmap = createImageBitmap(blob); + + (ctx as any).drawImage(bitmap, 0, 0); const imageData = (ctx as any).getImageData(0, 0, tabDimensions.tab.width, tabDimensions.tab.height).data; if (this.detectMarkers(checkPositions, tabDimensions.tab.width, imageData)) { - return TransparencyVerificationResult.Ok; + console.info('Verified transparency'); + return { + result: TransparencyVerificationResult.Ok, + dataUrl: dataUrl + }; } else { - return TransparencyVerificationResult.Fail; + console.info('Transparency checks came back negative'); + return { + result: TransparencyVerificationResult.Fail, + dataUrl: dataUrl + }; } } catch (e) { - return TransparencyVerificationResult.Error; + console.error('[transparency check] Error while checking for transparency:', e); + return {result: TransparencyVerificationResult.Error}; } } @@ -78,6 +90,8 @@ export class IframeTransparencyVerifier { // Determine which markers should be visible. // Visibility: TOP ROW + console.log('player:', player.y, tab.height); + if (player.y >= 0 && player.y < tab.height) { visibleY.push({ position: 'top', @@ -87,7 +101,7 @@ export class IframeTransparencyVerifier { // Visibility: CENTER const yHalfPosition = Math.floor(player.y + player.height / 2); - if (player.y + yHalfPosition - 2 > 0 || player.y + yHalfPosition + 2 < tab.height) { + if (player.y + yHalfPosition - 2 > 0 && player.y + yHalfPosition + 2 < tab.height) { visibleY.push({ position: 'center', offset: player.y + yHalfPosition, @@ -95,7 +109,7 @@ export class IframeTransparencyVerifier { } // Visibility: BOTTOM ROW - if (player.y + player.height - 1 > 0 || player.y + player.height + 1 < tab.height) { + if (player.y + player.height - 1 > 0 && player.y + player.height + 1 < tab.height) { visibleY.push({ position: 'bottom', offset: player.y + player.height - 1, @@ -112,21 +126,23 @@ export class IframeTransparencyVerifier { // Visibility: X CENTER const xHalfPosition = Math.floor(player.x + player.width / 2); - if (player.x + xHalfPosition - 2 > 0 || player.x + xHalfPosition + 2 < tab.width) { - visibleY.push({ + if (player.x + xHalfPosition - 2 > 0 && player.x + xHalfPosition + 2 < tab.width) { + visibleX.push({ position: 'center', offset: player.x + xHalfPosition, }); } // Visibility: RIGHT SIDE - if (player.x + player.width - 1 > 0 || player.x + player.width + 1 < tab.width) { + if (player.x + player.width - 1 > 0 && player.x + player.width + 1 < tab.width) { visibleX.push({ position: 'right', offset: player.x + player.width - 1, }); } + console.log('visible candidates', visibleX, visibleY); + return { visibleX, visibleY, diff --git a/src/ext/lib/Settings.ts b/src/ext/lib/Settings.ts index c212ca8..62be451 100644 --- a/src/ext/lib/Settings.ts +++ b/src/ext/lib/Settings.ts @@ -323,8 +323,38 @@ class Settings { this.active = activeSettings; } - async setProp(prop, value) { - this.active[prop] = value; + /** + * Sets value of a prop at given path. + * @param propPath path to property we want to set. If prop path does not exist, + * this function will recursively create it. It is assumed that uninitialized properties + * are objects. + * @param value + */ + async setProp(propPath: string | string[], value: any, options?: {forceReload?: boolean}, currentPath?: any) { + if (!Array.isArray(propPath)) { + propPath = propPath.split('.'); + } + + if (!currentPath) { + currentPath = this.active; + } + + const currentProp = propPath.shift(); + + if (propPath.length) { + if (!currentPath[currentProp]) { + currentPath[currentProp] = {}; + } + this.setProp(propPath, value, options, currentPath[currentProp]); + } else { + currentPath[currentProp] = value; + + if (options?.forceReload) { + this.save(); + } else { + this.saveWithoutReload(); + } + } } async save(options?) { diff --git a/src/ext/lib/uwui/UI.js b/src/ext/lib/uwui/UI.js index 2182417..96ec5ba 100644 --- a/src/ext/lib/uwui/UI.js +++ b/src/ext/lib/uwui/UI.js @@ -29,14 +29,10 @@ class UI { this.interfaceId = interfaceId; this.uiConfig = uiConfig; this.lastProbeResponseTs = null; + this.isVisible = false; + this.isOpaque = false; 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; @@ -44,15 +40,44 @@ class UI { this.saveState = undefined; this.playerData = uiConfig.playerData; this.uiSettings = uiConfig.uiSettings; + this.siteSettings = uiConfig.siteSettings; + this.settings = uiConfig.settings; this.iframeErrorCount = 0; this.iframeConfirmed = false; this.iframeRejected = false; + + this.preventHiding = false; + this.wantsToHide = false; + this.wantsToTransparent = false; + this.performedTransparencyCheck = false; + + // TODO: at some point, UI should be different for global popup and in-player UI + const csuiVersion = this.getCsuiVersion(); + this.uiURI = chrome.runtime.getURL(`/csui/${csuiVersion}.html`); + this.extensionBase = chrome.runtime.getURL('').replace(/\/$/, ""); } async init() { + try { this.initIframes(); this.initMessaging(); + } catch (e) { + console.error('failed to init ui:', e) + } + } + + + getCsuiVersion() { + if (this.siteSettings?.workarounds?.forceColorScheme) { + return csuiVersions[this.siteSettings.workarounds.forceColorScheme]; + } + if (this.siteSettings?.workarounds?.disableColorSchemeAwareness) { + return csuiVersions.normal; + } + + const preferredScheme = window.getComputedStyle( document.body ,null).getPropertyValue('color-scheme'); + return csuiVersions[preferredScheme] ?? csuiVersions.normal; } initIframes() { @@ -164,7 +189,6 @@ class UI { // * https://github.com/tamius-han/ultrawidify/issues/259 for (const x of ['left', 'center', 'right']) { for (const y of ['top', 'center', 'bottom']) { - console.log('') if (x !== y) { rootDiv.appendChild(this.generateDebugMarker(x, y)); } @@ -210,6 +234,11 @@ class UI { this.sendToIframe('uw-restore-ui-state', config, routingData); } } + }, + 'iframe-transparency-results': { + function: (data, routingData) => { + console.log('——————————— iframe transparency results are back!', data); + } } }, this @@ -225,6 +254,69 @@ class UI { } + verifyIframeTransparency() { + if (this.isGlobal) { + return; + } + if (!this.siteSettings || !this.settings) { + console.warn('settings not provided, not verifying transparency'); + return; + } + if (this.performedTransparencyCheck || !this.isOpaque || !this.uiConfig?.parentElement) { + // console.warn('transparency was already checked, opacity is zero, or parent element isnt a thing:', this.performedTransparencyCheck, 'is opaque:', this.isOpaque, this.uiConfig?.parentElement); + return; + } + + let reportTelemetry = true; + + // if (this.siteSettings.data.workarounds?.disableSchemeAwareness) { + // if (this.settings.active.telemetry?.iframeTransparency?.[window.location.hostname]?.reportedWithColorSchemeDisabled) { + // reportTelemetry = false; + // } else { + // this.settings.setProp(['telemetry', window.location.hostname, 'reportedWithColorSchemaDisabled'], true) + // } + // } else { + // if (this.settings.active.telemetry?.iframeTransparency?.[window.location.hostname]?.reportedWithColorSchemeAllowed) { + // reportTelemetry = false; + // } else { + // this.settings.setProp(['telemetry', window.location.hostname, 'reportedWithColorSchemeAllowed'], true) + // } + // } + + const rect = this.uiConfig.parentElement.getBoundingClientRect(); + + this.preventHiding = true; + setTimeout( () => { + this.eventBus.send( + 'verify-iframe-transparency', + { + playerData: { + y: rect.top, + x: rect.left, + width: rect.width, + height: rect.height, + }, + telemetryData: { + reportTelemetry, + } + } + ); + }, 50); + this.performedTransparencyCheck = true; + + setTimeout(() => { + this.preventHiding = false; + if (this.wantsToHide) { + this.setUiVisibility(false); + } + if (this.wantsToTransparent) { + this.setUiOpacity(false); + } + this.wantsToHide = false; + this.wantsToTransparent = false; + }); + } + /** * Generates marker positions for bug mitigations */ @@ -283,7 +375,29 @@ class UI { return template.content.firstChild; } + + setUiOpacity(visible) { + if (!visible && this.isVisible && this.preventHiding) { + this.wantsToTransparent = true; + return; + + } + this.uiIframe.style.opacity = visible ? '100' : '0'; + this.isOpaque = visible; + + if (visible) { + this.verifyIframeTransparency(); + } + } + setUiVisibility(visible) { + if (!visible && this.isVisible && this.preventHiding) { + this.wantsToHide = true; + return; + } + + this.isVisible = visible; + if (visible) { this.element.style.width = '100%'; this.element.style.height = '100%'; @@ -295,6 +409,7 @@ class UI { this.uiIframe.style.width = '0px'; this.uiIframe.style.height = '0px'; } + } async enable() { @@ -396,7 +511,7 @@ class UI { } this.uiIframe.style.pointerEvents = event.data.clickable ? 'auto' : 'none'; - this.uiIframe.style.opacity = event.data.opacity || this.isGlobal ? '100' : '0'; + this.setUiOpacity(event.data.opacity || this.isGlobal); // this.setUiVisibility( event.data.opacity || this.isGlobal ); break; case 'uw-bus-tunnel': @@ -410,7 +525,7 @@ class UI { this.setUiVisibility(!this.isGlobal); break; case 'uwui-hidden': - this.uiIframe.style.opacity = event.data.opacity || this.isGlobal ? '100' : '0'; + this.setUiOpacity(event.data.opacity || this.isGlobal); // this.setUiVisibility(event.data.opacity || this.isGlobal); break; case 'uwui-global-window-hidden': diff --git a/src/ext/lib/video-data/PlayerData.ts b/src/ext/lib/video-data/PlayerData.ts index d47f45a..aba7f98 100644 --- a/src/ext/lib/video-data/PlayerData.ts +++ b/src/ext/lib/video-data/PlayerData.ts @@ -248,7 +248,11 @@ class PlayerData { //#endregion deferredUiInitialization(playerDimensions) { - if (this.ui || ! this.videoData.settings.active.ui?.inPlayer?.enabled) { + if ( + this.ui + || ! this.videoData.settings.active.ui?.inPlayer?.enabled + || (this.siteSettings.data.ui && !this.siteSettings.data.ui.enabled) + ) { return; } @@ -265,7 +269,9 @@ class PlayerData { parentElement: this.element, eventBus: this.eventBus, playerData: this, - uiSettings: this.videoData.settings.active.ui + uiSettings: this.videoData.settings.active.ui, + settings: this.videoData.settings, + siteSettings: this.siteSettings } ); diff --git a/src/manifest.json b/src/manifest.json index 1d79aa7..e977294 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -50,7 +50,8 @@ ], "permissions": [ "storage", - "scripting" + "scripting", + "" ], "host_permissions": [ "*://*/*"