Checkpoint: aard status indicator + trigger zone editor now kinda works

This commit is contained in:
Tamius Han 2024-12-26 14:58:14 +01:00
parent aabd5e75d8
commit 13cfb9ff14
20 changed files with 951 additions and 242 deletions

View File

@ -52,6 +52,8 @@ export type SettingsReloadComponent = 'PlayerData' | 'VideoData';
export type SettingsReloadFlags = true | SettingsReloadComponent;
export interface AardSettings {
aardType: 'webgl' | 'legacy' | 'auto';
disabledReason: string, // if automatic aspect ratio has been disabled, show reason
allowedMisaligned: number, // top and bottom letterbox thickness can differ by this much.
// Any more and we don't adjust ar.

View File

@ -2,12 +2,11 @@
<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"
@mouseenter="(ev) => setTriggerZoneActive(true, ev)"
>
&nbsp;
</div>
@ -30,99 +29,126 @@
</div>
</template>
<slot>
<div class="menu-width">
<GhettoContextMenuItem :disableHover="true" :css="{'ard-blocked': true}">
<div v-if="statusFlags.hasDrm || true" class="smallcaps text-center">
<b>NOTE:</b><br/>
<b>Autodetection<br/>blocked by website</b>
</div>
<div>
<!--
Didn't manage to ensure that extension status pops up above other menu items in less than 3 minutes with z-index,
so wrapping 'status' and 'real menu items' in two different divs, ordering them in the opposite way, and then
ensuring correct ordering with flex-direction: column-reverse ended up being easier and faster.
-->
<div class="menu-width flex-reverse-order">
<div style="z-index: 1000">
<GhettoContextMenu alignment="right">
<template v-slot:activator>
Crop
</template>
<slot>
<GhettoContextMenuOption
v-for="(command, index) of settings?.active.commands.crop"
:key="index"
:label="command.label"
:shortcut="getKeyboardShortcutLabel(command)"
@click="execAction(command)"
>
</GhettoContextMenuOption>
</slot>
</GhettoContextMenu>
<GhettoContextMenu alignment="right">
<template v-slot:activator>
Stretch
</template>
<slot>
<GhettoContextMenuOption
v-for="(command, index) of settings?.active.commands.stretch"
:key="index"
:label="command.label"
:shortcut="getKeyboardShortcutLabel(command)"
@click="execAction(command)"
>
</GhettoContextMenuOption>
</slot>
</GhettoContextMenu>
<GhettoContextMenu alignment="right">
<template v-slot:activator>
<div class="context-item">
Align
</div>
</template>
<slot>
<GhettoContextMenuItem :disableHover="true" :css="{'reduced-padding': true}">
<AlignmentOptionsControlComponent
:eventBus="eventBus"
>
</AlignmentOptionsControlComponent>
</GhettoContextMenuItem>
</slot>
</GhettoContextMenu>
<!-- shortcut for configuring UI -->
<GhettoContextMenuOption
v-if="settings.active.newFeatureTracker?.['uw6.ui-popup']?.show > 0"
@click="showUwWindow('playerUiSettings')"
>
<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('playerUiSettings')">
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
@click="showUwWindow()"
label="Extension settings"
>
</GhettoContextMenuOption>
<GhettoContextMenuOption
@click="showUwWindow('playerDetection')"
label="Incorrect cropping?"
>
</GhettoContextMenuOption>
<GhettoContextMenuOption
@click="showUwWindow('about')"
label="Not working?"
>
</GhettoContextMenuOption>
</div>
</GhettoContextMenuItem>
<GhettoContextMenu alignment="right">
<template v-slot:activator>
Crop
</template>
<slot>
<GhettoContextMenuOption
v-for="(command, index) of settings?.active.commands.crop"
:key="index"
:label="command.label"
:shortcut="getKeyboardShortcutLabel(command)"
@click="execAction(command)"
<div style="z-index: 10000">
<GhettoContextMenuItem
class="extension-status-messages"
:disableHover="true"
>
</GhettoContextMenuOption>
</slot>
</GhettoContextMenu>
<GhettoContextMenu alignment="right">
<template v-slot:activator>
Stretch
</template>
<slot>
<GhettoContextMenuOption
v-for="(command, index) of settings?.active.commands.stretch"
:key="index"
:label="command.label"
:shortcut="getKeyboardShortcutLabel(command)"
@click="execAction(command)"
>
</GhettoContextMenuOption>
</slot>
</GhettoContextMenu>
<GhettoContextMenu alignment="right">
<template v-slot:activator>
<div class="context-item">
Align
</div>
</template>
<slot>
<GhettoContextMenuItem :disableHover="true" :css="{'reduced-padding': true}">
<AlignmentOptionsControlComponent
:eventBus="eventBus"
Site compatibility:
<SupportLevelIndicator
:siteSupportLevel="siteSupportLevel"
>
</AlignmentOptionsControlComponent>
</SupportLevelIndicator>
<div v-if="statusFlags.hasDrm" class="aard-blocked">
Autodetection potentially<br/>
unavailable due to <a href="https://en.wikipedia.org/wiki/Digital_rights_management">DRM</a>.
</div>
<div v-else-if="statusFlags.aardErrors?.cors" class="aard-blocked">
Autodetection blocked<br/>
by site/browser (CORS).
</div>
<div v-else-if="statusFlags.aardErrors?.webglError" class="aard-blocked">
Autodetection unavailable<br/>
due to webgl error.
</div>
</GhettoContextMenuItem>
</slot>
</GhettoContextMenu>
<!-- shortcut for configuring UI -->
<GhettoContextMenuOption
v-if="settings.active.newFeatureTracker?.['uw6.ui-popup']?.show > 0"
@click="showUwWindow('playerUiSettings')"
>
<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('playerUiSettings')">
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
@click="showUwWindow()"
label="Extension settings"
>
</GhettoContextMenuOption>
<GhettoContextMenuOption
@click="showUwWindow('about')"
label="Not working?"
>
</GhettoContextMenuOption>
</div>
</div>
</slot>
</GhettoContextMenu>
</div>
<div
v-if="settingsInitialized && uwWindowVisible"
class="uw-window flex flex-col uw-clickable"
@ -139,6 +165,19 @@
@preventClose="(event) => uwWindowFadeOutDisabled = event"
></PlayerUIWindow>
</div>
<div
class="context-spawn uw-ui-trigger"
style="z-index: 1000;"
>
<TriggerZoneEditor
class="uw-clickable"
:settings="settings"
:playerDimensions="playerDimensions"
>
</TriggerZoneEditor>
</div>
</template>
<script>
@ -154,6 +193,8 @@ import EventBus from '../ext/lib/EventBus';
import UIProbeMixin from './src/utils/UIProbeMixin';
import KeyboardShortcutParserMixin from './src/utils/KeyboardShortcutParserMixin';
import CommsMixin from './src/utils/CommsMixin';
import SupportLevelIndicator from './src/components/SupportLevelIndicator.vue';
import TriggerZoneEditor from './src/components/TriggerZoneEditor.vue';
export default {
components: {
@ -162,6 +203,8 @@ export default {
GhettoContextMenuItem,
GhettoContextMenuOption,
AlignmentOptionsControlComponent,
SupportLevelIndicator,
TriggerZoneEditor,
},
mixins: [
UIProbeMixin,
@ -220,10 +263,13 @@ export default {
statusFlags: {
hasDrm: undefined,
aardErrors: undefined,
},
defaultWindowTab: 'videoSettings',
saveState: {},
siteSettings: undefined,
previewZoneVisible: false,
};
},
computed: {
@ -236,6 +282,12 @@ export default {
windowHeight() {
return window.innerHeight;
},
// LPT: NO ARROW FUNCTIONS IN COMPUTED,
// IS SUPER HARAM
// THINGS WILL NOT WORK IF YOU USE ARROWS
siteSupportLevel() {
return (this.site && this.siteSettings) ? this.siteSettings.data.type || 'no-support' : 'waiting';
}
},
watch: {
showUi(visible) {
@ -262,6 +314,8 @@ export default {
});
this.settings = new Settings({afterSettingsSaved: this.updateConfig, logger: this.logger});
this.settings.listenAfterChange(() => this.updateTriggerZones());
await this.settings.init();
this.settingsInitialized = true;
@ -272,12 +326,20 @@ export default {
});
this.eventBus.subscribe('uw-config-broadcast', {function: (data) => {
if (data.type === 'drm-status') {
this.statusFlags.hasDrm = data.hasDrm;
switch (data.type) {
case 'drm-status':
this.statusFlags.hasDrm = data.hasDrm;
break;
case 'aard-error':
this.statusFlags.aardErrors = data.aardErrors;
break;
case 'player-dimensions':
console.log('player dimensions response received.', data);
this.playerDimensionsUpdate(data.data);
break;
}
}});
this.eventBus.subscribe('uw-set-ui-state', { function: (data) => {
if (data.globalUiVisible !== undefined) {
if (this.isGlobal) {
@ -318,11 +380,19 @@ export default {
}
);
this.eventBus.subscribe('ui-trigger-zone-update', {
function: (data) => {
this.showTriggerZonePreview = data.previewZoneVisible;
// this.;
}
});
this.sendToParentLowLevel('uwui-get-role', null);
this.sendToParentLowLevel('uwui-get-theme', null);
//
this.sendToParentLowLevel('uw-bus-tunnel', {
action: 'get-player-dimensions'
});
},
methods: {
@ -344,6 +414,7 @@ export default {
if (!this.site) {
this.origin = event.origin;
this.site = event.origin.split('//')[1];
this.siteSettings = this.settings.getSiteSettings(this.site);
}
return this.handleProbe(event.data, event.origin); // handleProbe is defined in UIProbeMixin
case 'uw-bus-tunnel':
@ -552,4 +623,34 @@ export default {
// }
}
.extension-status-messages {
z-index: 1000;
text-transform: uppercase;
display: flex;
flex-direction: column;
text-align: center;
width: 112.25%;
transform: translate(-12.5%, 12.5%) scale(0.75);
> * {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
}
.flex-reverse-order {
display: flex;
flex-direction: column-reverse;
}
.aard-blocked {
color: #fa6;
}
.trigger-zone-preview {
border: 4px solid #fa4;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 KiB

View File

@ -5,41 +5,13 @@
<div class="popup-window-header">
<div class="header-title">
<div class="popup-title">Ultrawidify <small>{{settings?.active?.version}} - {{BrowserDetect.processEnvChannel}}</small></div>
<div class="site-support-info">
<div class="site-support-info flex flex-row">
<div class="site-support-site">{{site}}</div>
<template v-if="inPlayer">
<div v-if="siteSupportLevel === 'official'" class="site-support official">
<mdicon name="check-decagram" />
<div>Verified</div>
<div class="tooltip">The extension is being tested and should work on this site.</div>
</div>
<div v-if="siteSupportLevel === 'community'" class="site-support community">
<mdicon name="handshake" />
<div>Community</div>
<div class="tooltip">
People say extension works on this site (or have provided help getting the extension to work if it didn't).<br/><br/>
Tamius (the dev) does not test the extension on this site, probably because it requires a subscription or
is geoblocked.
</div>
</div>
<div v-if="siteSupportLevel === 'no-support'" class="site-support no-support">
<mdicon name="help-circle-outline" />
<div>Unknown</div>
<div class="tooltip">
Not officially supported. Extension will try to fix things, but no promises.<br/><br/>
Tamius (the dev) does not test the extension on this site for various reasons
(unaware, not using the site, language barrier, geoblocking, paid services Tam doesn't use).
</div>
</div>
<div v-if="siteSupportLevel === 'user-added'" class="site-support user-added">
<mdicon name="account" />
<div>Custom</div>
<div class="tooltip">
You have manually changed settings for this site. The extension is doing what you told it to do.
</div>
</div>
<mdicon v-if="siteSupportLevel === 'community'" class="site-support supported" name="checkbox-marked-circle" />
</template>
<SupportLevelIndicator
v-if="inPlayer"
:siteSupportLevel="siteSupportLevel"
>
</SupportLevelIndicator>
</div>
</div>
<div class="header-buttons">
@ -116,7 +88,6 @@
:settings="settings"
:eventBus="eventBus"
>
</PlayerUiSettings>
<BaseExtensionSettings
v-if="selectedTab === 'extensionSettings'"
@ -146,6 +117,11 @@
v-if="selectedTab === 'about'"
>
</AboutPanel>
<ResetBackupPanel
v-if="selectedTab === 'resetBackup'"
:settings="settings"
>
</ResetBackupPanel>
</div>
</div>
</div>
@ -161,6 +137,9 @@ import BrowserDetect from '../../ext/conf/BrowserDetect'
import ChangelogPanel from './PlayerUiPanels/ChangelogPanel.vue'
import AboutPanel from './PlayerUiPanels/AboutPanel.vue'
import PlayerUiSettings from './PlayerUiPanels/PlayerUiSettings.vue'
import ResetBackupPanel from './PlayerUiPanels/ResetBackupPanel.vue'
import SupportLevelIndicator from './components/SupportLevelIndicator.vue'
export default {
components: {
@ -171,7 +150,9 @@ export default {
DebugPanel,
PlayerUiSettings,
ChangelogPanel,
AboutPanel
AboutPanel,
SupportLevelIndicator,
ResetBackupPanel,
},
mixins: [],
data() {
@ -185,11 +166,12 @@ export default {
{id: 'extensionSettings', label: 'Site and Extension options', icon: 'cogs' },
{id: 'playerUiSettings', label: 'In-player interface', icon: 'movie-cog-outline' },
{id: 'playerDetection', label: 'Player detection', icon: 'television-play'},
{id: 'autodetectionSettings', label: 'Autodetection options', icon: ''},
{id: 'autodetectionSettings', label: 'Autodetection options', icon: 'auto-fix'},
// {id: 'advancedOptions', label: 'Advanced options', icon: 'cogs' },
// {id: 'debugging', label: 'Debugging', icon: 'bug-outline' }
{id: 'changelog', label: 'What\'s new', icon: 'newspaper-plus' },
{id: 'about', label: 'About', icon: 'star-four-points-circle'}
{id: 'changelog', label: 'What\'s new', icon: 'alert-decagram' },
{id: 'about', label: 'About', icon: 'information-outline'},
{id: 'resetBackup', label: 'Reset and backup', icon: 'file-restore-outline'},
],
selectedTab: 'extensionSettings',
BrowserDetect: BrowserDetect,
@ -272,86 +254,6 @@ export default {
overflow: hidden;
}
.site-support-info {
display: flex;
flex-direction: row;
align-items: center;
.site-support-site {
font-size: 1.5em;
}
.site-support {
display: inline-flex;
flex-direction: row;
align-items: center;
margin-left: 1rem;
border-radius: 8px;
padding: 0rem 1.5rem 0rem 1rem;
position: relative;
.tooltip {
padding: 1rem;
display: none;
position: absolute;
bottom: 0;
transform: translateY(110%);
width: 42em;
background-color: rgba(0,0,0,0.90);
color: #ccc;
}
&:hover {
.tooltip {
display: block;
}
}
.mdi {
margin-right: 1rem;
}
&.official {
background-color: #fa6;
color: #000;
.mdi {
fill: #000 !important;
}
}
&.community {
background-color: rgb(85, 85, 179);
color: #fff;
.mdi {
fill: #fff !important;
}
}
&.no-support {
background-color: rgb(138, 65, 126);
color: #eee;
.mdi {
fill: #eee !important;
}
}
&.user-added {
border: 1px solid #ff0;
color: #ff0;
.mdi {
fill: #ff0 !important;
}
}
}
}
.content {
flex-grow: 1;
@ -392,6 +294,16 @@ export default {
}
.site-support-info {
display: flex;
flex-direction: row;
align-items: bottom;
.site-support-site {
font-size: 1.5em;
}
}
.popup-panel {
background-color: rgba(0,0,0,0.50);
color: #fff;

View File

@ -92,7 +92,7 @@ export default({
],
mounted() {
this.settings.active.whatsNewChecked = true;
this.settings.save();
this.settings.saveWithoutReload();
}
});
</script>

View File

@ -9,7 +9,7 @@
<div class="label">Enable in-player UI</div>
<input type="checkbox" v-model="settings.active.ui.inPlayer.enabled" />
</div>
<!--
<div
class="flex flex-col"
:class="{disabled: settings.active.ui.inPlayer.enabled}"
@ -53,8 +53,90 @@
</div>
</div>
<div class="field">
<div class="label">Edit trigger zone:</div>
<button>Edit</button>
</div>
<div v-if="settings.active.ui.inPlayer.activation === 'trigger-zone'">
Trigger zone size:
<div class="trigger-zone-editor">
<div class="heading">
<h3>Trigger zone editor</h3>
</div>
<div class="field">
<div class="label">Trigger zone width:</div>
<div class="input range-input">
<input
v-model="settings.active.ui.inPlayer.triggerZoneDimensions.width"
class="slider"
type="range"
min="0.1"
max="1"
step="0.01"
>
<input
:value="(settings.active.ui.inPlayer.triggerZoneDimensions.width * 100).toFixed(2)"
@input="(event) => setTriggerZoneSize('width', event.target.value)"
>
</div>
<div class="hint">
Width of the trigger zone (% of player area).
</div>
</div>
<div class="field">
<div class="label">Trigger zone height:</div>
<div class="input range-input">
<input
v-model="settings.active.ui.inPlayer.triggerZoneDimensions.height"
type="range"
min="0.1"
max="1"
step="0.01"
>
<input
:value="(settings.active.ui.inPlayer.triggerZoneDimensions.height * 100).toFixed(2)"
@input="(event) => setTriggerZoneSize('width', event.target.value)"
>
</div>
<div class="hint">
Height of the trigger zone (% of player area).
</div>
</div>
<div class="field">
<div class="label">Trigger zone horizontal offset:</div>
<div class="input range-input">
<input
v-model="settings.active.ui.inPlayer.triggerZoneDimensions.offsetX"
type="range"
min="-100"
max="100"
>
<input
v-model="settings.active.ui.inPlayer.triggerZoneDimensions.offsetX"
>
</div>
<div class="hint">
By default, trigger zone is centered around the button. This option moves trigger zone left and right.
</div>
</div>
<div class="field">
<div class="label">Trigger zone vertical offset:</div>
<div class="input range-input">
<input
v-model="settings.active.ui.inPlayer.triggerZoneDimensions.offsetY"
type="range"
min="-100"
max="100"
>
<input
v-model="settings.active.ui.inPlayer.triggerZoneDimensions.offsetY"
>
</div>
<div class="hint">
By default, trigger zone is centered around the button. This option moves trigger zone up and down.
</div>
</div>
</div>
</div>
<div class="field">
@ -63,7 +145,7 @@
</div>
<div>TODO: slider</div>
</div>
</div> -->
</div>
</div>
</div>
@ -96,6 +178,24 @@ export default {
setUiPage(key, event) {
},
forceNumber(value) {
// Change EU format to US if needed
// | remove everything after second period if necessary
// | | | remove non-numeric characters
// | | | |
return value.replaceAll(',', '.').split('.', 2).join('.').replace(/[^0-9.]/g, '');
},
setTriggerZoneSize(key, value) {
let size = (+this.forceNumber(value) / 100);
if (isNaN(+size)) {
size = 0.5;
}
this.settings.active.ui.inPlayer.triggerZoneDimensions[key] = size;
this.settings.saveWithoutReload();
},
async openOptionsPage() {
BrowserDetect.runtime.openOptionsPage();
@ -118,4 +218,37 @@ export default {
.mt-4{
margin-top: 1rem;
}
.input {
max-width: 24rem;
}
.range-input {
display: flex;
flex-direction: row;
* {
margin-left: 0.5rem;
margin-right: 0.5rem;
}
input {
max-width: 5rem;
}
input[type=range] {
max-width: none;
}
}
.trigger-zone-editor {
background-color: rgba(0,0,0,0.25);
padding-bottom: 2rem;
.field {
margin-bottom: -1em;
}
}
</style>

View File

@ -0,0 +1,30 @@
<template>
<div class="flex flex-col">
<h1>Reset and backup</h1>
<p>
Pressing the button will reset settings to default without asking.
</p>
<button
class="danger"
@click="resetSettings"
>
Reset settings
</button>
</div>
</template>
<script>
export default {
props: {
settings: Object
},
methods: {
resetSettings() {
this.settings.active = JSON.parse(JSON.stringify(this.settings.default));
this.settings.saveWithoutReload();
}
}
}
</script>
<style lang="scss" src="../../res/css/flex.scss" scoped module></style>
<style lang="scss" src="../res-common/panels.scss" scoped module></style>
<style lang="scss" src="../res-common/common.scss" scoped module></style>

View File

@ -0,0 +1,17 @@
<template>
<div>
</div>
</template>
<script>
export default {
props: {
siteSettings: Object,
hasDrm: Boolean,
problems: Object,
}
}
</script>
<style lang="scss">
</style>

View File

@ -0,0 +1,137 @@
<template>
<div v-if="siteSupportLevel === 'official'" class="site-support official">
<mdicon name="check-decagram" />
<div v-if="!small">Verified</div>
<div class="tooltip">
<template v-if="small">Verified&nbsp;&nbsp;</template>
The extension is being tested and should work on this site.
</div>
</div>
<div v-if="siteSupportLevel === 'community'" class="site-support community">
<mdicon name="handshake" />
<div v-if="!small">Community</div>
<div class="tooltip">
<template v-if="small">Community&nbsp;&nbsp;</template>
People say extension works on this site (or have provided help getting the extension to work if it didn't).<br/><br/>
Tamius (the dev) does not test the extension on this site, probably because it requires a subscription or
is geoblocked.
</div>
</div>
<div v-if="siteSupportLevel === 'no-support'" class="site-support no-support">
<mdicon name="help-circle-outline" />
<div v-if="!small">Unknown</div>
<div class="tooltip">
<template v-if="small">Unknown&nbsp;&nbsp;</template>
Not officially supported. Extension will try to fix things, but no promises.<br/><br/>
Tamius (the dev) does not test the extension on this site for various reasons
(unaware, not using the site, language barrier, geoblocking, paid services Tam doesn't use).
</div>
</div>
<div v-if="siteSupportLevel === 'user-added'" class="site-support user-added">
<mdicon name="account" />
<div v-if="!small">Custom</div>
<div class="tooltip">
<template v-if="small">Custom&nbsp;&nbsp;</template>
You have manually changed settings for this site. The extension is doing what you told it to do.
</div>
</div>
<div v-if="siteSupportLevel === 'community'">
<mdicon class="site-support no-support" name="checkbox-marked-circle" />
<div v-if="!small">Not supported</div>
<div class="tooltip">
<template v-if="small">Not supported&nbsp;&nbsp;</template>
Extension is known to not work with this site.
</div>
</div>
</template>
<script>
export default {
props: {
siteSupportLevel: String,
small: Boolean,
}
}
</script>
<style lang="scss" scoped>
.site-support {
display: inline-flex;
flex-direction: row;
align-items: center;
margin-left: 1rem;
border-radius: 8px;
padding: 0rem 1.5rem 0rem 1rem;
position: relative;
.tooltip {
padding: 1rem;
display: none;
position: absolute;
bottom: 0;
transform: translateY(110%);
width: 42em;
background-color: rgba(0,0,0,0.90);
color: #ccc;
z-index: 99999 !important;
white-space: normal;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
}
&:hover {
.tooltip {
display: block;
}
}
.mdi {
margin-right: 1rem;
}
&.official {
background-color: #fa6;
color: #000;
.mdi {
fill: #000 !important;
}
}
&.community {
background-color: rgb(85, 85, 179);
color: #fff;
.mdi {
fill: #fff !important;
}
}
&.no-support {
background-color: rgb(132, 24, 40);
color: #eee;
.mdi {
fill: #eee !important;
}
}
&.user-added {
border: 1px solid #ff0;
color: #ff0;
.mdi {
fill: #ff0 !important;
}
}
}
</style>

View File

@ -0,0 +1,196 @@
<template>
<div
v-if="settings?.active?.ui"
class="active-trigger-area uw-clickable"
:style="triggerZoneStyles"
>
<div class="trigger-zone-editor"
@mousedown="(event) => handleMouseDown('offset', event)"
>
<div
class="uw-clickable tl"
@mousedown.stop="(event) => handleMouseDown('tl', event)"
>
XX
</div>
<div
class="uw-clickable tr"
@mousedown.stop="(event) => handleMouseDown('tr', event)"
>
XX
</div>
<div
class="uw-clickable bl"
@mousedown.stop="(event) => handleMouseDown('bl', event)"
>
XX
</div>
<div
class="uw-clickable br"
@mousedown.stop="(event) => handleMouseDown('br', event)"
>
XX
</div>
</div>
</div>
</template>
<script>
export default {
props: [
'settings',
'playerDimensions',
],
watch: {
playerDimensions(newVal, oldVal) {
console.log('triggerzone -- dimensions changed!', this.playerDimensions, newVal, oldVal);
this.updateTriggerZones();
}
},
data() {
return {
triggerZoneStyles: {},
activeCornerDrag: undefined,
dragStartPosition: undefined,
dragStartConfiguration: undefined,
}
},
created() {
document.addEventListener("mouseup", this.handleMouseUp);
document.addEventListener("mousemove", this.handleMouseMove);
},
methods: {
updateTriggerZones() {
if (this.playerDimensions && this.settings?.active?.ui?.inPlayer?.triggerZoneDimensions) {
this.triggerZoneStyles = {
width: `${Math.round(this.playerDimensions.width * this.settings.active.ui.inPlayer.triggerZoneDimensions.width)}px`,
height: `${Math.round(this.playerDimensions.height * this.settings.active.ui.inPlayer.triggerZoneDimensions.height)}px`,
transform: `translate(${(this.settings.active.ui.inPlayer.triggerZoneDimensions.offsetX)}%, ${this.settings.active.ui.inPlayer.triggerZoneDimensions.offsetY}%)`,
};
}
},
handleMouseDown(corner, event) {
this.activeCornerDrag = corner;
// we need to save this because we don't know the location of the player element,
// just its dimensions ... that means we need to
this.dragStartPosition = {
x: event.clientX,
y: event.clientY
};
this.dragStartConfiguration = JSON.parse(JSON.stringify(this.settings.active.ui.inPlayer.triggerZoneDimensions));
console.log(`Mousedown on ${corner}`);
},
handleMouseUp(event) {
if (!this.activeCornerDrag) {
return;
}
this.activeCornerDrag = undefined;
this.settings.saveWithoutReload();
},
handleMouseMove(event) {
if (!this.activeCornerDrag) {
return;
}
if (this.activeCornerDrag === 'offset') {
this.handleMove(event);
} else {
this.handleResize(event);
}
this.updateTriggerZones();
},
handleResize(event) {
// drag distance in px
const dx = event.clientX - this.dragStartPosition.x;
const dy = event.clientY - this.dragStartPosition.y;
// convert drag distance to % of current width:
const dxr = dx / this.playerDimensions.width * 2;
const dyr = dy / this.playerDimensions.height * 2;
// // update settings:
let nw, nh;
switch (this.activeCornerDrag) {
case 'tl':
nw = this.dragStartConfiguration.width - dxr;
nh = this.dragStartConfiguration.height - dyr;
break;
case 'tr':
nw = this.dragStartConfiguration.width + dxr;
nh = this.dragStartConfiguration.height - dyr;
break;
case 'bl':
nw = this.dragStartConfiguration.width - dxr;
nh = this.dragStartConfiguration.height + dyr;
break;
case 'br':
nw = this.dragStartConfiguration.width + dxr;
nh = this.dragStartConfiguration.height + dyr;
break;
}
// ensure everything is properly limited
const cw = Math.min(0.95, Math.max(0.125, nw));
const ch = Math.min(0.95, Math.max(0.125, nh));
// // update properties
this.settings.active.ui.inPlayer.triggerZoneDimensions.width = cw;
this.settings.active.ui.inPlayer.triggerZoneDimensions.height = ch;
},
handleMove(event) {
const dx = event.clientX - this.dragStartPosition.x;
const dy = event.clientY - this.dragStartPosition.y;
// convert drag distance to % of current width:
const dxr = dx / this.playerDimensions.width;
const dyr = dy / this.playerDimensions.height;
// const [min, max] = this.settings.active.ui.inPlayer.popupAlignment === 'right' ? [5, 90] : [-90, -5];
// const [minCrossAxis, maxCrossAxis] = [-90, 90];
const min = -90;
const max = -5;
const minCrossAxis = -90;
const maxCrossAxis = 90;
const cx = Math.min(max, Math.max(min, this.settings.active.ui.inPlayer.triggerZoneDimensions.offsetX + dxr));
const cy = Math.min(maxCrossAxis, Math.max(minCrossAxis, this.settings.active.ui.inPlayer.triggerZoneDimensions.offsetY + dyr));
this.settings.active.ui.inPlayer.triggerZoneDimensions.offsetX = cx;
this.settings.active.ui.inPlayer.triggerZoneDimensions.offsetY = cy;
}
}
}
</script>
<style lang="scss" scoped>
.active-trigger-area {
background-image: url('/res/img/grid_512.webp');
}
.trigger-zone-editor {
width: 100%;
height: 100%;
position: relative;
> * {
position: absolute;
}
.tr, .tl {
top: 0;
}
.br, .bl {
bottom: 0;
}
.tl, .bl {
left: 0;
}
.tr, .br {
right: 0;
}
}
</style>

View File

@ -30,7 +30,7 @@ h1, h2, h3 {
padding: 0;
}
.button {
button, .button {
background-color: rgba($blackBg, $normalTransparentOpacity);
padding: 0.5rem 2rem;
@ -51,6 +51,11 @@ h1, h2, h3 {
background-color: $primaryBg;
border-color: rgba($primary, .5);
}
&.danger {
background-color: #ff2211 !important;
color:#000;
}
}
.b3 {
margin: 0.25rem;

View File

@ -5,14 +5,14 @@ export default {
* We can handle events with the same function we use to handle events from
* the content script.
*/
document.addEventListener('mousemove', (event) => {
this.handleProbe({
coords: {
x: event.clientX,
y: event.clientY
}
}, this.origin);
});
document.addEventListener('mousemove', (event) => {
this.handleProbe({
coords: {
x: event.clientX,
y: event.clientY
}
}, this.origin);
});
},
data() {
return {
@ -26,14 +26,28 @@ export default {
},
methods: {
playerDimensionsUpdate(dimensions) {
if (!dimensions.width || !dimensions.height) {
this.playerDimensions = undefined;
}
console.log('player dimensions update received:', dimensions);
if (dimensions?.width !== this.playerDimensions?.width || dimensions?.height !== this.playerDimensions?.height) {
this.playerDimensions = dimensions;
console.log('Player dimensions changed!', dimensions);
this.playerDimensions = dimensions;
this.updateTriggerZones();
}
},
updateTriggerZones() {
console.log('triggered zone style recheck. player dims:', this.playerDimensions, 'in player settings:', this.settings.active.ui);
if (this.playerDimensions && this.settings) {
this.triggerZoneStyles = {
height: `${this.playerDimensions.height * 0.5}px`,
width: `${this.playerDimensions.width * 0.5}px`,
width: `${Math.round(this.playerDimensions.width * this.settings.active.ui.inPlayer.triggerZoneDimensions.width)}px`,
height: `${Math.round(this.playerDimensions.height * this.settings.active.ui.inPlayer.triggerZoneDimensions.height)}px`,
transform: `translate(${(this.settings.active.ui.inPlayer.triggerZoneDimensions.offsetX)}%, ${this.settings.active.ui.inPlayer.triggerZoneDimensions.offsetY}%)`,
};
console.log(
'player trigger zone css:', this.triggerZoneStyles
);
}
},

View File

@ -166,6 +166,7 @@ const ExtensionConfPatch = [
for (const domOption in userOptions.sites[site].DOMConfig)
userOptions.sites[site].DOMConfig[domOption].customCss;
}
userOptions.arDetect.aardType = 'auto';
userOptions.ui = {
inPlayer: {
enabled: true, // enable by default on new installs

View File

@ -15,6 +15,7 @@ if(Debug.debug)
const ExtensionConf: SettingsInterface = {
arDetect: {
aardType: 'auto',
disabledReason: "", // if automatic aspect ratio has been disabled, show reason
allowedMisaligned: 0.05, // top and bottom letterbox thickness can differ by this much.
// Any more and we don't adjust ar.

View File

@ -36,6 +36,9 @@ class Settings {
//#region callbacks
onSettingsChanged: any;
afterSettingsSaved: any;
onChangedCallbacks: any[] = [];
afterSettingsChangedCallbacks: any[] = [];
//#endregion
constructor(options) {
@ -63,15 +66,31 @@ class Settings {
this.logger?.log('info', 'debug', 'Does parsedSettings.preventReload exist?', parsedSettings.preventReload, "Does callback exist?", !!this.onSettingsChanged);
if (!parsedSettings.preventReload && this.onSettingsChanged) {
if (!parsedSettings.preventReload) {
try {
this.onSettingsChanged();
for (const fn of this.onChangedCallbacks) {
try {
fn();
} catch (e) {
this.logger?.log('warn', 'settings', "[Settings] afterSettingsChanged fallback failed. It's possible that a vue component got destroyed, and this function is nothing more than vestigal remains. It would be nice if we implemented something that allows us to remove callback functions from array, and remove vue callbacks from the callback array when their respective UI component gets destroyed. Or this could be an error with the function itself. IDK, here's the error.", e)
}
}
if (this.onSettingsChanged) {
this.onSettingsChanged();
}
this.logger?.log('info', 'settings', '[Settings] Update callback finished.')
} catch (e) {
this.logger?.log('error', 'settings', "[Settings] CALLING UPDATE CALLBACK FAILED. Reason:", e)
}
}
for (const fn of this.afterSettingsChangedCallbacks) {
try {
fn();
} catch (e) {
this.logger?.log('warn', 'settings', "[Settings] afterSettingsChanged fallback failed. It's possible that a vue component got destroyed, and this function is nothing more than vestigal remains. It would be nice if we implemented something that allows us to remove callback functions from array, and remove vue callbacks from the callback array when their respective UI component gets destroyed. Or this could be an error with the function itself. IDK, here's the error.", e)
}
}
if (this.afterSettingsSaved) {
this.afterSettingsSaved();
}
@ -179,6 +198,7 @@ class Settings {
updateFn(this.active, this.getDefaultSettings());
} catch (e) {
this.logger?.log('error', 'settings', '[Settings::applySettingsPatches] Failed to execute update function. Keeping settings object as-is. Error:', e);
}
}
@ -358,6 +378,13 @@ class Settings {
getSiteSettings(site: string = window.location.hostname): SiteSettings {
return new SiteSettings(this, site);
}
listenOnChange(fn: () => void): void {
this.onChangedCallbacks.push(fn);
}
listenAfterChange(fn: () => void): void {
this.afterSettingsChangedCallbacks.push(fn);
}
}
export default Settings;

View File

@ -5,6 +5,7 @@ import Settings from '../Settings';
import VideoData from '../video-data/VideoData';
import { Corner } from './enums/corner.enum';
import { VideoPlaybackState } from './enums/video-playback-state.enum';
import { FallbackCanvas } from './gl/FallbackCanvas';
import { GlCanvas } from './gl/GlCanvas';
import { AardCanvasStore } from './interfaces/aard-canvas-store.interface';
import { AardDetectionSample, generateSampleArray, resetSamples } from './interfaces/aard-detection-sample.interface';
@ -234,6 +235,8 @@ export class Aard {
//#region internal state
public status: AardStatus = initAardStatus();
private timers: AardTimers = initAardTimers();
private inFallback: boolean = false;
private fallbackReason: any;
private canvasStore: AardCanvasStore;
private testResults: AardTestResults;
private canvasSamples: AardDetectionSample;
@ -245,6 +248,8 @@ export class Aard {
return undefined;
}
this.video.setAttribute('crossOrigin', 'anonymous');
const ratio = this.video.videoWidth / this.video.videoHeight;
if (isNaN(ratio)) {
return undefined;
@ -284,8 +289,10 @@ export class Aard {
* This method should only ever be called from constructor.
*/
private init() {
this.canvasStore = {
main: new GlCanvas(new GlCanvas({...this.settings.active.arDetect.canvasDimensions.sampleCanvas, id: 'main-gl'})),
main: this.createCanvas('main-gl')
};
@ -302,6 +309,42 @@ export class Aard {
this.start();
}
private createCanvas(canvasId: string, canvasType?: 'webgl' | 'fallback') {
if (canvasType) {
if (canvasType === this.settings.active.arDetect.aardType || this.settings.active.arDetect.aardType === 'auto') {
if (canvasType === 'webgl') {
return new GlCanvas({...this.settings.active.arDetect.canvasDimensions.sampleCanvas, id: 'main-gl'});
} else if (canvasType === 'fallback') {
return new FallbackCanvas({...this.settings.active.arDetect.canvasDimensions.sampleCanvas, id: 'main-fallback'});
} else {
// TODO: throw error
}
} else {
// TODO: throw error
}
}
if (['auto', 'webgl'].includes(this.settings.active.arDetect.aardType)) {
try {
return new GlCanvas({...this.settings.active.arDetect.canvasDimensions.sampleCanvas, id: 'main-gl'});
} catch (e) {
if (this.settings.active.arDetect.aardType !== 'webgl') {
return new FallbackCanvas({...this.settings.active.arDetect.canvasDimensions.sampleCanvas, id: 'main-fallback'});
}
console.error('[ultrawidify|Aard::createCanvas] could not create webgl canvas:', e);
this.eventBus.send('uw-config-broadcast', {type: 'aard-error', aardErrors: {webglError: true}});
throw e;
}
} else if (this.settings.active.arDetect.aardType === 'legacy') {
return new FallbackCanvas({...this.settings.active.arDetect.canvasDimensions.sampleCanvas, id: 'main-fallback'});
} else {
console.error('[ultrawidify|Aard::createCanvas] invalid value in settings.arDetect.aardType:', this.settings.active.arDetect.aardType);
this.eventBus.send('uw-config-broadcast', {type: 'aard-error', aardErrors: {invalidSettings: true}});
throw 'AARD_INVALID_SETTINGS';
}
}
//#endregion
/**
@ -393,8 +436,35 @@ export class Aard {
do {
const imageData = await new Promise<Uint8Array>(
resolve => {
this.canvasStore.main.drawVideoFrame(this.video);
resolve(this.canvasStore.main.getImageData());
try {
this.canvasStore.main.drawVideoFrame(this.video);
resolve(this.canvasStore.main.getImageData());
} catch (e) {
if (e.name === 'SecurityError') {
this.eventBus.send('uw-config-broadcast', {type: 'aard-error', aardErrors: {cors: true}});
this.stop();
}
if (this.canvasStore.main instanceof FallbackCanvas) {
if (this.inFallback) {
this.eventBus.send('uw-config-broadcast', {type: 'aard-error', aardErrors: this.fallbackReason});
this.stop();
} else {
this.eventBus.send('uw-config-broadcast', {type: 'aard-error', aardErrors: {fallbackCanvasError: true}});
this.stop();
}
} else {
if (this.settings.active.arDetect.aardType === 'auto') {
this.canvasStore.main.destroy();
this.canvasStore.main = this.createCanvas('main-gl', 'fallback');
}
this.inFallback = true;
this.fallbackReason = {cors: true};
if (this.settings.active.arDetect.aardType !== 'auto') {
this.stop();
}
}
}
}
);
@ -407,6 +477,7 @@ export class Aard {
);
if (this.testResults.notLetterbox) {
// TODO: reset aspect ratio to "AR not applied"
console.log('NOT LETTERBOX!');
this.testResults.lastStage = 1;
break;
}
@ -420,6 +491,7 @@ export class Aard {
this.settings.active.arDetect.canvasDimensions.sampleCanvas.width,
this.settings.active.arDetect.canvasDimensions.sampleCanvas.height
);
console.log('LETTERBOX SHRINK CHECK RESULT — IS GUARDLINE INVALIDATED?', this.testResults.guardLine.invalidated)
if (! this.testResults.guardLine.invalidated) {
this.checkLetterboxGrow(
imageData,
@ -452,12 +524,17 @@ export class Aard {
// if detection is uncertain, we don't do anything at all
if (this.testResults.aspectRatioUncertain) {
console.info('aspect ratio not cettain.');
console.warn('check finished:', JSON.parse(JSON.stringify(this.testResults)), JSON.parse(JSON.stringify(this.canvasSamples)), '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n');
return;
}
// TODO: emit debug values if debugging is enabled
this.testResults.isFinished = true;
console.warn('check finished:', JSON.parse(JSON.stringify(this.testResults)), JSON.parse(JSON.stringify(this.canvasSamples)), '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n');
// if edge width changed, emit update event.
if (this.testResults.aspectRatioUpdated) {
this.videoData.resizer.updateAr({
@ -1008,7 +1085,12 @@ export class Aard {
// fact that it makes the 'if' statement governing gradient detection
// bit more nicely visible (instead of hidden among spagheti)
this.edgeScan(imageData, width, height);
console.log('edge scan:', JSON.parse(JSON.stringify(this.canvasSamples)));
this.validateEdgeScan(imageData, width, height);
console.log('edge scan post valid:', JSON.parse(JSON.stringify(this.canvasSamples)));
// TODO: _if gradient detection is enabled, then:
this.sampleForGradient(imageData, width, height);
@ -1061,6 +1143,7 @@ export class Aard {
x = 0;
isImage = false;
finishedRows = 0;
while (row < topEnd) {
i = 0;
rowOffset = row * 4 * width;
@ -1126,6 +1209,7 @@ export class Aard {
|| imageData[rowOffset + x + 2] > this.testResults.blackLevel;
if (!isImage) {
// console.log('(row:', row, ')', 'val:', imageData[rowOffset + x], 'col', x >> 2, x, 'pxoffset:', rowOffset + x, 'len:', imageData.length)
// TODO: maybe some day mark this pixel as checked by writing to alpha channel
i++;
continue;
@ -1272,6 +1356,7 @@ export class Aard {
// didn't change meaningfully from the first, in which chance we aren't. If the brightness increased
// anywhere between 'not enough' and 'too much', we mark the measurement as invalid.
if (lastSubpixel - firstSubpixel > this.settings.active.arDetect.edgeDetection.gradientTestMinDelta) {
console.log('sample invalidated cus gradient:');
this.canvasSamples.top[i] = -1;
}
}
@ -1645,6 +1730,26 @@ export class Aard {
const compensatedWidth = fileAr === canvasAr ? this.canvasStore.main.width : this.canvasStore.main.width * fileAr;
console.log(`
ASPECT RATIO CALCULATION:
canvas size: ${this.canvasStore.main.width} x ${this.canvasStore.main.height} (1:${this.canvasStore.main.width / this.canvasStore.main.height})
file size: ${this.video.videoWidth} x ${this.video.videoHeight} (1:${this.video.videoWidth / this.video.videoHeight})
compensated size: ${compensatedWidth} x ${this.canvasStore.main.height} (1:${compensatedWidth / this.canvasStore.main.height})
letterbox height: ${this.testResults.letterboxWidth}
net video height: ${this.canvasStore.main.height - (this.testResults.letterboxWidth * 2)}
calculated aspect ratio -----
${compensatedWidth} ${compensatedWidth} ${compensatedWidth}
= = = ${compensatedWidth / (this.canvasStore.main.height - (this.testResults.letterboxWidth * 2))}
${this.canvasStore.main.height} - 2 x ${this.testResults.letterboxWidth} ${this.canvasStore.main.height} - ${2 * this.testResults.letterboxWidth} ${this.canvasStore.main.height - (this.testResults.letterboxWidth * 2)}
`);
return compensatedWidth / (this.canvasStore.main.height - (this.testResults.letterboxWidth * 2));
}

View File

@ -7,7 +7,6 @@ export class FallbackCanvas extends GlCanvas {
constructor(options: GlCanvasOptions) {
super(options);
this.context = this.canvas.getContext('2d');
}
/**
@ -18,9 +17,14 @@ export class FallbackCanvas extends GlCanvas {
destroy() { }
protected initWebgl() { }
protected initContext() {
this.context = this.canvas.getContext('2d', {desynchronized: true});
}
protected initWebgl() { }
drawVideoFrame(video: HTMLVideoElement) {
console.log('context:', this.context, 'canvas:', this.canvas );
this.context.drawImage(video, this.context.canvas.width, this.context.canvas.height);
}

View File

@ -95,16 +95,7 @@ export class GlCanvas {
this.canvas.setAttribute('width', `${options.width}`);
this.canvas.setAttribute('height', `${options.height}`);
this.gl = this.canvas.getContext('webgl');
if (!this.gl) {
throw new Error('WebGL not supported');
}
if(options.id) {
this.canvas.setAttribute('id', options.id);
}
this.frameBufferSize = options.width * options.height * 4;
this.initContext(options);
this.initWebgl();
}
@ -156,6 +147,24 @@ export class GlCanvas {
this.gl.deleteTexture(this.texture);
}
protected initContext(options: GlCanvasOptions) {
this.gl = this.canvas.getContext(
'webgl2',
{
preserveDrawingBuffer: true
}
);
if (!this.gl) {
throw new Error('WebGL not supported');
}
if(options.id) {
this.canvas.setAttribute('id', options.id);
}
this.frameBufferSize = options.width * options.height * 4;
}
protected initWebgl() {
// Initialize the GL context
this.gl.clearColor(0.0, 0.0, 0.0, 1.0);

View File

@ -97,6 +97,16 @@ class PlayerData {
'get-player-tree': [{
function: () => this.handlePlayerTreeRequest()
}],
'get-player-dimensions': [{
function: () => {
console.log('received get player dimensions! -- returning:', this.dimensions)
this.eventBus.send('uw-config-broadcast', {
type: 'player-dimensions',
data: this.dimensions
});
}
}],
'set-mark-element': [{ // NOTE: is this still used?
function: (data) => this.markElement(data)
}],
@ -371,6 +381,10 @@ class PlayerData {
this.eventBus.send('restore-ar', null);
this.eventBus.send('delayed-restore-ar', {delay: 500});
// this.videoData.resizer?.restore();
this.eventBus.send('uw-config-broadcast', {
type: 'player-dimensions',
data: newDimensions
});
}
}
@ -378,6 +392,7 @@ class PlayerData {
this.trackDimensionChanges();
}
//#region player element change detection
/**
* Starts change detection.

BIN
src/res/img/grid_512.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 KiB