WIP
This commit is contained in:
parent
14b3571af3
commit
a0cddb256a
@ -10,6 +10,16 @@ export class ClientMenu {
|
||||
|
||||
private menuPositionClasses: string[] = [];
|
||||
|
||||
|
||||
private isHovered = false;
|
||||
private isWithinActivation = false;
|
||||
private lastMouseMove = performance.now();
|
||||
private idleTimer?: number;
|
||||
|
||||
private onDocumentMouseMove?: (e: MouseEvent) => void;
|
||||
private onDocumentMouseLeave?: () => void;
|
||||
private idleIntervalId?: number;
|
||||
|
||||
constructor(private config: MenuConfig) {}
|
||||
|
||||
mount(anchorElement: HTMLElement) {
|
||||
@ -24,9 +34,55 @@ export class ClientMenu {
|
||||
anchorElement.appendChild(this.host);
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
// 1. Stop timers
|
||||
if (this.idleIntervalId != null) {
|
||||
clearInterval(this.idleIntervalId);
|
||||
this.idleIntervalId = undefined;
|
||||
}
|
||||
|
||||
// 2. Remove global listeners
|
||||
if (this.onDocumentMouseMove) {
|
||||
document.removeEventListener('mousemove', this.onDocumentMouseMove);
|
||||
this.onDocumentMouseMove = undefined;
|
||||
}
|
||||
|
||||
if (this.onDocumentMouseLeave) {
|
||||
document.removeEventListener('mouseleave', this.onDocumentMouseLeave);
|
||||
this.onDocumentMouseLeave = undefined;
|
||||
}
|
||||
|
||||
// 3. Remove DOM
|
||||
if (this.host && this.host.parentNode) {
|
||||
this.host.parentNode.removeChild(this.host);
|
||||
}
|
||||
|
||||
// 4. Clear references
|
||||
this.shadow = undefined as any;
|
||||
this.root = undefined as any;
|
||||
this.host = undefined as any;
|
||||
|
||||
// 5. Reset state
|
||||
this.visible = false;
|
||||
this.isHovered = false;
|
||||
this.isWithinActivation = false;
|
||||
}
|
||||
|
||||
private getActivationRadius(anchorEl: HTMLElement): number | null {
|
||||
if (this.config.activationRadius == null) return null;
|
||||
|
||||
if (typeof this.config.activationRadius === 'number') {
|
||||
return this.config.activationRadius;
|
||||
}
|
||||
|
||||
// percentage string
|
||||
const rect = anchorEl.getBoundingClientRect();
|
||||
const pct = parseFloat(this.config.activationRadius);
|
||||
return Math.max(rect.width, rect.height) * (pct / 100);
|
||||
}
|
||||
|
||||
private injectStyles() {
|
||||
const style = document.createElement("style");
|
||||
console.warn('imported styles: imported CSS (raw):', typeof extensionCss, extensionCss);
|
||||
const style = document.createElement('style');
|
||||
let css = extensionCss
|
||||
.trim()
|
||||
.replace(/'html, body \{/g, ':host {')
|
||||
@ -40,8 +96,8 @@ export class ClientMenu {
|
||||
css = `
|
||||
${cssArr[0]}
|
||||
@font-face {
|
||||
font-family: "Heebo";
|
||||
src: url(__FONT_HEEBO__) format("truetype-variations");
|
||||
font-family: 'Heebo';
|
||||
src: url(__FONT_HEEBO__) format('truetype-variations');
|
||||
font-weight: 100 900;
|
||||
font-stretch: 75% 125%;
|
||||
font-style: normal;
|
||||
@ -49,16 +105,16 @@ export class ClientMenu {
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Source Code Pro";
|
||||
src: url(__FONT_SCP__) format("truetype-variations");
|
||||
font-family: 'Source Code Pro';
|
||||
src: url(__FONT_SCP__) format('truetype-variations');
|
||||
font-weight: 200 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Source Code Pro";
|
||||
src: url(__FONT_SCPI__) format("truetype-variations");
|
||||
font-family: 'Source Code Pro';
|
||||
src: url(__FONT_SCPI__) format('truetype-variations');
|
||||
font-weight: 200 900;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
@ -66,8 +122,6 @@ export class ClientMenu {
|
||||
${cssRemainder};
|
||||
`;
|
||||
|
||||
console.log('+————————————————————————————————————', cssRemainder)
|
||||
|
||||
css = css
|
||||
.replace('__FONT_HEEBO__', chrome.runtime.getURL('/ui/res/fonts/Heebo.ttf'))
|
||||
.replace('__FONT_SCP__', chrome.runtime.getURL('/ui/res/fonts/SourceCodePro.ttf'))
|
||||
@ -80,25 +134,25 @@ export class ClientMenu {
|
||||
}
|
||||
|
||||
private createHost() {
|
||||
this.host = document.createElement("div");
|
||||
this.host = document.createElement('div');
|
||||
this.host.classList.add('uw-ultrawidify-container-root');
|
||||
|
||||
Object.assign(this.host.style, {
|
||||
position: this.config.isGlobal ? "fixed" : "absolute",
|
||||
position: this.config.isGlobal ? 'fixed' : 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
border: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
zIndex: this.config.isGlobal ? "2147483647" : "2147483640",
|
||||
// pointerEvents: "none",
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
zIndex: this.config.isGlobal ? '2147483647' : '2147483640',
|
||||
pointerEvents: 'none',
|
||||
});
|
||||
|
||||
console.log('UI host created:', this.host);
|
||||
}
|
||||
|
||||
private createShadow() {
|
||||
this.shadow = this.host.attachShadow({ mode: "open" });
|
||||
this.shadow = this.host.attachShadow({ mode: 'open' });
|
||||
this.injectStyles();
|
||||
}
|
||||
|
||||
@ -142,33 +196,45 @@ export class ClientMenu {
|
||||
}
|
||||
|
||||
private createMenu() {
|
||||
this.root = document.createElement("div");
|
||||
this.root.className = "uw-menu-root font-mono";
|
||||
this.root = document.createElement('div');
|
||||
this.root.className = 'uw-menu-root uw-hidden';
|
||||
|
||||
const trigger = document.createElement("div");
|
||||
trigger.classList = "uw-menu-trigger uw-trigger";
|
||||
trigger.textContent = "Ultrawidify";
|
||||
const trigger = document.createElement('div');
|
||||
trigger.classList = 'uw-menu-trigger uw-trigger';
|
||||
trigger.textContent = 'Ultrawidify';
|
||||
|
||||
const submenu = this.buildSubmenu(this.config.items);
|
||||
|
||||
trigger.addEventListener("mouseenter", () => this.show());
|
||||
this.root.addEventListener("mouseleave", () => this.hide());
|
||||
trigger.addEventListener('mouseenter', () => this.show());
|
||||
this.root.addEventListener('mouseleave', () => {
|
||||
this.isHovered = false;
|
||||
this.hide();
|
||||
});
|
||||
|
||||
console.log('menu position classes:', this.menuPositionClasses)
|
||||
this.root.classList.add(...this.menuPositionClasses);
|
||||
|
||||
trigger.appendChild(submenu);
|
||||
this.root.append(trigger);
|
||||
this.shadow.appendChild(this.root);
|
||||
|
||||
this.root.addEventListener('mouseenter', () => {
|
||||
this.isHovered = true;
|
||||
this.show();
|
||||
});
|
||||
|
||||
this.root.addEventListener('mouseleave', () => {
|
||||
this.isHovered = false;
|
||||
this.updateVisibility();
|
||||
});
|
||||
}
|
||||
|
||||
private buildSubmenu(items: MenuItemConfig[]): HTMLDivElement {
|
||||
const menu = document.createElement("div");
|
||||
menu.classList = "uw-submenu";
|
||||
const menu = document.createElement('div');
|
||||
menu.classList = 'uw-submenu';
|
||||
menu.classList.add(...this.menuPositionClasses);
|
||||
|
||||
for (const item of items) {
|
||||
const el = document.createElement("div");
|
||||
const el = document.createElement('div');
|
||||
el.className = `uw-menu-item uw-trigger`;
|
||||
|
||||
if (item.customHTML) {
|
||||
@ -178,10 +244,10 @@ export class ClientMenu {
|
||||
}
|
||||
|
||||
if (item.action) {
|
||||
el.addEventListener("click", e => {
|
||||
el.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
item.action?.();
|
||||
this.hide();
|
||||
// this.hide(); // maybe dont
|
||||
});
|
||||
}
|
||||
|
||||
@ -197,25 +263,7 @@ export class ClientMenu {
|
||||
|
||||
private position(anchorEl: HTMLElement) {
|
||||
let appendClassList: string[] = [];
|
||||
|
||||
|
||||
|
||||
anchorEl.classList.add(...appendClassList);
|
||||
|
||||
// switch (this.config.anchor) {
|
||||
// case "LeftCenter":
|
||||
// r.left = "0";
|
||||
// r.top = "50%";
|
||||
// r.transform = "translateY(-50%)";
|
||||
// break;
|
||||
|
||||
// case "TopLeft":
|
||||
// r.left = "0";
|
||||
// r.top = "0";
|
||||
// break;
|
||||
|
||||
// // others are trivial extensions
|
||||
// }
|
||||
}
|
||||
|
||||
private bindGlobalMouse(anchorEl: HTMLElement) {
|
||||
@ -223,28 +271,80 @@ export class ClientMenu {
|
||||
const cx = rect.left + rect.width / 2;
|
||||
const cy = rect.top + rect.height / 2;
|
||||
|
||||
document.addEventListener("mousemove", e => {
|
||||
const activationRadius = this.getActivationRadius(anchorEl);
|
||||
|
||||
this.onDocumentMouseMove = (e: MouseEvent) => {
|
||||
this.lastMouseMove = performance.now();
|
||||
|
||||
if (activationRadius != null) {
|
||||
const d = Math.hypot(e.clientX - cx, e.clientY - cy);
|
||||
if (d < this.config.activationRadius) {
|
||||
this.show();
|
||||
} else if (!this.root.matches(":hover")) {
|
||||
this.isWithinActivation = d <= activationRadius;
|
||||
} else {
|
||||
this.isWithinActivation =
|
||||
e.clientX >= rect.left &&
|
||||
e.clientX <= rect.right &&
|
||||
e.clientY >= rect.top &&
|
||||
e.clientY <= rect.bottom;
|
||||
}
|
||||
|
||||
this.updateVisibility();
|
||||
};
|
||||
|
||||
this.onDocumentMouseLeave = () => {
|
||||
this.isHovered = false;
|
||||
this.isWithinActivation = false;
|
||||
this.hide();
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', this.onDocumentMouseMove);
|
||||
document.addEventListener('mouseleave', this.onDocumentMouseLeave);
|
||||
|
||||
this.startIdleWatcher();
|
||||
}
|
||||
|
||||
private startIdleWatcher() {
|
||||
this.idleIntervalId = window.setInterval(() => {
|
||||
const idle = performance.now() - this.lastMouseMove > 1000;
|
||||
if (idle) {
|
||||
this.hide();
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
|
||||
|
||||
private updateVisibility() {
|
||||
const idle = performance.now() - this.lastMouseMove > 1000;
|
||||
|
||||
if (this.isHovered) {
|
||||
this.show();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isWithinActivation && !idle) {
|
||||
this.show();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.visible) {
|
||||
this.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private show() {
|
||||
this.lastMouseMove = performance.now();
|
||||
|
||||
if (!this.visible) {
|
||||
this.visible = true;
|
||||
this.root.classList.add("uw-visible");
|
||||
this.root.classList.remove("uw-hidden");
|
||||
this.root.classList.add('uw-visible');
|
||||
this.root.classList.remove('uw-hidden');
|
||||
}
|
||||
}
|
||||
|
||||
private hide() {
|
||||
if (this.visible) {
|
||||
this.visible = false;
|
||||
this.root.classList.remove("uw-visible");
|
||||
this.root.classList.remove('uw-visible');
|
||||
this.root.classList.add('uw-hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,7 +3,10 @@ import { ClientMenu } from './ClientMenu';
|
||||
import EventBus from '../EventBus';
|
||||
import PlayerData from '../video-data/PlayerData';
|
||||
import { SiteSettings } from '../settings/SiteSettings';
|
||||
import { MenuPosition as MenuPosition } from '../../../common/interfaces/ClientUiMenu.interface';
|
||||
import Settings from '../settings/Settings';
|
||||
|
||||
import { MenuPosition as MenuPosition } from '@src/common/interfaces/ClientUiMenu.interface';
|
||||
import { CommandInterface } from '@src/common/interfaces/SettingsInterface';
|
||||
|
||||
if (process.env.CHANNEL !== 'stable'){
|
||||
console.info("Loading: UI");
|
||||
@ -22,22 +25,19 @@ class UI {
|
||||
|
||||
isGlobal: boolean;
|
||||
isIframe: boolean;
|
||||
|
||||
private eventBus: EventBus;
|
||||
private playerData: PlayerData;
|
||||
private uiSettings: any;
|
||||
private settings: Settings;
|
||||
private siteSettings: SiteSettings;
|
||||
private disablePointerEvents: boolean = false;
|
||||
|
||||
private iframeErrorCount: number = 0;
|
||||
private iframeConfirmed: boolean = false;
|
||||
private iframeRejected: boolean = false;
|
||||
private delayedDestroyTimer: any = undefined;
|
||||
|
||||
private csuiScheme;
|
||||
private extensionBase: string;
|
||||
|
||||
// These can prolly be removed:
|
||||
lastProbeResponseTs: number
|
||||
lastProbeResponseTs: number;
|
||||
|
||||
private extensionMenu: ClientMenu;
|
||||
|
||||
constructor(
|
||||
public interfaceId,
|
||||
@ -53,18 +53,13 @@ class UI {
|
||||
this.playerData = uiConfig.playerData;
|
||||
this.uiSettings = uiConfig.uiSettings;
|
||||
this.siteSettings = uiConfig.siteSettings;
|
||||
|
||||
this.iframeErrorCount = 0;
|
||||
this.iframeConfirmed = false;
|
||||
this.iframeRejected = false;
|
||||
|
||||
this.delayedDestroyTimer = null;
|
||||
this.settings = uiConfig.settings;
|
||||
|
||||
// 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(/\/$/, "");
|
||||
// this.extensionBase = chrome.runtime.getURL('').replace(/\/$/, "");
|
||||
|
||||
// UI will be initialized when setUiVisibility is called
|
||||
if (!this.isGlobal) {
|
||||
@ -72,23 +67,85 @@ class UI {
|
||||
}
|
||||
}
|
||||
|
||||
canRun() {
|
||||
if (this.isGlobal) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.siteSettings?.data.enableUI !== ExtensionMode.Disabled;
|
||||
}
|
||||
|
||||
async init() {
|
||||
if (!this.canRun()) {
|
||||
return;
|
||||
this.destroy()
|
||||
this.createExtensionMenu();
|
||||
|
||||
// this.initUIContainer();
|
||||
// this.initMessaging();
|
||||
}
|
||||
|
||||
executeCommand(x: CommandInterface) {
|
||||
this.eventBus.send(x.action, x.arguments);
|
||||
}
|
||||
|
||||
this.initUIContainer();
|
||||
this.loadIframe();
|
||||
this.initMessaging();
|
||||
createExtensionMenu() {
|
||||
console.log('—>—— creating ext menu:. is enabled?', this.siteSettings?.data.enableUI, ExtensionMode[this.siteSettings?.data.enableUI], this.siteSettings?.data.enableUI === ExtensionMode.Disabled, 'global?', this.isGlobal)
|
||||
if (+this.siteSettings?.data.enableUI === ExtensionMode.Disabled || this.isGlobal) {
|
||||
console.log('we said no to UI')
|
||||
return; // don't
|
||||
}
|
||||
if (this.extensionMenu) {
|
||||
this.extensionMenu.destroy();
|
||||
}
|
||||
if (this.uiConfig.parentElement) {
|
||||
const menuConfig = {
|
||||
isGlobal: this.isGlobal,
|
||||
menuPosition: MenuPosition.Left,
|
||||
items: [
|
||||
{
|
||||
label: "todo: site + site compatibility info"
|
||||
},
|
||||
{
|
||||
label: 'Crop',
|
||||
subitems: this.settings.active.commands.crop.map((x: CommandInterface) => {
|
||||
return {
|
||||
label: x.label,
|
||||
action: () => this.executeCommand(x)
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
label: 'Zoom (presets)',
|
||||
subitems: [
|
||||
... this.settings.active.commands.zoom.map((x: CommandInterface) => {
|
||||
return {
|
||||
label: x.label,
|
||||
action: () => this.executeCommand(x)
|
||||
}
|
||||
}),
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Stretch',
|
||||
subitems: this.settings.active.commands.stretch.map((x: CommandInterface) => {
|
||||
return {
|
||||
label: x.label,
|
||||
action: () => this.executeCommand(x)
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
label: 'Zoom (free-form)',
|
||||
subitems: [
|
||||
{label: 'todo sliders'}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Align',
|
||||
subitems: [{label: 'todo pls'}]
|
||||
},
|
||||
{
|
||||
label: 'todo: open settings'
|
||||
},
|
||||
{
|
||||
label: 'todo: first-time "customize/hide menu" option'
|
||||
}
|
||||
]
|
||||
}
|
||||
this.extensionMenu = new ClientMenu(menuConfig);
|
||||
this.extensionMenu.mount(this.uiConfig.parentElement);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -118,249 +175,91 @@ class UI {
|
||||
const random = Math.round(Math.random() * 69420);
|
||||
const uwid = `uw-ultrawidify-${this.interfaceId}-root-${random}`
|
||||
|
||||
if (this.uiConfig.parentElement) {
|
||||
const uwMenu = new ClientMenu({
|
||||
isGlobal: this.isGlobal,
|
||||
menuPosition: MenuPosition.Left,
|
||||
items: [
|
||||
{label: 'test'},
|
||||
{label: 'test nested', subitems: [{label: 'sub test'}, {label: 'sub test 2'}]}
|
||||
]
|
||||
});
|
||||
uwMenu.mount(this.uiConfig.parentElement);
|
||||
}
|
||||
return;
|
||||
|
||||
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.rootDiv = rootDiv;
|
||||
}
|
||||
|
||||
loadIframe() {
|
||||
return;
|
||||
// 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();
|
||||
// initMessaging() {
|
||||
// // subscribe to events coming back to us. Unsubscribe if iframe vanishes.
|
||||
// window.addEventListener('message', this.messageHandlerFn);
|
||||
|
||||
const offsets = {
|
||||
top: window.scrollY + rect.top,
|
||||
left: window.scrollX + rect.left
|
||||
};
|
||||
// /* 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.
|
||||
// */
|
||||
|
||||
const coords = {
|
||||
x: event.pageX - offsets.left,
|
||||
y: event.pageY - offsets.top,
|
||||
frameOffset: offsets,
|
||||
};
|
||||
// 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 && !this.isIframe) {
|
||||
// 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) => {
|
||||
// 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
|
||||
// );
|
||||
// }
|
||||
|
||||
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,
|
||||
isIframe: this.isIframe,
|
||||
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);
|
||||
}
|
||||
|
||||
this.eventBus.forwardToIframe(
|
||||
this.uiIframe,
|
||||
(action, payload) => {
|
||||
this.sendToIframe(action, payload, {})
|
||||
}
|
||||
);
|
||||
|
||||
this.rootDiv.appendChild(iframe);
|
||||
}
|
||||
|
||||
unloadIframe() {
|
||||
this.eventBus.cancelIframeForwarding(this.uiIframe);
|
||||
window.removeEventListener('message', this.messageHandlerFn);
|
||||
this.uiIframe?.remove();
|
||||
delete this.uiIframe;
|
||||
}
|
||||
|
||||
|
||||
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 && !this.isIframe) {
|
||||
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) => {
|
||||
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);
|
||||
}
|
||||
// messageHandlerFn = (message) => {
|
||||
// if (!this.uiIframe?.contentWindow) {
|
||||
// window.removeEventListener('message', this.messageHandlerFn);
|
||||
// return;
|
||||
// }
|
||||
// this.handleMessage(message);
|
||||
// }
|
||||
|
||||
setUiVisibility(visible) {
|
||||
return;
|
||||
// console.log('uwui - setting ui visibility!', visible, this.isGlobal ? 'global' : 'page', this.uiIframe, this.rootDiv);
|
||||
// if (!this.uiIframe || !this.rootDiv) {
|
||||
// this.init();
|
||||
// }
|
||||
|
||||
if (visible) {
|
||||
this.rootDiv.style.width = '100%';
|
||||
this.rootDiv.style.height = '100%';
|
||||
this.uiIframe.style.width = '100%';
|
||||
this.uiIframe.style.height = '100%';
|
||||
|
||||
// if (this.delayedDestroyTimer) {
|
||||
// clearTimeout(this.delayedDestroyTimer);
|
||||
// }
|
||||
} else {
|
||||
this.rootDiv.style.width = '0px';
|
||||
this.rootDiv.style.height = '0px';
|
||||
this.uiIframe.style.width = '0px';
|
||||
this.uiIframe.style.height = '0px';
|
||||
|
||||
// destroy after 30 seconds of UI being hidden
|
||||
// this.delayedDestroyTimer = setTimeout( () => this.unloadIframe(), 30000);
|
||||
}
|
||||
}
|
||||
|
||||
async enable() {
|
||||
// if root element is not present, we need to init the UI.
|
||||
if (!this.rootDiv) {
|
||||
await this.init();
|
||||
}
|
||||
// if (!this.rootDiv) {
|
||||
// await this.init();
|
||||
// }
|
||||
// otherwise, we don't have to do anything
|
||||
}
|
||||
disable() {
|
||||
if (this.rootDiv) {
|
||||
this.destroy();
|
||||
}
|
||||
// if (this.rootDiv) {
|
||||
// this.destroy();
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -401,144 +300,144 @@ class UI {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles events received from the iframe.
|
||||
* @param {*} event
|
||||
*/
|
||||
handleMessage(event) {
|
||||
if (event.origin === this.extensionBase) {
|
||||
switch(event.data.action) {
|
||||
case 'uwui-clickable':
|
||||
if (event.data.ts < this.lastProbeResponseTs) {
|
||||
return;
|
||||
}
|
||||
if (this.disablePointerEvents) {
|
||||
return;
|
||||
}
|
||||
this.lastProbeResponseTs = event.data.ts;
|
||||
// /**
|
||||
// * Handles events received from the iframe.
|
||||
// * @param {*} event
|
||||
// */
|
||||
// handleMessage(event) {
|
||||
// if (event.origin === this.extensionBase) {
|
||||
// switch(event.data.action) {
|
||||
// case 'uwui-clickable':
|
||||
// if (event.data.ts < this.lastProbeResponseTs) {
|
||||
// return;
|
||||
// }
|
||||
// if (this.disablePointerEvents) {
|
||||
// return;
|
||||
// }
|
||||
// this.lastProbeResponseTs = event.data.ts;
|
||||
|
||||
// If iframe returns 'yes, we are clickable' and iframe is currently set to pointerEvents=auto,
|
||||
// but hasMouse is false, then UI is attached to the wrong element. This probably means our
|
||||
// detected player element is wrong. We need to perform this check if we aren't in global UI
|
||||
/**
|
||||
* action: 'uwui-clickable',
|
||||
* clickable: isClickable,
|
||||
* hoverStats: {
|
||||
* isOverTriggerZone,
|
||||
* isOverMenuTrigger,
|
||||
* isOverUIArea,
|
||||
* hasMouse: !!document.querySelector(':hover'),
|
||||
* },
|
||||
* ts: +new Date()
|
||||
*/
|
||||
// // If iframe returns 'yes, we are clickable' and iframe is currently set to pointerEvents=auto,
|
||||
// // but hasMouse is false, then UI is attached to the wrong element. This probably means our
|
||||
// // detected player element is wrong. We need to perform this check if we aren't in global UI
|
||||
// /**
|
||||
// * action: 'uwui-clickable',
|
||||
// * clickable: isClickable,
|
||||
// * hoverStats: {
|
||||
// * isOverTriggerZone,
|
||||
// * isOverMenuTrigger,
|
||||
// * isOverUIArea,
|
||||
// * hasMouse: !!document.querySelector(':hover'),
|
||||
// * },
|
||||
// * ts: +new Date()
|
||||
// */
|
||||
|
||||
if (!this.global) {
|
||||
if (
|
||||
this.uiIframe.style.pointerEvents === 'auto'
|
||||
) {
|
||||
if (
|
||||
event.data.hoverStats.isOverMenuTrigger
|
||||
&& !event.data.hoverStats.hasMouse
|
||||
) {
|
||||
if (!this.iframeConfirmed) {
|
||||
if (this.iframeErrorCount++ > MAX_IFRAME_ERROR_COUNT && !this.iframeRejected) {
|
||||
this.iframeRejected = true;
|
||||
this.eventBus.send('change-player-element');
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else if (event.data.hoverStats.isOverMenuTrigger && event.data.hoverStats.hasMouse) {
|
||||
this.iframeConfirmed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.uiIframe.style.pointerEvents = event.data.clickable ? 'auto' : 'none';
|
||||
this.uiIframe.style.opacity = event.data.opacity || this.isGlobal ? '100' : '0';
|
||||
break;
|
||||
case 'uw-bus-tunnel':
|
||||
const busCommand = event.data.payload;
|
||||
this.eventBus.send(
|
||||
busCommand.action,
|
||||
busCommand.config,
|
||||
{
|
||||
...busCommand?.context,
|
||||
borderCrossings: {
|
||||
...busCommand?.context?.borderCrossings,
|
||||
iframe: true,
|
||||
}
|
||||
}
|
||||
);
|
||||
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-hidden':
|
||||
this.uiIframe.style.opacity = event.data.opacity || this.isGlobal ? '100' : '0';
|
||||
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', {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends messages to iframe. Messages sent with this function _generally_
|
||||
* bypass eventBus on the receiving end.
|
||||
* @param {*} action
|
||||
* @param {*} payload
|
||||
* @param {*} uiURI
|
||||
*/
|
||||
sendToIframeLowLevel(action, payload, uiURI = this.uiURI) {
|
||||
// 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.rootDiv && this.uiIframe) {
|
||||
this.uiIframe.contentWindow?.postMessage(
|
||||
{
|
||||
action,
|
||||
payload
|
||||
},
|
||||
uiURI
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.');
|
||||
// if (!this.global) {
|
||||
// if (
|
||||
// this.uiIframe.style.pointerEvents === 'auto'
|
||||
// ) {
|
||||
// if (
|
||||
// event.data.hoverStats.isOverMenuTrigger
|
||||
// && !event.data.hoverStats.hasMouse
|
||||
// ) {
|
||||
// if (!this.iframeConfirmed) {
|
||||
// if (this.iframeErrorCount++ > MAX_IFRAME_ERROR_COUNT && !this.iframeRejected) {
|
||||
// this.iframeRejected = true;
|
||||
// this.eventBus.send('change-player-element');
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
// if (!routingData) {
|
||||
// routingData = { };
|
||||
// } else if (event.data.hoverStats.isOverMenuTrigger && event.data.hoverStats.hasMouse) {
|
||||
// this.iframeConfirmed = true;
|
||||
// }
|
||||
// }
|
||||
// if (!routingData.crossedConnections) {
|
||||
// routingData.crossedConnections = [];
|
||||
// }
|
||||
// routingData.crossedConnections.push(EventBusConnector.IframeBoundaryIn);
|
||||
|
||||
this.sendToIframeLowLevel(
|
||||
'uw-bus-tunnel',
|
||||
{
|
||||
action,
|
||||
config: actionConfig,
|
||||
routingData
|
||||
},
|
||||
uiURI
|
||||
);
|
||||
}
|
||||
// this.uiIframe.style.pointerEvents = event.data.clickable ? 'auto' : 'none';
|
||||
// this.uiIframe.style.opacity = event.data.opacity || this.isGlobal ? '100' : '0';
|
||||
// break;
|
||||
// case 'uw-bus-tunnel':
|
||||
// const busCommand = event.data.payload;
|
||||
// this.eventBus.send(
|
||||
// busCommand.action,
|
||||
// busCommand.config,
|
||||
// {
|
||||
// ...busCommand?.context,
|
||||
// borderCrossings: {
|
||||
// ...busCommand?.context?.borderCrossings,
|
||||
// iframe: true,
|
||||
// }
|
||||
// }
|
||||
// );
|
||||
// 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-hidden':
|
||||
// this.uiIframe.style.opacity = event.data.opacity || this.isGlobal ? '100' : '0';
|
||||
// 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', {});
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Sends messages to iframe. Messages sent with this function _generally_
|
||||
// * bypass eventBus on the receiving end.
|
||||
// * @param {*} action
|
||||
// * @param {*} payload
|
||||
// * @param {*} uiURI
|
||||
// */
|
||||
// sendToIframeLowLevel(action, payload, uiURI = this.uiURI) {
|
||||
// // 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.rootDiv && this.uiIframe) {
|
||||
// this.uiIframe.contentWindow?.postMessage(
|
||||
// {
|
||||
// action,
|
||||
// payload
|
||||
// },
|
||||
// uiURI
|
||||
// )
|
||||
// };
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * 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
|
||||
// );
|
||||
// }
|
||||
|
||||
/**
|
||||
* Replaces ui config and re-inits the UI
|
||||
@ -554,14 +453,17 @@ class UI {
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.unloadIframe();
|
||||
if (this.extensionMenu) {
|
||||
this.extensionMenu.destroy();
|
||||
}
|
||||
// this.unloadIframe();
|
||||
|
||||
this.eventBus.unsubscribeAll(this);
|
||||
// this.comms?.destroy();
|
||||
this.rootDiv?.remove();
|
||||
// this.eventBus.unsubscribeAll(this);
|
||||
// // this.comms?.destroy();
|
||||
// this.rootDiv?.remove();
|
||||
|
||||
delete this.uiIframe;
|
||||
delete this.rootDiv;
|
||||
// delete this.uiIframe;
|
||||
// delete this.rootDiv;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -290,6 +290,7 @@ class PlayerData {
|
||||
playerData: this,
|
||||
uiSettings: this.videoData.settings.active.ui,
|
||||
siteSettings: this.siteSettings,
|
||||
settings: this.videoData.settings,
|
||||
}
|
||||
);
|
||||
|
||||
@ -590,6 +591,7 @@ class PlayerData {
|
||||
playerData: this,
|
||||
uiSettings: this.videoData.settings.active.ui,
|
||||
siteSettings: this.siteSettings,
|
||||
settings: this.videoData.settings,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@ -4,6 +4,22 @@
|
||||
.uw-menu-root {
|
||||
@apply h-full w-full font-mono text-stone-200 text-[16px] flex flex-row;
|
||||
|
||||
pointer-events: auto;
|
||||
transition: opacity 120ms ease;
|
||||
|
||||
&:not(.uw-visible) {
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
}
|
||||
&.uw-hidden {
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
}
|
||||
&.uw-visible {
|
||||
pointer-events: auto;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.uw-menu-left {
|
||||
@apply justify-start;
|
||||
}
|
||||
@ -39,13 +55,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
.uw-menu-trigger {
|
||||
|
||||
}
|
||||
|
||||
.uw-submenu {
|
||||
@apply absolute flex flex-col gap-0;
|
||||
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
|
||||
/* we'll also animate a bit */
|
||||
transform: translateX(-4px);
|
||||
transition: opacity 120ms ease, transform 120ms ease;
|
||||
|
||||
&.uw-menu-left {
|
||||
@apply left-full;
|
||||
}
|
||||
@ -81,6 +100,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
.uw-menu-item, .uw-menu-trigger {
|
||||
&:hover > .uw-submenu,
|
||||
&:focus-within > .uw-submenu {
|
||||
opacity: 1 !important;
|
||||
pointer-events: auto;
|
||||
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.uw-trigger {
|
||||
@apply p-4 relative block
|
||||
bg-stone-950/75 backdrop-blur-[1em] backdrop-brightness-75 backdrop-saturate-50
|
||||
|
||||
Loading…
Reference in New Issue
Block a user