Open in-page settings inside floating windows

This commit is contained in:
Tamius Han 2026-01-04 00:54:59 +01:00
parent b6dec385dc
commit b62f6feaea
8 changed files with 591 additions and 13 deletions

View File

@ -15,6 +15,11 @@ import alignmentIndicatorSvg from '!!raw-loader!@ui/res/img/alignment-indicators
import lockBarIndicatorSvg from '!!raw-loader!@ui/res/img/lock-bar-indicators.svg'; import lockBarIndicatorSvg from '!!raw-loader!@ui/res/img/lock-bar-indicators.svg';
import VideoAlignmentType from '../../../common/enums/VideoAlignmentType.enum'; import VideoAlignmentType from '../../../common/enums/VideoAlignmentType.enum';
import { setVideoAlignmentIndicatorState } from '../../../ui/utils/video-alignment-indicator-handling'; import { setVideoAlignmentIndicatorState } from '../../../ui/utils/video-alignment-indicator-handling';
import { UwuiWindow } from './UwuiWindow';
import { createApp } from 'vue';
import SettingsWindowContent from '@components/SettingsWindowContent.vue';
// import jsonEditorCSS from 'vanilla-jsoneditor/themes/jse-theme-dark.css?inline'
if (process.env.CHANNEL !== 'stable'){ if (process.env.CHANNEL !== 'stable'){
console.info("Loading: UI"); console.info("Loading: UI");
@ -293,10 +298,16 @@ class UI {
}] }]
}, },
{ {
label: 'todo: open settings' label: 'Extension settings',
action: () => this.createSettingsWindow(),
}, },
{ {
label: 'todo: first-time "customize/hide menu" option' label: 'Cropping issues?',
action: () => this.createSettingsWindow('ui-settings'),
},
{
label: 'Report a problem',
action: () => this.createSettingsWindow('about'),
} }
] ]
} }
@ -393,6 +404,28 @@ class UI {
} }
} }
createSettingsWindow(path?: string) {
const iframe = document.createElement('iframe');
iframe.src = chrome.runtime.getURL(`ui/pages/settings/index.html#ui${path ? `/${path}` : ''}`);
iframe.setAttribute('allowtransparency', 'true');
Object.assign(iframe.style, {
width: '100%',
height: '100%',
border: 'none',
background: 'transparent', // important
});
new UwuiWindow({
title: 'Ultrawidify settings (iframe)',
width: 1200,
height: 800,
x: 0,
y: 0,
content: iframe,
});
}
setUiVisibility(visible) { setUiVisibility(visible) {
return; return;
} }

View File

