diff --git a/src/ext/lib/uwui/UI.ts b/src/ext/lib/uwui/UI.ts index 858dc91..ece5402 100644 --- a/src/ext/lib/uwui/UI.ts +++ b/src/ext/lib/uwui/UI.ts @@ -9,9 +9,12 @@ import { MenuPosition as MenuPosition } from '@src/common/interfaces/ClientUiMen import { CommandInterface } from '@src/common/interfaces/SettingsInterface'; import { SiteSupportLevel } from '@src/common/enums/SiteSupportLevel.enum'; import { ComponentLogger } from '../logging/ComponentLogger'; +import { setupVideoAlignmentIndicatorInteraction } from '@ui/utils/video-alignment-indicator-handling'; import alignmentIndicatorSvg from '!!raw-loader!@ui/res/img/alignment-indicators.svg'; import lockBarIndicatorSvg from '!!raw-loader!@ui/res/img/lock-bar-indicators.svg'; +import VideoAlignmentType from '../../../common/enums/VideoAlignmentType.enum'; +import { setVideoAlignmentIndicatorState } from '../../../ui/utils/video-alignment-indicator-handling'; if (process.env.CHANNEL !== 'stable'){ console.info("Loading: UI"); @@ -31,6 +34,18 @@ class UI { private extensionMenu: ClientMenu; private logger: ComponentLogger; + private uiState = { + lockXY: true, + zoom: { // log2 scale — 100% is 0 + x: 0, + y: 0, + }, + videoAlignment: { + x: VideoAlignmentType.Default, + y: VideoAlignmentType.Default + } + } + constructor( public interfaceId, public uiConfig, // {parentElement?, eventBus?, isGlobal?, playerData} @@ -239,7 +254,7 @@ class UI { -
@@ -258,7 +273,7 @@ class UI { id="_button_reset_zoom" class="uw-freeform-zoom-button" > - + @@ -268,8 +283,14 @@ class UI { ] }, { - label: 'Align', - subitems: [{label: 'todo pls'}] + label: 'Align video to ...', + subitems: [{ + customId: 'videoAlignmentController', + customClassList: '', + customHTML: ``, + }] }, { label: 'todo: open settings' @@ -281,6 +302,94 @@ class UI { } this.extensionMenu = new ClientMenu(menuConfig); this.extensionMenu.mount(this.uiConfig.parentElement); + + /** + * SETUP MENU INTERACTIONS + * ——————————————————————————————————— + * Interactions are needed for the following things: + * 0. [ ] both sliders. Needs to read the state of the sliders and update the labels + * 1. [ ] lock X/Y button — needs to update icon state of the button and the linked bar + * 2. [ ] reset button — just needs to reset, no icon changes necessary + * 3. [X] alignment indicator — also needs to update indicator state + */ + const menuElement = this.extensionMenu.root; + + // 0. SLIDERS ————————————————————————————————————————————————————————————————————————————— + const zoomWidthSlider: HTMLInputElement = menuElement.querySelector('#_input_zoom_slider'); + const zoomHeightSlider: HTMLInputElement = menuElement.querySelector('#_input_zoom_slider_2'); + const zoomWidthLabel: HTMLDivElement = menuElement.querySelector('#zoomWidth'); + const zoomHeightLabel: HTMLDivElement = menuElement.querySelector('#zoomHeight'); + + const updateZoomDisplayValues = () => { + zoomWidthLabel.textContent = `${Math.round((Math.exp(this.uiState.zoom.x) * 100))}%`; + zoomHeightLabel.textContent = `${Math.round((Math.exp(this.uiState.zoom.y) * 100))}%`; + zoomWidthSlider.value = this.uiState.zoom.x.toString(); + zoomHeightSlider.value = this.uiState.zoom.y.toString(); + } + + const updateZoom = (event: InputEvent, axis: 'x' | 'y') => { + const parsedValue = parseFloat((event.currentTarget as HTMLInputElement).value); + + if (this.uiState.lockXY) { + this.uiState.zoom = {x: parsedValue, y: parsedValue}; + } else { + this.uiState.zoom[axis] = parsedValue; + } + + updateZoomDisplayValues(); + + this.eventBus?.send('set-zoom', { + zoom: { + x: Math.pow(2, this.uiState.zoom.x), + y: Math.pow(2, this.uiState.zoom.y) + } + }); + } + + zoomWidthSlider.addEventListener('input', (event: InputEvent) => updateZoom(event, 'x')); + zoomHeightSlider.addEventListener('input', (event: InputEvent) => updateZoom(event, 'y')); + + // 1. LOCK X/Y ———————————————————————————————————————————————————————————————————————————— + const lockXYButton = menuElement.querySelector('#_button_toggle_aspect_lock'); + const sliderLockBar = menuElement.querySelector('#slider-lock'); + lockXYButton.addEventListener('click', () => { + this.uiState.lockXY = !this.uiState.lockXY; + + if (this.uiState.lockXY) { + lockXYButton.classList.add('uw-linked'); + lockXYButton.classList.remove('uw-unlinked'); + sliderLockBar.classList.add('uw-linked'); + sliderLockBar.classList.remove('uw-unlinked'); + } else { + lockXYButton.classList.add('uw-unlinked'); + lockXYButton.classList.remove('uw-linked'); + sliderLockBar.classList.add('uw-unlinked'); + sliderLockBar.classList.remove('uw-linked'); + } + }); + + // 2. ZOOM RESET BUTTON ——————————————————————————————————————————————————————————————————— + const zoomResetButton = menuElement.querySelector('#_button_reset_zoom'); + zoomResetButton.addEventListener('click', () => { + this.eventBus?.send('set-zoom', { zoom: {x: 1, y: 1} }); + this.uiState.zoom = {x: 0, y: 0}; + updateZoomDisplayValues(); + }); + + // 3. VIDEO ALIGNMENT INDICATOR ——————————————————————————————————————————————————————————— + const videoAlignmentIndicatorElement: SVGSVGElement = menuElement.querySelector('#_uw_ui_alignment_indicator'); + setupVideoAlignmentIndicatorInteraction( + videoAlignmentIndicatorElement, + (x: VideoAlignmentType, y: VideoAlignmentType) => { + setVideoAlignmentIndicatorState(videoAlignmentIndicatorElement, x, y); + if (this.eventBus) { + this.eventBus?.send('set-alignment', {x,y}); + this.uiState.videoAlignment = {x,y}; + } + } + ); + + } } diff --git a/src/ui/res/styles/player-menu.css b/src/ui/res/styles/player-menu.css index 026d8dc..09d4377 100644 --- a/src/ui/res/styles/player-menu.css +++ b/src/ui/res/styles/player-menu.css @@ -1,14 +1,6 @@ @import "tailwindcss"; @import "tailwindcss/utilities"; -svg { - @apply text-inherit fill-current stroke-current; - - * { - @apply text-inherit; - } -} - .uw-menu-root { @apply h-full w-full font-mono text-stone-200 text-[16px] flex flex-row bg-transparent; @@ -61,6 +53,18 @@ svg { /* * { @apply font-mono text-stone-200 text-[1em]; } */ + + svg { + @apply text-inherit fill-current stroke-transparent; + + * { + @apply text-inherit; + } + } + + .svg-icon { + @apply fill-stone-300 stroke-transparent border-transparent; + } } .uw-hidden { @@ -154,7 +158,6 @@ svg { @apply inline-flex flex-row items-center !gap-4 rounded-[0.5rem] relative; svg { - @apply fill-current stroke-current; height: 1em; } @@ -171,7 +174,6 @@ svg { border: 1px solid; svg { - @apply fill-current stroke-current; height: 1.5em; } @@ -189,7 +191,6 @@ svg { @apply flex flex-row items-center justify-center w-[420px]; svg { - @apply fill-current stroke-current text-inherit; height: 1.5em; } @@ -250,6 +251,26 @@ svg { } } +#videoAlignmentController { + @apply text-stone-400; + + svg { + @apply border-transparent fill-stone-300 stroke-transparent; + + * { + @apply border-transparent text-inherit; + } + + rect { + stroke: transparent; + } + + .selected { + @apply fill-primary-400 stroke-primary-400; + } + } +} + .uw-link-button.uw-linked .uw-unlinked { display: none; }