Implement zoom in extension popup, in-player UI

This commit is contained in:
Tamius Han 2025-05-19 01:18:48 +02:00
parent a5f35248bd
commit ec0896e17a
7 changed files with 246 additions and 151 deletions

View File

@ -64,7 +64,7 @@
</GhettoContextMenuOption> </GhettoContextMenuOption>
</slot> </slot>
</GhettoContextMenu> </GhettoContextMenu>
<!-- <GhettoContextMenu alignment="right"> <GhettoContextMenu alignment="right">
<template v-slot:activator> <template v-slot:activator>
Zoom Zoom
</template> </template>
@ -86,7 +86,7 @@
/> />
</GhettoContextMenuItem> </GhettoContextMenuItem>
</slot> </slot>
</GhettoContextMenu> --> </GhettoContextMenu>
<GhettoContextMenu alignment="right"> <GhettoContextMenu alignment="right">
<template v-slot:activator> <template v-slot:activator>
<div class="context-item"> <div class="context-item">

View File

@ -108,26 +108,6 @@
</div> </div>
</div> </div>
<!-- <div v-if="siteSettings && allowSettingSiteDefault" class="edit-action-area">
<div class="field">
<div class="label">Default for this site</div>
<div class="select">
<select
:value="siteDefaultZoom"
@change="setDefaultZoom($event, 'site')"
>
<option
v-for="(command, index) of settings?.active.commands.zoom"
:key="index"
:value="JSON.stringify(command.arguments)"
>
{{command.label}}
</option>
</select>
</div>
</div>
</div> -->
</template> </template>
<template v-else> <template v-else>
<!-- <!--
@ -228,6 +208,13 @@ export default {
KeyboardShortcutParserMixin, KeyboardShortcutParserMixin,
CommsMixin CommsMixin
], ],
props: [
'settings', // required for buttons and actions, which are global
'siteSettings',
'eventBus',
'isEditing',
'compact',
],
data() { data() {
return { return {
AspectRatioType, AspectRatioType,
@ -247,13 +234,6 @@ export default {
} }
} }
}, },
props: [
'settings', // required for buttons and actions, which are global
'siteSettings',
'eventBus',
'isEditing',
'compact',
],
created() { created() {
if (this.isEditing) { if (this.isEditing) {
this.enableEditMode(); this.enableEditMode();
@ -272,7 +252,6 @@ export default {
getZoomForDisplay(axis) { getZoomForDisplay(axis) {
// zoom is internally handled logarithmically, because we want to have x0.5, x1, x2, x4 ... magnifications // zoom is internally handled logarithmically, because we want to have x0.5, x1, x2, x4 ... magnifications
// spaced out at regular intervals. When displaying, we need to convert that to non-logarithmic values. // spaced out at regular intervals. When displaying, we need to convert that to non-logarithmic values.
return `${(Math.pow(2, this.zoom[axis]) * 100).toFixed()}%` return `${(Math.pow(2, this.zoom[axis]) * 100).toFixed()}%`
}, },
toggleZoomAr() { toggleZoomAr() {

View File

@ -6,74 +6,79 @@
current settings do not allow the extension to only be disabled while in full screen current settings do not allow the extension to only be disabled while in full screen
--> -->
<template v-if="siteSettings.isEnabledForEnvironment(false, true) === ExtensionMode.Disabled"> <template v-if="siteSettings.isEnabledForEnvironment(false, true) === ExtensionMode.Disabled">
<div class="info"> <div class="h-full flex flex-col items-center justify-center">
Extension is not enabled for this site. <div class="info">
Extension is not enabled for this site.
</div>
<div>
Please enable extension for this site.
</div>
</div> </div>
</template> </template>
<template v-else>
<div class="flex flex-row"> <div class="flex flex-row">
<mdicon name="crop" :size="16" />&nbsp;&nbsp; <mdicon name="crop" :size="16" />&nbsp;&nbsp;
<span>CROP</span> <span>CROP</span>
</div> </div>
<div <div
style="margin-top: -0.69rem; margin-bottom: 0.88rem;" style="margin-top: -0.69rem; margin-bottom: 0.88rem;"
>
<CropOptionsPanel
:settings="settings"
:eventBus="eventBus"
:siteSettings="siteSettings"
:isEditing="false"
:compact="true"
> >
</CropOptionsPanel> <CropOptionsPanel
</div> :settings="settings"
:eventBus="eventBus"
:siteSettings="siteSettings"
:isEditing="false"
:compact="true"
>
</CropOptionsPanel>
</div>
<div class="flex flex-row"> <div class="flex flex-row">
<mdicon name="crop" :size="16" />&nbsp;&nbsp; <mdicon name="crop" :size="16" />&nbsp;&nbsp;
<span>STRETCH</span> <span>STRETCH</span>
</div> </div>
<div <div
style="margin-top: -0.69rem; margin-bottom: 0.88rem;" style="margin-top: -0.69rem; margin-bottom: 0.88rem;"
>
<StretchOptionsPanel
:settings="settings"
:eventBus="eventBus"
:siteSettings="siteSettings"
:isEditing="false"
:compact="true"
></StretchOptionsPanel>
</div>
<div class="flex flex-row">
<mdicon name="crop" :size="16" />&nbsp;&nbsp;
<span>ZOOM</span>
</div>
<div
style="margin-top: -0.69rem; margin-bottom: 0.88rem;"
>
<ZoomOptionsPanel
:settings="settings"
:eventBus="eventBus"
:siteSettings="siteSettings"
:isEditing="false"
:compact="true"
> >
</ZoomOptionsPanel> <StretchOptionsPanel
</div> :settings="settings"
:eventBus="eventBus"
:siteSettings="siteSettings"
:isEditing="false"
:compact="true"
></StretchOptionsPanel>
</div>
<div class="flex flex-row"> <div class="flex flex-row">
<mdicon name="crop" :size="16" />&nbsp;&nbsp; <mdicon name="crop" :size="16" />&nbsp;&nbsp;
<span>ALIGN</span> <span>ZOOM</span>
</div> </div>
<div <div
style="margin-bottom: 0.88rem;" style="margin-top: -0.69rem; margin-bottom: 0.88rem;"
> >
<AlignmentOptionsControlComponent <ZoomOptionsPanel
:eventBus="eventBus" :settings="settings"
:large="true" :eventBus="eventBus"
> </AlignmentOptionsControlComponent> :siteSettings="siteSettings"
</div> :isEditing="false"
:compact="true"
>
</ZoomOptionsPanel>
</div>
<div class="flex flex-row">
<mdicon name="crop" :size="16" />&nbsp;&nbsp;
<span>ALIGN</span>
</div>
<div
style="margin-bottom: 0.88rem;"
>
<AlignmentOptionsControlComponent
:eventBus="eventBus"
:large="true"
> </AlignmentOptionsControlComponent>
</div>
</template>
</div> </div>

View File

@ -1,42 +1,109 @@
<template> <template>
<div> <div class="flex flex-row" style="width: 250px;">
Custom zoom <div class="flex-grow">
Custom zoom
</div>
<div class="flex flex-row">
<Button
v-if="zoomAspectRatioLocked"
icon="lock"
:iconSize="16"
:fixedWidth="true"
:noPad="true"
@click="toggleZoomAr()"
>
</Button>
<Button
v-else
icon="lock-open-variant"
:iconSize="16"
:fixedWidth="true"
:noPad="true"
@click="toggleZoomAr()"
>
</Button>
<Button
icon="restore"
:iconSize="16"
:noPad="true"
@click="resetZoom()"
></Button>
</div>
</div> </div>
<div class="top-label">Zoom:</div> <template v-if="zoomAspectRatioLocked">
<div class="input range-input"> <div class="input range-input no-bg">
<input
type="range"
class="slider"
min="0"
max="3"
step="0.01"
/>
<input
/>
</div>
<template v-if="true">
<div class="top-label">Vertical zoom:</div>
<div class="input range-input">
<input <input
type="range" type="range"
class="slider" class="slider"
min="0" min="-1"
max="3" max="3"
step="0.01" step="0.01"
:value="zoom.x"
@input="changeZoom($event.target.value)"
/> />
<input <input
class="disabled"
style="width: 2rem;"
:value="getZoomForDisplay('x')"
/>
</div>
</template>
<template v-else>
<div class="top-label">Horizontal zoom:</div>
<div class="input range-input no-bg">
<input
type="range"
class="slider"
min="-1"
max="3"
step="0.01"
:value="zoom.x"
@input="changeZoom($event.target.value, 'x')"
/>
<input
class="disabled"
style="width: 2rem;"
:value="getZoomForDisplay('x')"
/>
</div>
<div class="top-label">Vertical zoom:</div>
<div class="input range-input no-bg">
<input
type="range"
class="slider"
min="-1"
max="3"
step="0.01"
:value="zoom.y"
@input="changeZoom($event.target.value, 'y')"
/>
<input
class="disabled"
style="width: 2rem;"
:value="getZoomForDisplay('y')"
/> />
</div> </div>
</template> </template>
<div><input type="checkbox"/> Control vertical and horizontal zoom independently.</div>
</template> </template>
<script> <script>
import Button from '@csui/src/components/Button.vue';
import * as _ from 'lodash';
export default { export default {
components: {
Button,
},
mixins: [
],
props: [
'settings', // required for buttons and actions, which are global
'eventBus'
],
data() { data() {
return { return {
zoomAspectRatioLocked: true, zoomAspectRatioLocked: true,
@ -51,17 +118,42 @@ export default {
stretch: null, stretch: null,
zoom: null, zoom: null,
pan: null pan: null
} },
pollingInterval: undefined,
debouncedGetEffectiveZoom: undefined,
} }
}, },
mixins: [ created() {
this.eventBus?.subscribeMulti(
], {
props: [ 'announce-zoom': {
'settings', // required for buttons and actions, which are global function: (data) => {
'eventBus' this.zoom = {
], x: Math.log2(data.x),
y: Math.log2(data.y)
};
}
}
},
this
);
this.debouncedGetEffectiveZoom = _.debounce(
() => {
this.getEffectiveZoom();
},
250
),
this.getEffectiveZoom();
this.pollingInterval = setInterval(this.debouncedGetEffectiveZoom, 2000);
},
destroyed() {
this.eventBus.unsubscribe(this);
clearInterval(this.pollingInterval);
},
methods: { methods: {
getEffectiveZoom() {
this.eventBus?.sendToTunnel('get-effective-zoom', {});
},
getZoomForDisplay(axis) { getZoomForDisplay(axis) {
// zoom is internally handled logarithmically, because we want to have x0.5, x1, x2, x4 ... magnifications // zoom is internally handled logarithmically, because we want to have x0.5, x1, x2, x4 ... magnifications
// spaced out at regular intervals. When displaying, we need to convert that to non-logarithmic values. // spaced out at regular intervals. When displaying, we need to convert that to non-logarithmic values.
@ -81,25 +173,36 @@ export default {
// this.eventBus.send('set-zoom', {zoom: 1, axis: 'y'}); // this.eventBus.send('set-zoom', {zoom: 1, axis: 'y'});
// this.eventBus.send('set-zoom', {zoom: 1, axis: 'x'}); // this.eventBus.send('set-zoom', {zoom: 1, axis: 'x'});
this.eventBus?.sendToTunnel('set-zoom', {zoom: 1, axis: 'y'}); this.eventBus?.sendToTunnel('set-zoom', {zoom: 1});
this.eventBus?.sendToTunnel('set-zoom', {zoom: 1, axis: 'x'});
}, },
changeZoom(newZoom, axis) { changeZoom(newZoom, axis, isLinear) {
// we store zoom logarithmically on this compnent if (isNaN(+newZoom)) {
if (!axis) { return;
this.zoom.x = newZoom; }
let logZoom, linZoom;
if (isLinear) {
newZoom /= 100;
logZoom = Math.log2(newZoom);
linZoom = newZoom;
} else { } else {
this.zoom[axis] = newZoom; logZoom = newZoom;
linZoom = Math.pow(2, newZoom);
}
// we store zoom logarithmically on this component
if (!axis) {
this.zoom.x = logZoom;
} else {
this.zoom[axis] = logZoom;
} }
// we do not use logarithmic zoom elsewhere, therefore we need to convert // we do not use logarithmic zoom elsewhere, therefore we need to convert
newZoom = Math.pow(2, newZoom);
if (this.zoomAspectRatioLocked) { if (this.zoomAspectRatioLocked) {
this.eventBus?.sendToTunnel('set-zoom', {zoom: newZoom, axis: 'y'}); this.eventBus?.sendToTunnel('set-zoom', {zoom: linZoom});
this.eventBus?.sendToTunnel('set-zoom', {zoom: newZoom, axis: 'x'});
} else { } else {
this.eventBus?.sendToTunnel('set-zoom', {zoom: newZoom, axis: axis ?? 'x'}); this.eventBus?.sendToTunnel('set-zoom', {zoom: {[axis ?? 'x']: linZoom}});
} }
}, },
} }

View File

@ -89,6 +89,12 @@ button, .button {
border-bottom: 1px solid rgba($primary, 0.5); border-bottom: 1px solid rgba($primary, 0.5);
} }
&.no-bg {
background-color: transparent;
border-color: transparent;
}
input { input {
width: 100%; width: 100%;
outline: none; outline: none;

View File

@ -63,6 +63,8 @@ class Resizer {
currentCssValidFor: any; currentCssValidFor: any;
currentVideoSettings: any; currentVideoSettings: any;
private effectiveZoom: {x: number, y: number} = {x: 1, y: 1};
_lastAr: Ar = {type: AspectRatioType.Initial}; _lastAr: Ar = {type: AspectRatioType.Initial};
set lastAr(x: Ar) { set lastAr(x: Ar) {
// emit updates for UI when setting lastAr, but only if AR really changed // emit updates for UI when setting lastAr, but only if AR really changed
@ -88,6 +90,11 @@ class Resizer {
//#region event bus configuration //#region event bus configuration
private eventBusCommands = { private eventBusCommands = {
'get-effective-zoom': [{
function: () => {
this.eventBus.send('announce-zoom', this.manualZoom ? {x: this.zoom.scale, y: this.zoom.scaleY} : this.zoom.effectiveZoom);
}
}],
'set-ar': [{ 'set-ar': [{
function: (config: any) => { function: (config: any) => {
this.manualZoom = false; // this only gets called from UI or keyboard shortcuts, making this action safe. this.manualZoom = false; // this only gets called from UI or keyboard shortcuts, making this action safe.
@ -136,7 +143,9 @@ class Resizer {
} }
}], }],
'set-zoom': [{ 'set-zoom': [{
function: (config: any) => this.setZoom(config.zoom) function: (config: any) => {
this.setZoom(config?.zoom ?? {zoom: 1});
}
}], }],
'change-zoom': [{ 'change-zoom': [{
function: (config: any) => this.zoomStep(config.zoom) function: (config: any) => this.zoomStep(config.zoom)
@ -449,12 +458,12 @@ class Resizer {
} }
applyScaling(stretchFactors: VideoDimensions, options?: {noAnnounce?: boolean, ar?: Ar}) { applyScaling(stretchFactors: VideoDimensions, options?: {noAnnounce?: boolean, ar?: Ar}) {
// this.stretcher.chromeBugMitigation(stretchFactors); this.zoom.effectiveZoom = {x: stretchFactors.xFactor, y: stretchFactors.yFactor};
// let the UI know // announcing zoom somehow keeps incorrectly resetting zoom sliders in UI — UI is now polling for effective zoom while visible
if(!options?.noAnnounce) { // if(!options?.noAnnounce) {
this.videoData.eventBus.send('announce-zoom', {x: stretchFactors.xFactor, y: stretchFactors.yFactor}); // this.videoData.eventBus.send('announce-zoom', this.manualZoom ? {x: this.zoom.scale, y: this.zoom.scaleY} : this.zoom.effectiveZoom);
} // }
let translate = this.computeOffsets(stretchFactors, options?.ar); let translate = this.computeOffsets(stretchFactors, options?.ar);
this.applyCss(stretchFactors, translate); this.applyCss(stretchFactors, translate);

View File

@ -30,6 +30,7 @@ class Zoom {
maxScale = 8; maxScale = 8;
//#endregion //#endregion
effectiveZoom: {x: number, y: number}; // we're setting this in Resizer based on Resizer data!
constructor(videoData) { constructor(videoData) {
this.conf = videoData; this.conf = videoData;
@ -50,7 +51,12 @@ class Zoom {
* @param axis leave undefined to apply zoom to both axes * @param axis leave undefined to apply zoom to both axes
*/ */
zoomStep(amount: number, axis?: 'x' | 'y') { zoomStep(amount: number, axis?: 'x' | 'y') {
let newLog = axis === 'y' ? this.logScaleY : this.logScale; const effectiveLog = {
x: Math.log2(this.effectiveZoom.x),
y: Math.log2(this.effectiveZoom.y)
};
let newLog = axis === 'y' ? effectiveLog.y : effectiveLog.x;
newLog += amount; newLog += amount;
newLog = Math.min(Math.max(newLog, LOG_MIN_SCALE), LOG_MAX_SCALE); newLog = Math.min(Math.max(newLog, LOG_MIN_SCALE), LOG_MAX_SCALE);
@ -65,7 +71,6 @@ 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.logger.info('zoomStep', "changing zoom by", amount, ". New zoom level:", this.scale);
this.processZoom(); this.processZoom();
} }
@ -94,18 +99,6 @@ class Zoom {
this.conf.resizer.toFixedAr(); this.conf.resizer.toFixedAr();
this.conf.resizer.applyScaling({xFactor: this.scale, yFactor: this.scaleY}, {noAnnounce: true}); this.conf.resizer.applyScaling({xFactor: this.scale, yFactor: this.scaleY}, {noAnnounce: true});
} }
applyZoom(stretchFactors){
if (!stretchFactors) {
return;
}
this.logger.info('setZoom', "Applying zoom. Stretch factors pre:", stretchFactors, " —> scale:", this.scale);
stretchFactors.xFactor *= this.scale;
stretchFactors.yFactor *= this.scale;
this.logger.info('setZoom', "Applying zoom. Stretch factors post:", stretchFactors);
}
} }
export default Zoom; export default Zoom;