2024-06-05 01:08:50 +02:00
|
|
|
import { EventBusConnector } from '../EventBus';
|
|
|
|
|
2020-12-03 01:35:22 +01:00
|
|
|
if (process.env.CHANNEL !== 'stable'){
|
|
|
|
console.info("Loading: UI");
|
|
|
|
}
|
|
|
|
|
2024-06-05 01:08:50 +02:00
|
|
|
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
|
|
|
|
};
|
|
|
|
|
2020-12-03 00:34:50 +01:00
|
|
|
class UI {
|
|
|
|
constructor(
|
|
|
|
interfaceId,
|
2024-12-17 02:02:49 +01:00
|
|
|
uiConfig, // {parentElement?, eventBus?, isGlobal?, playerData}
|
2020-12-03 00:34:50 +01:00
|
|
|
) {
|
|
|
|
this.interfaceId = interfaceId;
|
2020-12-03 01:35:22 +01:00
|
|
|
this.uiConfig = uiConfig;
|
2022-03-21 00:50:54 +01:00
|
|
|
this.lastProbeResponseTs = null;
|
2024-06-02 16:06:26 +02:00
|
|
|
|
|
|
|
this.isGlobal = uiConfig.isGlobal ?? false;
|
|
|
|
// TODO: at some point, UI should be different for global popup and in-player UI
|
2024-06-05 01:08:50 +02:00
|
|
|
const preferredScheme = window.getComputedStyle( document.body ,null).getPropertyValue('color-scheme');
|
|
|
|
const csuiVersion = csuiVersions[preferredScheme] ?? csuiVersions.normal;
|
|
|
|
|
|
|
|
this.uiURI = chrome.runtime.getURL(`/csui/${csuiVersion}.html`);
|
2024-06-03 00:15:23 +02:00
|
|
|
this.extensionBase = chrome.runtime.getURL('').replace(/\/$/, "");
|
2020-12-03 01:35:22 +01:00
|
|
|
|
2022-03-22 01:23:15 +01:00
|
|
|
this.eventBus = uiConfig.eventBus;
|
2024-06-05 01:08:50 +02:00
|
|
|
this.disablePointerEvents = false;
|
|
|
|
|
|
|
|
this.saveState = undefined;
|
2024-12-17 02:02:49 +01:00
|
|
|
this.playerData = uiConfig.playerData;
|
|
|
|
this.uiSettings = uiConfig.uiSettings;
|
|
|
|
|
2020-12-03 00:34:50 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
async init() {
|
2024-06-05 01:08:50 +02:00
|
|
|
this.initIframes();
|
|
|
|
this.initMessaging();
|
|
|
|
}
|
|
|
|
|
|
|
|
initIframes() {
|
2020-12-03 00:34:50 +01:00
|
|
|
const random = Math.round(Math.random() * 69420);
|
2022-01-07 00:50:58 +01:00
|
|
|
const uwid = `uw-ultrawidify-${this.interfaceId}-root-${random}`
|
2020-12-03 00:34:50 +01:00
|
|
|
|
|
|
|
const rootDiv = document.createElement('div');
|
|
|
|
|
2020-12-15 00:26:19 +01:00
|
|
|
if (this.uiConfig.additionalStyle) {
|
|
|
|
rootDiv.setAttribute('style', this.uiConfig.additionalStyle);
|
|
|
|
}
|
2020-12-05 03:30:43 +01:00
|
|
|
rootDiv.setAttribute('id', uwid);
|
2020-12-12 00:38:51 +01:00
|
|
|
rootDiv.classList.add('uw-ultrawidify-container-root');
|
2024-06-05 01:08:50 +02:00
|
|
|
// rootDiv.style.width = "100%";
|
|
|
|
// rootDiv.style.height = "100%";
|
|
|
|
rootDiv.style.position = this.isGlobal ? "fixed" : "absolute";
|
|
|
|
rootDiv.style.zIndex = this.isGlobal ? '90009' : '90000';
|
2022-06-14 00:26:59 +02:00
|
|
|
rootDiv.style.border = 0;
|
|
|
|
rootDiv.style.top = 0;
|
2022-06-15 00:53:43 +02:00
|
|
|
rootDiv.style.pointerEvents = 'none';
|
2020-12-04 02:02:25 +01:00
|
|
|
|
|
|
|
if (this.uiConfig?.parentElement) {
|
2020-12-03 00:34:50 +01:00
|
|
|
this.uiConfig.parentElement.appendChild(rootDiv);
|
|
|
|
} else {
|
|
|
|
document.body.appendChild(rootDiv);
|
|
|
|
}
|
|
|
|
|
|
|
|
this.element = rootDiv;
|
|
|
|
|
2022-03-21 00:50:54 +01:00
|
|
|
// 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);
|
2024-06-05 01:08:50 +02:00
|
|
|
iframe.setAttribute("allowTransparency", 'true');
|
|
|
|
// iframe.style.width = "100%";
|
|
|
|
// iframe.style.height = "100%";
|
2022-03-21 00:50:54 +01:00
|
|
|
iframe.style.position = "absolute";
|
2024-06-05 01:08:50 +02:00
|
|
|
iframe.style.zIndex = this.isGlobal ? '90009' : '90000';
|
2022-03-21 00:50:54 +01:00
|
|
|
iframe.style.border = 0;
|
|
|
|
iframe.style.pointerEvents = 'none';
|
2024-06-05 01:08:50 +02:00
|
|
|
iframe.style.backgroundColor = 'transparent !important';
|
2022-03-21 00:50:54 +01:00
|
|
|
|
2022-03-22 01:23:15 +01:00
|
|
|
/* 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.
|
|
|
|
*/
|
2022-03-21 00:50:54 +01:00
|
|
|
|
2022-03-20 20:40:11 +01:00
|
|
|
|
2023-07-11 00:48:34 +02:00
|
|
|
// set uiIframe for handleMessage
|
|
|
|
this.uiIframe = iframe;
|
|
|
|
|
2024-06-05 01:08:50 +02:00
|
|
|
// set not visible by default
|
|
|
|
this.setUiVisibility(false);
|
|
|
|
|
2023-07-11 00:48:34 +02:00
|
|
|
const fn = (event) => {
|
|
|
|
// remove self on fucky wuckies
|
|
|
|
if (!iframe?.contentWindow ) {
|
|
|
|
document.removeEventListener('mousemove', fn, true);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-12-26 17:41:00 +01:00
|
|
|
const rect = this.uiIframe.getBoundingClientRect();
|
|
|
|
const offsets = {
|
|
|
|
top: window.scrollY + rect.top,
|
|
|
|
left: window.scrollX + rect.left
|
|
|
|
};
|
|
|
|
|
|
|
|
|
2023-07-11 00:48:34 +02:00
|
|
|
const coords = {
|
2024-12-26 17:41:00 +01:00
|
|
|
x: event.pageX - offsets.left,
|
|
|
|
y: event.pageY - offsets.top,
|
|
|
|
frameOffset: offsets,
|
2023-07-11 00:48:34 +02:00
|
|
|
};
|
|
|
|
|
2024-12-17 02:02:49 +01:00
|
|
|
const playerData = this.canShowUI(coords);
|
|
|
|
|
2023-07-11 00:48:34 +02:00
|
|
|
// ask the iframe to check whether there's a clickable element
|
|
|
|
this.uiIframe.contentWindow.postMessage(
|
|
|
|
{
|
|
|
|
action: 'uwui-probe',
|
|
|
|
coords,
|
2024-12-17 02:02:49 +01:00
|
|
|
playerDimensions: playerData.playerDimensions,
|
|
|
|
canShowUI: playerData.canShowUI,
|
|
|
|
ts: +new Date() // this should be accurate enough for our purposes,
|
2023-07-11 00:48:34 +02:00
|
|
|
},
|
|
|
|
uiURI
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2024-06-05 01:08:50 +02:00
|
|
|
// 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.
|
2022-03-22 01:23:15 +01:00
|
|
|
iframe.onload = function() {
|
2023-07-11 00:48:34 +02:00
|
|
|
document.addEventListener('mousemove', fn, true);
|
2020-12-04 02:02:25 +01:00
|
|
|
}
|
2021-10-26 23:13:11 +02:00
|
|
|
|
2022-03-21 00:50:54 +01:00
|
|
|
rootDiv.appendChild(iframe);
|
2024-06-05 01:08:50 +02:00
|
|
|
}
|
2024-12-17 02:02:49 +01:00
|
|
|
|
2024-06-05 01:08:50 +02:00
|
|
|
initMessaging() {
|
2023-07-11 00:48:34 +02:00
|
|
|
// subscribe to events coming back to us. Unsubscribe if iframe vanishes.
|
|
|
|
const messageHandlerFn = (message) => {
|
2024-06-05 01:08:50 +02:00
|
|
|
if (!this.uiIframe?.contentWindow) {
|
2023-07-11 00:48:34 +02:00
|
|
|
window.removeEventListener('message', messageHandlerFn);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.handleMessage(message);
|
|
|
|
}
|
|
|
|
window.addEventListener('message', messageHandlerFn);
|
2022-03-21 00:50:54 +01:00
|
|
|
|
2022-03-29 01:53:16 +02:00
|
|
|
|
|
|
|
/* 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.
|
|
|
|
*/
|
2022-09-27 01:48:08 +02:00
|
|
|
|
2022-03-29 01:53:16 +02:00
|
|
|
this.eventBus.subscribe(
|
|
|
|
'uw-config-broadcast',
|
|
|
|
{
|
2024-06-05 01:08:50 +02:00
|
|
|
function: (config, routingData) => {
|
|
|
|
this.sendToIframe('uw-config-broadcast', config, routingData);
|
2022-03-29 01:53:16 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
2024-06-02 16:06:26 +02:00
|
|
|
this.eventBus.subscribe(
|
|
|
|
'uw-set-ui-state',
|
|
|
|
{
|
2024-06-05 01:08:50 +02:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
)
|
|
|
|
this.eventBus.subscribe(
|
|
|
|
'uw-restore-ui-state', {
|
|
|
|
function: (config, routingData) => {
|
|
|
|
if (!this.isGlobal) {
|
|
|
|
this.setUiVisibility(true);
|
|
|
|
this.sendToIframe('uw-restore-ui-state', config, routingData);
|
|
|
|
}
|
2024-06-02 16:06:26 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
)
|
2022-03-20 20:40:11 +01:00
|
|
|
}
|
2022-03-21 00:50:54 +01:00
|
|
|
|
2024-06-05 01:08:50 +02:00
|
|
|
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';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-11 00:48:34 +02:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
}
|
2022-03-22 01:23:15 +01:00
|
|
|
|
2024-12-17 02:02:49 +01:00
|
|
|
/**
|
|
|
|
* Checks whether mouse is moving over either:
|
|
|
|
* * <video> element
|
|
|
|
* * player element ()
|
|
|
|
* * uwui-clickable element
|
|
|
|
*/
|
|
|
|
canShowUI(coords) {
|
|
|
|
const playerCssClass = 'uw-ultrawidify-player-css';
|
|
|
|
|
|
|
|
const result = {
|
|
|
|
playerDimensions: undefined,
|
|
|
|
canShowUI: false
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.playerData?.dimensions) {
|
|
|
|
result.playerDimensions = this.playerData.dimensions;
|
|
|
|
}
|
|
|
|
|
|
|
|
// if player is not wide enough, we do nothing
|
|
|
|
if (
|
|
|
|
!this.isGlobal && // this.isGlobal is basically 'yes, do as I say'
|
|
|
|
!document.fullscreenElement && // if we are in full screen, we allow it in every case as player detection is not 100% reliable,
|
|
|
|
result.playerDimensions?.width && // which makes playerDimensions.width unreliable as well (we assume nobody uses browser in
|
|
|
|
// fullscreen mode unless watching videos)
|
|
|
|
result.playerDimensions.width < window.screen.width * (this.uiSettings.inPlayer.minEnabledWidth ?? 0)
|
|
|
|
) {
|
|
|
|
result.canShowUI = false;
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
const elements = document.elementsFromPoint(coords.x, coords.y);
|
|
|
|
|
|
|
|
for (const element of elements) {
|
|
|
|
if (
|
|
|
|
element instanceof HTMLVideoElement
|
|
|
|
|| element.classList.contains(playerCssClass)
|
|
|
|
) {
|
|
|
|
result.canShowUI = true;
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2022-03-21 00:50:54 +01:00
|
|
|
/**
|
|
|
|
* Handles events received from the iframe.
|
|
|
|
* @param {*} event
|
|
|
|
*/
|
|
|
|
handleMessage(event) {
|
|
|
|
if (event.origin === this.extensionBase) {
|
2024-06-05 01:08:50 +02:00
|
|
|
switch(event.data.action) {
|
|
|
|
case 'uwui-clickable':
|
|
|
|
if (event.data.ts < this.lastProbeResponseTs) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (this.disablePointerEvents) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.lastProbeResponseTs = event.data.ts;
|
|
|
|
this.uiIframe.style.pointerEvents = event.data.clickable ? 'auto' : 'none';
|
|
|
|
break;
|
|
|
|
case 'uw-bus-tunnel':
|
|
|
|
const busCommand = event.data.payload;
|
|
|
|
this.eventBus.send(busCommand.action, busCommand.config, busCommand.routingData);
|
|
|
|
break;
|
|
|
|
case 'uwui-get-role':
|
|
|
|
this.sendToIframeLowLevel('uwui-set-role', {role: this.isGlobal ? 'global' : 'player'});
|
|
|
|
break;
|
|
|
|
case 'uwui-interface-ready':
|
|
|
|
this.setUiVisibility(!this.isGlobal);
|
|
|
|
break;
|
|
|
|
case 'uwui-global-window-hidden':
|
|
|
|
if (!this.isGlobal) {
|
|
|
|
return; // This shouldn't even happen in non-global windows
|
|
|
|
}
|
|
|
|
this.setUiVisibility(false);
|
|
|
|
this.eventBus.send('uw-restore-ui-state', {});
|
2022-03-21 00:50:54 +01:00
|
|
|
}
|
|
|
|
}
|
2020-12-03 00:34:50 +01:00
|
|
|
}
|
|
|
|
|
2024-06-02 16:06:26 +02:00
|
|
|
/**
|
2024-06-05 01:08:50 +02:00
|
|
|
* Sends messages to iframe. Messages sent with this function _generally_
|
|
|
|
* bypass eventBus on the receiving end.
|
|
|
|
* @param {*} action
|
|
|
|
* @param {*} payload
|
|
|
|
* @param {*} uiURI
|
2024-06-02 16:06:26 +02:00
|
|
|
*/
|
2024-06-05 01:08:50 +02:00
|
|
|
sendToIframeLowLevel(action, payload, uiURI = this.uiURI) {
|
2024-06-02 16:06:26 +02:00
|
|
|
// because existence of UI is not guaranteed — UI is not shown when extension is inactive.
|
|
|
|
// If extension is inactive due to "player element isn't big enough to justify it", however,
|
|
|
|
// we can still receive eventBus messages.
|
|
|
|
if (this.element && this.uiIframe) {
|
2024-06-05 01:08:50 +02:00
|
|
|
this.uiIframe.contentWindow?.postMessage(
|
2024-06-02 16:06:26 +02:00
|
|
|
{
|
2024-06-05 01:08:50 +02:00
|
|
|
action,
|
|
|
|
payload
|
2024-06-02 16:06:26 +02:00
|
|
|
},
|
|
|
|
uiURI
|
|
|
|
)
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2024-06-05 01:08:50 +02:00
|
|
|
/**
|
|
|
|
* Sends message to iframe. Messages sent with this function will be routed to eventbus.
|
|
|
|
*/
|
|
|
|
sendToIframe(action, actionConfig, routingData, uiURI = this.uiURI) {
|
|
|
|
// if (routingData) {
|
|
|
|
// if (routingData.crossedConnections?.includes(EventBusConnector.IframeBoundaryIn)) {
|
|
|
|
// console.warn('Denied message propagation. It has already crossed INTO an iframe once.');
|
|
|
|
// return;
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
// if (!routingData) {
|
|
|
|
// routingData = { };
|
|
|
|
// }
|
|
|
|
// if (!routingData.crossedConnections) {
|
|
|
|
// routingData.crossedConnections = [];
|
|
|
|
// }
|
|
|
|
// routingData.crossedConnections.push(EventBusConnector.IframeBoundaryIn);
|
|
|
|
|
|
|
|
this.sendToIframeLowLevel(
|
|
|
|
'uw-bus-tunnel',
|
|
|
|
{
|
|
|
|
action,
|
|
|
|
config: actionConfig,
|
|
|
|
routingData
|
|
|
|
},
|
|
|
|
uiURI
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-12-03 00:34:50 +01:00
|
|
|
/**
|
|
|
|
* Replaces ui config and re-inits the UI
|
2021-10-22 00:30:36 +02:00
|
|
|
* @param {*} newUiConfig
|
2020-12-03 00:34:50 +01:00
|
|
|
*/
|
|
|
|
replace(newUiConfig) {
|
|
|
|
this.uiConfig = newUiConfig;
|
2023-07-11 00:48:34 +02:00
|
|
|
|
|
|
|
if (this.element) {
|
|
|
|
this.element?.remove();
|
|
|
|
this.init();
|
|
|
|
}
|
2020-12-03 00:34:50 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
destroy() {
|
2020-12-04 02:02:25 +01:00
|
|
|
// this.comms?.destroy();
|
2023-07-11 00:48:34 +02:00
|
|
|
this.uiIframe?.remove();
|
2020-12-03 00:34:50 +01:00
|
|
|
this.element?.remove();
|
2023-07-11 00:48:34 +02:00
|
|
|
|
|
|
|
this.uiIframe = undefined;
|
|
|
|
this.element = undefined;
|
2020-12-03 00:34:50 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-12-03 01:35:22 +01:00
|
|
|
if (process.env.CHANNEL !== 'stable'){
|
|
|
|
console.info("UI.js loaded");
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2020-12-03 01:16:57 +01:00
|
|
|
export default UI;
|