Indicate currently selected option in the in-player popup menu

This commit is contained in:
Tamius Han 2026-01-28 18:11:57 +01:00
parent 3d8d8eb199
commit 8af9f2cedf
10 changed files with 348 additions and 24 deletions

View File

@ -9,6 +9,7 @@ enum AspectRatioType {
Fixed = 4, // pre-determined aspect ratio Fixed = 4, // pre-determined aspect ratio
Manual = 5, // ratio achieved by zooming in/zooming out Manual = 5, // ratio achieved by zooming in/zooming out
AutomaticUpdate = 6, // set by Aard AutomaticUpdate = 6, // set by Aard
Cover = 7, // replaces FitWidth and FitHeight
} }
export default AspectRatioType; export default AspectRatioType;

View File

@ -1,14 +1,17 @@
import { InPlayerUIOptions } from '@src/common/interfaces/SettingsInterface'; import { CommandInterface, InPlayerUIOptions } from '@src/common/interfaces/SettingsInterface';
export interface MenuItemConfig { export interface MenuItemBaseConfig {
label: string; label?: string;
subitems?: MenuItemConfig[]; subitems?: MenuItemConfig[];
command?: CommandInterface,
action?: () => void; action?: () => void;
customHTML?: HTMLElement | string; customHTML?: HTMLElement | string;
customId?: string; customId?: string;
customClassList?: string; customClassList?: string;
} }
export type MenuItemConfig = MenuItemBaseConfig & ({label: string} | {customHTML: HTMLElement | string});
export enum MenuPosition { export enum MenuPosition {
TopLeft = 'top-left', TopLeft = 'top-left',
Left = 'left', Left = 'left',

View File

@ -0,0 +1,12 @@
import { Ar } from '@src/common/interfaces/ArInterface';
import { Stretch } from '@src/common/interfaces/StretchInterface';
export interface ScalingParamsBroadcast {
effectiveZoom: {
x: number,
y: number
},
lastAr: Ar,
manualZoom: boolean,
stretch: Stretch,
}

View File

@ -1,4 +1,8 @@
import AspectRatioType from '@src/common/enums/AspectRatioType.enum';
import StretchType from '@src/common/enums/StretchType.enum';
import { ArVariant } from '@src/common/interfaces/ArInterface';
import { MenuPosition, MenuConfig, MenuItemConfig } from '@src/common/interfaces/ClientUiMenu.interface'; import { MenuPosition, MenuConfig, MenuItemConfig } from '@src/common/interfaces/ClientUiMenu.interface';
import { ScalingParamsBroadcast } from '@src/common/interfaces/ScalingParamsBroadcast.interface';
import extensionCss from '@src/main.css?inline'; import extensionCss from '@src/main.css?inline';
export class ClientMenu { export class ClientMenu {
@ -15,8 +19,6 @@ export class ClientMenu {
private trigger: HTMLDivElement; private trigger: HTMLDivElement;
private visible = false; private visible = false;
private hiddenClass = 'uw-hidden';
private menuPositionClasses: string[] = []; private menuPositionClasses: string[] = [];
@ -284,6 +286,7 @@ export class ClientMenu {
} }
if (item.subitems) { if (item.subitems) {
el.classList.add('uw-has-submenu');
el.appendChild(this.buildSubmenu(item.subitems)); el.appendChild(this.buildSubmenu(item.subitems));
} }
@ -420,4 +423,99 @@ export class ClientMenu {
this.root.classList.add('uw-hidden'); this.root.classList.add('uw-hidden');
} }
} }
markActiveElements(scalingParams: ScalingParamsBroadcast) {
const currentlyActiveElements = this._root.querySelectorAll('.uw-active-within, .uw-active');
for (const element of currentlyActiveElements) {
element.classList.remove('uw-active-within');
element.classList.remove('uw-active');
}
// we only set active options when manual zoom is NOT set
if (!scalingParams.manualZoom) {
const cropMenuGroup = scalingParams.lastAr.variant === ArVariant.Zoom ? 'zoom' : 'crop';
const cropCommand = scalingParams.lastAr.variant === ArVariant.Zoom ? 'set-ar-zoom' : 'set-ar';
let typeCrop;
switch (scalingParams.lastAr.type) {
case AspectRatioType.Automatic:
case AspectRatioType.AutomaticUpdate:
typeCrop = `${AspectRatioType.Automatic}-x`;
break;
case AspectRatioType.Cover:
case AspectRatioType.FitWidth:
case AspectRatioType.FitHeight:
typeCrop = `${scalingParams.lastAr.type}-x`;
break;
case AspectRatioType.Cycle:
case AspectRatioType.Initial:
typeCrop = 'non-selectable';
break;
default:
typeCrop = `${scalingParams.lastAr.type}-${scalingParams.lastAr.ratio ?? 'x'}`;
}
const activeGroupId = `#uw-${cropMenuGroup}`.replaceAll('.', '_');
const activeCropCommandId = `#uw-${cropCommand}-${typeCrop}`.replaceAll('.', '_');
const groupEl = this._root.querySelector(activeGroupId);
const optionEl = groupEl?.querySelector(activeCropCommandId);
/**
* When manual zoom is in effect, optionEl (probably) won't match any of the options.
* Therefore, optionEl will be undefined. We can use this fact to sus out whether
* we're _really_ using a zoom preset, or did the user went for the sliders n stuff.
*/
if (optionEl) {
groupEl.classList.add('uw-active-within');
optionEl.classList.add('uw-active');
}
let typeStretch;
switch (scalingParams.stretch.type) {
case StretchType.FixedSource:
case StretchType.Fixed:
typeStretch = `${scalingParams.stretch.type}-${scalingParams.stretch.ratio ?? 'x'}`.replaceAll('.', '_');
break;
case StretchType.Default:
case StretchType.NoStretch:
typeStretch = `non-selectable`;
break;
default:
typeStretch = `${scalingParams.stretch.type}-x`;
}
const stretchGroupEl = this._root.querySelector('#uw-stretch');
const stretchOptionEl = stretchGroupEl?.querySelector(`#uw-set-stretch-${typeStretch}`);
if (stretchOptionEl) {
stretchGroupEl.classList.add('uw-active-within');
stretchOptionEl.classList.add('uw-active');
}
}
const zoomWLabel = this._root.querySelector('#zoomWidth');
const zoomWSlider = this._root.querySelector('#_input_zoom_slider') as undefined | null | (HTMLInputElement & {isInteracting?: boolean});
const zoomHLabel = this._root.querySelector('#zoomHeight');
const zoomHSlider = this._root.querySelector('#_input_zoom_slider_2') as undefined | null | (HTMLInputElement & {isInteracting?: boolean});
if (zoomWSlider?.isInteracting || zoomHSlider?.isInteracting) {
// do nothing
} else {
if (zoomWLabel) {
zoomWLabel.textContent = `${(scalingParams.effectiveZoom.x * 100).toFixed()}%`;
}
if (zoomHLabel) {
zoomHLabel.textContent = `${(scalingParams.effectiveZoom.y * 100).toFixed()}%`;
}
if (zoomWSlider) {
zoomWSlider.value = Math.log2(scalingParams.effectiveZoom.x) as any;
}
if (zoomHSlider) {
zoomHSlider.value = Math.log2(scalingParams.effectiveZoom.y) as any;
}
}
}
} }

