Compare commits

...

4 Commits

16 changed files with 292 additions and 116 deletions

2
package-lock.json generated
View File

@ -1,6 +1,6 @@
{ {
"name": "ultrawidify", "name": "ultrawidify",
"version": "6.0.1", "version": "6.0.2",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

View File

@ -1,6 +1,6 @@
{ {
"name": "ultrawidify", "name": "ultrawidify",
"version": "6.0.1", "version": "6.0.2",
"description": "Aspect ratio fixer for youtube and other sites, with automatic aspect ratio detection. Supports ultrawide and other ratios.", "description": "Aspect ratio fixer for youtube and other sites, with automatic aspect ratio detection. Supports ultrawide and other ratios.",
"author": "Tamius Han <tamius.han@gmail.com>", "author": "Tamius Han <tamius.han@gmail.com>",
"scripts": { "scripts": {

View File

@ -213,7 +213,15 @@ interface SettingsInterface {
ui: { ui: {
inPlayer: { inPlayer: {
enabled: boolean enabled: boolean,
minEnabledWidth: number, // don't show UI if player is narrower than % of screen width
activation: 'trigger-zone' | 'player', // what needs to be hovered in order for UI to be visible
triggerZoneDimensions: { // how large the trigger zone is (relative to player size)
width: number
height: number,
offsetX: number, // fed to translateX(offsetX + '%'). Valid range [-100, 0]
offsetY: number // fed to translateY(offsetY + '%'). Valid range [-100, 100]
},
} }
} }
@ -311,6 +319,7 @@ interface SettingsInterface {
internal?: CommandInterface[], internal?: CommandInterface[],
}, },
whatsNewChecked: boolean, whatsNewChecked: boolean,
newFeatureTracker: any,
// ----------------------------------------- // -----------------------------------------
// ::: SITE CONFIGURATION ::: // ::: SITE CONFIGURATION :::
// ----------------------------------------- // -----------------------------------------

View File

@ -1,12 +1,29 @@
<template> <template>
<div
class="context-spawn uw-ui-trigger"
style="z-index: 1000"
@mouseenter="(ev) => setTriggerZoneActive(true, ev)"
@mouseleave="(ev) => setTriggerZoneActive(false, ev)"
>
<div
class="spawn-container uw-trigger"
:style="triggerZoneStyles"
>
&nbsp;
</div>
</div>
<div <div
v-if="contextMenuActive || settingsInitialized && uwTriggerZoneVisible && !isGlobal" v-if="contextMenuActive || settingsInitialized && uwTriggerZoneVisible && !isGlobal"
class="context-spawn uw-clickable" class="context-spawn uw-clickable"
style="z-index: 1001"
@mouseenter="preventContextMenuHide()" @mouseenter="preventContextMenuHide()"
@mouseleave="allowContextMenuHide()" @mouseleave="allowContextMenuHide()"
> >
<GhettoContextMenu
<GhettoContextMenu alignment="right"> alignment="right" class="uw-menu"
@mouseenter="newFeatureViewUpdate('uw6.ui-popup')"
>
<template v-slot:activator> <template v-slot:activator>
<div class="context-item"> <div class="context-item">
Ultrawidify Ultrawidify
@ -68,12 +85,38 @@
</GhettoContextMenuItem> </GhettoContextMenuItem>
</slot> </slot>
</GhettoContextMenu> </GhettoContextMenu>
<!-- shortcut for configuring UI -->
<GhettoContextMenuOption
v-if="settings.active.newFeatureTracker?.['uw6.ui-popup']?.show > 0"
@click="showUwWindow('ui-config')"
>
<span style="color: #fa6;">I hate this popup<br/></span>
<span style="font-size: 0.8em">
<span style="text-transform: uppercase; font-size: 0.8em">
<a @click="showUwWindow('ui-config')">
Do something about it
</a> × <a @click="acknowledgeNewFeature('uw6.ui-popup')">keep the popup</a>
</span>
<br/>
<span style="opacity: 0.5">This menu option will show {{settings.active.newFeatureTracker?.['uw6.ui-popup']?.show}} more<br/> times; or until clicked or dismissed.<br/>
Also accessible via <span style="font-variant: small-caps">extension settings</span>.
</span>
</span>
</GhettoContextMenuOption>
<!-- -->
<GhettoContextMenuOption <GhettoContextMenuOption
@click="showUwWindow()" @click="showUwWindow()"
label="Extension settings" label="Extension settings"
> >
</GhettoContextMenuOption> </GhettoContextMenuOption>
<button @click="showUwWindow()">Not working?</button> <GhettoContextMenuOption
@click="showUwWindow('about')"
label="Not working?"
>
</GhettoContextMenuOption>
</div> </div>
</slot> </slot>
</GhettoContextMenu> </GhettoContextMenu>
@ -160,6 +203,7 @@ export default {
disabled: false, disabled: false,
contextMenuActive: false, contextMenuActive: false,
triggerZoneActive: false,
uiVisible: true, uiVisible: true,
debugData: { debugData: {
@ -311,6 +355,34 @@ export default {
} }
}, },
/**
* Handles trigger zone
*/
handleTriggerZone(mouseInside) {
console.log('handing trigger zone!', mouseInside);
// this.triggerZoneActive = mouseInside;
},
acknowledgeNewFeature(featureKey) {
delete this.settings.active.newFeatureTracker[featureKey];
this.settings.saveWithoutReload();
},
newFeatureViewUpdate(featureKey) {
if (!this.settings.active.newFeatureTracker[featureKey]) {
return;
}
try {
this.settings.active.newFeatureTracker[featureKey].show--;
this.settings.saveWithoutReload();
if (this.settings.active.newFeatureTracker[featureKey]?.show < 0) {
this.acknowledgeNewFeature(featureKey);
}
} catch (e) {
// do nothing
}
},
/** /**
* Sends message to parent _without_ using event bus. * Sends message to parent _without_ using event bus.
*/ */
@ -324,14 +396,16 @@ export default {
}, },
preventContextMenuHide() { preventContextMenuHide() {
console.log('entered context menu ...');
this.contextMenuActive = true; this.contextMenuActive = true;
}, },
allowContextMenuHide() { allowContextMenuHide() {
console.log('exited context menu ...');
this.contextMenuActive = false; this.contextMenuActive = false;
}, },
setTriggerZoneActive(active, event) {
this.triggerZoneActive = active;
},
showUwWindow() { showUwWindow() {
this.uwWindowFadeOut = false; this.uwWindowFadeOut = false;
this.uwWindowVisible = true; this.uwWindowVisible = true;
@ -473,6 +547,10 @@ export default {
// white-space: nowrap; // white-space: nowrap;
// } // }
// .spawn-container {
// border: 1px solid white;
// }
} }
</style> </style>

View File

@ -43,26 +43,8 @@
<p class="text-center"> <p class="text-center">
<a class="donate" href="https://www.paypal.com/paypalme/tamius" target="_blank">Donate on Paypal</a> <a class="donate" href="https://www.paypal.com/paypalme/tamius" target="_blank">Donate on Paypal</a>
</p> </p>
<h2>Fun stuff</h2>
<p> <p>
This is probably a bad idea but I also have <a href="https://instagram.com/shaman_urkog" target="_blank">instagram with nerdy shit</a> <small>(mini painting + various fantasy events)</small>.
</p>
<p>
Are you attending Isle of Wonders on Cres, Croatia, between 28. 6. and 30. 6.? So am I, by official duty.
</p>
<p>
Club Amulet D20 is forecasted to have a stand there, and I am forecasted to be in the general vicinity of it (barring any unexpected circumstances). I'll be either taking photos, painting minis, or doing heatstroke any% in rather rudamentary costume.
</p>
<p>
If you're there, you can swing around to say 'hi' or provide some validation, or paint some minis. Rumor has it Conquest will have paint&take event.
</p>
<p>
Tamius
</p>
<p>
</p>
<p>
<small>I am not paid to shill this.</small>
</p> </p>
</div> </div>
</div> </div>

View File

@ -77,27 +77,7 @@
<p class="text-center"> <p class="text-center">
<a class="donate" href="https://www.paypal.com/paypalme/tamius" target="_blank">Donate on Paypal</a> <a class="donate" href="https://www.paypal.com/paypalme/tamius" target="_blank">Donate on Paypal</a>
</p> </p>
<h2>Fun stuff</h2>
<p>
This is probably a bad idea but
</p>
<p>
Are you attending Isle of Wonders on Cres, Croatia, between 28. 6. and 30. 6.? So am I, by official duty.
</p>
<p>
Club Amulet D20 is forecasted to have a stand there, and I am forecasted to be in the general vicinity of it (barring any unexpected circumstances). I'll be either taking photos, painting minis, or doing heatstroke any% in rather rudamentary costume.
</p>
<p>
If you're there, you can swing around to say 'hi' or provide some validation, or paint some minis. Rumor has it Conquest will have paint&take event.
</p>
<p>
Tamius
</p>
<p>
</p>
<p>
<small>I am not paid to shill this.</small>
</p>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="context-container" @mouseleave="hideContextMenu()"> <div class="context-container" @mouseleave="hideContextMenu()">
<GhettoContextMenuItem <GhettoContextMenuItem
class="activator" class="activator uw-clickable"
:css="{ :css="{
'expand-left': alignment === 'left', 'expand-left': alignment === 'left',
'expand-right': alignment === 'right', 'expand-right': alignment === 'right',
@ -13,7 +13,7 @@
</GhettoContextMenuItem> </GhettoContextMenuItem>
<div <div
v-if="contextMenuVisible" v-if="contextMenuVisible"
class="context-menu" class="context-menu uw-clickable"
:class="{ :class="{
'menu-left': alignment === 'left', 'menu-left': alignment === 'left',
'menu-right': alignment === 'right' 'menu-right': alignment === 'right'

View File

@ -1,7 +1,12 @@
<template> <template>
<div> <div>
<GhettoContextMenuItem> <GhettoContextMenuItem>
<template v-if="label">
{{label}} {{shortcut ? `(${shortcut})` : ''}} {{label}} {{shortcut ? `(${shortcut})` : ''}}
</template>
<template v-else>
<slot></slot>
</template>
</GhettoContextMenuItem> </GhettoContextMenuItem>
</div> </div>
</template> </template>

View File

@ -14,7 +14,29 @@ export default {
}, this.origin); }, this.origin);
}); });
}, },
data() {
return {
playerDimensions: undefined,
triggerZoneStyles: {
height: '50dvh',
width: '50dvw',
transform: 'translateX(-50%)'
},
}
},
methods: { methods: {
playerDimensionsUpdate(dimensions) {
if (dimensions?.width !== this.playerDimensions?.width || dimensions?.height !== this.playerDimensions?.height) {
this.playerDimensions = dimensions;
this.triggerZoneStyles = {
height: `${this.playerDimensions.height * 0.5}px`,
width: `${this.playerDimensions.width * 0.5}px`,
transform: `translate(${(this.settings.active.ui.inPlayer.triggerZoneDimensions.offsetX)}%, ${this.settings.active.ui.inPlayer.triggerZoneDimensions.offsetY}%)`,
};
}
},
/** /**
* Handles 'uwui-probe' events. It checks whether there's a clickable element under * Handles 'uwui-probe' events. It checks whether there's a clickable element under
* cursor, and sends a reply to the content scripts that indicates whether pointer-events * cursor, and sends a reply to the content scripts that indicates whether pointer-events
@ -26,16 +48,6 @@ export default {
} }
this.lastProbeTs = eventData.ts; this.lastProbeTs = eventData.ts;
// show ultrawidify trigger zone and set it to vanish after 250ms
// but don't show the trigger zone behind an active popup
if (! this.uwWindowVisible) {
this.uwTriggerZoneVisible = true;
clearTimeout(this.uwTriggerZoneTimeout);
this.uwTriggerZoneTimeout = setTimeout(
() => this.uwTriggerZoneVisible = false,
250
);
}
/* we check if our mouse is hovering over an element. /* we check if our mouse is hovering over an element.
* *
@ -47,16 +59,36 @@ export default {
* our top-level element. * our top-level element.
*/ */
let isClickable = false; let isClickable = false;
let element = document.elementFromPoint(eventData.coords.x, eventData.coords.y); let isOverTriggerZone = false;
const elements = document.elementsFromPoint(eventData.coords.x, eventData.coords.y);
while (element) { for (const element of elements) {
if (element?.classList.contains('uw-clickable')) { if (element.classList?.contains('uw-clickable')) {
// we could set 'pointerEvents' here and now & simply use return, but that
// might cause us a problem if we ever try to add more shit to this function
isClickable = true; isClickable = true;
break;
} }
element = element.parentElement; if (element.classList?.contains('uw-ui-trigger')) {
isOverTriggerZone = true;
}
}
this.triggerZoneActive = isOverTriggerZone;
// show ultrawidify trigger zone and set it to vanish after 250ms
// but don't show the trigger zone behind an active popup
if (
eventData.canShowUI
&& (this.settings.active.ui.inPlayer.activation !== 'player' || isOverTriggerZone)
) {
if (! this.uwWindowVisible) {
this.uwTriggerZoneVisible = true;
clearTimeout(this.uwTriggerZoneTimeout);
this.uwTriggerZoneTimeout = setTimeout(
() => this.uwTriggerZoneVisible = false,
250
);
}
} else {
this.uwTriggerZoneVisible = false;
} }
window.parent.postMessage( window.parent.postMessage(

View File

@ -159,7 +159,7 @@ const ExtensionConfPatch = [
userOptions.sites['@empty'].defaultType = 'modified'; userOptions.sites['@empty'].defaultType = 'modified';
} }
}, { }, {
forVersion: '6.0.1-7', forVersion: '6.0.2-0',
updateFn: (userOptions: SettingsInterface, defaultOptions) => { updateFn: (userOptions: SettingsInterface, defaultOptions) => {
// remove custom CSS, as it is no longer needed // remove custom CSS, as it is no longer needed
for (const site in userOptions.sites) { for (const site in userOptions.sites) {
@ -168,9 +168,18 @@ const ExtensionConfPatch = [
} }
userOptions.ui = { userOptions.ui = {
inPlayer: { inPlayer: {
enabled: false, // keep disabled for existing users enabled: true, // enable by default on new installs
minEnabledWidth: 0.75,
activation: 'player',
triggerZoneDimensions: {
width: 0.5,
height: 0.5,
offsetX: -50,
offsetY: 0,
} }
} }
},
userOptions.newFeatureTracker['uw6.ui-popup'] = {show: 10};
} }
} }
]; ];

