From ef12dc8cc5f04d533e190e4a33be9e166411da3d Mon Sep 17 00:00:00 2001 From: Tamius Han Date: Mon, 29 Dec 2025 00:21:03 +0100 Subject: [PATCH] Start working on client menu, sort out CSS injection --- .vscode/settings.json | 1 + .../interfaces/ClientUiMenu.interface.ts | 22 ++ src/ext/lib/uwui/ClientMenu.ts | 196 ++++++++++++++++++ src/main.css | 3 + src/manifest.json | 3 +- .../segments/VideoSettings/VideoSettings.vue | 2 +- src/ui/res/styles/player-menu.css | 14 ++ src/ui/res/styles/theme.css | 3 + src/ui/utils/svg.menu.ts | 4 +- tailwind.config.json | 5 + webpack.config.js | 73 ++++--- 11 files changed, 297 insertions(+), 29 deletions(-) create mode 100644 src/common/interfaces/ClientUiMenu.interface.ts create mode 100644 src/ext/lib/uwui/ClientMenu.ts create mode 100644 src/ui/res/styles/player-menu.css diff --git a/.vscode/settings.json b/.vscode/settings.json index 62d5e2f..ce80cb1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -61,6 +61,7 @@ "rescan", "resizer", "resizers", + "SCPI", "scrollbar", "shitiness", "smallcaps", diff --git a/src/common/interfaces/ClientUiMenu.interface.ts b/src/common/interfaces/ClientUiMenu.interface.ts new file mode 100644 index 0000000..f3ee332 --- /dev/null +++ b/src/common/interfaces/ClientUiMenu.interface.ts @@ -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[]; +} diff --git a/src/ext/lib/uwui/ClientMenu.ts b/src/ext/lib/uwui/ClientMenu.ts new file mode 100644 index 0000000..00867b7 --- /dev/null +++ b/src/ext/lib/uwui/ClientMenu.ts @@ -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"); + } + } +} diff --git a/src/main.css b/src/main.css index b26ef20..7a846a2 100644 --- a/src/main.css +++ b/src/main.css @@ -6,6 +6,9 @@ @import '@ui/res/styles/fonts.css'; @import '@ui/res/styles/buttons.css'; +@import '@ui/res/styles/player-menu.css'; + + html, body { @apply bg-stone-950 text-stone-300; diff --git a/src/manifest.json b/src/manifest.json index 839a983..addcc56 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -48,7 +48,8 @@ "res/icons/*", "res/img/*", "csui/*", - "ui/*" + "ui/*", + "main.css" ], "matches": [ "*://*/*" diff --git a/src/ui/components/segments/VideoSettings/VideoSettings.vue b/src/ui/components/segments/VideoSettings/VideoSettings.vue index 2c68ab4..34810f9 100644 --- a/src/ui/components/segments/VideoSettings/VideoSettings.vue +++ b/src/ui/components/segments/VideoSettings/VideoSettings.vue @@ -116,7 +116,7 @@ import ShortcutButton from './components/ShortcutButton.vue'; import CommsMixin from '@ui/utils/mixins/CommsMixin.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 VideoAlignmentType from '@src/common/enums/VideoAlignmentType.enum'; diff --git a/src/ui/res/styles/player-menu.css b/src/ui/res/styles/player-menu.css new file mode 100644 index 0000000..e9f33e9 --- /dev/null +++ b/src/ui/res/styles/player-menu.css @@ -0,0 +1,14 @@ +@import "tailwindcss"; +@import "tailwindcss/utilities"; + +@layer base { + .menu-root { + @apply font-mono; + font-size: 5rem; + color: #fa6; + + * { + @apply font-mono; + } + } +} diff --git a/src/ui/res/styles/theme.css b/src/ui/res/styles/theme.css index 91d5d84..2cb5007 100644 --- a/src/ui/res/styles/theme.css +++ b/src/ui/res/styles/theme.css @@ -5,6 +5,9 @@ --breakpoint-popup: 640px; --breakpoint-window: 960px; + --font-sans: "Heebo", ui-sans-serif, system-ui; + --font-mono: "Source Code Pro", ui-monospace, monospace; + --color-primary: #ff872c; --color-primary-50: #ffddbe; --color-primary-100: #ffcda0; diff --git a/src/ui/utils/svg.menu.ts b/src/ui/utils/svg.menu.ts index 6ca08cf..595d294 100644 --- a/src/ui/utils/svg.menu.ts +++ b/src/ui/utils/svg.menu.ts @@ -1,6 +1,7 @@ import { CommandInterface } from '@src/common/interfaces/SettingsInterface'; import Settings from '@src/ext/lib/settings/Settings'; + 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) { const menu = new MenuItem( {label: 'Ultrawidify'} diff --git a/tailwind.config.json b/tailwind.config.json index 26b7e32..a2c555c 100644 --- a/tailwind.config.json +++ b/tailwind.config.json @@ -2,6 +2,11 @@ "content": [ "./src/**/*.{vue,js,ts,jsx,tsx}" ], + "safelist": [ + { + "pattern": "/^(bg|text|hover:bg|rounded|p|px|py)-/" + } + ], "theme": { "extend": { "screens": { diff --git a/webpack.config.js b/webpack.config.js index f42525f..25a7d13 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -36,6 +36,20 @@ const config = { }, module: { 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$/, exclude: /node_modules/, @@ -68,34 +82,45 @@ const config = { }, { test: /\.(sc|c|postc)ss$/, - use: [ - 'vue-style-loader', + oneOf: [ + // Content script exports { - loader: 'css-loader', - options: { - // Uncomment if you want CSS modules - // modules: { - // localIdentName: "[name]-[hash]" - // } - } + 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'), + } + } + } + ], }, - 'postcss-loader', + // For proper pages + { + use: [ + 'vue-style-loader', + { + loader: 'css-loader', + options: { + // Uncomment if you want CSS modules + // modules: { + // localIdentName: "[name]-[hash]" + // } + } + }, + '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: [