Start working on client menu, sort out CSS injection

This commit is contained in:
Tamius Han 2025-12-29 00:21:03 +01:00
parent fcfc0361dc
commit ef12dc8cc5
11 changed files with 297 additions and 29 deletions

View File

@ -61,6 +61,7 @@
"rescan", "rescan",
"resizer", "resizer",
"resizers", "resizers",
"SCPI",
"scrollbar", "scrollbar",
"shitiness", "shitiness",
"smallcaps", "smallcaps",

View File

@ -0,0 +1,22 @@
export interface MenuItemConfig {
label: string;
subitems?: MenuItemConfig[];
action?: () => void;
customHTML?: HTMLElement;
}
export type MenuAnchor =
| "LeftCenter"
| "RightCenter"
| "TopCenter"
| "BottomCenter"
| "TopLeft"
| "TopRight"
| "BottomLeft"
| "BottomRight";
export interface MenuConfig {
anchor: MenuAnchor;
activationRadius: number;
items: MenuItemConfig[];
}

View File

@ -0,0 +1,196 @@
import { MenuConfig, MenuItemConfig } from '@src/common/interfaces/ClientUiMenu.interface';
import extensionCss from '@src/main.css?inline';
export class ClientMenu {
private host!: HTMLDivElement;
private shadow!: ShadowRoot;
private root!: HTMLDivElement;
private visible = false;
constructor(private config: MenuConfig) {}
mount(anchorElement: HTMLElement) {
this.createHost();
this.createShadow();
this.createMenu();
this.position(anchorElement);
this.bindGlobalMouse(anchorElement);
document.documentElement.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() {
const style = document.createElement("style");
console.warn('imported styles: imported CSS (raw):', typeof extensionCss, extensionCss);
let css = extensionCss
.trim()
.replace(/'html, body \{/g, ':host {')
.replace(/'body \{/g, ':host {')
;
// this is bad but I can't be bothered to do it the proper way
const cssArr: string[] = css.split('@font-face');
css = `
${cssArr[0]}
@font-face {
font-family: "Heebo";
src: url(__FONT_HEEBO__) format("truetype-variations");
font-weight: 100 900;
font-stretch: 75% 125%;
font-style: normal;
font-display: swap;
}
@font-face {
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-weight: 200 900;
font-style: italic;
font-display: swap;
}
${cssArr[cssArr.length - 1].split('}', 2)[1]};
`;
// function scopeCss(css: string) {
// return css.replace(
// /(^|})\s*([^{@}][^{]*){/g,
// (_, sep, selector) => `${sep} :host ${selector} {`
// );
// }
css = css
.replace('__FONT_HEEBO__', chrome.runtime.getURL('/ui/res/fonts/Heebo.ttf'))
.replace('__FONT_SCP__', chrome.runtime.getURL('/ui/res/fonts/SourceCodePro.ttf'))
.replace('__FONT_SCPI__', chrome.runtime.getURL('/ui/res/fonts/SourceCodePro-Italic.ttf'))
.replaceAll('html,', '')
;
// console.warn('CSS TO APPEND', css);
style.textContent = css;
this.shadow.appendChild(style);
}
private position(anchorEl: HTMLElement) {
const rect = anchorEl.getBoundingClientRect();
const r = this.root.style;
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) {
const rect = anchorEl.getBoundingClientRect();
const cx = rect.left + rect.width / 2;
const cy = rect.top + rect.height / 2;
document.addEventListener("mousemove", e => {
const d = Math.hypot(e.clientX - cx, e.clientY - cy);
if (d < this.config.activationRadius) {
this.show();
} else if (!this.root.matches(":hover")) {
this.hide();
}
});
}
private show() {
if (!this.visible) {
this.visible = true;
this.root.classList.add("visible");
}
}
private hide() {
if (this.visible) {
this.visible = false;
this.root.classList.remove("visible");
}
}
}

View File

@ -6,6 +6,9 @@
@import '@ui/res/styles/fonts.css'; @import '@ui/res/styles/fonts.css';
@import '@ui/res/styles/buttons.css'; @import '@ui/res/styles/buttons.css';
@import '@ui/res/styles/player-menu.css';
html, body { html, body {
@apply bg-stone-950 text-stone-300; @apply bg-stone-950 text-stone-300;

View File

@ -48,7 +48,8 @@
"res/icons/*", "res/icons/*",
"res/img/*", "res/img/*",
"csui/*", "csui/*",
"ui/*" "ui/*",
"main.css"
], ],
"matches": [ "matches": [
"*://*/*" "*://*/*"

View File

@ -116,7 +116,7 @@ import ShortcutButton from './components/ShortcutButton.vue';
import CommsMixin from '@ui/utils/mixins/CommsMixin.vue'; import CommsMixin from '@ui/utils/mixins/CommsMixin.vue';
import KeyboardShortcutParserMixin from '@ui/utils/mixins/KeyboardShortcutParserMixin.vue'; import KeyboardShortcutParserMixin from '@ui/utils/mixins/KeyboardShortcutParserMixin.vue';
import alignmentIndicatorSvg from '!!raw-loader!@ui/res/img/alignment-indicators.svg?raw'; import alignmentIndicatorSvg from '!!raw-loader!@ui/res/img/alignment-indicators.svg';
import {setupVideoAlignmentIndicatorInteraction, setVideoAlignmentIndicatorState} from '@ui/utils/video-alignment-indicator-handling'; import {setupVideoAlignmentIndicatorInteraction, setVideoAlignmentIndicatorState} from '@ui/utils/video-alignment-indicator-handling';
import VideoAlignmentType from '@src/common/enums/VideoAlignmentType.enum'; import VideoAlignmentType from '@src/common/enums/VideoAlignmentType.enum';

View File

@ -0,0 +1,14 @@
@import "tailwindcss";
@import "tailwindcss/utilities";
@layer base {
.menu-root {
@apply font-mono;
font-size: 5rem;
color: #fa6;
* {
@apply font-mono;
}
}
}

View File

@ -5,6 +5,9 @@
--breakpoint-popup: 640px; --breakpoint-popup: 640px;
--breakpoint-window: 960px; --breakpoint-window: 960px;
--font-sans: "Heebo", ui-sans-serif, system-ui;
--font-mono: "Source Code Pro", ui-monospace, monospace;
--color-primary: #ff872c; --color-primary: #ff872c;
--color-primary-50: #ffddbe; --color-primary-50: #ffddbe;
--color-primary-100: #ffcda0; --color-primary-100: #ffcda0;

View File

@ -1,6 +1,7 @@
import { CommandInterface } from '@src/common/interfaces/SettingsInterface'; import { CommandInterface } from '@src/common/interfaces/SettingsInterface';
import Settings from '@src/ext/lib/settings/Settings'; import Settings from '@src/ext/lib/settings/Settings';
const SVG_NS = "http://www.w3.org/2000/svg"; const SVG_NS = "http://www.w3.org/2000/svg";
@ -80,9 +81,6 @@ class MenuItem {
} }
} }
function buildMenu(settings: Settings, executor: (command: string, args?: any) => void) { function buildMenu(settings: Settings, executor: (command: string, args?: any) => void) {
const menu = new MenuItem( const menu = new MenuItem(
{label: 'Ultrawidify'} {label: 'Ultrawidify'}

View File

@ -2,6 +2,11 @@
"content": [ "content": [
"./src/**/*.{vue,js,ts,jsx,tsx}" "./src/**/*.{vue,js,ts,jsx,tsx}"
], ],
"safelist": [
{
"pattern": "/^(bg|text|hover:bg|rounded|p|px|py)-/"
}
],
"theme": { "theme": {
"extend": { "extend": {
"screens": { "screens": {

View File

@ -36,6 +36,20 @@ const config = {
}, },
module: { module: {
rules: [ rules: [
{
test: /\.(png|jpg|webp|gif|svg|ico)$/,
type: 'asset/resource',
generator: {
filename: '[path][name][ext]' // Webpack 5 uses generator.filename
}
},
{
test: /\.(woff(2)?)$/,
type: 'asset/resource',
generator: {
filename: '[path][name][ext]'
}
},
{ {
test: /\.ts$/, test: /\.ts$/,
exclude: /node_modules/, exclude: /node_modules/,
@ -68,6 +82,29 @@ const config = {
}, },
{ {
test: /\.(sc|c|postc)ss$/, test: /\.(sc|c|postc)ss$/,
oneOf: [
// Content script exports
{
resourceQuery: query => query && query.includes('inline'),
use: [
{
loader: 'css-loader',
options: {
exportType: "string"
}
},
{
loader: 'postcss-loader',
options: {
postcssOptions: {
config: path.resolve(__dirname, 'postcss.config.js'),
}
}
}
],
},
// For proper pages
{
use: [ use: [
'vue-style-loader', 'vue-style-loader',
{ {
@ -81,21 +118,9 @@ const config = {
}, },
'postcss-loader', 'postcss-loader',
] ]
}
]
}, },
{
test: /\.(png|jpg|webp|gif|svg|ico)$/,
type: 'asset/resource',
generator: {
filename: '[path][name][ext]' // Webpack 5 uses generator.filename
}
},
{
test: /\.(woff(2)?)$/,
type: 'asset/resource',
generator: {
filename: '[path][name][ext]'
}
}
] ]
}, },
plugins: [ plugins: [