Fix CSS processing

This commit is contained in:
Tamius Han 2025-12-29 02:34:46 +01:00
parent ef12dc8cc5
commit af08c5094d
8 changed files with 179 additions and 419 deletions

View File

@ -16,6 +16,7 @@ export type MenuAnchor =
| "BottomRight"; | "BottomRight";
export interface MenuConfig { export interface MenuConfig {
isGlobal?: boolean;
anchor: MenuAnchor; anchor: MenuAnchor;
activationRadius: number; activationRadius: number;
items: MenuItemConfig[]; items: MenuItemConfig[];

View File

@ -2,6 +2,7 @@ import { MenuConfig, MenuItemConfig } from '@src/common/interfaces/ClientUiMenu.
import extensionCss from '@src/main.css?inline'; import extensionCss from '@src/main.css?inline';
export class ClientMenu { export class ClientMenu {
private host!: HTMLDivElement; private host!: HTMLDivElement;
private shadow!: ShadowRoot; private shadow!: ShadowRoot;
private root!: HTMLDivElement; private root!: HTMLDivElement;
@ -16,74 +17,10 @@ export class ClientMenu {
this.position(anchorElement); this.position(anchorElement);
this.bindGlobalMouse(anchorElement); this.bindGlobalMouse(anchorElement);
document.documentElement.appendChild(this.host); // document.documentElement.appendChild(this.host);
anchorElement.appendChild(this.host);
} }
private createHost() {
this.host = document.createElement("div");
Object.assign(this.host.style, {
position: "fixed",
inset: "0",
zIndex: "2147483647",
// pointerEvents: "none",
backgroundColor: "#00000099",
});
this.host.textContent="SAMPLE TEXT SAMPLE TEXT SAMPLE TEXT"
}
private createShadow() {
this.shadow = this.host.attachShadow({ mode: "open" });
this.injectStyles();
}
private createMenu() {
this.root = document.createElement("div");
this.root.className = "menu-root font-mono";
const trigger = document.createElement("div");
trigger.className = "menu-trigger";
trigger.textContent = "☰";
const submenu = this.buildSubmenu(this.config.items);
trigger.addEventListener("mouseenter", () => this.show());
this.root.addEventListener("mouseleave", () => this.hide());
this.root.append(trigger, submenu);
this.shadow.appendChild(this.root);
}
private buildSubmenu(items: MenuItemConfig[]): HTMLDivElement {
const menu = document.createElement("div");
menu.className = "submenu";
for (const item of items) {
const el = document.createElement("div");
el.className = "menu-item";
if (item.customHTML) {
el.appendChild(item.customHTML);
} else {
el.textContent = item.label;
}
if (item.action) {
el.addEventListener("click", e => {
e.stopPropagation();
item.action?.();
this.hide();
});
}
if (item.subitems) {
el.appendChild(this.buildSubmenu(item.subitems));
}
menu.appendChild(el);
}
return menu;
}
private injectStyles() { private injectStyles() {
const style = document.createElement("style"); const style = document.createElement("style");
console.warn('imported styles: imported CSS (raw):', typeof extensionCss, extensionCss); console.warn('imported styles: imported CSS (raw):', typeof extensionCss, extensionCss);
@ -95,6 +32,7 @@ export class ClientMenu {
// this is bad but I can't be bothered to do it the proper way // this is bad but I can't be bothered to do it the proper way
const cssArr: string[] = css.split('@font-face'); const cssArr: string[] = css.split('@font-face');
const cssRemainder = cssArr[cssArr.length - 1].split('}').slice(1).join('}');
css = ` css = `
${cssArr[0]} ${cssArr[0]}
@ -122,15 +60,10 @@ export class ClientMenu {
font-style: italic; font-style: italic;
font-display: swap; font-display: swap;
} }
${cssArr[cssArr.length - 1].split('}', 2)[1]}; ${cssRemainder};
`; `;
// function scopeCss(css: string) { console.log('+————————————————————————————————————', cssRemainder)
// return css.replace(
// /(^|})\s*([^{@}][^{]*){/g,
// (_, sep, selector) => `${sep} :host ${selector} {`
// );
// }
css = css css = css
.replace('__FONT_HEEBO__', chrome.runtime.getURL('/ui/res/fonts/Heebo.ttf')) .replace('__FONT_HEEBO__', chrome.runtime.getURL('/ui/res/fonts/Heebo.ttf'))
@ -139,30 +72,102 @@ export class ClientMenu {
.replaceAll('html,', '') .replaceAll('html,', '')
; ;
// console.warn('CSS TO APPEND', css);
style.textContent = css; style.textContent = css;
this.shadow.appendChild(style); this.shadow.appendChild(style);
}
private createHost() {
this.host = document.createElement("div");
this.host.classList.add('uw-ultrawidify-container-root');
Object.assign(this.host.style, {
position: this.config.isGlobal ? "fixed" : "absolute",
left: 0,
top: 0,
border: 0,
width: "100%",
height: "100%",
zIndex: this.config.isGlobal ? "2147483647" : "2147483640",
// pointerEvents: "none",
backgroundColor: "#00000099",
});
console.log('UI host created:', this.host);
}
private createShadow() {
this.shadow = this.host.attachShadow({ mode: "open" });
this.injectStyles();
}
private createMenu() {
this.root = document.createElement("div");
this.root.className = "uw-menu-root font-mono";
const trigger = document.createElement("div");
trigger.className = "uw-menu-trigger";
trigger.textContent = "☰";
const submenu = this.buildSubmenu(this.config.items);
trigger.addEventListener("mouseenter", () => this.show());
this.root.addEventListener("mouseleave", () => this.hide());
this.root.append(trigger, submenu);
this.shadow.appendChild(this.root);
}
private buildSubmenu(items: MenuItemConfig[]): HTMLDivElement {
const menu = document.createElement("div");
menu.className = "uw-submenu";
for (const item of items) {
const el = document.createElement("div");
el.className = "uw-menu-item";
if (item.customHTML) {
el.appendChild(item.customHTML);
} else {
el.textContent = item.label;
}
if (item.action) {
el.addEventListener("click", e => {
e.stopPropagation();
item.action?.();
this.hide();
});
}
if (item.subitems) {
el.appendChild(this.buildSubmenu(item.subitems));
}
menu.appendChild(el);
}
return menu;
} }
private position(anchorEl: HTMLElement) { private position(anchorEl: HTMLElement) {
const rect = anchorEl.getBoundingClientRect(); const rect = anchorEl.getBoundingClientRect();
const r = this.root.style; const r = this.root.style;
switch (this.config.anchor) { // switch (this.config.anchor) {
case "LeftCenter": // case "LeftCenter":
r.left = "0"; // r.left = "0";
r.top = "50%"; // r.top = "50%";
r.transform = "translateY(-50%)"; // r.transform = "translateY(-50%)";
break; // break;
case "TopLeft": // case "TopLeft":
r.left = "0"; // r.left = "0";
r.top = "0"; // r.top = "0";
break; // break;
// others are trivial extensions // // others are trivial extensions
} // }
} }
private bindGlobalMouse(anchorEl: HTMLElement) { private bindGlobalMouse(anchorEl: HTMLElement) {

View File

@ -1,4 +1,8 @@
import ExtensionMode from '@src/common/enums/ExtensionMode.enum'; import ExtensionMode from '@src/common/enums/ExtensionMode.enum';
import { ClientMenu } from './ClientMenu';
import EventBus from '../EventBus';
import PlayerData from '../video-data/PlayerData';
import { SiteSettings } from '../settings/SiteSettings';
if (process.env.CHANNEL !== 'stable'){ if (process.env.CHANNEL !== 'stable'){
console.info("Loading: UI"); console.info("Loading: UI");
@ -13,21 +17,38 @@ const csuiVersions = {
const MAX_IFRAME_ERROR_COUNT = 5; const MAX_IFRAME_ERROR_COUNT = 5;
class UI { class UI {
isGlobal: boolean;
isIframe: boolean;
private eventBus: EventBus;
private playerData: PlayerData;
private uiSettings: any;
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
constructor( constructor(
interfaceId, public interfaceId,
uiConfig, // {parentElement?, eventBus?, isGlobal?, playerData} public uiConfig, // {parentElement?, eventBus?, isGlobal?, playerData}
) { ) {
this.interfaceId = interfaceId;
this.uiConfig = uiConfig;
this.lastProbeResponseTs = null; this.lastProbeResponseTs = null;
this.isGlobal = uiConfig.isGlobal ?? false; this.isGlobal = uiConfig.isGlobal ?? false;
this.isIframe = window.self !== window.top; this.isIframe = window.self !== window.top;
this.eventBus = uiConfig.eventBus; this.eventBus = uiConfig.eventBus;
this.disablePointerEvents = false;
this.saveState = undefined;
this.playerData = uiConfig.playerData; this.playerData = uiConfig.playerData;
this.uiSettings = uiConfig.uiSettings; this.uiSettings = uiConfig.uiSettings;
this.siteSettings = uiConfig.siteSettings; this.siteSettings = uiConfig.siteSettings;
@ -45,7 +66,9 @@ class UI {
this.extensionBase = chrome.runtime.getURL('').replace(/\/$/, ""); this.extensionBase = chrome.runtime.getURL('').replace(/\/$/, "");
// UI will be initialized when setUiVisibility is called // UI will be initialized when setUiVisibility is called
this.init(); if (!this.isGlobal) {
this.init();
}
} }
canRun() { canRun() {
@ -94,6 +117,12 @@ class UI {
const random = Math.round(Math.random() * 69420); const random = Math.round(Math.random() * 69420);
const uwid = `uw-ultrawidify-${this.interfaceId}-root-${random}` const uwid = `uw-ultrawidify-${this.interfaceId}-root-${random}`
if (this.uiConfig.parentElement) {
const uwMenu = new ClientMenu({isGlobal: this.isGlobal, anchor: "LeftCenter", items: [{label: 'test'}]});
uwMenu.mount(this.uiConfig.parentElement);
}
return;
const rootDiv = document.createElement('div'); const rootDiv = document.createElement('div');
if (this.uiConfig.additionalStyle) { if (this.uiConfig.additionalStyle) {

View File

@ -9,12 +9,19 @@
@import '@ui/res/styles/player-menu.css'; @import '@ui/res/styles/player-menu.css';
:host {
@apply bg-stone-950 text-stone-300;
font-size: 16px;
}
html, body { html, body {
@apply bg-stone-950 text-stone-300; @apply bg-stone-950 text-stone-300;
font-size: 16px; font-size: 16px;
} }
@media (max-width: 999px) { @media (max-width: 999px) {
:host {
font-size: 12px;
}
html, body { html, body {
font-size: 12px; font-size: 12px;
} }

View File

@ -1,14 +1,10 @@
@import "tailwindcss"; @import "tailwindcss";
@import "tailwindcss/utilities"; @import "tailwindcss/utilities";
@layer base { .uw-menu-root {
.menu-root { @apply h-full w-full font-mono text-stone-200 text-[16px];
@apply font-mono;
font-size: 5rem;
color: #fa6;
* { * {
@apply font-mono; @apply font-mono text-stone-200 text-[1em];
}
} }
} }

View File

@ -1,279 +0,0 @@
import { CommandInterface } from '@src/common/interfaces/SettingsInterface';
import Settings from '@src/ext/lib/settings/Settings';
const SVG_NS = "http://www.w3.org/2000/svg";
class MenuItemConfig {
textSize: number;
width: number
height: number;
xPad: number;
yPad: number;
constructor(options: {textSize: number, width: number, xPad: number, yPad: number}) {
this.textSize = options.textSize;
this.xPad = options.xPad;
this.yPad = options.yPad;
this.height = options.textSize + 2 * options.yPad;
this.width = options.width;
}
}
class MenuItem {
label: string;
hasHover: boolean;
clickAction: () => void;
parent?: MenuItem;
children?: MenuItem[];
svg: any;
static config: MenuItemConfig;
static fromCommand(command: CommandInterface, executor: (command: string, args?: any) => void, parent?: MenuItem) {
return new MenuItem({
label: command.label,
clickAction: () => {executor(command.action, command.arguments)},
parent: parent
});
}
constructor(data: any) {
this.label = data.label;
this.clickAction = data.clickAction;
this.hasHover = false;
this.children = [] as MenuItem[];
};
private createSvg() {
const group = document.createElementNS(SVG_NS, "g");
const textElem = document.createElementNS(SVG_NS, "text");
textElem.textContent = this.label;
textElem.setAttribute('x', `${MenuItem.config.xPad}`);
textElem.setAttribute('y', `${MenuItem.config.yPad}`);
group.appendChild(textElem);
// Measure text width safely
// const width = textElem.getComputedTextLength() + 2 * hPadding;
// const height = fontSize + 2 * vPadding;
// Background rectangle
const rect = document.createElementNS(SVG_NS, "rect");
rect.setAttribute('x', '0');
rect.setAttribute('y', `0`);
rect.setAttribute('width', `${MenuItem.config.width}`);
rect.setAttribute('height', `${MenuItem.config.height}`);
group.insertBefore(rect, textElem);
this.svg = group;
}
addSubitem(subitem: MenuItem) {
this.children.push(subitem);
if (!subitem.parent) {
subitem.parent = this;
}
}
}
function buildMenu(settings: Settings, executor: (command: string, args?: any) => void) {
const menu = new MenuItem(
{label: 'Ultrawidify'}
);
const topLevelItems: { label: string; submenu?: CommandInterface[]; isZoom?: boolean }[] = [
{ label: "Ultrawidify" },
...(settings.active.commands.crop ? [{ label: "Crop", submenu: settings.active.commands.crop }] : []),
...(settings.active.commands.stretch ? [{ label: "Stretch", submenu: settings.active.commands.stretch }] : []),
...(settings.active.commands.zoom ? [{ label: "Zoom", submenu: settings.active.commands.zoom, isZoom: true }] : []),
{ label: "Alignment", submenu: [] },
{ label: "Extension settings" },
{ label: "Incorrect cropping?" },
{ label: "Report problem" }
];
}
function createMenuItem(label) {
}
export function createSvgMenu(playerEl: HTMLElement, settings: Settings) {
if (!settings) {
console.warn('trying to create menu, but settings are not defined:', settings, 'player el:', playerEl);
return;
}
const style = document.createElement('style');
style.textContent = `
@font-face {
font-family: 'Heebo';
src: url(${chrome.runtime.getURL('/ui/res/fonts/Heebo.tff')}) format('truetype');
}
@font-face {
font-family: 'SourceCodePro';
src: url(${chrome.runtime.getURL('/ui/res/fonts/SourceCodePro.tff')}) format('truetype');
}
#_uw_player_menu { width:100%; height:100%; position:absolute; top:0; left:0; pointer-events:none; }
#_uw_player_menu {
.menu { pointer-events:auto; }
.menu-item text { font-family: 'Heebo'; font-size:14px; fill:#fff; }
.submenu { display:none; }
.menu-item:hover > .submenu { display:block; }
.hover-zone { fill:transparent; pointer-events:auto; }
.menu-item rect { fill:#222; rx:4; }
.menu-item:hover > rect { fill:#444; }
}
`;
const svg = document.createElementNS(SVG_NS, "svg");
svg.setAttribute('id', '_uw_player_menu');
svg.append(style);
playerEl.appendChild(svg);
// shadow.appendChild(svg);
// menuGroup.setAttribute('class', 'menu');
svg.appendChild(menuGroup);
const fontSize = 14;
const vPadding = fontSize * 0.4;
const hPadding = fontSize * 0.6;
let currentY = 0;
// --- Top level items ---
const topLevelItems: { label: string; submenu?: CommandInterface[]; isZoom?: boolean }[] = [
{ label: "Ultrawidify" },
...(settings.active.commands.crop ? [{ label: "Crop", submenu: settings.active.commands.crop }] : []),
...(settings.active.commands.stretch ? [{ label: "Stretch", submenu: settings.active.commands.stretch }] : []),
...(settings.active.commands.zoom ? [{ label: "Zoom", submenu: settings.active.commands.zoom, isZoom: true }] : []),
{ label: "Alignment", submenu: [] },
{ label: "Extension settings" },
{ label: "Incorrect cropping?" },
{ label: "Report problem" }
];
topLevelItems.forEach(item => {
const itemGroup = document.createElementNS(SVG_NS, "g");
itemGroup.setAttribute('class','menu-item');
menuGroup.appendChild(itemGroup);
// Text element
const textElem = document.createElementNS(SVG_NS, "text");
textElem.textContent = item.label;
textElem.setAttribute('x', `${hPadding}`);
textElem.setAttribute('y', `${currentY + fontSize}`);
itemGroup.appendChild(textElem);
// Measure text width safely
const width = textElem.getComputedTextLength() + 2 * hPadding;
const height = fontSize + 2 * vPadding;
// Background rectangle
const rect = document.createElementNS(SVG_NS, "rect");
rect.setAttribute('x', '0');
rect.setAttribute('y', `${currentY}`);
rect.setAttribute('width', `${width}`);
rect.setAttribute('height', `${height}`);
itemGroup.insertBefore(rect, textElem);
// --- Submenu ---
if(item.submenu && item.submenu.length > 0) {
const submenuGroup = document.createElementNS(SVG_NS, "g");
submenuGroup.setAttribute('class','submenu');
itemGroup.appendChild(submenuGroup);
let subY = currentY;
item.submenu.forEach(cmd => {
const subGroup = document.createElementNS(SVG_NS, "g");
subGroup.setAttribute('class','menu-item');
submenuGroup.appendChild(subGroup);
// Submenu text
const subText = document.createElementNS(SVG_NS, "text");
subText.textContent = cmd.label + (cmd.shortcut ? ` (${cmd.shortcut.key})` : '');
subText.setAttribute('x', `${width + hPadding}`);
subText.setAttribute('y', `${subY + fontSize}`);
subGroup.appendChild(subText);
const subWidth = subText.getComputedTextLength() + 2*hPadding;
const subHeight = fontSize + 2*vPadding;
const subRect = document.createElementNS(SVG_NS, "rect");
subRect.setAttribute('x', `${width}`);
subRect.setAttribute('y', `${subY}`);
subRect.setAttribute('width', `${subWidth}`);
subRect.setAttribute('height', `${subHeight}`);
subGroup.insertBefore(subRect, subText);
subY += subHeight;
});
// Zoom slider
if(item.isZoom) {
const sliderGroup = document.createElementNS(SVG_NS, "foreignObject");
sliderGroup.setAttribute('x', `${width}`);
sliderGroup.setAttribute('y', `${subY}`);
sliderGroup.setAttribute('width', '150');
sliderGroup.setAttribute('height', '40');
sliderGroup.innerHTML = `<div style="width:100%;height:100%;background:#333;color:#fff;font-family:Heebo;padding:5px;">Zoom slider placeholder</div>`;
submenuGroup.appendChild(sliderGroup);
}
// Hover retention zone
const hoverZone = document.createElementNS(SVG_NS, "rect");
hoverZone.setAttribute('class','hover-zone');
hoverZone.setAttribute('x', `${width}`);
hoverZone.setAttribute('y', `${currentY}`);
hoverZone.setAttribute('width', '150');
hoverZone.setAttribute('height', `${subY - currentY}`);
itemGroup.appendChild(hoverZone);
}
currentY += height;
});
// --- Trigger logic ---
const activation = settings.active.ui.inPlayer.activation;
const triggerZone = settings.active.ui.inPlayer.triggerZoneDimensions;
const distanceOptions = settings.active.ui.inPlayer.distanceOptions;
function updateMenuVisibility(x: number, y: number) {
let show = false;
const rect = playerEl.getBoundingClientRect();
if(activation === 'player') {
if(x >= 0 && x <= rect.width && y >= 0 && y <= rect.height) show = true;
} else if(activation === 'trigger-zone' && triggerZone) {
const tx = rect.width * (triggerZone.offsetX / 100);
const ty = rect.height * (triggerZone.offsetY / 100);
if(x >= tx && x <= tx + triggerZone.width && y >= ty && y <= ty + triggerZone.height) show = true;
} else if(activation === 'distance' && distanceOptions) {
const dist = distanceOptions.distance / 100 * (distanceOptions.relativeTo === 'width' ? rect.width : rect.height);
const cx = rect.width/2;
const cy = rect.height/2;
if(Math.sqrt((x-cx)**2 + (y-cy)**2) <= dist) show = true;
}
menuGroup.style.display = show ? 'block' : 'none';
}
svg.addEventListener('mousemove', e => {
const rect = svg.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
updateMenuVisibility(x,y);
});
menuGroup.style.display = 'none';
return { svg, menuGroup };
}

39
tailwind.config.js Normal file
View File

@ -0,0 +1,39 @@
module.exports = {
content: [
"./src/**/*.{vue,js,ts,jsx,tsx}"
],
safelist: [
{
pattern: /^uw-/,
variants: ['hover', 'focus', 'active', 'disabled'],
}
],
theme: {
extend: {
screens: {
"popup-sm": {"max": "639px"},
"popup-lg": {"min": "640px", "max": "899px"},
window: {"min": "900px"}
},
colors: {
primary: {
50: "#ffddbe",
100: "#ffcda0",
200: "#ffbd83",
300: "#ffac66",
400: "#ff9a4a",
500: "#ff872c",
600: "#de7622",
700: "#be6518",
800: "#9f540e",
900: "#824406",
950: "#663400"
}
},
fontFamily: {
sans: ["Heebo", "ui-sans-serif", "system-ui"],
mono: ["Source Code Pro", "ui-monospace", "monospace"]
}
}
}
}

View File

@ -1,38 +0,0 @@
{
"content": [
"./src/**/*.{vue,js,ts,jsx,tsx}"
],
"safelist": [
{
"pattern": "/^(bg|text|hover:bg|rounded|p|px|py)-/"
}
],
"theme": {
"extend": {
"screens": {
"popup-sm": {"max": "639px"},
"popup-lg": {"min": "640px", "max": "899px"},
"window": {"min": "900px"}
},
"colors": {
"primary": {
"50": "#ffddbe",
"100": "#ffcda0",
"200": "#ffbd83",
"300": "#ffac66",
"400": "#ff9a4a",
"500": "#ff872c",
"600": "#de7622",
"700": "#be6518",
"800": "#9f540e",
"900": "#824406",
"950": "#663400"
}
},
"fontFamily": {
"sans": ["Heebo", "ui-sans-serif", "system-ui"],
"mono": ["Source Code Pro", "ui-monospace", "monospace"]
}
}
}
}