View File

@ -21,6 +21,7 @@ import { createApp } from 'vue';
import SettingsWindowContent from '@components/SettingsWindowContent.vue'; import SettingsWindowContent from '@components/SettingsWindowContent.vue';
import { Ar } from '@src/common/interfaces/ArInterface'; import { Ar } from '@src/common/interfaces/ArInterface';
import { Stretch } from '@src/common/interfaces/StretchInterface'; import { Stretch } from '@src/common/interfaces/StretchInterface';
import { ScalingParamsBroadcast } from '@src/common/interfaces/ScalingParamsBroadcast.interface';
// import jsonEditorCSS from 'vanilla-jsoneditor/themes/jse-theme-dark.css?inline' // import jsonEditorCSS from 'vanilla-jsoneditor/themes/jse-theme-dark.css?inline'
if (process.env.CHANNEL !== 'stable'){ if (process.env.CHANNEL !== 'stable'){
@ -44,6 +45,8 @@ class UI {
private forwardedCommandIds: string[] = new Array(64); private forwardedCommandIds: string[] = new Array(64);
private lastForwardedCommandIndex = 0; private lastForwardedCommandIndex = 0;
private currentScalingParams?: ScalingParamsBroadcast;
private uiState = { private uiState = {
lockXY: true, lockXY: true,
zoom: { // log2 scale — 100% is 0 zoom: { // log2 scale — 100% is 0
@ -97,9 +100,11 @@ class UI {
} }
}, },
'broadcast-scaling-params': { 'broadcast-scaling-params': {
function: (commandData: {effectiveZoom: {x: number, y: number}, lastAr: Ar, stretch: Stretch}, context) => { function: (commandData: ScalingParamsBroadcast, context) => {
console.warn('got scaling params:', commandData) this.currentScalingParams = commandData;
this.extensionMenu?.markActiveElements(commandData);
} }
} }
}); });
@ -293,28 +298,37 @@ class UI {
}, },
{ {
label: 'Crop', label: 'Crop',
customId: 'uw-crop',
subitems: this.settings.active.commands.crop.map((x: CommandInterface) => { subitems: this.settings.active.commands.crop.map((x: CommandInterface) => {
return { return {
label: x.label, label: x.label,
command: x,
customId: `uw-${x.action}-${x.arguments.type}-${x.arguments.ratio ?? 'x'}`.replaceAll('.', '_'),
action: () => this.executeCommand(x) action: () => this.executeCommand(x)
} }
}) })
}, },
{ {
label: 'Stretch', label: 'Stretch',
customId: 'uw-stretch',
subitems: this.settings.active.commands.stretch.map((x: CommandInterface) => { subitems: this.settings.active.commands.stretch.map((x: CommandInterface) => {
return { return {
label: x.label, label: x.label,
command: x,
customId: `uw-${x.action}-${x.arguments.type}-${x.arguments.ratio ?? 'x'}`.replaceAll('.', '_'),
action: () => this.executeCommand(x) action: () => this.executeCommand(x)
} }
}) })
}, },
{ {
label: 'Zoom (presets)', label: 'Zoom (presets)',
customId: 'uw-zoom',
subitems: [ subitems: [
... this.settings.active.commands.zoom.map((x: CommandInterface) => { ... this.settings.active.commands.zoom.map((x: CommandInterface) => {
return { return {
label: x.label, label: x.label,
command: x,
customId: `uw-${x.action}-${x.arguments.type}-${x.arguments.ratio ?? 'x'}`.replaceAll('.', '_'),
action: () => this.executeCommand(x) action: () => this.executeCommand(x)
} }
}), }),
@ -412,6 +426,9 @@ class UI {
}; };
this.extensionMenu = new ClientMenu(menuConfig); this.extensionMenu = new ClientMenu(menuConfig);
this.extensionMenu.mount(this.uiConfig.parentElement); this.extensionMenu.mount(this.uiConfig.parentElement);
if (this.currentScalingParams) {
this.extensionMenu.markActiveElements(this.currentScalingParams);
}
/** /**
* SETUP MENU INTERACTIONS * SETUP MENU INTERACTIONS
@ -433,6 +450,24 @@ class UI {
const zoomWidthLabel: HTMLDivElement = menuElement.querySelector('#zoomWidth'); const zoomWidthLabel: HTMLDivElement = menuElement.querySelector('#zoomWidth');
const zoomHeightLabel: HTMLDivElement = menuElement.querySelector('#zoomHeight'); const zoomHeightLabel: HTMLDivElement = menuElement.querySelector('#zoomHeight');
for (const slider of [zoomWidthSlider, zoomHeightSlider]) {
slider.addEventListener('pointerdown', function() {
(this as any).isInteracting = true;
});
slider.addEventListener('pointerup', function() {
(this as any).isInteracting = false;
});
slider.addEventListener('pointercancel', function() {
(this as any).isInteracting = false;
});
slider.addEventListener('pointerleave', function() {
(this as any).isInteracting = false;
});
}
const updateZoomDisplayValues = () => { const updateZoomDisplayValues = () => {
zoomWidthLabel.textContent = `${Math.round((Math.exp(this.uiState.zoom.x) * 100))}%`; zoomWidthLabel.textContent = `${Math.round((Math.exp(this.uiState.zoom.x) * 100))}%`;
zoomHeightLabel.textContent = `${Math.round((Math.exp(this.uiState.zoom.y) * 100))}%`; zoomHeightLabel.textContent = `${Math.round((Math.exp(this.uiState.zoom.y) * 100))}%`;

View File

@ -2,6 +2,7 @@ import AspectRatioType from '@src/common/enums/AspectRatioType.enum';
import StretchType from '@src/common/enums/StretchType.enum'; import StretchType from '@src/common/enums/StretchType.enum';
import VideoAlignmentType from '@src/common/enums/VideoAlignmentType.enum'; import VideoAlignmentType from '@src/common/enums/VideoAlignmentType.enum';
import { Ar, ArVariant } from '@src/common/interfaces/ArInterface'; import { Ar, ArVariant } from '@src/common/interfaces/ArInterface';
import { ScalingParamsBroadcast } from '@src/common/interfaces/ScalingParamsBroadcast.interface';
import { Stretch } from '@src/common/interfaces/StretchInterface'; import { Stretch } from '@src/common/interfaces/StretchInterface';
import getElementStyles from '@src/common/utils/getElementStyles'; import getElementStyles from '@src/common/utils/getElementStyles';
import Debug from '@src/ext/conf/Debug'; import Debug from '@src/ext/conf/Debug';
@ -149,6 +150,11 @@ class Resizer {
}], }],
'set-zoom': [{ 'set-zoom': [{
function: (config: any) => { function: (config: any) => {
if (!config || config.zoom === 1 || (config.zoom.x === 1 && config.zoom.y === 1)) {
this.manualZoom = false;
} else {
this.manualZoom = true;
}
this.setZoom(config?.zoom ?? {zoom: 1}); this.setZoom(config?.zoom ?? {zoom: 1});
} }
}], }],
@ -334,7 +340,7 @@ class Resizer {
return true; return true;
} }
async setAr(ar: Ar, lastAr?: Ar) { async setAr(ar: Ar, lastAr?: Ar, flags?: {manualZoom?: boolean}) {
if (this.destroyed || ar == null) { if (this.destroyed || ar == null) {
return; return;
} }
@ -375,8 +381,12 @@ class Resizer {
return; return;
} }
if (ar.type !== AspectRatioType.AutomaticUpdate) { if (ar.type !== AspectRatioType.AutomaticUpdate && !flags?.manualZoom) {
this.manualZoom = false; if (flags?.manualZoom) {
this.manualZoom = true;
} else {
this.manualZoom = false;
}
} }
if (!this.video.videoWidth || !this.video.videoHeight) { if (!this.video.videoWidth || !this.video.videoHeight) {
@ -424,7 +434,7 @@ class Resizer {
this.logger.info('setAr', `<rid:${this.resizerId}> Something wrong with ar or the player. Doing nothing.`); this.logger.info('setAr', `<rid:${this.resizerId}> Something wrong with ar or the player. Doing nothing.`);
return; return;
} }
this.lastAr = {type: ar.type, ratio: ar.ratio}; this.lastAr = {type: ar.type, ratio: ar.ratio, variant: ar.variant};
} }
if (! this.video) { if (! this.video) {
@ -499,21 +509,29 @@ class Resizer {
this.eventBus.send('broadcast-scaling-params', { this.eventBus.send('broadcast-scaling-params', {
effectiveZoom: {x: stretchFactors.xFactor, y: stretchFactors.yFactor}, effectiveZoom: {x: stretchFactors.xFactor, y: stretchFactors.yFactor},
lastAr: this.lastAr, lastAr: this.lastAr,
manualZoom: this.manualZoom,
stretch: this.stretcher.stretch stretch: this.stretcher.stretch
}); } as ScalingParamsBroadcast);
} catch (e) { } catch (e) {
this.logger.warn('applyScaling', 'error while applying CSS:', e); this.logger.warn('applyScaling', 'error while applying CSS:', e);
// don't apply CSS if there's an error // don't apply CSS if there's an error
} }
} }
toFixedAr() { toFixedAr(flags?: {manualZoom?: boolean}) {
// converting to fixed AR means we also turn off autoAR // converting to fixed AR means we also turn off autoAR
this.setAr(
{
ratio: this.lastAr.ratio ?? this.getFileAr(),
type: AspectRatioType.Fixed
},
undefined,
flags
);
this.setAr({ if (flags?.manualZoom) {
ratio: this.lastAr.ratio ?? this.getFileAr(), this.manualZoom = true;
type: AspectRatioType.Fixed }
});
} }
resetLastAr() { resetLastAr() {

View File

@ -71,7 +71,7 @@ class Zoom {
this.scale = Math.pow(2, this.logScale); this.scale = Math.pow(2, this.logScale);
this.scaleY = Math.pow(2, this.logScaleY); this.scaleY = Math.pow(2, this.logScaleY);
this.processZoom(); this.processZoom({manualZoom: true});
} }
/** /**
@ -92,11 +92,11 @@ class Zoom {
this.scale = Math.min(Math.max(scaleIn.x, MIN_SCALE), MAX_SCALE); this.scale = Math.min(Math.max(scaleIn.x, MIN_SCALE), MAX_SCALE);
this.scaleY = Math.min(Math.max(scaleIn.y, MIN_SCALE), MAX_SCALE); this.scaleY = Math.min(Math.max(scaleIn.y, MIN_SCALE), MAX_SCALE);
this.processZoom(); this.processZoom({manualZoom: true});
} }
processZoom() { processZoom(flags?: {manualZoom?: boolean}) {
this.conf.resizer.toFixedAr(); this.conf.resizer.toFixedAr(flags);
this.conf.resizer.applyScaling({xFactor: this.scale, yFactor: this.scaleY}, {noAnnounce: true}); this.conf.resizer.applyScaling({xFactor: this.scale, yFactor: this.scaleY}, {noAnnounce: true});
} }
} }

View File

@ -101,8 +101,8 @@
<optgroup label="corners"> <optgroup label="corners">
<option :value="MenuPosition.TopLeft">Top left</option> <option :value="MenuPosition.TopLeft">Top left</option>
<option :value="MenuPosition.BottomLeft">Bottom left</option> <option :value="MenuPosition.BottomLeft">Bottom left</option>
<option :value="MenuPosition.TopRight">Top right"</option> <option :value="MenuPosition.TopRight">Top right</option>
<option :value="MenuPosition.BottomRight">Bottom right"</option> <option :value="MenuPosition.BottomRight">Bottom right</option>
</optgroup> </optgroup>
</select> </select>
</div> </div>

View File

@ -86,6 +86,156 @@
pointer-events: auto; pointer-events: auto;
} }
/* Menu + active markers for left/right menus */
.uw-menu-left {
.uw-menu-item {
&.uw-active {
padding-right: 2rem;
&::after {
position: absolute;
display: block;
top: 50%;
right: 0.5rem;
content: '◈';
transform: translateY(calc(-50% + 0.25rem));
}
}
}
.uw-has-submenu {
padding-right: 2rem;
position: relative;
&.uw-active-within {
padding-right: 5rem;
&::after {
position: absolute;
display: block;
top: 50%;
right: 1.5rem;
content: '◈';
/* transform: translateY(-50%); */
transform: translateY(calc(-50% + 0.125rem));
}
}
&::before {
position: absolute;
display: block;
right: 0;
content: '▶';
transform: scale(0.5);
}
}
}
.uw-menu-right {
.uw-menu-item {
padding-left: 2rem;
&.uw-active {
padding-right: 2rem;
&::after {
position: absolute;
display: block;
top: 50%;
right: 0.5rem;
content: '◈';
transform: translateY(calc(-50% + 0.25rem));
}
}
}
.uw-has-submenu {
&.uw-active-within {
padding-right: 2rem;
&::after {
position: absolute;
display: block;
top: 50%;
right: 0.5rem;
content: '◈';
transform: translateY(calc(-50% + 0.125rem));
}
}
position: relative;
&::before {
position: absolute;
display: block;
left: 0;
content: '◀';
transform: scale(0.5);
}
}
}
/* Menus on top/center and bottom/center */
.uw-menu-center {
.uw-menu-top {
.uw-has-submenu {
position: relative;
&::before {
position: absolute;
display: block;
bottom: 0;
left: 50%;
content: '▼';
transform: scale(0.5) translateX(-50%);
}
/* We don't add :after marker, as it's already used for other purposes */
/* &.uw-active-within {
&::before {
position: absolute;
display: block;
bottom: 0;
left: 50%;
content: '◈';
transform: translateX(-50%);
}
} */
}
}
.uw-menu-bottom {
.uw-menu-item {
padding-top: 1.5rem;
}
.uw-has-submenu {
position: relative;
&::before {
position: absolute;
display: block;
top: 0;
left: 50%;
content: '▲';
transform: scale(0.5) translateX(-50%);
}
/* We don't add :after marker, as it's already used for other purposes */
/* &.uw-active-within {
&::after {
position: absolute;
display: block;
top: 0;
left: 50%;
content: '◈';
transform: translateX(-50%);
}
} */
}
}
}
.uw-submenu { .uw-submenu {
@apply absolute flex-col gap-0; @apply absolute flex-col gap-0;
@ -148,6 +298,7 @@
} }
.uw-menu-item, .uw-menu-trigger { .uw-menu-item, .uw-menu-trigger {
@apply cursor-pointer select-none;
&:hover > .uw-submenu { &:hover > .uw-submenu {
display: flex; display: flex;
@ -158,6 +309,12 @@
} }
} }
.uw-menu-item {
&.uw-active-within, &.uw-active {
@apply text-primary-500;
}
}
.uw-trigger { .uw-trigger {
@apply p-4 relative block @apply p-4 relative block

View File

@ -5,7 +5,7 @@
"outDir": "./ts-out", "outDir": "./ts-out",
"allowJs": true, "allowJs": true,
"target": "esnext", "target": "esnext",
"lib": ["esnext", "DOM"], "lib": ["esnext", "DOM", "DOM.Iterable"],
"types": [ "types": [
"chrome", "chrome",
"node" "node"