@ -0,0 +1,336 @@
import extensionCss from '@src/main.css?inline';
type WindowContent = string | HTMLElement;
type WindowBackgroundMode = 'transparent' | 'blur' | 'solid';
interface WindowOptions {
title?: string;
content: WindowContent;
background?: WindowBackgroundMode;
width?: number;
height?: number;
x?: number;
y?: number;
extraStyles?: string[];
onFocus?: () => void;
onClose?: () => void;
}
export class UwuiWindow {
private static baseZ = 1000000;
private static instances: UwuiWindow[] = [];
private host: HTMLDivElement;
private shadow: ShadowRoot;
private windowEl: HTMLDivElement;
private titleBar: HTMLDivElement;
private dragging = false;
private resizing = false;
private dragOffsetX = 0;
private dragOffsetY = 0;
private background: WindowBackgroundMode = 'blur';
private extraStyles?: string[];
private onFocus?: () => void;
private onClose?: () => void;
constructor(options: WindowOptions) {
this.onFocus = options.onFocus;
this.onClose = options.onClose;
this.host = document.createElement('div');
this.host.style.position = 'fixed';
this.host.style.inset = '0';
this.host.style.pointerEvents = 'none';
this.background = options.background ?? 'blur';
this.extraStyles = options.extraStyles;
document.body.appendChild(this.host);
this.shadow = this.host.attachShadow({ mode: 'closed' });
this.shadow.innerHTML = this.template(options);
this.windowEl = this.shadow.querySelector('.uw-window')!;
this.titleBar = this.shadow.querySelector('.uw-titlebar')!;
this.setGeometry(
options.x ?? 100,
options.y ?? 100,
options.width ?? 320,
options.height ?? 220
);
this.injectContent(options.content);
this.bindEvents();
UwuiWindow.instances.unshift(this);
UwuiWindow.recomputeZIndices();
this.applyBackground();
this.bindBackgroundMenu();
}
/**
* Sets window stacking order
* @param order stacking order (0 = topmost)
*/
setOrder(order: number) {
const list = UwuiWindow.instances;
// Remove this window from current position
const currentIndex = list.indexOf(this);
if (currentIndex !== -1) {
list.splice(currentIndex, 1);
}
// Clamp target index
const clamped = Math.max(0, Math.min(order, list.length));
// Insert at new position
list.splice(clamped, 0, this);
// Re-assign z-indexes
UwuiWindow.recomputeZIndices();
}
destroy() {
this.onClose?.();
const list = UwuiWindow.instances;
const index = list.indexOf(this);
if (index !== -1) {
list.splice(index, 1);
UwuiWindow.recomputeZIndices();
}
this.host.remove();
}
private static recomputeZIndices() {
const base = UwuiWindow.baseZ;
UwuiWindow.instances.forEach((win, index) => {
// index 0 = topmost → highest z-index
win.host.style.zIndex = String(base + (UwuiWindow.instances.length - index));
});
}
private bringToFront = () => {
this.setOrder(0);
this.onFocus?.();
};
//#region setup
private template(options: WindowOptions): string {
return `
<style>
${this.generateStyles()}
${this.extraStyles?.join('\n')}
</style>
<div class="uw-window">
<div class="uw-titlebar">
<span class="uw-title">${options.title ?? ''}</span>
<div class="uw-actions">
<div class="uw-bg-toggle">
<!-- YOUR SVG ICON GOES HERE -->
<button type="button" class="uw-bg-button">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>layers-outline</title><path d="M12,18.54L19.37,12.8L21,14.07L12,21.07L3,14.07L4.62,12.81L12,18.54M12,16L3,9L12,2L21,9L12,16M12,4.53L6.26,9L12,13.47L17.74,9L12,4.53Z" /></svg>
</button>
<div class="uw-bg-menu">
<button data-bg="transparent">Transparent</button>
<button data-bg="blur">Blur</button>
<button data-bg="solid">Solid</button>
</div>
</div>
<button type="button" class="uw-close">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>close</title><path d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z" /></svg>
</button>
</div>
</div>
<div class="uw-content"></div>
<div class="uw-resize-handle"></div>
</div>
`;
}
private generateStyles() {
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');
const cssRemainder = cssArr[cssArr.length - 1].split('}').slice(1).join('}');
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;
}
${cssRemainder};
`;
return 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,', '')
;
}
private injectContent(content: WindowContent) {
const container = this.shadow.querySelector('.uw-content')!;
if (typeof content === 'string') {
container.innerHTML = content;
} else {
container.appendChild(content);
}
}
//#endregion
//#region interactivity
private bindEvents() {
this.windowEl.addEventListener('mousedown', this.bringToFront);
this.titleBar.addEventListener('mousedown', this.startDrag);
this.shadow.querySelector('.uw-close')!
.addEventListener('click', () => this.destroy());
this.shadow.querySelector('.uw-resize-handle')!
.addEventListener('mousedown', this.startResize);
window.addEventListener('mousemove', this.onMouseMove);
window.addEventListener('mouseup', this.stopActions);
}
private startDrag = (e: MouseEvent) => {
const rect = this.windowEl.getBoundingClientRect();
this.dragging = true;
this.dragOffsetX = e.clientX - rect.left;
this.dragOffsetY = e.clientY - rect.top;
e.preventDefault();
};
private startResize = (e: MouseEvent) => {
this.resizing = true;
e.preventDefault();
};
private onMouseMove = (e: MouseEvent) => {
const rect = this.windowEl.getBoundingClientRect();
if (this.dragging) {
const x = Math.min(
window.innerWidth - rect.width,
Math.max(0, e.clientX - this.dragOffsetX)
);
const y = Math.min(
window.innerHeight - rect.height,
Math.max(0, e.clientY - this.dragOffsetY)
);
this.windowEl.style.left = `${x}px`;
this.windowEl.style.top = `${y}px`;
}
if (this.resizing) {
const width = Math.max(160, e.clientX - rect.left);
const height = Math.max(120, e.clientY - rect.top);
this.windowEl.style.width = `${width}px`;
this.windowEl.style.height = `${height}px`;
}
};
private stopActions = () => {
this.dragging = false;
this.resizing = false;
};
private setGeometry(x: number, y: number, w: number, h: number) {
Object.assign(this.windowEl.style, {
left: `${x}px`,
top: `${y}px`,
width: `${w}px`,
height: `${h}px`,
});
}
//#endregion interactivity
//#region bakcground handling
setBackground(mode: WindowBackgroundMode) {
this.background = mode;
this.applyBackground();
}
private applyBackground() {
this.windowEl.classList.remove(
'uw-window-bg-transparent',
'uw-window-bg-blurred',
'uw-window-bg-solid'
);
switch (this.background) {
case 'transparent':
this.windowEl.classList.add('uw-window-bg-transparent');
break;
case 'blur':
this.windowEl.classList.add('uw-window-bg-blurred');
break;
case 'solid':
this.windowEl.classList.add('uw-window-bg-solid');
break;
}
}
private bindBackgroundMenu() {
this.shadow.querySelectorAll('[data-bg]')
.forEach(el => {
el.addEventListener('click', () => {
const mode = el.getAttribute('data-bg') as WindowBackgroundMode;
this.setBackground(mode);
});
});
}
//#endregion
}

View File

@ -7,14 +7,15 @@
@import '@ui/res/styles/buttons.css'; @import '@ui/res/styles/buttons.css';
@import '@ui/res/styles/player-menu.css'; @import '@ui/res/styles/player-menu.css';
@import '@ui/res/styles/player-window.css';
:host { :host {
@apply bg-stone-950 text-stone-300; @apply bg-transparent text-stone-300;
font-size: 16px; font-size: 16px;
} }
html, body { html, body {
@apply bg-stone-950 text-stone-300; @apply bg-transparent text-stone-300;
font-size: 16px; font-size: 16px;
} }

View File

@ -95,6 +95,15 @@
></SiteExtensionSettings> ></SiteExtensionSettings>
</template> </template>
<template v-if="settings && selectedTab === 'window.site-extension-settings'" >
<h3>Settings for {{site?.host}}</h3>
<SiteExtensionSettings
:settings="settings"
:siteSettings="siteSettings"
:isDefaultConfiguration="false"
></SiteExtensionSettings>
</template>
<template v-if="settings && selectedTab === 'embedded-extension-settings'" > <template v-if="settings && selectedTab === 'embedded-extension-settings'" >
<h3>Settings for embedded sites</h3> <h3>Settings for embedded sites</h3>
<FrameSiteSettings <FrameSiteSettings
@ -223,6 +232,15 @@ const AVAILABLE_TABS = {
{ id: 'default-extension-settings', label: 'Default settings' } { id: 'default-extension-settings', label: 'Default settings' }
] ]
}, },
'window.site-extension-settings': {
id: 'window.site-extension-settings', label: 'Site and Extension options', icon: 'cogs',
children: [
{ id: 'window.site-extension-settings', label: 'For this site', },
{ id: 'embedded-extension-settings', label: 'For embedded sites', disabled: true, badgeCount: 0, },
{ id: 'default-extension-settings', label: 'Default settings' },
{ id: 'website-extension-settings', label: 'Website exceptions' },
]
},
'default-extension-settings': { 'default-extension-settings': {
id: 'default-extension-settings', label: 'Site and Extension options', icon: 'cogs', id: 'default-extension-settings', label: 'Site and Extension options', icon: 'cogs',
children: [ children: [
@ -258,7 +276,15 @@ const TAB_LOADOUT = {
'debugging', 'debugging',
], ],
'ui': [ 'ui': [
'window.player-element-settings' 'window.site-extension-settings',
'window.player-element-settings',
'autodetectionSettings',
'ui-settings',
'keyboardShortcuts',
'changelog',
'about',
'import-export-settings',
'debugging',
], ],
'updated': [ 'updated': [
'updated', 'updated',

View File

@ -23,6 +23,7 @@
@change="settings.saveWithoutReload" @change="settings.saveWithoutReload"
> >
</div> </div>
<div class="w-full grid grid-cols-1 min-[1200px]:grid-cols-2 min-[1920px]:grid-cols-3 max-w-[2300px] gap-8"> <div class="w-full grid grid-cols-1 min-[1200px]:grid-cols-2 min-[1920px]:grid-cols-3 max-w-[2300px] gap-8">
<div class="grow shrink"> <div class="grow shrink">
<h3 class="mb-4">Logger configuration</h3> <h3 class="mb-4">Logger configuration</h3>

View File

@ -1,14 +1,18 @@
<template> <template>
<div class=" <div
w-full h-[100dvh] overflow-hidden class="
p-1 popup-lg:py-2 popup-lg:px-2 window:py-4 window:px-8 w-full h-[100dvh] overflow-hidden
flex flex-row justify-center items-center flex flex-row justify-center items-center
"> "
:class="{
'p-1 popup-lg:py-2 popup-lg:px-2 window:py-4 window:px-8 bg-stone-950': role !== 'ui'
}"
>
<!-- page content --> <!-- page content -->
<div <div
class="w-full h-[100dvh] overflow-hidden flex flex-col" class="w-full h-[100dvh] overflow-hidden flex flex-col"
:class="{'max-w-[1920px]': !isDebugging}" :class="{'max-w-[1920px]': !isDebugging && role !== 'ui'}"
> >
<PopupHead <PopupHead
v-if="role === 'popup' && settings && siteSettings && eventBus" v-if="role === 'popup' && settings && siteSettings && eventBus"
@ -18,6 +22,9 @@
:eventBus="eventBus" :eventBus="eventBus"
> >
</PopupHead> </PopupHead>
<div v-else-if="role === 'ui'" class="flex flex-row font-mono text-[0.8rem] text-stone-500 border-stone-500">
url: {{getUrl}}
</div>
<div v-else> <div v-else>
<h1 class="text-[3em] grow-0 shrink-0">Ultrawidify settings</h1> <h1 class="text-[3em] grow-0 shrink-0">Ultrawidify settings</h1>
</div> </div>
@ -78,6 +85,11 @@ export default defineComponent({
isDebugging: false, isDebugging: false,
} }
}, },
computed: {
getUrl() {
return window.location.href;
}
},
async created() { async created() {
const [segment, ...path] = window.location.hash.split('/'); const [segment, ...path] = window.location.hash.split('/');
this.initialPath = path; this.initialPath = path;

View File

@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"; style="position: relative; width: 799px; min-width: 320px; height: 600px; overflow: hidden;"> <html lang="en"; style="position: relative; width: 799px; min-width: 320px; height: 600px; overflow: hidden; background-color: transparent;">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="color-scheme" content="light dark"> <meta name="color-scheme" content="light dark">

View File

@ -0,0 +1,169 @@
@import "tailwindcss";
@import "tailwindcss/utilities";
:host {
all: initial;
color-scheme: dark light;
@apply font-mono text-stone-300;
}
.uw-window {
@apply fixed flex flex-col pointer-events-auto border-stone-500;
.uw-content {
@apply p-4 flex grow-1 font-sans overflow-hidden;
}
&.uw-window-bg-transparent {
}
&.uw-window-bg-blurred {
@apply bg-stone-950/75;
backdrop-filter: brightness(50%) saturate(50%) blur(16px);
}
&.uw-window-bg-solid {
@apply bg-stone-950;
}
}
.uw-resize-handle {
position: absolute;
right: 0;
bottom: 0;
width: 16px;
height: 16px;
cursor: nwse-resize;
background:
linear-gradient(135deg, transparent 50%, #666 50%);
}
.uw-titlebar {
@apply flex flex-row gap-4 items-center cursor-move select-none justify-between
bg-stone-950/80 border-b border-b-stone-700;
.uw-title {
@apply px-4 py-2;
}
.uw-actions {
@apply flex flex-row;
button {
@apply h-[32px] w-[28px] relative p-0 m-0
text-stone-300 hover:text-primary-200 bg-transparent border-none border-transparent px-2;
svg {
@apply h-[24px] w-[24px] fill-current;
}
&.uw-close {
@apply w-[40px] px-[8px];
&:hover {
@apply bg-red-600 text-[#fff];
}
}
}
.uw-bg-toggle {
@apply relative;
.uw-bg-menu {
@apply absolute top-[100%] right-0 bg-stone-950/75;
display: none;
z-index: 1;
button {
@apply w-full text-left px-4 py-2 hover:bg-stone-800;
}
}
&:hover .uw-bg-menu {
@apply flex flex-col gap-2
min-w-[220px]
text-stone-300 hover:text-primary-200
}
}
}
}
.settings-categories {
@apply
relative
w-[4.5em] popup:w-[18em] window:w-[24em]
mr-[1em]
grow-0 shrink-0 flex flex-col
text-right;
.tab-column {
@apply absolute popup:relative top-0 right-0
w-[18em] window:w-full h-full z-[1000]
hover:bg-stone-950 popup:hover:bg-transparent
border-r border-r-stone-800
hover:translate-x-[calc(100%-4.5em)] popup:hover:translate-x-0
transition-transform duration-200;
.tab {
@apply
flex flex-col
cursor-pointer
border-r-2 border-r-transparent
border-r-primary-400;
&.active {
.main-tab {
@apply !bg-transparent !text-primary-300 bg-gradient-to-r from-transparent to-black hover:!text-primary-200 ;
}
}
&.disabled {
@apply pointer-events-none;
}
}
.main-tab {
@apply
px-[1em] py-[0.5em]
flex flex-row gap-4 justify-end items-center
text-[1.125em] text-stone-300 text-right font-mono
cursor-pointer
hover:bg-stone-800
hover:text-primary-200
hover:border-r-stone-600;
.label {
@apply grow-0;
}
}
.suboption {
@apply
px-6 py-2
uppercase text-stone-400/75 font-mono
relative
hover:bg-stone-800
hover:text-primary-200
hover:border-r-stone-600;
&.active {
@apply !bg-transparent !text-primary-300 bg-gradient-to-r from-transparent to-black hover:!text-primary-200;
}
&.disabled {
@apply pointer-events-none text-stone-600/75;
}
.badge {
@apply absolute right-[0.25rem] bottom-0 w-4 h-4 text-stone-950 bg-primary-300 rounded flex justify-center items-center font-bold text-[0.8rem];
}
&:last-child {
@apply
mb-2;
}
}
}
}