View File

@ -160,6 +160,14 @@ const ExtensionConf: SettingsInterface = {
ui: { ui: {
inPlayer: { inPlayer: {
enabled: true, // enable by default on new installs enabled: true, // enable by default on new installs
minEnabledWidth: 0.75,
activation: 'player',
triggerZoneDimensions: {
width: 0.5,
height: 0.5,
offsetX: -50,
offsetY: 0
}
} }
}, },
@ -1415,6 +1423,9 @@ const ExtensionConf: SettingsInterface = {
} }
}, },
whatsNewChecked: true, whatsNewChecked: true,
newFeatureTracker: {
'uw6.ui-popup': {show: 10}
},
// ----------------------------------------- // -----------------------------------------
// ::: SITE CONFIGURATION ::: // ::: SITE CONFIGURATION :::
// ----------------------------------------- // -----------------------------------------

View File

@ -262,13 +262,7 @@ export class Aard {
this.settings = videoData.settings; this.settings = videoData.settings;
this.eventBus = videoData.eventBus; this.eventBus = videoData.eventBus;
this.testResults = initAardTestResults(this.settings.active.arDetect)
this.initEventBus(); this.initEventBus();
// this.sampleCols = [];
// this.blackLevel = this.settings.active.arDetect.blackbar.blackLevel;
this.arid = (Math.random()*100).toFixed(); this.arid = (Math.random()*100).toFixed();
// we can tick manually, for debugging // we can tick manually, for debugging
@ -319,6 +313,9 @@ export class Aard {
this.videoData.resizer.lastAr = {type: AspectRatioType.AutomaticUpdate, ratio: this.defaultAr}; this.videoData.resizer.lastAr = {type: AspectRatioType.AutomaticUpdate, ratio: this.defaultAr};
} }
// do full reset of test samples
this.testResults = initAardTestResults(this.settings.active.arDetect);
if (this.animationFrame) { if (this.animationFrame) {
window.cancelAnimationFrame(this.animationFrame); window.cancelAnimationFrame(this.animationFrame);
} }
@ -637,9 +634,6 @@ export class Aard {
return; return;
} }
const cornerViolations = [0,0,0,0];
let subpixelViolation = false;
let edgePosition = this.settings.active.arDetect.sampling.edgePosition; let edgePosition = this.settings.active.arDetect.sampling.edgePosition;
const segmentPixels = width * edgePosition; const segmentPixels = width * edgePosition;
const edgeSegmentSize = segmentPixels * 4; const edgeSegmentSize = segmentPixels * 4;
@ -655,14 +649,12 @@ export class Aard {
let i = rowStart; let i = rowStart;
while (i < firstSegment) { while (i < firstSegment) {
subpixelViolation = false;
if ( if (
imageData[i] > this.testResults.blackThreshold imageData[i] > this.testResults.blackThreshold
|| imageData[i + 1] > this.testResults.blackThreshold || imageData[i + 1] > this.testResults.blackThreshold
|| imageData[i + 2] > this.testResults.blackThreshold || imageData[i + 2] > this.testResults.blackThreshold
) { ) {
cornerViolations[Corner.TopLeft]++; this.testResults.guardLine.cornerPixelsViolated[Corner.TopLeft]++;
} }
i += 4; i += 4;
} }
@ -672,6 +664,10 @@ export class Aard {
|| imageData[i + 1] > this.testResults.blackThreshold || imageData[i + 1] > this.testResults.blackThreshold
|| imageData[i + 2] > this.testResults.blackThreshold || imageData[i + 2] > this.testResults.blackThreshold
) { ) {
// DONT FORGET TO INVALIDATE GUARDL LINE
this.testResults.guardLine.top = -1;
this.testResults.guardLine.bottom = -1;
this.testResults.guardLine.invalidated = true;
return; return;
}; };
i += 4; i += 4;
@ -682,7 +678,7 @@ export class Aard {
|| imageData[i + 1] > this.testResults.blackThreshold || imageData[i + 1] > this.testResults.blackThreshold
|| imageData[i + 2] > this.testResults.blackThreshold || imageData[i + 2] > this.testResults.blackThreshold
) { ) {
cornerViolations[Corner.TopRight]++; this.testResults.guardLine.cornerPixelsViolated[Corner.TopRight]++;
} }
i += 4; // skip over alpha channel i += 4; // skip over alpha channel
} }
@ -704,7 +700,7 @@ export class Aard {
|| imageData[i + 1] > this.testResults.blackThreshold || imageData[i + 1] > this.testResults.blackThreshold
|| imageData[i + 2] > this.testResults.blackThreshold || imageData[i + 2] > this.testResults.blackThreshold
) { ) {
cornerViolations[Corner.BottomLeft]++; this.testResults.guardLine.cornerPixelsViolated[Corner.BottomLeft]++;
} }
i += 4; // skip over alpha channel i += 4; // skip over alpha channel
} }
@ -717,6 +713,10 @@ export class Aard {
|| imageData[i + 1] > this.testResults.blackThreshold || imageData[i + 1] > this.testResults.blackThreshold
|| imageData[i + 2] > this.testResults.blackThreshold || imageData[i + 2] > this.testResults.blackThreshold
) { ) {
// DONT FORGET TO INVALIDATE GUARDL LINE
this.testResults.guardLine.top = -1;
this.testResults.guardLine.bottom = -1;
this.testResults.guardLine.invalidated = true;
return; return;
}; };
i += 4; i += 4;
@ -730,7 +730,7 @@ export class Aard {
|| imageData[i + 1] > this.testResults.blackThreshold || imageData[i + 1] > this.testResults.blackThreshold
|| imageData[i + 2] > this.testResults.blackThreshold || imageData[i + 2] > this.testResults.blackThreshold
) { ) {
cornerViolations[Corner.BottomRight]++; this.testResults.guardLine.cornerPixelsViolated[Corner.BottomRight]++;
} }
i += 4; // skip over alpha channel i += 4; // skip over alpha channel
} }
@ -738,21 +738,23 @@ export class Aard {
const maxViolations = segmentPixels * 0.20; // TODO: move the 0.2 threshold into settings const maxViolations = segmentPixels * 0.20; // TODO: move the 0.2 threshold into settings
console.log('Corner violations counts — segment px & max violations,', segmentPixels, maxViolations )
// we won't do a loop for this few elements // we won't do a loop for this few elements
// corners with stuff in them will also be skipped in image test // corners with stuff in them will also be skipped in image test
this.testResults.guardLine.cornerViolations[0] = cornerViolations[0] > maxViolations; this.testResults.guardLine.cornerViolated[0] = this.testResults.guardLine.cornerPixelsViolated[0] > maxViolations;
this.testResults.guardLine.cornerViolations[1] = cornerViolations[1] > maxViolations; this.testResults.guardLine.cornerViolated[1] = this.testResults.guardLine.cornerPixelsViolated[1] > maxViolations;
this.testResults.guardLine.cornerViolations[2] = cornerViolations[2] > maxViolations; this.testResults.guardLine.cornerViolated[2] = this.testResults.guardLine.cornerPixelsViolated[2] > maxViolations;
this.testResults.guardLine.cornerViolations[3] = cornerViolations[3] > maxViolations; this.testResults.guardLine.cornerViolated[3] = this.testResults.guardLine.cornerPixelsViolated[3] > maxViolations;
const maxInvalidCorners = 1; // TODO: move this into settings — by default, we allow one corner to extend past the const maxInvalidCorners = 1; // TODO: move this into settings — by default, we allow one corner to extend past the
// guard line in order to prevent watermarks/logos from preventing cropping the video // guard line in order to prevent watermarks/logos from preventing cropping the video
// this works because +true converts to 1 and +false converts to 0 // this works because +true converts to 1 and +false converts to 0
const dirtyCount = +this.testResults.guardLine.cornerViolations[0] const dirtyCount = +this.testResults.guardLine.cornerViolated[0]
+ +this.testResults.guardLine.cornerViolations[1] + +this.testResults.guardLine.cornerViolated[1]
+ +this.testResults.guardLine.cornerViolations[2] + +this.testResults.guardLine.cornerViolated[2]
+ +this.testResults.guardLine.cornerViolations[3]; + +this.testResults.guardLine.cornerViolated[3];
if (dirtyCount > maxInvalidCorners) { if (dirtyCount > maxInvalidCorners) {
this.testResults.guardLine.invalidated = true; this.testResults.guardLine.invalidated = true;
@ -802,7 +804,7 @@ export class Aard {
// we don't run image detection in corners that may contain logos, as such corners // we don't run image detection in corners that may contain logos, as such corners
// may not be representative // may not be representative
if (! this.testResults.guardLine.cornerViolations[Corner.TopLeft]) { if (! this.testResults.guardLine.cornerViolated[Corner.TopLeft]) {
while (i < firstSegment) { while (i < firstSegment) {
imagePixel = false; imagePixel = false;
imagePixel ||= imageData[i++] > this.testResults.blackThreshold; imagePixel ||= imageData[i++] > this.testResults.blackThreshold;
@ -826,7 +828,7 @@ export class Aard {
}; };
i++; // skip over alpha channel i++; // skip over alpha channel
} }
if (! this.testResults.guardLine.cornerViolations[Corner.TopRight]) { if (! this.testResults.guardLine.cornerViolated[Corner.TopRight]) {
while (i < rowEnd) { while (i < rowEnd) {
imagePixel = false; imagePixel = false;
imagePixel ||= imageData[i++] > this.testResults.blackThreshold; imagePixel ||= imageData[i++] > this.testResults.blackThreshold;
@ -842,7 +844,7 @@ export class Aard {
// we don't run image detection in corners that may contain logos, as such corners // we don't run image detection in corners that may contain logos, as such corners
// may not be representative // may not be representative
if (! this.testResults.guardLine.cornerViolations[Corner.TopLeft]) { if (! this.testResults.guardLine.cornerViolated[Corner.TopLeft]) {
while (i < firstSegment) { while (i < firstSegment) {
imagePixel = false; imagePixel = false;
imagePixel ||= imageData[i++] > this.testResults.blackThreshold; imagePixel ||= imageData[i++] > this.testResults.blackThreshold;
@ -866,7 +868,7 @@ export class Aard {
}; };
i++; // skip over alpha channel i++; // skip over alpha channel
} }
if (! this.testResults.guardLine.cornerViolations[Corner.TopRight]) { if (! this.testResults.guardLine.cornerViolated[Corner.TopRight]) {
while (i < rowEnd) { while (i < rowEnd) {
imagePixel = false; imagePixel = false;
imagePixel ||= imageData[i++] > this.testResults.blackThreshold; imagePixel ||= imageData[i++] > this.testResults.blackThreshold;
@ -892,7 +894,7 @@ export class Aard {
// we don't run image detection in corners that may contain logos, as such corners // we don't run image detection in corners that may contain logos, as such corners
// may not be representative // may not be representative
if (! this.testResults.guardLine.cornerViolations[Corner.TopLeft]) { if (! this.testResults.guardLine.cornerViolated[Corner.TopLeft]) {
while (i < firstSegment) { while (i < firstSegment) {
imagePixel = false; imagePixel = false;
imagePixel ||= imageData[i++] > this.testResults.blackThreshold; imagePixel ||= imageData[i++] > this.testResults.blackThreshold;
@ -916,7 +918,7 @@ export class Aard {
}; };
i++; // skip over alpha channel i++; // skip over alpha channel
} }
if (! this.testResults.guardLine.cornerViolations[Corner.TopRight]) { if (! this.testResults.guardLine.cornerViolated[Corner.TopRight]) {
while (i < rowEnd) { while (i < rowEnd) {
imagePixel = false; imagePixel = false;
imagePixel ||= imageData[i++] > this.testResults.blackThreshold; imagePixel ||= imageData[i++] > this.testResults.blackThreshold;
@ -932,7 +934,7 @@ export class Aard {
// we don't run image detection in corners that may contain logos, as such corners // we don't run image detection in corners that may contain logos, as such corners
// may not be representative // may not be representative
if (! this.testResults.guardLine.cornerViolations[Corner.TopLeft]) { if (! this.testResults.guardLine.cornerViolated[Corner.TopLeft]) {
while (i < firstSegment) { while (i < firstSegment) {
imagePixel = false; imagePixel = false;
imagePixel ||= imageData[i++] > this.testResults.blackThreshold; imagePixel ||= imageData[i++] > this.testResults.blackThreshold;
@ -956,7 +958,7 @@ export class Aard {
}; };
i++; // skip over alpha channel i++; // skip over alpha channel
} }
if (! this.testResults.guardLine.cornerViolations[Corner.TopRight]) { if (! this.testResults.guardLine.cornerViolated[Corner.TopRight]) {
while (i < rowEnd) { while (i < rowEnd) {
imagePixel = false; imagePixel = false;
imagePixel ||= imageData[i++] > this.testResults.blackThreshold; imagePixel ||= imageData[i++] > this.testResults.blackThreshold;
@ -1538,7 +1540,9 @@ export class Aard {
} }
} }
// BOTTOM: // BOTTOM
// Note that bottomRows candidates are measured from the top
// Well have to invert our candidate after we're done
if ( if (
this.testResults.aspectRatioCheck.bottomRows[0] === this.testResults.aspectRatioCheck.bottomRows[1] this.testResults.aspectRatioCheck.bottomRows[0] === this.testResults.aspectRatioCheck.bottomRows[1]
&& this.testResults.aspectRatioCheck.bottomRows[0] === this.testResults.aspectRatioCheck.bottomRows[2] && this.testResults.aspectRatioCheck.bottomRows[0] === this.testResults.aspectRatioCheck.bottomRows[2]
@ -1586,6 +1590,9 @@ export class Aard {
} }
} }
// Convert bottom candidate to letterbox width
this.testResults.aspectRatioCheck.bottomCandidateDistance = this.testResults.aspectRatioCheck.bottomCandidate === Infinity ? -1 : height - this.testResults.aspectRatioCheck.bottomCandidate;
/** /**
* Get final results. * Get final results.
* Let candidateA hold better-quality candidate, and let the candidateB hold the lower-quality candidate. * Let candidateA hold better-quality candidate, and let the candidateB hold the lower-quality candidate.
@ -1609,8 +1616,8 @@ export class Aard {
} }
const maxOffset = ~~(height * this.settings.active.arDetect.edgeDetection.maxLetterboxOffset) const maxOffset = ~~(height * this.settings.active.arDetect.edgeDetection.maxLetterboxOffset)
const diff = this.testResults.aspectRatioCheck.topCandidate - this.testResults.aspectRatioCheck.bottomCandidate; const diff = this.testResults.aspectRatioCheck.topCandidate - this.testResults.aspectRatioCheck.bottomCandidateDistance;
const candidateAvg = ~~((this.testResults.aspectRatioCheck.topCandidate + this.testResults.aspectRatioCheck.bottomCandidate) / 2); const candidateAvg = ~~((this.testResults.aspectRatioCheck.topCandidate + this.testResults.aspectRatioCheck.bottomCandidateDistance) / 2);
if (diff > maxOffset) { if (diff > maxOffset) {
this.testResults.aspectRatioUncertain = true; this.testResults.aspectRatioUncertain = true;
@ -1620,7 +1627,7 @@ export class Aard {
this.testResults.imageLine.top = this.testResults.aspectRatioCheck.topCandidate === Infinity ? -1 : this.testResults.aspectRatioCheck.topCandidate; this.testResults.imageLine.top = this.testResults.aspectRatioCheck.topCandidate === Infinity ? -1 : this.testResults.aspectRatioCheck.topCandidate;
this.testResults.imageLine.bottom = this.testResults.aspectRatioCheck.bottomCandidate === Infinity ? -1 : this.testResults.aspectRatioCheck.bottomCandidate; this.testResults.imageLine.bottom = this.testResults.aspectRatioCheck.bottomCandidate === Infinity ? -1 : this.testResults.aspectRatioCheck.bottomCandidate;
this.testResults.guardLine.top = Math.max(this.testResults.imageLine.top - 2, 0); this.testResults.guardLine.top = Math.max(this.testResults.imageLine.top - 2, 0);
this.testResults.guardLine.bottom = Math.max(this.testResults.imageLine.bottom + 2, this.canvasStore.main.height - 1); this.testResults.guardLine.bottom = Math.min(this.testResults.imageLine.bottom + 2, this.canvasStore.main.height - 1);
} }
this.testResults.aspectRatioUncertain = false; this.testResults.aspectRatioUncertain = false;
this.testResults.letterboxWidth = candidateAvg; this.testResults.letterboxWidth = candidateAvg;

View File

@ -10,7 +10,8 @@ export interface AardTestResults {
top: number, // is cumulative top: number, // is cumulative
bottom: number, // is cumulative bottom: number, // is cumulative
invalidated: boolean, invalidated: boolean,
cornerViolations: [boolean, boolean, boolean, boolean], cornerViolated: [boolean, boolean, boolean, boolean],
cornerPixelsViolated: [0,0,0,0]
}, },
imageLine: { imageLine: {
top: number, // is cumulative top: number, // is cumulative
@ -25,6 +26,7 @@ export interface AardTestResults {
topCandidate: number, topCandidate: number,
topCandidateQuality: number, topCandidateQuality: number,
bottomCandidate: number, bottomCandidate: number,
bottomCandidateDistance: number,
bottomCandidateQuality: number, bottomCandidateQuality: number,
}, },
aspectRatioUncertain: boolean, aspectRatioUncertain: boolean,
@ -45,7 +47,8 @@ export function initAardTestResults(settings: AardSettings): AardTestResults {
top: -1, top: -1,
bottom: -1, bottom: -1,
invalidated: false, invalidated: false,
cornerViolations: [false, false, false, false], cornerViolated: [false, false, false, false],
cornerPixelsViolated: [0,0,0,0]
}, },
imageLine: { imageLine: {
top: -1, top: -1,
@ -60,6 +63,7 @@ export function initAardTestResults(settings: AardSettings): AardTestResults {
topCandidate: 0, topCandidate: 0,
topCandidateQuality: 0, topCandidateQuality: 0,
bottomCandidate: 0, bottomCandidate: 0,
bottomCandidateDistance: 0,
bottomCandidateQuality: 0, bottomCandidateQuality: 0,
}, },
aspectRatioUncertain: false, aspectRatioUncertain: false,
@ -76,10 +80,14 @@ export function resetAardTestResults(results: AardTestResults): void {
results.notLetterbox = false; results.notLetterbox = false;
results.imageLine.invalidated = false; results.imageLine.invalidated = false;
results.guardLine.invalidated = false; results.guardLine.invalidated = false;
results.guardLine.cornerViolations[0] = false; results.guardLine.cornerViolated[0] = false;
results.guardLine.cornerViolations[1] = false; results.guardLine.cornerViolated[1] = false;
results.guardLine.cornerViolations[2] = false; results.guardLine.cornerViolated[2] = false;
results.guardLine.cornerViolations[3] = false; results.guardLine.cornerViolated[3] = false;
results.guardLine.cornerPixelsViolated[0] = 0;
results.guardLine.cornerPixelsViolated[1] = 0;
results.guardLine.cornerPixelsViolated[2] = 0;
results.guardLine.cornerPixelsViolated[3] = 0;
results.letterboxWidth = 0; results.letterboxWidth = 0;
results.letterboxOffset = 0; results.letterboxOffset = 0;
results.aspectRatioUpdated = false; results.aspectRatioUpdated = false;

View File

@ -13,7 +13,7 @@ const csuiVersions = {
class UI { class UI {
constructor( constructor(
interfaceId, interfaceId,
uiConfig, // {parentElement?, eventBus?} uiConfig, // {parentElement?, eventBus?, isGlobal?, playerData}
) { ) {
this.interfaceId = interfaceId; this.interfaceId = interfaceId;
this.uiConfig = uiConfig; this.uiConfig = uiConfig;
@ -31,6 +31,9 @@ class UI {
this.disablePointerEvents = false; this.disablePointerEvents = false;
this.saveState = undefined; this.saveState = undefined;
this.playerData = uiConfig.playerData;
this.uiSettings = uiConfig.uiSettings;
} }
async init() { async init() {
@ -108,12 +111,16 @@ class UI {
y: event.pageY - this.uiIframe.offsetTop y: event.pageY - this.uiIframe.offsetTop
}; };
const playerData = this.canShowUI(coords);
// ask the iframe to check whether there's a clickable element // ask the iframe to check whether there's a clickable element
this.uiIframe.contentWindow.postMessage( this.uiIframe.contentWindow.postMessage(
{ {
action: 'uwui-probe', action: 'uwui-probe',
coords, coords,
ts: +new Date() // this should be accurate enough for our purposes playerDimensions: playerData.playerDimensions,
canShowUI: playerData.canShowUI,
ts: +new Date() // this should be accurate enough for our purposes,
}, },
uiURI uiURI
); );
@ -130,6 +137,7 @@ class UI {
rootDiv.appendChild(iframe); rootDiv.appendChild(iframe);
} }
initMessaging() { initMessaging() {
// subscribe to events coming back to us. Unsubscribe if iframe vanishes. // subscribe to events coming back to us. Unsubscribe if iframe vanishes.
const messageHandlerFn = (message) => { const messageHandlerFn = (message) => {
@ -209,6 +217,51 @@ class UI {
} }
} }
/**
* Checks whether mouse is moving over either:
* * <video> element
* * player element ()
* * uwui-clickable element
*/
canShowUI(coords) {
const playerCssClass = 'uw-ultrawidify-player-css';
const result = {
playerDimensions: undefined,
canShowUI: false
}
if (this.playerData?.dimensions) {
result.playerDimensions = this.playerData.dimensions;
}
// if player is not wide enough, we do nothing
if (
!this.isGlobal && // this.isGlobal is basically 'yes, do as I say'
!document.fullscreenElement && // if we are in full screen, we allow it in every case as player detection is not 100% reliable,
result.playerDimensions?.width && // which makes playerDimensions.width unreliable as well (we assume nobody uses browser in
// fullscreen mode unless watching videos)
result.playerDimensions.width < window.screen.width * (this.uiSettings.inPlayer.minEnabledWidth ?? 0)
) {
result.canShowUI = false;
return result;
}
const elements = document.elementsFromPoint(coords.x, coords.y);
for (const element of elements) {
if (
element instanceof HTMLVideoElement
|| element.classList.contains(playerCssClass)
) {
result.canShowUI = true;
return result;
}
}
return result;
}
/** /**
* Handles events received from the iframe. * Handles events received from the iframe.
* @param {*} event * @param {*} event

View File

@ -157,7 +157,9 @@ class PlayerData {
'ultrawidifyUi', 'ultrawidifyUi',
{ {
parentElement: this.element, parentElement: this.element,
eventBus: this.eventBus eventBus: this.eventBus,
playerData: this,
uiSettings: this.videoData.settings.active.ui
} }
); );
} }

View File

@ -2,7 +2,7 @@
"manifest_version": 3, "manifest_version": 3,
"name": "Ultrawidify", "name": "Ultrawidify",
"description": "Removes black bars on ultrawide videos and offers advanced options to fix aspect ratio.", "description": "Removes black bars on ultrawide videos and offers advanced options to fix aspect ratio.",
"version": "6.0.1", "version": "6.0.2",
"icons": { "icons": {
"32":"res/icons/uw-32.png", "32":"res/icons/uw-32.png",
"64":"res/icons/uw-64.png" "64":"res/icons/uw-64.png"