Compare commits

...

20 Commits

Author SHA1 Message Date
78395e3ea0 Minor fixes for comms and mutation detectìon 2026-06-06 03:29:46 +02:00
e269ff3d79 Don't trip on subtitles that don't fall outside current crop area
Todo: don't trip on subtitles that wouldn't get cropped, but that would require Aard to know player's aspect ratio, and it currently doesnt
2026-06-06 03:27:49 +02:00
be495b1c56 daon't error if context isnt defined 2026-04-03 16:44:41 +02:00
3f032619b4 Fix firefox errors in cross-iframe communiccation 2026-04-03 16:44:12 +02:00
ef5ba706b7 Changelog update 2026-01-29 00:00:11 +01:00
3b91c2224c Allow zoom modes to be set as default crop 2026-01-28 23:54:26 +01:00
822d069b82 Finish syncing UI with extension state
Add active mode indicators to popup. Add handling of locked zoom & alignment indicator to both popup and UI. Improve message passing through Comms.
2026-01-28 21:49:51 +01:00
8af9f2cedf Indicate currently selected option in the in-player popup menu 2026-01-28 18:11:57 +01:00
3d8d8eb199 broadcast scaling parameters on ar change 2026-01-28 02:44:38 +01:00
a0d599ce2f add amazon to extconfpatches 2026-01-28 02:05:11 +01:00
fd7b5ec04e Fix messaging 2026-01-28 02:02:27 +01:00
c9818c92b3 Update readme 2026-01-27 20:37:32 +01:00
f57163f2bb Player selection should be functional enough 2026-01-27 20:36:50 +01:00
dbdcb4e367 add option to always activate menu while holding CTRL 2026-01-25 23:33:37 +01:00
e1dd0f9c04 Open in-player settings window, get player selection to somewhat work 2026-01-25 21:47:31 +01:00
2cb6a88c5e fix UI settings a bit 2026-01-25 21:41:01 +01:00
eb89ba03db Fix config 2026-01-25 20:54:25 +01:00
cb02c8619d Ensure each command gets executed once, even if it gets duplicated on the way 2026-01-25 20:52:25 +01:00
7d296e6a3d Get in-page player picker to at least show up 2026-01-23 22:46:59 +01:00
35bb5b477b Finalize menu position settings, replace eventBus.sendToTunnel() with eventBus.send() 2026-01-15 02:33:21 +01:00
45 changed files with 1710 additions and 788 deletions

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "ultrawidify", "name": "ultrawidify",
"version": "6.3.997", "version": "6.3.998",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ultrawidify", "name": "ultrawidify",
"version": "6.3.997", "version": "6.3.998",
"dependencies": { "dependencies": {
"@babel/plugin-proposal-class-properties": "^7.18.6", "@babel/plugin-proposal-class-properties": "^7.18.6",
"@mdi/font": "^7.4.47", "@mdi/font": "^7.4.47",

View File

@ -1,6 +1,6 @@
{ {
"name": "ultrawidify", "name": "ultrawidify",
"version": "6.3.997", "version": "6.3.998",
"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

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

View File

@ -4,7 +4,8 @@ enum VideoAlignmentType {
Right = 2, Right = 2,
Top = 3, Top = 3,
Bottom = 4, Bottom = 4,
Default = -1 Default = -1,
Custom = -2,
}; };
export default VideoAlignmentType; export default VideoAlignmentType;

View File

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

View File

@ -24,6 +24,7 @@ export interface EventBusContext {
// port?: string; // port?: string;
visitedBusses?: string[]; visitedBusses?: string[];
commandId?: string;
comms?: { comms?: {
forwardTo?: 'all' | 'active' | 'popup' | 'contentScript' | 'all-frames'; forwardTo?: 'all' | 'active' | 'popup' | 'contentScript' | 'all-frames';

View File

@ -0,0 +1,19 @@
import VideoAlignmentType from '@src/common/enums/VideoAlignmentType.enum';
import { Ar } from '@src/common/interfaces/ArInterface';
import { Stretch } from '@src/common/interfaces/StretchInterface';
export interface ScalingParamsBroadcast {
effectiveZoom: {
x: number,
y: number
},
videoAlignment: {
x: VideoAlignmentType,
y: VideoAlignmentType,
xPos?: number,
yPos?: number,
}
lastAr: Ar,
manualZoom: boolean,
stretch: Stretch,
}

View File

@ -9,6 +9,7 @@ import StretchType from '@src/common/enums/StretchType.enum'
import VideoAlignmentType from '@src/common/enums/VideoAlignmentType.enum' import VideoAlignmentType from '@src/common/enums/VideoAlignmentType.enum'
import { PlayerDetectionMode } from '@src/common/enums/PlayerDetectionMode.enum'; import { PlayerDetectionMode } from '@src/common/enums/PlayerDetectionMode.enum';
import { InputHandlingMode } from '@src/common/enums/InputHandlingMode.enum'; import { InputHandlingMode } from '@src/common/enums/InputHandlingMode.enum';
import { MenuPosition } from '@src/common/interfaces/ClientUiMenu.interface';
export enum ExtensionEnvironment { export enum ExtensionEnvironment {
Normal = ExtensionMode.All, Normal = ExtensionMode.All,
@ -309,14 +310,15 @@ interface DevSettings {
} }
export interface InPlayerUIOptions { export interface InPlayerUIOptions {
activatorAlignment: 'left' | 'right', activatorAlignment: MenuPosition,
minEnabledWidth: number, // don't show UI if player is narrower than % of screen width minEnabledWidth: number, // don't show UI if player is narrower than % of screen width
minEnabledHeight: number, // don't show UI if player is narrower than % of screen height minEnabledHeight: number, // don't show UI if player is narrower than % of screen height
activation: 'player' | 'player-ctrl' | 'trigger-zone' | 'distance' | 'none', // what needs to be hovered in order for UI to be visible activation: 'player' | 'trigger-zone' | 'distance' | 'none', // what needs to be hovered in order for UI to be visible
activateWithCtrl: boolean,
activationDistance: number, activationDistance: number,
activationDistanceUnits: '%' | 'px', activationDistanceUnits: '%' | 'px',
activatorPadding: 10, activatorPadding: {x: number, y: number}
activatorPaddingUnit: '%' | 'px', activatorPaddingUnit: {x: '%' | 'px', y: '%' | 'px'},
triggerZoneDimensions: { // how large the trigger zone is (relative to player size) triggerZoneDimensions: { // how large the trigger zone is (relative to player size)
width: number width: number
height: number, height: number,
@ -444,6 +446,8 @@ interface SettingsInterface {
} }
export interface SiteSettingsInterface { export interface SiteSettingsInterface {
notes?: string; // any special things related to this site.
enable: ExtensionMode; enable: ExtensionMode;
enableAard: ExtensionMode; enableAard: ExtensionMode;
enableKeyboard: InputHandlingMode; enableKeyboard: InputHandlingMode;

View File

@ -1,5 +1,5 @@
import Debug from '@src/ext/conf/Debug'; import Debug from '@src/ext/conf/Debug';
import CommsClient from '@src/ext/module/comms/CommsClient'; import CommsClient, { CommsOrigin } from '@src/ext/module/comms/CommsClient';
import EventBus from '@src/ext/module/EventBus'; import EventBus from '@src/ext/module/EventBus';
import KeyboardHandler from '@src/ext/module/kbm/KeyboardHandler'; import KeyboardHandler from '@src/ext/module/kbm/KeyboardHandler';
import { ComponentLogger } from '@src/ext/module/logging/ComponentLogger'; import { ComponentLogger } from '@src/ext/module/logging/ComponentLogger';
@ -82,7 +82,7 @@ export default class UWContent {
this.siteSettings = this.settings.getSiteSettings({site: window.location.hostname, isIframe: this.isIframe, parentHostname: this.parentHostname}); this.siteSettings = this.settings.getSiteSettings({site: window.location.hostname, isIframe: this.isIframe, parentHostname: this.parentHostname});
} }
this.eventBus = new EventBus({name: 'content-script'}); this.eventBus = new EventBus({name: 'content-script', commsOrigin: CommsOrigin.ContentScript});
this.eventBus.subscribe( this.eventBus.subscribe(
'uw-restart', 'uw-restart',
{ {
@ -120,9 +120,12 @@ export default class UWContent {
this.logger.debug('setup', "KeyboardHandler initiated."); this.logger.debug('setup', "KeyboardHandler initiated.");
this.globalUi = new UI('ultrawidify-global-ui', {eventBus: this.eventBus, isGlobal: true}); if (this.globalUi) {
this.globalUi.enable(); this.globalUi.destroy();
this.globalUi.setUiVisibility(false); }
// this.globalUi = new UI('ultrawidify-global-ui', {eventBus: this.eventBus, isGlobal: true});
// this.globalUi.enable();
// this.globalUi.setUiVisibility(false);
} catch (e) { } catch (e) {
console.error('Ultrawidify: failed to start extension. Error:', e) console.error('Ultrawidify: failed to start extension. Error:', e)

View File

@ -7,6 +7,7 @@ import CommsServer from '@src/ext/module/comms/CommsServer';
import BrowserDetect from '@src/ext/conf/BrowserDetect'; import BrowserDetect from '@src/ext/conf/BrowserDetect';
import { HostInfo } from '@src/common/interfaces/HostData.interface'; import { HostInfo } from '@src/common/interfaces/HostData.interface';
import { ExtensionEnvironment } from '@src/common/interfaces/SettingsInterface'; import { ExtensionEnvironment } from '@src/common/interfaces/SettingsInterface';
import { CommsOrigin } from '@src/ext/module/comms/CommsClient';
const BASE_LOGGING_STYLES = { const BASE_LOGGING_STYLES = {
@ -81,7 +82,7 @@ export default class UWServer {
this.settings = new Settings({logAggregator: this.logAggregator}); this.settings = new Settings({logAggregator: this.logAggregator});
await this.settings.init(); await this.settings.init();
this.eventBus = new EventBus({isUWServer: true}); this.eventBus = new EventBus({isUWServer: true, commsOrigin: CommsOrigin.Server});
this.eventBus.subscribeMulti(this.eventBusCommands, this); this.eventBus.subscribeMulti(this.eventBusCommands, this);
@ -105,6 +106,11 @@ export default class UWServer {
//#region CSS management //#region CSS management
async injectCss(css, sender) { async injectCss(css, sender) {
if (!sender?.tab?.id) {
// console.warn('invalid injectCss received!');
return;
}
this.logger.info('injectCss', 'Trying to inject CSS into tab', sender.tab.id, ', frameId:', sender.frameId, 'css:\n', css) this.logger.info('injectCss', 'Trying to inject CSS into tab', sender.tab.id, ', frameId:', sender.frameId, 'css:\n', css)
if (!css) { if (!css) {
return; return;
@ -125,6 +131,11 @@ export default class UWServer {
} }
} }
async removeCss(css, sender) { async removeCss(css, sender) {
if (!sender?.tab) {
// console.warn('invalid removeCss received!');
return;
}
try { try {
await chrome.scripting.removeCSS({ await chrome.scripting.removeCSS({
target: { target: {
@ -141,6 +152,10 @@ export default class UWServer {
} }
} }
async replaceCss(oldCss, newCss, sender) { async replaceCss(oldCss, newCss, sender) {
if (!sender?.tab) {
// console.warn('invalid replaceCss received!');
return;
}
if (oldCss !== newCss) { if (oldCss !== newCss) {
this.removeCss(oldCss, sender); this.removeCss(oldCss, sender);
this.injectCss(newCss, sender); this.injectCss(newCss, sender);
@ -197,6 +212,10 @@ export default class UWServer {
} }
registerVideo(sender) { registerVideo(sender) {
if (!sender?.tab?.url) {
// console.warn('invalid registerVideo received!');
return;
}
this.logger.info('registerVideo', 'Registering video.\nsender:', sender); this.logger.info('registerVideo', 'Registering video.\nsender:', sender);
const tabHostname = this.extractHostname(sender.tab.url); const tabHostname = this.extractHostname(sender.tab.url);
@ -235,6 +254,11 @@ export default class UWServer {
} }
unregisterVideo(sender) { unregisterVideo(sender) {
if (!sender?.tab) {
// console.warn('invalid unregisterVideo received!');
return;
}
this.logger.info('unregisterVideo', 'Unregistering video.\nsender:', sender); this.logger.info('unregisterVideo', 'Unregistering video.\nsender:', sender);
if (this.videoTabs[sender.tab.id]) { if (this.videoTabs[sender.tab.id]) {
if ( Object.keys(this.videoTabs[sender.tab.id].frames).length <= 1) { if ( Object.keys(this.videoTabs[sender.tab.id].frames).length <= 1) {
@ -254,8 +278,10 @@ export default class UWServer {
} }
async getCurrentSite(sender: Runtime.MessageSender) { async getCurrentSite(sender: Runtime.MessageSender) {
this.logger.info('getCurrentSite', 'received get-current-site ...'); // if (!sender?.tab) {
// console.warn('invalid unregisterVideo received!');
// return;
// }
const site = await this.getVideoTab(); const site = await this.getVideoTab();
// Don't propagate 'INVALID SITE' to the popup. // Don't propagate 'INVALID SITE' to the popup.

View File

@ -9,6 +9,7 @@ import LegacyExtensionMode from '@src/common/enums/LegacyExtensionMode.enum';
import { PlayerDetectionMode } from '@src/common/enums/PlayerDetectionMode.enum'; import { PlayerDetectionMode } from '@src/common/enums/PlayerDetectionMode.enum';
import { SiteSupportLevel } from '@src/common/enums/SiteSupportLevel.enum'; import { SiteSupportLevel } from '@src/common/enums/SiteSupportLevel.enum';
import SettingsInterface from '@src/common/interfaces/SettingsInterface'; import SettingsInterface from '@src/common/interfaces/SettingsInterface';
import { _cp } from '@src/common/utils/_cp';
import { update } from 'lodash'; import { update } from 'lodash';
@ -478,6 +479,58 @@ const ExtensionConfPatch = Object.freeze([
} }
} }
} }
}, {
forVersion: '6.3.998',
updateFn: (userOptions: SettingsInterface, defaultOptions: SettingsInterface, logger?) => {
if (!userOptions.sites["www.amazon.com"]) {
userOptions.sites["www.amazon.com"] = _cp(defaultOptions.sites["www.amazon.com"] );
}
if (userOptions.commands?.zoom) {
const firstFixed = userOptions.commands?.zoom.findIndex(x => x.arguments.type === AspectRatioType.Fixed);
if (firstFixed === -1) {
userOptions.commands.zoom.push({
action: 'set-ar-zoom',
label: 'Cover',
comment: 'Covers the entire screen, cropping as much as needed',
arguments: {
type: AspectRatioType.Cover
},
shortcut: {
key: 'w',
code: 'KeyW',
ctrlKey: false,
metaKey: false,
altKey: false,
shiftKey: true,
onKeyUp: true,
onKeyDown: false,
}
});
} else {
userOptions.commands.zoom.splice(firstFixed, 0, {
action: 'set-ar-zoom',
label: 'Cover',
comment: 'Covers the entire screen, cropping as much as needed',
arguments: {
type: AspectRatioType.Cover
},
shortcut: {
key: 'w',
code: 'KeyW',
ctrlKey: false,
metaKey: false,
altKey: false,
shiftKey: true,
onKeyUp: true,
onKeyDown: false,
}
});
}
}
}
} }
]); ]);

View File

@ -7,6 +7,7 @@ import { PlayerDetectionMode } from '@src/common/enums/PlayerDetectionMode.enum'
import { SiteSupportLevel } from '@src/common/enums/SiteSupportLevel.enum'; import { SiteSupportLevel } from '@src/common/enums/SiteSupportLevel.enum';
import StretchType from '@src/common/enums/StretchType.enum'; import StretchType from '@src/common/enums/StretchType.enum';
import VideoAlignmentType from '@src/common/enums/VideoAlignmentType.enum'; import VideoAlignmentType from '@src/common/enums/VideoAlignmentType.enum';
import { MenuPosition } from '@src/common/interfaces/ClientUiMenu.interface';
import SettingsInterface from '@src/common/interfaces/SettingsInterface'; import SettingsInterface from '@src/common/interfaces/SettingsInterface';
import Debug from '@src/ext/conf/Debug'; import Debug from '@src/ext/conf/Debug';
import { AardPollingOptions } from '@src/ext/module/aard/enums/aard-polling-options.enum'; import { AardPollingOptions } from '@src/ext/module/aard/enums/aard-polling-options.enum';
@ -266,9 +267,9 @@ const ExtensionConf: SettingsInterface = {
activation: 'player', activation: 'player',
activationDistance: 100, activationDistance: 100,
activationDistanceUnits: '%', activationDistanceUnits: '%',
activatorAlignment: 'left', activatorAlignment: MenuPosition.Left,
activatorPadding: 10, activatorPadding: {x: 16, y: 16},
activatorPaddingUnit: '%', activatorPaddingUnit: {x: 'px', y: 'px'},
triggerZoneDimensions: { triggerZoneDimensions: {
width: 0.5, width: 0.5,
height: 0.5, height: 0.5,
@ -353,40 +354,6 @@ const ExtensionConf: SettingsInterface = {
onKeyUp: true, onKeyUp: true,
onKeyDown: false, onKeyDown: false,
} }
}, {
action: 'set-ar',
label: 'Fit width',
comment: 'Make the video fit the entire width of the player, crop top and bottom if necessary',
arguments: {
type: AspectRatioType.FitWidth
},
shortcut: {
key: 'w',
code: 'KeyW',
ctrlKey: false,
metaKey: false,
altKey: false,
shiftKey: false,
onKeyUp: true,
onKeyDown: false,
}
}, {
action: 'set-ar',
label: 'Fit height',
comment: 'Make the video fit the entire height of the player, crop left and right if necessary',
arguments: {
type: AspectRatioType.FitHeight
},
shortcut: {
key: 'e',
code: 'KeyE',
ctrlKey: false,
metaKey: false,
altKey: false,
shiftKey: false,
onKeyUp: true,
onKeyDown: false,
}
}, { }, {
action: 'set-ar', action: 'set-ar',
label: 'Cycle', label: 'Cycle',
@ -572,6 +539,23 @@ const ExtensionConf: SettingsInterface = {
onKeyUp: true, onKeyUp: true,
onKeyDown: false, onKeyDown: false,
} }
}, {
action: 'set-ar-zoom',
label: 'Cover',
comment: 'Covers the entire screen, cropping as much as needed',
arguments: {
type: AspectRatioType.Cover
},
shortcut: {
key: 'w',
code: 'KeyW',
ctrlKey: false,
metaKey: false,
altKey: false,
shiftKey: false,
onKeyUp: true,
onKeyDown: false,
}
}, { }, {
action: 'set-ar-zoom', action: 'set-ar-zoom',
label: 'Cycle', label: 'Cycle',
@ -982,6 +966,29 @@ const ExtensionConf: SettingsInterface = {
} }
} }
}, },
"www.amazon.com": {
enable: ExtensionMode.Default,
enableAard: ExtensionMode.Default,
enableKeyboard: InputHandlingMode.Enabled,
enableUI: ExtensionMode.Default,
applyToEmbeddedContent: EmbeddedContentSettingsOverridePolicy.Default,
overrideWhenEmbedded: EmbeddedContentSettingsOverridePolicy.Default,
type: SiteSupportLevel.CommunitySupport,
defaultType: SiteSupportLevel.CommunitySupport,
persistCSA: CropModePersistence.Default,
activeDOMConfig: "@community",
DOMConfig: {
"@community": {
type: SiteSupportLevel.CommunitySupport,
elements: {
player: {
detectionMode: PlayerDetectionMode.Auto,
allowAutoFallback: true
}
}
},
},
},
"www.twitch.tv": { "www.twitch.tv": {
enable: ExtensionMode.All, enable: ExtensionMode.All,
enableAard: ExtensionMode.All, enableAard: ExtensionMode.All,

View File

@ -14,6 +14,8 @@ export default class EventBus {
private comms?: CommsClient | CommsServer; private comms?: CommsClient | CommsServer;
private commsOrigin: CommsOrigin; private commsOrigin: CommsOrigin;
private lastExecutedCommandIds: string[] = new Array(32);
private lastExecutedCommandIndex: number = 0;
private disableTunnel: boolean = false; private disableTunnel: boolean = false;
private popupContext: any = {}; private popupContext: any = {};
@ -22,7 +24,7 @@ export default class EventBus {
// private uiUri = window.location.href; // private uiUri = window.location.href;
constructor(options?: {isUWServer?: boolean, name?: string, commsOrigin?: CommsOrigin}) { constructor(options?: {isUWServer?: boolean, name?: string, commsOrigin: CommsOrigin}) {
if (!options?.isUWServer) { if (!options?.isUWServer) {
this.setupIframeTunnelling(); this.setupIframeTunnelling();
} }
@ -88,10 +90,38 @@ export default class EventBus {
} }
} }
send(command: string, commandData: any, context: EventBusContext = {}) { private cloneContext(context: EventBusContext = {}): EventBusContext {
context.visitedBusses = [...context.visitedBusses ?? [], this.uuid]; return {
// execute commands we have subscriptions for stopPropagation: context.stopPropagation,
origin: context.origin,
frameUrl: context.frameUrl,
visitedBusses: context.visitedBusses ? [...context.visitedBusses] : undefined,
commandId: context.commandId,
comms: context.comms ? {
forwardTo: context.comms.forwardTo,
sourceFrame: context.comms?.sourceFrame ? { ...context.comms?.sourceFrame } : undefined
} : undefined,
borderCrossings: context.borderCrossings ? { ...context.borderCrossings } : undefined
};
}
send(command: string, commandData: any, context: EventBusContext = {}) {
// context = this.cloneContext(context); // Firefox throws an error if we don't clone the context.
if (context.visitedBusses?.includes(this.uuid)) {
console.warn('this bus was already visited before. Doing nothing.');
return;
}
if (context.commandId && this.lastExecutedCommandIds.includes(context.commandId)) {
console.warn('this command was already sent');
return;
}
// we want to avoid re-assigning context.visitedBusses if possible
// in order to reduce the amount of garbage that needs to be collected.
context.visitedBusses ? context.visitedBusses.push(this.uuid) : context.visitedBusses = [this.uuid];
// execute commands we have subscriptions for
if (this.commands?.[command]) { if (this.commands?.[command]) {
for (const eventBusCommand of this.commands[command]) { for (const eventBusCommand of this.commands[command]) {
eventBusCommand.function(commandData, context); eventBusCommand.function(commandData, context);
@ -102,17 +132,30 @@ export default class EventBus {
// CommsServer's job. EventBus does not have enough data for this decision. // CommsServer's job. EventBus does not have enough data for this decision.
// We do, however, have enough data to prevent backflow of messages that // We do, however, have enough data to prevent backflow of messages that
// crossed CommsServer once already. // crossed CommsServer once already.
if (!context.commandId) {
context.commandId = crypto.randomUUID();
}
if (context.commandId) {
const i = this.lastExecutedCommandIndex++ % this.lastExecutedCommandIds.length;
this.lastExecutedCommandIds[i] = context.commandId;
}
if (!context.origin) {
context.origin = this.commsOrigin;
}
if ( if (
this.comms this.comms
&& context?.origin !== CommsOrigin.Server && ( // ensure each message only enters commsServer once!
&& !context?.borderCrossings?.commsServer this.commsOrigin === context.origin // if these two differ, we already sent that message through Comms once,
|| this.commsOrigin === CommsOrigin.Server // CommsServer needs to forward everything, otherwise messages stop on
)
) { ) {
try { try {
this.comms.sendMessage({command, config: commandData, context}, context); this.comms.sendMessage({command, config: commandData, context}, context);
} catch (e) { } catch (e) {
if (command !== 'reload-required') { if (command !== 'reload-required') {
// We shouldn't let reload-required command to trigger new reload-required commands. // We shouldn't let reload-required command to trigger new reload-required commands.
this.send('reload-required', {}, {visitedBusses: [this.uuid]}); this.send('reload-required', {});
} }
} }
}; };
@ -126,12 +169,21 @@ export default class EventBus {
...context, ...context,
borderCrossings: { borderCrossings: {
...context?.borderCrossings, ...context?.borderCrossings,
// iframe: true // we actually no longer check this prop, we should instead rely on visitedBusses
} }
} }
); );
} }
this.sendToTunnel(command, commandData, context);
// send to parent iframe
if (!this.disableTunnel && typeof window !== 'undefined') {
window.parent.postMessage(
{
action: 'uw-bus-tunnel',
payload: {command, config: commandData, context} as EventBusMessage
},
'*'
);
}
if (context?.stopPropagation) { if (context?.stopPropagation) {
return; return;
@ -139,52 +191,6 @@ export default class EventBus {
} }
//#endregion //#endregion
/**
* Send, but intended for sending commands from iframe to content scripts
* @param command
* @param config
*/
sendToTunnel(command: string, config: any, context: EventBusContext = {}) {
if (!context.visitedBusses) {
console.error('Visited busses is missing from contextn. This is illegal.');
return;
}
context.visitedBusses = [...context.visitedBusses ?? [], this.uuid];
if (!this.disableTunnel && typeof window !== 'undefined') {
window.parent.postMessage(
{
action: 'uw-bus-tunnel',
payload: {command, config, context} as EventBusMessage
},
'*'
);
} else {
// because iframe UI components get reused in the popup, we
// also need to set up a detour because the tunnel is closed
// in the popup
if (this.comms) {
try {
this.comms.sendMessage(
{
command,
config,
context: {
...this.popupContext,
...context
}
},
this.popupContext
);
} catch (e) {
if (command !== 'reload-required') {
this.send('reload-required', {}, context);
}
}
}
}
}
//#region iframe tunnelling //#region iframe tunnelling
private setupIframeTunnelling() { private setupIframeTunnelling() {
// forward messages coming from iframe tunnels // forward messages coming from iframe tunnels
@ -205,8 +211,8 @@ export default class EventBus {
const payload = event.data.payload as EventBusMessage; const payload = event.data.payload as EventBusMessage;
console.info(this.name, 'received message from iframe. command:', payload);
if (!payload.context?.visitedBusses) { if (!payload.context?.visitedBusses) {
// this should never be visible to a real user
console.warn('Received iframe message without context. Doing nothing in order to avoid infinite loop. Event:', event); console.warn('Received iframe message without context. Doing nothing in order to avoid infinite loop. Event:', event);
return; return;
} }

View File

@ -197,7 +197,6 @@ class Logger {
storageChangeListener(changes: any, area: any) { storageChangeListener(changes: any, area: any) {
if (!changes.uwLogger) { if (!changes.uwLogger) {
console.info('We dont have any logging settings, not processing frther');
return; return;
} }

View File

@ -530,16 +530,26 @@ export class Aard {
// console.warn('DETECTED NOT LETTERBOX! (resetting)') // console.warn('DETECTED NOT LETTERBOX! (resetting)')
this.timer.arChanged(); this.timer.arChanged();
this.updateAspectRatio(this.defaultAr, {forceReset: true}); this.updateAspectRatio(this.defaultAr, {forceReset: true});
this.testResults.activeLetterbox.width = 0;
this.testResults.activeLetterbox.offset = 0;
this.testResults.activeLetterbox.orientation = LetterboxOrientation.NotLetterbox;
break processUpdate; break processUpdate;
} }
if (this.testResults.subtitleDetected && arConf.subtitles.subtitleCropMode !== AardSubtitleCropMode.CropSubtitles) { if (this.testResults.subtitleDetected && arConf.subtitles.subtitleCropMode !== AardSubtitleCropMode.CropSubtitles) {
if (arConf.subtitles.subtitleCropMode === AardSubtitleCropMode.ResetAR) { if (arConf.subtitles.subtitleCropMode === AardSubtitleCropMode.ResetAR) {
this.updateAspectRatio(this.defaultAr, {forceReset: true}); this.updateAspectRatio(this.defaultAr, {forceReset: true});
this.testResults.activeLetterbox.width = 0;
this.testResults.activeLetterbox.offset = 0;
this.testResults.activeLetterbox.orientation = LetterboxOrientation.NotLetterbox;
this.timers.pauseUntil = Date.now() + arConf.subtitles.resumeAfter; this.timers.pauseUntil = Date.now() + arConf.subtitles.resumeAfter;
} else if (arConf.subtitles.subtitleCropMode === AardSubtitleCropMode.ResetAndDisable) { } else if (arConf.subtitles.subtitleCropMode === AardSubtitleCropMode.ResetAndDisable) {
this.updateAspectRatio(this.defaultAr, {forceReset: true}); this.updateAspectRatio(this.defaultAr, {forceReset: true});
this.testResults.activeLetterbox.width = 0;
this.testResults.activeLetterbox.offset = 0;
this.testResults.activeLetterbox.orientation = LetterboxOrientation.NotLetterbox;
this.status.autoDisabled = true; this.status.autoDisabled = true;
} }
@ -568,10 +578,13 @@ export class Aard {
this.testResults.guardLine.front = this.testResults.aspectRatioCheck.frontCandidate; this.testResults.guardLine.front = this.testResults.aspectRatioCheck.frontCandidate;
this.testResults.guardLine.back = this.testResults.aspectRatioCheck.backCandidate; this.testResults.guardLine.back = this.testResults.aspectRatioCheck.backCandidate;
// TODO: set flag if subtitles are far enough from edge to avoid getting cropped
const finalAr = this.getAr(); const finalAr = this.getAr();
if (finalAr > 0) { if (finalAr > 0) {
this.updateAspectRatio(finalAr); this.updateAspectRatio(finalAr);
this.testResults.activeLetterbox.width = this.testResults.letterboxSize;
this.testResults.activeLetterbox.offset = this.testResults.letterboxOffset;
this.testResults.activeLetterbox.orientation = this.testResults.letterboxOrientation;
} else { } else {
this.testResults.aspectRatioInvalid = true; this.testResults.aspectRatioInvalid = true;
this.testResults.aspectRatioInvalidReason = finalAr.toFixed(3); this.testResults.aspectRatioInvalidReason = finalAr.toFixed(3);
@ -843,6 +856,15 @@ export class Aard {
* @returns * @returns
*/ */
private updateLetterboxEdgeCandidates(crossDimension: number, topCandidate: number, bottomCandidate: number) { private updateLetterboxEdgeCandidates(crossDimension: number, topCandidate: number, bottomCandidate: number) {
// if new topCandidate or bottomCandidate aren't valid,
// use existing value.
if (topCandidate < 0) {
topCandidate = this.testResults.aspectRatioCheck.frontCandidate;
}
if (bottomCandidate < 0) {
bottomCandidate = this.testResults.aspectRatioCheck.backCandidate;
}
const bottomDistance = (crossDimension - bottomCandidate); const bottomDistance = (crossDimension - bottomCandidate);
const maxOffset = ~~(crossDimension * this.settings.active.aard.edgeDetection.maxLetterboxOffset); const maxOffset = ~~(crossDimension * this.settings.active.aard.edgeDetection.maxLetterboxOffset);
const diff = Math.abs(topCandidate - bottomDistance); const diff = Math.abs(topCandidate - bottomDistance);
@ -858,6 +880,13 @@ export class Aard {
this.testResults.letterboxSize = candidateAvg; this.testResults.letterboxSize = candidateAvg;
this.testResults.letterboxOffset = diff; this.testResults.letterboxOffset = diff;
if (this.testResults.letterboxOrientation === LetterboxOrientation.Letterbox && this.testResults.subtitleDetected) {
const top = this.testResults.subtitleScan.regions.top.firstSubtitle === -1 ? topCandidate : Math.min(topCandidate, this.testResults.subtitleScan.regions.top.firstSubtitle);
const bottom = Math.max(bottomCandidate, this.testResults.subtitleScan.regions.bottom.firstSubtitle);
this.testResults.letterboxSizeWithSubtitles = ~~((top + (crossDimension - bottomCandidate)) * 0.5);
}
} }
@ -899,16 +928,36 @@ export class Aard {
} else { } else {
this.testResults.aspectRatioUncertain = false; this.testResults.aspectRatioUncertain = false;
} }
if (ssrRegions.top.firstSubtitle !== -1 || ssrRegions.bottom.firstSubtitle !== -1) {
this.testResults.subtitleDetected = true;
}
// 1. updateLetterboxEdgeCandidates runs regardless of whether we detected subtitles or not.
// 2. it's also not affected by whether subtitleDetected is set
// 3. we actually need to only reset letterbox if subtitle is actually outside of the crop area
this.updateLetterboxEdgeCandidates( this.updateLetterboxEdgeCandidates(
height, height,
ssrRegions.top.firstImage, ssrRegions.top.firstImage,
ssrRegions.bottom.firstImage ssrRegions.bottom.firstImage
); );
// we can only do this in actual letterbox
if (this.testResults.activeLetterbox.orientation === LetterboxOrientation.Letterbox) {
// we only reset letterbox if letters are outside the video area, otherwise we risk
// getting whacked by credits, ppt youtubers, and other shit like that
const borderTop = this.testResults.activeLetterbox.width;
const borderBottom = this.settings.active.aard.canvasDimensions.sampleCanvas.height - this.testResults.activeLetterbox.width;
if (
(ssrRegions.top.firstSubtitle !== -1 && ssrRegions.top.firstSubtitle < borderTop)
|| (ssrRegions.bottom.firstSubtitle !== -1 && ssrRegions.bottom.firstSubtitle > borderBottom)
) {
this.testResults.subtitleDetected = true;
}
}
this.timer.current.subtitleScan = performance.now() - this.timer.current.start; this.timer.current.subtitleScan = performance.now() - this.timer.current.start;
} }
@ -1370,9 +1419,17 @@ export class Aard {
if (this.testResults.letterboxOrientation === LetterboxOrientation.Pillarbox) { if (this.testResults.letterboxOrientation === LetterboxOrientation.Pillarbox) {
const compensationFactor = compensatedWidth / this.canvasStore.main.width; const compensationFactor = compensatedWidth / this.canvasStore.main.width;
const pillarboxCompensated = (this.testResults.letterboxSize * 2 * compensationFactor); const pillarboxCompensated = (this.testResults.letterboxSize * 2 * compensationFactor);
return (compensatedWidth - pillarboxCompensated) / this.canvasStore.main.height; return (compensatedWidth - pillarboxCompensated) / this.canvasStore.main.height;
} else { } else {
const heightWithoutLetterbox = this.canvasStore.main.height - (this.testResults.letterboxSize * 2); const heightWithoutLetterbox = this.canvasStore.main.height - (this.testResults.letterboxSize * 2);
// TODO: set flag if subtitles are far enough from edge to avoid getting cropped
// if (this.testResults.subtitleDetected) {
// const hwlWithSubtitles = this.canvasStore.main.height - (this.testResults.letterboxSizeWithSubtitles * 2)
// const subtitleRatio = compensatedWidth / hwlWithSubtitles;
// }
return compensatedWidth / heightWithoutLetterbox; return compensatedWidth / heightWithoutLetterbox;
} }
} }

View File

@ -364,8 +364,15 @@ export class AardDebugUi {
Active: ${ar}, changed since last check? ${testResults.aspectRatioUpdated} letterbox width: ${testResults.letterboxWidth} offset ${testResults.letterboxOffset}<br/> Active: ${ar}, changed since last check? ${testResults.aspectRatioUpdated} letterbox width: ${testResults.letterboxWidth} offset ${testResults.letterboxOffset}<br/>
<sup>(last: ${this._lastAr})</sup> <sup>(last: ${this._lastAr})</sup>
Paused until? ${Date.now() < timers.pauseUntil ? ((timers.pauseUntil - Date.now()) / 1000) + 's' : 'not paused'}; ${
<sup>now: ${Date.now()}ms; until:${timers.pauseUntil}ms; diff: ${(+timers.pauseUntil - Date.now())} </sup> timers ?
`
Paused until? ${Date.now() < timers?.pauseUntil ? ((timers?.pauseUntil - Date.now()) / 1000) + 's' : 'not paused'};
<sup>now: ${Date.now()}ms; until:${timers?.pauseUntil}ms; diff: ${(+timers?.pauseUntil - Date.now())} </sup>
` :
`- timers are missing -`
}
image in black level probe (aka "not letterbox"): ${testResults.notLetterbox} image in black level probe (aka "not letterbox"): ${testResults.notLetterbox}
`; `;

View File

@ -33,6 +33,7 @@ export interface AardTestResults {
activeAspectRatio: number, // is cumulative activeAspectRatio: number, // is cumulative
letterboxSize: number, letterboxSize: number,
letterboxOffset: number, letterboxOffset: number,
letterboxSizeWithSubtitles: number,
aspectRatioInvalid: boolean, aspectRatioInvalid: boolean,
subtitleScan: { subtitleScan: {
top: number, top: number,
@ -43,6 +44,11 @@ export interface AardTestResults {
bottom: AardTestResult_SubtitleRegion bottom: AardTestResult_SubtitleRegion
} }
}, },
activeLetterbox: {
width: number,
offset: number,
orientation: LetterboxOrientation
}
aspectRatioUncertainReason?: AardUncertainReason, aspectRatioUncertainReason?: AardUncertainReason,
aspectRatioInvalidReason?: string, aspectRatioInvalidReason?: string,
} }
@ -90,10 +96,16 @@ export function initAardTestResults(settings: AardSettings): AardTestResults {
} }
} }
}, },
activeLetterbox: {
width: 0,
offset: 0,
orientation: LetterboxOrientation.NotLetterbox
},
aspectRatioUpdated: false, aspectRatioUpdated: false,
activeAspectRatio: 0, activeAspectRatio: 0,
letterboxSize: 0, letterboxSize: 0,
letterboxOffset: 0, letterboxOffset: 0,
letterboxSizeWithSubtitles: 0,
aspectRatioInvalid: false, aspectRatioInvalid: false,
} }
} }
@ -107,7 +119,7 @@ export function resetAardTestResults(results: AardTestResults): void {
results.isFinished = false; results.isFinished = false;
results.lastStage = 0; results.lastStage = 0;
results.aspectRatioUpdated = false; results.aspectRatioUpdated = false;
results.aspectRatioUncertainReason = null; results.aspectRatioUncertainReason = undefined;
results.aspectRatioInvalid = false; results.aspectRatioInvalid = false;
results.letterboxOrientation = LetterboxOrientation.NotKnown; results.letterboxOrientation = LetterboxOrientation.NotKnown;
} }

View File

@ -149,7 +149,11 @@ class CommsClient {
// send to server // send to server
if (!context?.borderCrossings?.commsServer) { if (!context?.borderCrossings?.commsServer) {
try {
return chrome?.runtime?.sendMessage(null, message, null); return chrome?.runtime?.sendMessage(null, message, null);
} catch (e) {
console.warn(`Failed to send message to background script. Error:`, e, 'data:', {message, context});
}
} }
} }

View File

@ -172,35 +172,47 @@ class CommsServer {
context = message.context; context = message.context;
} }
/**
* Here's how message forwarding works:
* * messages NOT originating from a content script get forwarded to content script
* * messages NOT originating from extension popup get forwarded to extension popup
*
* This way, messages originating from background script get forwarded both to
* content script as well as popup for absolutely free.
*/
forwardToContentScript:
{
if (context?.origin !== CommsOrigin.ContentScript) { if (context?.origin !== CommsOrigin.ContentScript) {
if (context?.comms.forwardTo === 'all') { if (context?.comms.forwardTo === 'all') {
return this.sendToAll(message); this.sendToAll(message);
break forwardToContentScript;
} }
if (context?.comms.forwardTo === 'active') { if (context?.comms.forwardTo === 'active') {
return this.sendToActive(message); this.sendToActive(message);
break forwardToContentScript;
} }
if (context?.comms.forwardTo === 'contentScript') { if (context?.comms.forwardTo === 'contentScript') {
return this.sendToFrame(message, context.tab, context.frame, context.port); this.sendToFrame(message, context.tab, context.frame, context.port);
break forwardToContentScript;
} }
}
if (context?.origin !== CommsOrigin.Popup) { this.sendToActive(message);
if (context?.comms.forwardTo === 'popup') { break forwardToContentScript;
return this.sendToPopup(message);
} }
} }
if (context?.origin !== CommsOrigin.Popup) {
this.sendToPopup(message);
}
// okay I lied! Messages originating from content script can be forwarded to // okay I lied! Messages originating from content script can be forwarded to
// content scripts running in _other_ frames of the tab // content scripts running in _other_ frames of the tab.
let forwarded = false;
if (context?.origin === CommsOrigin.ContentScript) { if (context?.origin === CommsOrigin.ContentScript) {
if (context?.comms.forwardTo === 'all-frames') { if (context?.comms.forwardTo === 'all-frames') {
forwarded = true;
this.sendToOtherFrames(message, context); this.sendToOtherFrames(message, context);
} }
} }
if (!forwarded) {
this.logger.warn('sendMessage', `message ${message.command ?? ''} was not forwarded to any destination!`, {message, context});
}
} }
/** /**
@ -254,11 +266,11 @@ class CommsServer {
* @param frame * @param frame
*/ */
private async sendToOtherFrames(message, context) { private async sendToOtherFrames(message, context) {
const sender = context.comms.sourceFrame; const sender = context.comms?.sourceFrame;
const enrichedMessage = { const enrichedMessage = {
message, message,
_sourceFrame: context.comms.sourceFrame, _sourceFrame: context.comms?.sourceFrame,
_sourcePort: context.comms.port _sourcePort: context.comms.port
} }

View File

@ -451,6 +451,18 @@ export class SiteSettings {
return this.site; return this.site;
} }
/**
* Ensures current site has its own separate copy of settings.
*/
async ensureSettings() {
if (!this.settings.active.sites[this._site]) {
this.settings.active.sites[this._site] = _cp(this.data);
this.settings.active.sites[this._site].type = SiteSupportLevel.UserDefined;
await this.settings.saveWithoutReload();
this.raw = this.settings.active.sites[this._site];
}
}
/** /**
* Sets option value. * Sets option value.
* @param optionPath path to value in object notation (dot separated) * @param optionPath path to value in object notation (dot separated)

View File

@ -1,5 +1,11 @@
import AspectRatioType from '@src/common/enums/AspectRatioType.enum';
import StretchType from '@src/common/enums/StretchType.enum';
import VideoAlignmentType from '@src/common/enums/VideoAlignmentType.enum';
import { ArVariant } from '@src/common/interfaces/ArInterface';
import { MenuPosition, MenuConfig, MenuItemConfig } from '@src/common/interfaces/ClientUiMenu.interface'; import { MenuPosition, MenuConfig, MenuItemConfig } from '@src/common/interfaces/ClientUiMenu.interface';
import { ScalingParamsBroadcast } from '@src/common/interfaces/ScalingParamsBroadcast.interface';
import extensionCss from '@src/main.css?inline'; import extensionCss from '@src/main.css?inline';
import { setVideoAlignmentIndicatorState } from '@src/ui/utils/video-alignment-indicator-handling';
export class ClientMenu { export class ClientMenu {
@ -19,6 +25,7 @@ export class ClientMenu {
private isHovered = false; private isHovered = false;
private forceShow = false;
private isWithinActivation = false; private isWithinActivation = false;
private lastMouseMove = performance.now(); private lastMouseMove = performance.now();
private idleTimer?: number; private idleTimer?: number;
@ -29,7 +36,11 @@ export class ClientMenu {
private onDocumentMouseLeave?: () => void; private onDocumentMouseLeave?: () => void;
private idleIntervalId?: number; private idleIntervalId?: number;
constructor(private config: MenuConfig) {} constructor(private config: MenuConfig) {
if (config.options?.forceShow !== undefined) {
this.forceShow = config.options.forceShow;
}
}
mount(anchorElement: HTMLElement) { mount(anchorElement: HTMLElement) {
this.buildMenuPositionClassList(); this.buildMenuPositionClassList();
@ -173,7 +184,8 @@ export class ClientMenu {
*/ */
private buildMenuPositionClassList() { private buildMenuPositionClassList() {
let classList; let classList;
switch (this.config.menuPosition) { console.log('BUILDING MENU POS:', MenuPosition[this.config.ui.activatorAlignment]);
switch (this.config.ui.activatorAlignment) {
case MenuPosition.TopLeft: case MenuPosition.TopLeft:
classList = ['uw-menu-left','uw-menu-top']; classList = ['uw-menu-left','uw-menu-top'];
break; break;
@ -209,10 +221,14 @@ export class ClientMenu {
private createMenu() { private createMenu() {
this.root = document.createElement('div'); this.root = document.createElement('div');
this.root.className = 'uw-menu-root uw-hidden'; this.root.className = 'uw-menu-root uw-hidden';
this.root.setAttribute(
'style',
`padding: ${this.config.ui.activatorPadding.y ?? 16}${this.config.ui.activatorPaddingUnit.y ?? 'px'} ${this.config.ui.activatorPadding.x ?? 16}${this.config.ui.activatorPaddingUnit.x ?? 'px'}`
);
const trigger = document.createElement('div'); const trigger = document.createElement('div');
trigger.classList = 'uw-menu-trigger uw-trigger'; trigger.classList = 'uw-menu-trigger uw-trigger';
trigger.style = `margin: ${this.config.ui.activatorPadding ?? 10} ${this.config.ui.activatorPaddingUnit ?? '%'}`;
trigger.textContent = 'Ultrawidify'; trigger.textContent = 'Ultrawidify';
this.trigger = trigger; this.trigger = trigger;
@ -272,6 +288,7 @@ export class ClientMenu {
} }
if (item.subitems) { if (item.subitems) {
el.classList.add('uw-has-submenu');
el.appendChild(this.buildSubmenu(item.subitems)); el.appendChild(this.buildSubmenu(item.subitems));
} }
@ -287,11 +304,13 @@ export class ClientMenu {
} }
private bindGlobalMouse(anchorEl: HTMLElement) { private bindGlobalMouse(anchorEl: HTMLElement) {
// const forceRecheckAfter = 5000;
// let lastRecalculation = performance.now();
const playerRect = anchorEl.getBoundingClientRect(); const playerRect = anchorEl.getBoundingClientRect();
let menuActivatorRect, cx, cy; let menuActivatorRect, cx, cy;
let activationRadius = this.getActivationRadius(anchorEl);
const activationRadius = this.getActivationRadius(anchorEl);
const recalculateActivator = () => { const recalculateActivator = () => {
menuActivatorRect = this.trigger.getBoundingClientRect(); menuActivatorRect = this.trigger.getBoundingClientRect();
@ -302,13 +321,20 @@ export class ClientMenu {
recalculateActivator(); recalculateActivator();
this.onDocumentMouseMove = (e: MouseEvent) => { this.onDocumentMouseMove = (e: MouseEvent) => {
this.lastMouseMove = performance.now(); const now = performance.now();
this.lastMouseMove = now;
if (activationRadius != null) { // activateWithCtrl is independent from other options.
if (! menuActivatorRect.width) { // Depending on user settings, UI can be triggered even if we aren't holding CTRL
recalculateActivator(); if (this.config.ui.activateWithCtrl) {
this.isWithinActivation = e.ctrlKey;
} }
if (!this.isWithinActivation && this.config.ui.activation !== 'none') {
if (activationRadius) {
if (! menuActivatorRect.width || !menuActivatorRect.height) {
recalculateActivator();
}
const d = Math.hypot(e.clientX - cx, e.clientY - cy); const d = Math.hypot(e.clientX - cx, e.clientY - cy);
this.isWithinActivation = d <= activationRadius; this.isWithinActivation = d <= activationRadius;
@ -317,8 +343,8 @@ export class ClientMenu {
e.clientX >= playerRect.left && e.clientX >= playerRect.left &&
e.clientX <= playerRect.right && e.clientX <= playerRect.right &&
e.clientY >= playerRect.top && e.clientY >= playerRect.top &&
e.clientY <= playerRect.bottom && e.clientY <= playerRect.bottom;
(this.config.ui.activation !== 'player-ctrl' || e.ctrlKey); }
} }
this.updateVisibility(); this.updateVisibility();
@ -364,7 +390,7 @@ export class ClientMenu {
} }
} }
private show() { show(options?: {forceShow?: boolean}) {
this.lastMouseMove = performance.now(); this.lastMouseMove = performance.now();
if (!this.visible) { if (!this.visible) {
@ -372,13 +398,152 @@ export class ClientMenu {
this.root.classList.add('uw-visible'); this.root.classList.add('uw-visible');
this.root.classList.remove('uw-hidden'); this.root.classList.remove('uw-hidden');
} }
if (options?.forceShow !== undefined) {
if (!this.forceShow && options.forceShow) {
this.root.classList.add('uw-force-visible');
}
if (this.forceShow && !options.forceShow) {
this.root.classList.remove('uw-force-visible');
}
this.forceShow = options.forceShow;
}
}
hide(options?: {forceShow?: boolean}) {
if (options?.forceShow !== undefined) {
if (this.forceShow !== options.forceShow) {
this.root.classList.remove('uw-force-visible');
this.forceShow = options.forceShow;
}
} }
private hide() {
if (this.visible) { if (this.visible) {
this.visible = false; this.visible = false;
this.root.classList.remove('uw-visible'); this.root.classList.remove('uw-visible');
this.root.classList.add('uw-hidden'); this.root.classList.add('uw-hidden');
} }
} }
markActiveElements(scalingParams: ScalingParamsBroadcast) {
if (!this._root) {
return;
}
const currentlyActiveElements = this._root.querySelectorAll('.uw-active-within, .uw-active');
for (const element of currentlyActiveElements) {
element.classList.remove('uw-active-within');
element.classList.remove('uw-active');
}
// we only set active options when manual zoom is NOT set
if (!scalingParams.manualZoom) {
const cropMenuGroup = scalingParams.lastAr.variant === ArVariant.Zoom ? 'zoom' : 'crop';
const cropCommand = scalingParams.lastAr.variant === ArVariant.Zoom ? 'set-ar-zoom' : 'set-ar';
let typeCrop;
switch (scalingParams.lastAr.type) {
case AspectRatioType.Automatic:
case AspectRatioType.AutomaticUpdate:
typeCrop = `${AspectRatioType.Automatic}-x`;
break;
case AspectRatioType.Cover:
case AspectRatioType.FitWidth:
case AspectRatioType.FitHeight:
typeCrop = `${scalingParams.lastAr.type}-x`;
break;
case AspectRatioType.Cycle:
case AspectRatioType.Initial:
typeCrop = 'non-selectable';
break;
default:
typeCrop = `${scalingParams.lastAr.type}-${scalingParams.lastAr.ratio ?? 'x'}`;
}
const activeGroupId = `#uw-${cropMenuGroup}`.replaceAll('.', '_');
const activeCropCommandId = `#uw-${cropCommand}-${typeCrop}`.replaceAll('.', '_');
const groupEl = this._root.querySelector(activeGroupId);
const optionEl = groupEl?.querySelector(activeCropCommandId);
/**
* When manual zoom is in effect, optionEl (probably) won't match any of the options.
* Therefore, optionEl will be undefined. We can use this fact to sus out whether
* we're _really_ using a zoom preset, or did the user went for the sliders n stuff.
*/
if (optionEl) {
groupEl.classList.add('uw-active-within');
optionEl.classList.add('uw-active');
}
let typeStretch;
switch (scalingParams.stretch.type) {
case StretchType.FixedSource:
case StretchType.Fixed:
typeStretch = `${scalingParams.stretch.type}-${scalingParams.stretch.ratio ?? 'x'}`.replaceAll('.', '_');
break;
case StretchType.Default:
case StretchType.NoStretch:
typeStretch = `non-selectable`;
break;
default:
typeStretch = `${scalingParams.stretch.type}-x`;
}
const stretchGroupEl = this._root.querySelector('#uw-stretch');
const stretchOptionEl = stretchGroupEl?.querySelector(`#uw-set-stretch-${typeStretch}`);
if (stretchOptionEl) {
stretchGroupEl.classList.add('uw-active-within');
stretchOptionEl.classList.add('uw-active');
}
}
const zoomWLabel = this._root.querySelector('#zoomWidth');
const zoomWSlider = this._root.querySelector('#_input_zoom_slider') as undefined | null | (HTMLInputElement & {isInteracting?: boolean});
const zoomHLabel = this._root.querySelector('#zoomHeight');
const zoomHSlider = this._root.querySelector('#_input_zoom_slider_2') as undefined | null | (HTMLInputElement & {isInteracting?: boolean});
if (zoomWSlider?.isInteracting || zoomHSlider?.isInteracting) {
// do nothing
} else {
if (zoomWLabel) {
zoomWLabel.textContent = `${(scalingParams.effectiveZoom.x * 100).toFixed()}%`;
}
if (zoomHLabel) {
zoomHLabel.textContent = `${(scalingParams.effectiveZoom.y * 100).toFixed()}%`;
}
if (zoomWSlider) {
zoomWSlider.value = Math.log2(scalingParams.effectiveZoom.x) as any;
}
if (zoomHSlider) {
zoomHSlider.value = Math.log2(scalingParams.effectiveZoom.y) as any;
}
}
const lockXYButton = this._root.querySelector('#_button_toggle_aspect_lock');
const sliderLockBar = this._root.querySelector('#slider-lock');
if (scalingParams.effectiveZoom.x === scalingParams.effectiveZoom.y) {
lockXYButton?.classList.add('uw-linked');
lockXYButton?.classList.remove('uw-unlinked');
sliderLockBar?.classList.add('uw-linked');
sliderLockBar?.classList.remove('uw-unlinked');
} else {
lockXYButton?.classList.add('uw-unlinked');
lockXYButton?.classList.remove('uw-linked');
sliderLockBar?.classList.add('uw-unlinked');
sliderLockBar?.classList.remove('uw-linked');
}
// set alignment indicator
const videoAlignmentIndicator = this._root.querySelector('#_uw_ui_alignment_indicator') as SVGSVGElement;
if (videoAlignmentIndicator) {
setVideoAlignmentIndicatorState(videoAlignmentIndicator, scalingParams.videoAlignment.x, scalingParams.videoAlignment.y);
}
}
} }

View File

@ -19,6 +19,9 @@ import { UwuiWindow } from './UwuiWindow';
import { createApp } from 'vue'; import { createApp } from 'vue';
import SettingsWindowContent from '@components/SettingsWindowContent.vue'; import SettingsWindowContent from '@components/SettingsWindowContent.vue';
import { Ar } from '@src/common/interfaces/ArInterface';
import { Stretch } from '@src/common/interfaces/StretchInterface';
import { ScalingParamsBroadcast } from '@src/common/interfaces/ScalingParamsBroadcast.interface';
// import jsonEditorCSS from 'vanilla-jsoneditor/themes/jse-theme-dark.css?inline' // import jsonEditorCSS from 'vanilla-jsoneditor/themes/jse-theme-dark.css?inline'
if (process.env.CHANNEL !== 'stable'){ if (process.env.CHANNEL !== 'stable'){
@ -39,6 +42,11 @@ class UI {
private extensionMenu: ClientMenu; private extensionMenu: ClientMenu;
private logger: ComponentLogger; private logger: ComponentLogger;
private forwardedCommandIds: string[] = new Array(64);
private lastForwardedCommandIndex = 0;
private currentScalingParams?: ScalingParamsBroadcast;
private uiState = { private uiState = {
lockXY: true, lockXY: true,
zoom: { // log2 scale — 100% is 0 zoom: { // log2 scale — 100% is 0
@ -68,6 +76,38 @@ class UI {
// UI will be initialized when setUiVisibility is called // UI will be initialized when setUiVisibility is called
if (!this.isGlobal) { if (!this.isGlobal) {
this.init(); this.init();
this.eventBus.subscribeMulti({
'reload-menu': {
function: () => {
this.createExtensionMenu({forceShow: true});
this.extensionMenu.show({forceShow: true});
}
},
'force-menu-activator-state': {
function: (commandData) => {
if (commandData.visibility) {
this.extensionMenu.show({forceShow: true});
} else {
this.extensionMenu.show({forceShow: false});
}
}
},
'uw-show-settings-window': {
function: (commandData, context) => {
this.createSettingsWindow(commandData?.initialState);
}
},
'broadcast-scaling-params': {
function: (commandData: ScalingParamsBroadcast, context) => {
this.currentScalingParams = commandData;
this.updateMenuStatus(commandData);
}
}
});
} }
} }
@ -93,17 +133,42 @@ class UI {
this.initMessaging(); this.initMessaging();
} }
private messageListener?: (event?: MessageEvent) => void;
private initMessaging() { private initMessaging() {
window.addEventListener('message', (event: MessageEvent) => { if (this.messageListener) {
this.destroyMessaging();
} else {
this.messageListener = (event: MessageEvent) => {
const data = event.data; const data = event.data;
if (data?.action !== 'uw-bus-tunnel') { if (data?.action !== 'uw-bus-tunnel') {
return; return;
} }
const payload = data.payload; const payload = data.payload;
console.log('forwarding from tunnel to event bus. payload', payload); /**
* it appears that forwarded commands can be multiplying to ridiculous degree,
* but i didn't find anything that would obviously cause the message forwarding storm
* this means we'll try to avoid that via brute force.
*
* How bad is it?
* can get as bad as 100k messages a minute (!)
*/
if (!payload.context?.commandId) {
// user should never see this log
console.warn('Command context does not contain commandId. This is illegal. Message will not be forwarded.', {payload});
return;
}
if (this.forwardedCommandIds.includes(payload.context.commandId)) {
console.warn('this command was already forwarded, doing nothing:', {payload});
return;
}
const i = this.lastForwardedCommandIndex++ % this.forwardedCommandIds.length;
this.forwardedCommandIds[i] = payload.id;
// Forward to all iframes except the source // Forward to all iframes except the source
(UwuiWindow as any).instances?.forEach(win => { (UwuiWindow as any).instances?.forEach(win => {
@ -118,7 +183,16 @@ class UI {
); );
} }
}); });
}); };
}
window.addEventListener('message', this.messageListener);
}
private destroyMessaging() {
if (this.messageListener) {
window.removeEventListener('message', this.messageListener);
}
} }
executeCommand(x: CommandInterface) { executeCommand(x: CommandInterface) {
@ -164,7 +238,7 @@ class UI {
} }
} }
createExtensionMenu() { createExtensionMenu(options?: {forceShow?: boolean}) {
if (+this.siteSettings?.data.enableUI === ExtensionMode.Disabled || this.isGlobal) { if (+this.siteSettings?.data.enableUI === ExtensionMode.Disabled || this.isGlobal) {
return; // don't return; // don't
} }
@ -185,6 +259,7 @@ class UI {
const menuConfig = { const menuConfig = {
isGlobal: this.isGlobal, isGlobal: this.isGlobal,
ui: this.settings.active.ui.inPlayer, ui: this.settings.active.ui.inPlayer,
options,
items: [ items: [
{ {
customClassList: 'uw-site-info', customClassList: 'uw-site-info',
@ -223,28 +298,37 @@ class UI {
}, },
{ {
label: 'Crop', label: 'Crop',
customId: 'uw-crop',
subitems: this.settings.active.commands.crop.map((x: CommandInterface) => { subitems: this.settings.active.commands.crop.map((x: CommandInterface) => {
return { return {
label: x.label, label: x.label,
command: x,
customId: `uw-${x.action}-${x.arguments.type}-${x.arguments.ratio ?? 'x'}`.replaceAll('.', '_'),
action: () => this.executeCommand(x) action: () => this.executeCommand(x)
} }
}) })
}, },
{ {
label: 'Stretch', label: 'Stretch',
customId: 'uw-stretch',
subitems: this.settings.active.commands.stretch.map((x: CommandInterface) => { subitems: this.settings.active.commands.stretch.map((x: CommandInterface) => {
return { return {
label: x.label, label: x.label,
command: x,
customId: `uw-${x.action}-${x.arguments.type}-${x.arguments.ratio ?? 'x'}`.replaceAll('.', '_'),
action: () => this.executeCommand(x) action: () => this.executeCommand(x)
} }
}) })
}, },
{ {
label: 'Zoom (presets)', label: 'Zoom (presets)',
customId: 'uw-zoom',
subitems: [ subitems: [
... this.settings.active.commands.zoom.map((x: CommandInterface) => { ... this.settings.active.commands.zoom.map((x: CommandInterface) => {
return { return {
label: x.label, label: x.label,
command: x,
customId: `uw-${x.action}-${x.arguments.type}-${x.arguments.ratio ?? 'x'}`.replaceAll('.', '_'),
action: () => this.executeCommand(x) action: () => this.executeCommand(x)
} }
}), }),
@ -339,18 +423,24 @@ class UI {
action: () => this.createSettingsWindow('about'), action: () => this.createSettingsWindow('about'),
} }
] ]
} };
this.extensionMenu = new ClientMenu(menuConfig); this.extensionMenu = new ClientMenu(menuConfig);
this.extensionMenu.mount(this.uiConfig.parentElement); this.extensionMenu.mount(this.uiConfig.parentElement);
if (this.currentScalingParams) {
this.updateMenuStatus(this.currentScalingParams);
}
/** /**
* SETUP MENU INTERACTIONS * SETUP MENU INTERACTIONS
* *
* Interactions are needed for the following things: * Interactions are needed for the following things:
* 0. [ ] both sliders. Needs to read the state of the sliders and update the labels * 0. [X] both sliders. Needs to read the state of the sliders and update the labels
* 1. [ ] lock X/Y button needs to update icon state of the button and the linked bar * 1. [X] lock X/Y button needs to update icon state of the button and the linked bar
* 2. [ ] reset button just needs to reset, no icon changes necessary * 2. [X] reset button just needs to reset, no icon changes necessary
* 3. [X] alignment indicator also needs to update indicator state * 3. [X] alignment indicator also needs to update indicator state
* A
* |
* (yay done!)
*/ */
const menuElement = this.extensionMenu.root; const menuElement = this.extensionMenu.root;
@ -360,6 +450,24 @@ class UI {
const zoomWidthLabel: HTMLDivElement = menuElement.querySelector('#zoomWidth'); const zoomWidthLabel: HTMLDivElement = menuElement.querySelector('#zoomWidth');
const zoomHeightLabel: HTMLDivElement = menuElement.querySelector('#zoomHeight'); const zoomHeightLabel: HTMLDivElement = menuElement.querySelector('#zoomHeight');
for (const slider of [zoomWidthSlider, zoomHeightSlider]) {
slider.addEventListener('pointerdown', function() {
(this as any).isInteracting = true;
});
slider.addEventListener('pointerup', function() {
(this as any).isInteracting = false;
});
slider.addEventListener('pointercancel', function() {
(this as any).isInteracting = false;
});
slider.addEventListener('pointerleave', function() {
(this as any).isInteracting = false;
});
}
const updateZoomDisplayValues = () => { const updateZoomDisplayValues = () => {
zoomWidthLabel.textContent = `${Math.round((Math.exp(this.uiState.zoom.x) * 100))}%`; zoomWidthLabel.textContent = `${Math.round((Math.exp(this.uiState.zoom.x) * 100))}%`;
zoomHeightLabel.textContent = `${Math.round((Math.exp(this.uiState.zoom.y) * 100))}%`; zoomHeightLabel.textContent = `${Math.round((Math.exp(this.uiState.zoom.y) * 100))}%`;
@ -433,9 +541,28 @@ class UI {
} }
} }
updateMenuStatus(scalingParams: ScalingParamsBroadcast) {
this.extensionMenu?.markActiveElements(scalingParams);
// we also need to handle this here
this.uiState.lockXY = scalingParams.effectiveZoom.x === scalingParams.effectiveZoom.y;
}
createSettingsWindow(path?: string) { createSettingsWindow(path?: string) {
const iframe = document.createElement('iframe'); const iframe = document.createElement('iframe');
// we don't enforce minimum margin on small screens
const margin = (window.innerWidth < 1024 || window.innerHeight < 720) ? 0 : 64;
const params = {
width: Math.min(1600, window.innerWidth - margin),
height: Math.min(920, window.innerHeight - margin),
x: 0,
y: 0
};
params.x = Math.floor((window.innerWidth - params.width) / 2);
params.y = Math.floor((window.innerHeight - params.height) / 2);
iframe.src = chrome.runtime.getURL(`ui/pages/settings/index.html#ui${path ? `/${path}` : ''}`); iframe.src = chrome.runtime.getURL(`ui/pages/settings/index.html#ui${path ? `/${path}` : ''}`);
iframe.setAttribute('allowtransparency', 'true'); iframe.setAttribute('allowtransparency', 'true');
Object.assign(iframe.style, { Object.assign(iframe.style, {
@ -447,10 +574,7 @@ class UI {
new UwuiWindow({ new UwuiWindow({
title: `Ultrawidify settings (${window.location.host})`, title: `Ultrawidify settings (${window.location.host})`,
width: 1200, ...params,
height: 800,
x: 0,
y: 0,
content: iframe, content: iframe,
onClose: () => { onClose: () => {
this.eventBus.cancelIframeForwarding(iframe) this.eventBus.cancelIframeForwarding(iframe)
@ -465,10 +589,6 @@ class UI {
}); });
} }
setUiVisibility(visible) {
return;
}
async enable() { async enable() {
// if root element is not present, we need to init the UI. // if root element is not present, we need to init the UI.
// if (!this.rootDiv) { // if (!this.rootDiv) {
@ -489,6 +609,7 @@ class UI {
* @param {*} newUiConfig * @param {*} newUiConfig
*/ */
replace(newUiConfig) { replace(newUiConfig) {
this.destroy();
this.uiConfig = newUiConfig; this.uiConfig = newUiConfig;
this.init(); this.init();
} }
@ -497,6 +618,7 @@ class UI {
if (this.extensionMenu) { if (this.extensionMenu) {
this.extensionMenu.destroy(); this.extensionMenu.destroy();
} }
this.destroyMessaging();
} }

View File

@ -99,14 +99,14 @@ export default class IframeManager {
} else { } else {
this.iframeList.push({ this.iframeList.push({
...data, ...data,
...context.comms.sourceFrame ...context.comms?.sourceFrame
}); });
} }
} }
private handleFrameDestroyed(context) { private handleFrameDestroyed(context) {
// tab IDs should be the same for all items, making frameId sufficiently unique to filter stuff // tab IDs should be the same for all items, making frameId sufficiently unique to filter stuff
this.iframeList = this.iframeList.filter(x => x.frameId !== context.comms.sourceFrame.frameId); this.iframeList = this.iframeList.filter(x => x.frameId !== context.comms?.sourceFrame?.frameId);
} }
} }

View File

@ -103,6 +103,7 @@ class PlayerData {
private trackChangesTimeout: any; private trackChangesTimeout: any;
private markedElement: HTMLElement; private markedElement: HTMLElement;
private markedElementIndex: number | undefined;
private ui: UI; private ui: UI;
@ -114,7 +115,7 @@ class PlayerData {
//#region event bus configuration //#region event bus configuration
private eventBusCommands = { private eventBusCommands = {
'get-player-tree': [{ 'get-player-tree': [{
function: () => this.handlePlayerTreeRequest() function: (data) => this.handlePlayerTreeRequest(data)
}], }],
'get-player-dimensions': [{ 'get-player-dimensions': [{
function: () => { function: () => {
@ -194,6 +195,10 @@ class PlayerData {
//#region lifecycle //#region lifecycle
constructor(videoData) { constructor(videoData) {
if (!(window as any).uiCount) {
(window as any).uiCount = 1;
}
try { try {
// set all our helper objects // set all our helper objects
this.logger = new ComponentLogger(videoData.logAggregator, 'PlayerData', {styles: {}}); this.logger = new ComponentLogger(videoData.logAggregator, 'PlayerData', {styles: {}});
@ -530,6 +535,7 @@ class PlayerData {
private getElementStack(): ElementStack { private getElementStack(): ElementStack {
const elementStack: ElementStack = [{ const elementStack: ElementStack = [{
index: 0,
element: this.videoElement, element: this.videoElement,
type: 'video', type: 'video',
tagName: 'video', tagName: 'video',
@ -540,8 +546,10 @@ class PlayerData {
let element = this.videoElement.parentNode as HTMLElement; let element = this.videoElement.parentNode as HTMLElement;
// first pass to generate the element stack and translate it into array // first pass to generate the element stack and translate it into array
let i = 1;
while (element) { while (element) {
elementStack.push({ elementStack.push({
index: i,
element, element,
type: '', type: '',
tagName: element.tagName, tagName: element.tagName,
@ -552,6 +560,7 @@ class PlayerData {
heuristics: {}, heuristics: {},
}); });
element = element.parentElement; element = element.parentElement;
i++;
} }
this.elementStack = elementStack; this.elementStack = elementStack;
@ -610,6 +619,7 @@ class PlayerData {
// on verbose, get both qs and index player // on verbose, get both qs and index player
if (options?.verbose) { if (options?.verbose) {
this.getPlayerAuto(elementStack, videoHeight, videoHeight, {listOnly: true});
if (playerIndex) { if (playerIndex) {
playerCandidate = elementStack[playerIndex]; playerCandidate = elementStack[playerIndex];
playerCandidate.heuristics['manualElementByParentIndex'] = true; playerCandidate.heuristics['manualElementByParentIndex'] = true;
@ -622,6 +632,7 @@ class PlayerData {
if (detectionMode === PlayerDetectionMode.AncestorIndex) { if (detectionMode === PlayerDetectionMode.AncestorIndex) {
playerCandidate = elementStack[playerIndex]; playerCandidate = elementStack[playerIndex];
playerCandidate.heuristics['manualElementByParentIndex'] = true; playerCandidate.heuristics['manualElementByParentIndex'] = true;
playerCandidate.heuristics['activePlayer'] = true;
} else if (detectionMode === PlayerDetectionMode.QuerySelectors) { } else if (detectionMode === PlayerDetectionMode.QuerySelectors) {
playerCandidate = this.getPlayerQs(playerQs, elementStack, videoWidth, videoHeight); playerCandidate = this.getPlayerQs(playerQs, elementStack, videoWidth, videoHeight);
} }
@ -677,7 +688,7 @@ class PlayerData {
* @param videoHeight * @param videoHeight
* @returns * @returns
*/ */
private getPlayerAuto(elementStack: ElementStack, videoWidth, videoHeight) { private getPlayerAuto(elementStack: ElementStack, videoWidth, videoHeight, options?: {listOnly?: boolean}) {
let penaltyMultiplier = 2; let penaltyMultiplier = 2;
const sizePenaltyMultiplier = 0.1; const sizePenaltyMultiplier = 0.1;
const perLevelScorePenalty = 10; const perLevelScorePenalty = 10;
@ -791,7 +802,7 @@ class PlayerData {
// Some sites (youtube) can re-parent elements, causing current player element to vanish from DOM // Some sites (youtube) can re-parent elements, causing current player element to vanish from DOM
// Which means we need to set up an observer that will re-acquire the player when that happens. // Which means we need to set up an observer that will re-acquire the player when that happens.
// TODO: Ideally, observer should request a tick // TODO: Ideally, observer should request a tick
if (bestCandidate) { if (bestCandidate && !options?.listOnly) {
const observer = new MutationObserver( const observer = new MutationObserver(
(mutations) => { (mutations) => {
mutations.forEach((mutation) => { mutations.forEach((mutation) => {
@ -868,30 +879,67 @@ class PlayerData {
bestCandidate.heuristics['qsMatch'] = true; bestCandidate.heuristics['qsMatch'] = true;
} }
if (bestCandidate) {
bestCandidate.heuristics['activePlayer'] = true;
}
return bestCandidate; return bestCandidate;
} }
/** /**
* Lists elements between video and DOM root for display in player selector (UI) * Lists elements between video and DOM root for display in player selector (UI)
*/ */
private handlePlayerTreeRequest() { private handlePlayerTreeRequest(data: {requestId: string}) {
// this populates this.elementStack fully // this populates this.elementStack fully
// this.updatePlayer({verbose: true}); // this.updatePlayer({verbose: true});
this.eventBus.send('uw-config-broadcast', {type: 'player-tree', config: JSON.parse(JSON.stringify(this.elementStack))}); this.eventBus.send(
'uw-config-broadcast',
{
type: 'player-tree',
requestId: data.requestId,
elementStack: JSON.parse(JSON.stringify(this.elementStack))
}
);
} }
private markElement(data: {parentIndex: number, enable: boolean}) { private markElement(data: {parentIndex: number, enable: boolean}) {
if (data.enable === false) { if (data.enable === false) {
this.markedElement.remove(); this.markedElement.remove();
this.elementStack[this.markedElementIndex]?.element.classList.remove('uw-mark-element');
this.markedElementIndex = undefined;
return; return;
} }
if (this.markedElement) { if (this.markedElement) {
this.markedElement.remove(); this.markedElement.remove();
} }
const elementBB = this.elementStack[data.parentIndex].element.getBoundingClientRect();; if (this.markedElementIndex !== undefined) {
this.elementStack[this.markedElementIndex]?.element.classList.remove('uw-mark-element');
} else {
this.eventBus.send(
'inject-css',
{
cssString: `
.uw-mark-element {
border: 5px solid #fa6 !important;
box-sizing: border-box !important;
filter:
sepia(1)
saturate(5)
hue-rotate(-20deg)
brightness(1.15);
}
`
}
);
}
this.markedElementIndex = data.parentIndex;
this.elementStack[this.markedElementIndex]?.element.classList.add('uw-mark-element');
console.log('———— setting element as active:', data.parentIndex, this.elementStack.length, this.elementStack, this.elementStack[data.parentIndex])
const elementBB = this.elementStack[data.parentIndex].element.getBoundingClientRect();
// console.log('element bounding box:', elementBB); // console.log('element bounding box:', elementBB);
@ -910,6 +958,8 @@ class PlayerData {
document.body.insertBefore(div, document.body.firstChild); document.body.insertBefore(div, document.body.firstChild);
this.markedElement = div; this.markedElement = div;
// this.elementStack[data.parentIndex].element.style.outline = data.enable ? '5px dashed #fa6' : null; // this.elementStack[data.parentIndex].element.style.outline = data.enable ? '5px dashed #fa6' : null;
// this.elementStack[data.parentIndex].element.style.filter = data.enable ? 'sepia(1) brightness(2) contrast(0.5)' : null; // this.elementStack[data.parentIndex].element.style.filter = data.enable ? 'sepia(1) brightness(2) contrast(0.5)' : null;
} }

View File

@ -7,6 +7,7 @@ import { RunLevel } from '@src/ext/enum/run-level.enum';
import { Aard } from '@src/ext/module/aard/Aard'; import { Aard } from '@src/ext/module/aard/Aard';
import { AardLegacy } from '@src/ext/module/aard/AardLegacy'; import { AardLegacy } from '@src/ext/module/aard/AardLegacy';
import { hasDrm } from '@src/ext/module/ar-detect/DrmDetector'; import { hasDrm } from '@src/ext/module/ar-detect/DrmDetector';
import { CommsOrigin } from '@src/ext/module/comms/CommsClient';
import EventBus from '@src/ext/module/EventBus'; import EventBus from '@src/ext/module/EventBus';
import { ComponentLogger } from '@src/ext/module/logging/ComponentLogger'; import { ComponentLogger } from '@src/ext/module/logging/ComponentLogger';
import { LogAggregator } from '@src/ext/module/logging/LogAggregator'; import { LogAggregator } from '@src/ext/module/logging/LogAggregator';
@ -46,6 +47,7 @@ class VideoData {
videoLoaded: boolean = false; videoLoaded: boolean = false;
videoDimensionsLoaded: boolean = false; videoDimensionsLoaded: boolean = false;
active: boolean = false; active: boolean = false;
private preventVideoOffsetValidation: boolean = false;
//#endregion //#endregion
//#region misc stuff //#region misc stuff
@ -147,7 +149,7 @@ class VideoData {
}; };
if (!pageInfo.eventBus) { if (!pageInfo.eventBus) {
this.eventBus = new EventBus({name: 'video-data'}); this.eventBus = new EventBus({name: 'video-data', commsOrigin: CommsOrigin.ContentScript});
} else { } else {
this.eventBus = pageInfo.eventBus; this.eventBus = pageInfo.eventBus;
} }
@ -298,10 +300,9 @@ class VideoData {
initializeObservers() { initializeObservers() {
try { try {
if (BrowserDetect.firefox) {
this.observer = new ResizeObserver( this.observer = new ResizeObserver(
_.debounce( _.debounce(
this.onVideoDimensionsChanged, () => this.onVideoDimensionsChanged,
250, 250,
{ {
leading: true, leading: true,
@ -309,22 +310,6 @@ class VideoData {
} }
) )
); );
} else {
// Chrome for some reason insists that this.onPlayerDimensionsChanged is not a function
// when it's not wrapped into an anonymous function
this.observer = new ResizeObserver(
_.debounce(
(m, o) => {
this.onVideoDimensionsChanged(m, o)
},
250,
{
leading: true,
trailing: true
}
)
);
}
} catch (e) { } catch (e) {
console.error('[VideoData] Observer setup failed:', e); console.error('[VideoData] Observer setup failed:', e);
} }
@ -332,11 +317,13 @@ class VideoData {
} }
setupMutationObserver() { setupMutationObserver() {
if (this.mutationObserver) {
this.mutationObserver.disconnect();
}
try { try {
if (BrowserDetect.firefox) {
this.mutationObserver = new MutationObserver( this.mutationObserver = new MutationObserver(
_.debounce( _.debounce(
this.onVideoMutation, () => this.onVideoMutation(),
250, 250,
{ {
leading: true, leading: true,
@ -344,22 +331,6 @@ class VideoData {
} }
) )
) )
} else {
// Chrome for some reason insists that this.onPlayerDimensionsChanged is not a function
// when it's not wrapped into an anonymous function
this.mutationObserver = new MutationObserver(
_.debounce(
(m, o) => {
this.onVideoMutation(m, o)
},
250,
{
leading: true,
trailing: true
}
)
)
}
} catch (e) { } catch (e) {
console.error('[VideoData] Observer setup failed:', e); console.error('[VideoData] Observer setup failed:', e);
} }
@ -372,6 +343,17 @@ class VideoData {
destroy() { destroy() {
this.logger.info('destroy', `<vdid:${this.vdid}> received destroy command`); this.logger.info('destroy', `<vdid:${this.vdid}> received destroy command`);
// Disconnect observer and set destroyed to 'true' _before_ removing classes from
// the video element
this.destroyed = true;
try {
this.observer.disconnect();
} catch (e) {}
try {
this.mutationObserver.disconnect();
} catch (e) {}
if (this.video) { if (this.video) {
this.video.classList.remove(this.userCssClassName); this.video.classList.remove(this.userCssClassName);
this.video.classList.remove('uw-ultrawidify-base-wide-screen'); this.video.classList.remove('uw-ultrawidify-base-wide-screen');
@ -381,8 +363,6 @@ class VideoData {
this.video.removeEventListener('ontimeupdate', this.onTimeUpdate); this.video.removeEventListener('ontimeupdate', this.onTimeUpdate);
} }
this.eventBus.send('set-run-level', RunLevel.Off);
this.destroyed = true;
this.eventBus.unsubscribeAll(this); this.eventBus.unsubscribeAll(this);
try { try {
@ -397,9 +377,6 @@ class VideoData {
try { try {
this.player.destroy(); this.player.destroy();
} catch (e) {} } catch (e) {}
try {
this.observer.disconnect();
} catch (e) {}
this.player = undefined; this.player = undefined;
this.video = undefined; this.video = undefined;
} }
@ -463,7 +440,7 @@ class VideoData {
this.runLevel = runLevel; this.runLevel = runLevel;
if (!options?.fromPlayer) { if (!options?.fromPlayer) {
this.player.setRunLevel(runLevel); this.player?.setRunLevel(runLevel);
} }
} }
@ -532,6 +509,14 @@ class VideoData {
} }
onVideoMutation(mutationList?: MutationRecord[], observer?) { onVideoMutation(mutationList?: MutationRecord[], observer?) {
if (this.destroyed) {
return;
}
if (!mutationList) {
this.logger.warn('onVideoMutation', 'mutation was triggered, but mutationList is missing. Something is fishy. Mutation will be ignored. Observer:', observer);
return;
}
// verify that mutation didn't remove our class. Some pages like to do that. // verify that mutation didn't remove our class. Some pages like to do that.
let confirmAspectRatioRestore = false; let confirmAspectRatioRestore = false;
@ -610,7 +595,9 @@ class VideoData {
// sometimes something fucky wucky happens and mutations aren't detected correctly, so we // sometimes something fucky wucky happens and mutations aren't detected correctly, so we
// try to get around that // try to get around that
this.preventVideoOffsetValidation = true;
setTimeout( () => { setTimeout( () => {
this.preventVideoOffsetValidation = false;
this.validateVideoOffsets(); this.validateVideoOffsets();
}, 100); }, 100);
} }
@ -631,6 +618,9 @@ class VideoData {
} }
validateVideoOffsets() { validateVideoOffsets() {
if (this.preventVideoOffsetValidation) {
return;
}
// validate if current video still exists. If not, we destroy current object // validate if current video still exists. If not, we destroy current object
try { try {
if (! document.body.contains(this.video)) { if (! document.body.contains(this.video)) {

View File

@ -2,6 +2,7 @@ import AspectRatioType from '@src/common/enums/AspectRatioType.enum';
import StretchType from '@src/common/enums/StretchType.enum'; import StretchType from '@src/common/enums/StretchType.enum';
import VideoAlignmentType from '@src/common/enums/VideoAlignmentType.enum'; import VideoAlignmentType from '@src/common/enums/VideoAlignmentType.enum';
import { Ar, ArVariant } from '@src/common/interfaces/ArInterface'; import { Ar, ArVariant } from '@src/common/interfaces/ArInterface';
import { ScalingParamsBroadcast } from '@src/common/interfaces/ScalingParamsBroadcast.interface';
import { Stretch } from '@src/common/interfaces/StretchInterface'; import { Stretch } from '@src/common/interfaces/StretchInterface';
import getElementStyles from '@src/common/utils/getElementStyles'; import getElementStyles from '@src/common/utils/getElementStyles';
import Debug from '@src/ext/conf/Debug'; import Debug from '@src/ext/conf/Debug';
@ -63,6 +64,16 @@ class Resizer {
currentVideoSettings: any; currentVideoSettings: any;
private effectiveZoom: {x: number, y: number} = {x: 1, y: 1}; private effectiveZoom: {x: number, y: number} = {x: 1, y: 1};
private currentScalingParams: ScalingParamsBroadcast = {
effectiveZoom: {x: 1, y: 1},
lastAr: {type: AspectRatioType.Initial},
stretch: {type: StretchType.Default},
videoAlignment: {
x: VideoAlignmentType.Center,
y: VideoAlignmentType.Center
},
manualZoom: false
};
private pendingAr?: {ar: Ar, lastAr?: Ar}; private pendingAr?: {ar: Ar, lastAr?: Ar};
@ -100,6 +111,11 @@ class Resizer {
this.eventBus.send('announce-zoom', this.manualZoom ? {x: this.zoom.scale, y: this.zoom.scaleY} : this.zoom.effectiveZoom); this.eventBus.send('announce-zoom', this.manualZoom ? {x: this.zoom.scale, y: this.zoom.scaleY} : this.zoom.effectiveZoom);
} }
}], }],
'request-scaling-params': [{
function: () => {
this.eventBus.send('broadcast-scaling-params', this.currentScalingParams);
}
}],
'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.
@ -149,6 +165,11 @@ class Resizer {
}], }],
'set-zoom': [{ 'set-zoom': [{
function: (config: any) => { function: (config: any) => {
if (!config || config.zoom === 1 || (config.zoom.x === 1 && config.zoom.y === 1)) {
this.manualZoom = false;
} else {
this.manualZoom = true;
}
this.setZoom(config?.zoom ?? {zoom: 1}); this.setZoom(config?.zoom ?? {zoom: 1});
} }
}], }],
@ -278,16 +299,21 @@ class Resizer {
let fileAr = this.getFileAr(); let fileAr = this.getFileAr();
if (ar.type === AspectRatioType.FitWidth){ switch (ar.type) {
case AspectRatioType.FitWidth:
ar.ratio = ratioOut > fileAr ? ratioOut : fileAr; ar.ratio = ratioOut > fileAr ? ratioOut : fileAr;
} break;
else if(ar.type === AspectRatioType.FitHeight){ case AspectRatioType.FitHeight:
ar.ratio = ratioOut < fileAr ? ratioOut : fileAr; ar.ratio = ratioOut < fileAr ? ratioOut : fileAr;
} break;
else if(ar.type === AspectRatioType.Reset){ case AspectRatioType.Cover:
ar.ratio = Math.max(ratioOut, fileAr);
break;
case AspectRatioType.Reset:
this.logger.info('modeToAr', "Using original aspect ratio -", fileAr); this.logger.info('modeToAr', "Using original aspect ratio -", fileAr);
ar.ratio = fileAr; ar.ratio = fileAr;
} else { break;
default:
return null; return null;
} }
@ -334,7 +360,7 @@ class Resizer {
return true; return true;
} }
async setAr(ar: Ar, lastAr?: Ar) { async setAr(ar: Ar, lastAr?: Ar, flags?: {manualZoom?: boolean}) {
if (this.destroyed || ar == null) { if (this.destroyed || ar == null) {
return; return;
} }
@ -375,9 +401,13 @@ class Resizer {
return; return;
} }
if (ar.type !== AspectRatioType.AutomaticUpdate) { if (ar.type !== AspectRatioType.AutomaticUpdate && !flags?.manualZoom) {
if (flags?.manualZoom) {
this.manualZoom = true;
} else {
this.manualZoom = false; this.manualZoom = false;
} }
}
if (!this.video.videoWidth || !this.video.videoHeight) { if (!this.video.videoWidth || !this.video.videoHeight) {
this.logger.warn('setAr', `<rid:${this.resizerId}> Video has no width or no height. This is not allowed. Aspect ratio will not be set, and videoData will be uninitialized.`); this.logger.warn('setAr', `<rid:${this.resizerId}> Video has no width or no height. This is not allowed. Aspect ratio will not be set, and videoData will be uninitialized.`);
@ -424,7 +454,7 @@ class Resizer {
this.logger.info('setAr', `<rid:${this.resizerId}> Something wrong with ar or the player. Doing nothing.`); this.logger.info('setAr', `<rid:${this.resizerId}> Something wrong with ar or the player. Doing nothing.`);
return; return;
} }
this.lastAr = {type: ar.type, ratio: ar.ratio}; this.lastAr = {type: ar.type, ratio: ar.ratio, variant: ar.variant};
} }
if (! this.video) { if (! this.video) {
@ -495,19 +525,36 @@ class Resizer {
try { try {
const translate = this.computeOffsets(stretchFactors, options?.ar); const translate = this.computeOffsets(stretchFactors, options?.ar);
this.applyCss(stretchFactors, translate); this.applyCss(stretchFactors, translate);
this.currentScalingParams = {
effectiveZoom: {x: stretchFactors.xFactor, y: stretchFactors.yFactor},
videoAlignment: this.videoAlignment,
lastAr: this.lastAr,
manualZoom: this.manualZoom,
stretch: this.stretcher.stretch
}
this.eventBus.send('broadcast-scaling-params', this.currentScalingParams);
} catch (e) { } catch (e) {
this.logger.warn('setAr', 'error while applying CSS:', e); this.logger.warn('applyScaling', 'error while applying CSS:', e);
// don't apply CSS if there's an error // don't apply CSS if there's an error
} }
} }
toFixedAr() { toFixedAr(flags?: {manualZoom?: boolean}) {
// converting to fixed AR means we also turn off autoAR // converting to fixed AR means we also turn off autoAR
this.setAr(
this.setAr({ {
ratio: this.lastAr.ratio ?? this.getFileAr(), ratio: this.lastAr.ratio ?? this.getFileAr(),
type: AspectRatioType.Fixed type: AspectRatioType.Fixed
}); },
undefined,
flags
);
if (flags?.manualZoom) {
this.manualZoom = true;
}
} }
resetLastAr() { resetLastAr() {

View File

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

View File

@ -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.3.997", "version": "6.3.998",
"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"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 43 KiB

View File

@ -86,7 +86,7 @@ export default defineComponent({
}, },
methods: { methods: {
showInPlayerUi() { showInPlayerUi() {
this.eventBus.send('uw-set-ui-state', {globalUiVisible: true}, {comms: {forwardTo: 'active'}}); this.eventBus.send('uw-show-settings-window', {initialState: '', allFrames: true}, {comms: {forwardTo: 'active'}});
}, },
openSettingsInTab() { openSettingsInTab() {
chrome.runtime.openOptionsPage(); chrome.runtime.openOptionsPage();

View File

@ -160,13 +160,35 @@
:settings="settings" :settings="settings"
></OtherSiteSettings> ></OtherSiteSettings>
<!--
<PlayerElementSettings <PlayerElementSettings
v-if="selectedTab === 'settings.player-element-settings'" v-if="selectedTab === 'settings.player-element-settings'"
:settings="settings" :settings="settings"
:eventBus="eventBus" :eventBus="eventBus"
> >
</PlayerElementSettings> </PlayerElementSettings>
-->
<template v-if="selectedTab === 'window.player-element-advanced-settings'">
<PlayerSelectorAdvancedForm
v-if="settings && siteSettings"
:settings="settings"
:siteSettings="siteSettings"
></PlayerSelectorAdvancedForm>
<template v-else>Loading site settings, please wait ...</template>
</template>
<template v-if="selectedTab === 'window.player-element-settings'">
<PlayerSelectorSimple
v-if="settings && siteSettings"
:site="site"
:eventBus="eventBus"
:settings="settings"
:siteSettings="siteSettings"
></PlayerSelectorSimple>
<template v-else>Loading site settings, please wait ...</template>
</template>
<AutodetectionSettings <AutodetectionSettings
v-if="selectedTab === 'autodetectionSettings'" v-if="selectedTab === 'autodetectionSettings'"
@ -234,7 +256,6 @@ import { defineComponent } from 'vue';
import VideoSettings from '@components/segments/VideoSettings/VideoSettings.vue'; import VideoSettings from '@components/segments/VideoSettings/VideoSettings.vue';
import OtherSiteSettings from '@components/segments/ExtensionSettings/Panels/OtherSiteSettings.vue'; import OtherSiteSettings from '@components/segments/ExtensionSettings/Panels/OtherSiteSettings.vue';
import PlayerElementSettings from '@components/segments/PlayerElementSelection/PlayerElementSettings.vue'; import PlayerElementSettings from '@components/segments/PlayerElementSelection/PlayerElementSettings.vue';
import PlayerElementWindow from '@components/segments/PlayerElementSelection/PlayerElementWindow.vue';
import AutodetectionSettings from '@components/segments/AutodetectionSettings/AutodetectionSettings.vue'; import AutodetectionSettings from '@components/segments/AutodetectionSettings/AutodetectionSettings.vue';
import UISettings from '@components/segments/UISettings/UISettings.vue'; import UISettings from '@components/segments/UISettings/UISettings.vue';
import KeyboardShortcutSettings from '@components/segments/KeyboardShortcuts/KeyboardShortcutSettings.vue'; import KeyboardShortcutSettings from '@components/segments/KeyboardShortcuts/KeyboardShortcutSettings.vue';
@ -242,6 +263,8 @@ import SiteExtensionSettings from '@components/segments/ExtensionSettings/Panels
import FrameSiteSettings from '@components/segments/ExtensionSettings/Panels/FrameSiteSettings.vue'; import FrameSiteSettings from '@components/segments/ExtensionSettings/Panels/FrameSiteSettings.vue';
import Debugging from '@components/segments/Debugging/Debugging.vue'; import Debugging from '@components/segments/Debugging/Debugging.vue';
import ImportExportSettings from '@components/segments/ImportExportSettings/ImportExportSettings.vue'; import ImportExportSettings from '@components/segments/ImportExportSettings/ImportExportSettings.vue';
import PlayerSelectorAdvancedForm from '@components/segments/PlayerElementSelection/Panels/PlayerSelectorAdvancedForm.vue';
import PlayerSelectorSimple from '@components/segments/PlayerElementSelection/Panels/PlayerSelectorSimple.vue';
import AfterUpdate from '@components/segments/AfterUpdate/AfterUpdate.vue'; import AfterUpdate from '@components/segments/AfterUpdate/AfterUpdate.vue';
@ -250,6 +273,7 @@ import About from '@components/segments/ExtensionInfo/About.vue';
// not component: // not component:
import BrowserDetect from '@src/ext/conf/BrowserDetect' import BrowserDetect from '@src/ext/conf/BrowserDetect'
import WarningsMixin from '../utils/mixins/WarningsMixin.vue';
const AVAILABLE_TABS = { const AVAILABLE_TABS = {
@ -281,11 +305,17 @@ const AVAILABLE_TABS = {
{ id: 'website-extension-settings', label: 'Website exceptions', }, { id: 'website-extension-settings', label: 'Website exceptions', },
] ]
}, },
'window.player-element-settings': { id: 'window.player-element-settings', label: 'Advanced video player options', icon: 'play-box-edit-outline' }, 'window.player-element-settings': {
id: 'window.player-element-settings', label: 'Video player options', icon: 'play-box-edit-outline',
children: [
{id: 'window.player-element-settings', label: 'Video player picker'},
{id: 'window.player-element-advanced-settings', label: 'Advanced options'},
],
},
'autodetectionSettings': {id: 'autodetectionSettings', label: 'Autodetection options', icon: 'auto-fix'}, 'autodetectionSettings': {id: 'autodetectionSettings', label: 'Autodetection options', icon: 'auto-fix'},
'ui-settings': {id: 'ui-settings', label: 'UI settings', icon: 'movie-cog-outline' }, 'ui-settings': {id: 'ui-settings', label: 'UI settings', icon: 'movie-cog-outline' },
'keyboardShortcuts': {id: 'keyboardShortcuts', label: 'Keyboard shortcuts', icon: 'keyboard-outline' }, 'keyboardShortcuts': {id: 'keyboardShortcuts', label: 'Keyboard shortcuts', icon: 'keyboard-outline' },
'playerDetection': {id: 'playerDetection', label: 'Player detection', icon: 'television-play'}, 'settings.player-element-settings': {id: 'settings.player-element-settings', label: 'Player detection', icon: 'television-play'},
'installed': { id: 'installed', label: 'Update completed', icon: 'monitor-arrow-down-variant'}, 'installed': { id: 'installed', label: 'Update completed', icon: 'monitor-arrow-down-variant'},
'updated': { id: 'updated', label: 'Update completed', icon: 'update'}, 'updated': { id: 'updated', label: 'Update completed', icon: 'update'},
@ -299,7 +329,7 @@ const AVAILABLE_TABS = {
const TAB_LOADOUT = { const TAB_LOADOUT = {
'settings': [ 'settings': [
'default-extension-settings', 'default-extension-settings',
'settings.player-element-settings', // 'settings.player-element-settings',
'autodetectionSettings', 'autodetectionSettings',
'ui-settings', 'ui-settings',
'keyboardShortcuts', 'keyboardShortcuts',
@ -343,7 +373,8 @@ export default defineComponent({
VideoSettings, VideoSettings,
OtherSiteSettings, OtherSiteSettings,
PlayerElementSettings, PlayerElementSettings,
PlayerElementWindow, PlayerSelectorAdvancedForm,
PlayerSelectorSimple,
AutodetectionSettings, AutodetectionSettings,
KeyboardShortcutSettings, KeyboardShortcutSettings,
UISettings, UISettings,
@ -357,7 +388,9 @@ export default defineComponent({
WhatsNew, WhatsNew,
About, About,
}, },
mixins: [], mixins: [
WarningsMixin
],
data() { data() {
return { return {
statusFlags: { statusFlags: {

View File

@ -206,7 +206,7 @@
<div class="flex flex-row w-full gap-2"> <div class="flex flex-row w-full gap-2">
<div class="grow flex flex-col gap-2"> <div class="grow flex flex-col gap-2">
<div> <div>
<button v-if="eventBus" @click="eventBus?.sendToTunnel('aard-enable-debug', true)">Show debug overlay</button> <button v-if="eventBus" @click="eventBus?.send('aard-enable-debug', true)">Show debug overlay</button>
</div> </div>
<div class="field"> <div class="field">
<div class="label">Show debug overlay on startup</div> <div class="label">Show debug overlay on startup</div>
@ -277,7 +277,7 @@ export default defineComponent({
); );
}, },
mounted() { mounted() {
this.eventBus?.sendToTunnel('get-aard-timing'); this.eventBus?.send('get-aard-timing');
// this.graphRefreshInterval = setInterval(() => this.eventBus.sendToTunnel('get-aard-timing'), 500); // this.graphRefreshInterval = setInterval(() => this.eventBus.sendToTunnel('get-aard-timing'), 500);
this.resetSettingsEditor(); this.resetSettingsEditor();
}, },
@ -302,7 +302,7 @@ export default defineComponent({
this.settings.saveWithoutReload(); this.settings.saveWithoutReload();
}, },
refreshGraph() { refreshGraph() {
this.eventBus.sendToTunnel('get-aard-timing'); this.eventBus.send('get-aard-timing');
}, },
handleConfigBroadcast(data) { handleConfigBroadcast(data) {
if (data.type === 'aard-performance-data') { if (data.type === 'aard-performance-data') {

View File

@ -36,6 +36,21 @@
<li> <li>
Settings, popup and in-page UI have been combined into a single HTML file in order to cut down on the file size. Settings, popup and in-page UI have been combined into a single HTML file in order to cut down on the file size.
</li> </li>
<li>
In-player UI has been made a bit lighter (previously, in-player UI utilized vue + iframe. Now, in-player UI uses vanilla HTML/javascript (unless you open settings window)).
</li>
<li>
Removed some UI activation options: UI can no longer be activated by defining a trigger zone.
</li>
<li>
Added new UI activation options: UI can be set to show on mouse movement (default), when mouse moves within user-defined distance to the menu activator, or while holding the CTRL key (you need to move your mouse while holding CTRL for the menu to show)
</li>
<li>
In-player menu position can be somewhat customized.
</li>
<li>
Default crop mode can now use zoom options as well (previously, it could only use crop options).
</li>
</ul> </ul>
<b class="text-white">Other updates and fixes:</b> <b class="text-white">Other updates and fixes:</b>

View File

@ -209,7 +209,8 @@
<!-- Default crop --> <!-- Default crop -->
<div class="field"> <div class="field">
<div class="label">Default crop:</div> <div class="label">Default crop:</div>
<div class="select"> <div class="select grow shrink flex flex-row">
<select <select
:value="siteDefaultCrop" :value="siteDefaultCrop"
@change="setOption('defaults.crop', $event)" @change="setOption('defaults.crop', $event)"
@ -218,15 +219,48 @@
v-if="!isDefaultConfiguration" v-if="!isDefaultConfiguration"
:value="JSON.stringify({useDefault: true})" :value="JSON.stringify({useDefault: true})"
> >
Use default ({{getCommandValue(settings?.active.commands.crop, siteSettings.data.defaults.crop)}}) Use default ({{getDefaultCrop(siteSettings.defaultSettings.defaults.crop)}})
</option> </option>
<option <option
:value="JSON.stringify({type: AspectRatioType.Reset})"
>
Initial/reset
</option>
<optgroup label="Crop">
<template
v-for="(command, index) of settings?.active.commands.crop" v-for="(command, index) of settings?.active.commands.crop"
:key="index" :key="index"
>
<option
v-if="
command.arguments.type === AspectRatioType.Fixed
|| command.arguments.type === AspectRatioType.Automatic
|| command.arguments.type === AspectRatioType.FitWidth
|| command.arguments.type === AspectRatioType.FitHeight
"
:value="JSON.stringify(command.arguments)" :value="JSON.stringify(command.arguments)"
> >
{{command.label}} {{command.label}} (crop)
</option> </option>
</template>
</optgroup>
<optgroup label="Zoom">
<template
v-for="(command, index) of settings?.active.commands.zoom"
:key="index"
>
<option
v-if="
command.arguments.type === AspectRatioType.Fixed
|| command.arguments.type === AspectRatioType.Automatic
|| command.arguments.type === AspectRatioType.Cover
"
:value="JSON.stringify({...command.arguments, variant: ArVariant.Zoom})"
>
{{command.label}} (zoom)
</option>
</template>
</optgroup>
</select> </select>
</div> </div>
</div> </div>
@ -244,7 +278,7 @@
v-if="!isDefaultConfiguration" v-if="!isDefaultConfiguration"
:value="JSON.stringify({useDefault: true})" :value="JSON.stringify({useDefault: true})"
> >
Use default ({{getCommandValue(settings?.active.commands.stretch, siteSettings.data.defaults.stretch)}}) Use default ({{getCommandValue(settings?.active.commands.stretch, siteSettings.defaultSettings.defaults.stretch)}})
</option> </option>
<option <option
v-for="(command, index) of settings?.active.commands.stretch" v-for="(command, index) of settings?.active.commands.stretch"
@ -269,7 +303,7 @@
v-if="!isDefaultConfiguration" v-if="!isDefaultConfiguration"
:value="JSON.stringify({useDefault: true})" :value="JSON.stringify({useDefault: true})"
> >
Use default ({{getAlignmentLabel(siteSettings.data.defaults.alignment)}}) Use default ({{getAlignmentLabel(siteSettings.defaultSettings.defaults.alignment)}})
</option> </option>
<option <option
v-for="(command, index) of alignmentOptions" v-for="(command, index) of alignmentOptions"
@ -337,9 +371,10 @@ import VideoAlignmentType from '@src/common/enums/VideoAlignmentType.enum';
import CropModePersistence from '@src/common/enums/CropModePersistence.enum'; import CropModePersistence from '@src/common/enums/CropModePersistence.enum';
import EmbeddedContentSettingsOverridePolicy from '@src/common/enums/EmbeddedContentSettingsOverridePolicy.enum'; import EmbeddedContentSettingsOverridePolicy from '@src/common/enums/EmbeddedContentSettingsOverridePolicy.enum';
import { InputHandlingMode } from '@src/common/enums/InputHandlingMode.enum'; import { InputHandlingMode } from '@src/common/enums/InputHandlingMode.enum';
import AspectRatioType from '@src/common/enums/AspectRatioType.enum';
import { ArVariant } from '@src/common/interfaces/ArInterface';
export default defineComponent({ export default defineComponent({
components: { components: {
Popup, Popup,
PlayerSelectorAdvancedForm, PlayerSelectorAdvancedForm,
@ -353,7 +388,9 @@ export default defineComponent({
], ],
data() { data() {
return { return {
CropModePersistence: CropModePersistence, CropModePersistence,
AspectRatioType,
ArVariant,
ExtensionMode, ExtensionMode,
EmbeddedContentSettingsOverridePolicy, EmbeddedContentSettingsOverridePolicy,
InputHandlingMode, InputHandlingMode,
@ -370,7 +407,8 @@ export default defineComponent({
], ],
playerDetectionOptionsDialog: { playerDetectionOptionsDialog: {
visible: false, visible: false,
} },
defaultCropCommand: 'set-ar',
} }
}, },
mixins: [ mixins: [
@ -395,9 +433,6 @@ export default defineComponent({
siteDefaultAlignment() { siteDefaultAlignment() {
return this.siteSettings.raw?.defaults?.alignment ? JSON.stringify(this.siteSettings.raw?.defaults?.alignment) : JSON.stringify({useDefault: true}); return this.siteSettings.raw?.defaults?.alignment ? JSON.stringify(this.siteSettings.raw?.defaults?.alignment) : JSON.stringify({useDefault: true});
}, },
siteDefaultCropPersistence() {
return this.siteSettings.raw?.persistCSA ?? undefined;
},
defaultPersistenceLabel() { defaultPersistenceLabel() {
switch (this.siteSettings.defaultSettings.persistCSA) { switch (this.siteSettings.defaultSettings.persistCSA) {
case CropModePersistence.CurrentSession: case CropModePersistence.CurrentSession:
@ -511,6 +546,22 @@ export default defineComponent({
} }
}, },
getDefaultCrop(command) {
for (const cmd of this.settings?.active.commands.crop) {
if (JSON.stringify(cmd.arguments) === JSON.stringify(command)) {
return cmd.label;
}
}
for (const cmd of this.settings?.active.commands.zoom) {
if (JSON.stringify({...cmd.arguments, variant: ArVariant.Zoom}) === JSON.stringify(command)) {
return `${cmd.label} (zoom)`;
}
}
return 'Unknown';
},
getCommandValue(availableCommands, command) { getCommandValue(availableCommands, command) {
for (const cmd of availableCommands) { for (const cmd of availableCommands) {
if (JSON.stringify(cmd.arguments) === JSON.stringify(command)) { if (JSON.stringify(cmd.arguments) === JSON.stringify(command)) {
@ -555,13 +606,14 @@ export default defineComponent({
getOption(option) { getOption(option) {
}, },
async setOption(option, $event) { async setOption(option, $event, extras = {}) {
const value = $event.target.value; const value = $event?.target?.value ?? {};
let commandArguments;
let commandArguments;
// if argument is json, parse json. Otherwise, pass the value as-is // if argument is json, parse json. Otherwise, pass the value as-is
try { try {
commandArguments = value !== undefined ? JSON.parse(value) : undefined; const intermediary = value !== undefined ? JSON.parse(value) : undefined;
commandArguments = {...intermediary, ...extras};
} catch(e) { } catch(e) {
commandArguments = value; commandArguments = value;
} }

View File

@ -1,141 +1,165 @@
<template> <template>
<div class="w-full flex flex-row" style="margin-top: 1rem;"> <div class="w-full flex flex-col" style="margin-top: 1rem;">
<!-- PLAYER ELEMENT SELECTOR FOR DUMMIES --> <h2 class="text-[1.75em]">Simple video player picker</h2>
<div style="width: 48%">
<div class="sub-panel-content">
<div v-if="showAdvancedOptions" style="display: flex; flex-direction: row"> <template v-if="tutorialStep">
<div style="display: flex; flex-direction: column"> <div v-if="tutorialStep == 1" class="flex flex-col w-full justify-center items-center tutorial-step">
<div> <h3 class="mb-4">1. Start hovering over elements on this list</h3>
<input :checked="playerManualQs" <div class="flex flex-row w-full flex-wrap justify-center">
@change="togglePlayerManualQs" <div class="bg-black/50 rounded flex flex-col justify-center items-center p-4">
type="checkbox" <div class="">
/> <p>This list contains all the elements on the webpage that could be a video player.</p>
<div> <p>Icons to the left of the list indicate which element is currently selected as the player element, which element the extension thinks to be the player element, and which element should be the player according to manual settings.</p>
Use <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors" target="_blank">CSS selector</a> for player<br/> <p>Move your mouse over the first element on the list, but do not click it.</p>
<small>If defining multiple selectors, separate them with commas.</small> <p>Hovering over elements on the list will highlight parts of the page.</p>
</div>
<img src="/res/img/player-select-demo/uw_player___element-list-hover.webp" class="w-1/2 min-w-[420px]" />
</div> </div>
</div> </div>
<div>Selector</div> <div class="p-4 flex flex-row w-full gap-2 item-center justify-center">
<input type="text" <button @click="tutorialStep = 2">Next</button>
v-model="playerQs"
@change="updatePlayerQuerySelector"
@blur="updatePlayerQuerySelector"
:disabled="playerByNodeIndex || !playerManualQs"
/>
</div>
<div style="display: flex; flex-direction: column">
<b>Custom CSS for site</b>
<textarea></textarea>
</div> </div>
</div> </div>
<div style="display: flex; flex-direction: row;"> <div v-if="tutorialStep == 2" class="flex flex-col w-full justify-center items-center tutorial-step">
<div class="element-tree"> <h3 class="mb-4">2. Observe highlight</h3>
<table>
<thead>
<tr>
<th>
<div class="status-relative">
Status <mdicon name="help-circle" @click="showLegend = !showLegend" />
<div v-if="showLegend" class="element-symbol-legend"> <div class="grid grid-cols-2 gap-2 tutorial-list">
<b>Symbols:</b><br /> <div class="bg-black/50 rounded flex flex-col justify-end items-center p-4">
<mdicon name="alert-remove" class="invalid" /> Element of invalid dimensions<br /> <div class="card-text">
<mdicon name="refresh-auto" class="auto-match" /> Ultrawidify's player detection thinks this should be the player<br /> <p>Hovering over the elements will highlight part of the page. Highlighted area should cover the player area.</p>
<mdicon name="bookmark" class="parent-offset-match" /> Site settings say this should be the player (based on counting parents)<br /> <p>If the highlighted area covers the player, click the item on the list to select it and reload the page.</p>
<mdicon name="crosshairs" class="qs-match" /> Site settings say this should be the player (based on query selectors)<br /> <p>If more than one element covers the player area, select the first (topmost) one on the list.</p>
<mdicon name="check-circle" class="activePlayer" /> Element that is actually the player
</div> </div>
</div> <img src="/res/img/player-select-demo/uw_player_select___just-right.webp" />
</th>
<th>Element</th>
<!-- <th>Actions</th> -->
<!-- <th>Quick fixes</th> -->
</tr>
</thead>
<tbody>
<tr
v-for="(element, index) of elementStack"
:key="index"
class="element-row"
>
<td>
<div class="status">
<div
v-if="element.heuristics?.invalidSize"
class="invalid"
>
<mdicon name="alert-remove" />
</div>
<div
v-if="element.heuristics?.autoMatch"
class="auto-match"
>
<mdicon name="refresh-auto" />
</div>
<div
v-if="element.heuristics?.qsMatch"
class="qs-match"
>
<mdicon name="crosshairs" />
</div>
<div
v-if="element.heuristics?.manualElementByParentIndex"
class="parent-offset-match"
>
<mdicon name="bookmark" />
</div>
<div
v-if="element.heuristics?.activePlayer"
class="activePlayer"
>
<mdicon name="check-circle" />
</div>
</div>
</td>
<td>
<div
class="element-data"
@mouseover="markElement(elementStack.length - index - 1, true)" <div class="icon text-teal-500">
@mouseleave="markElement(elementStack.length - index - 1, false)" <mdicon name="check-circle" size="64"></mdicon>
@click="setPlayer(elementStack.length - index - 1)"
>
<div class="tag">
<b>{{element.tagName}}</b> <i class="id">{{element.id ? `#`:''}}{{element.id}}</i> @ <span class="dimensions">{{element.width}}</span>x<span class="dimensions">{{element.height}}</span>
</div>
<div v-if="element.classList" class="class-list">
<div v-for="cls of element.classList" :key="cls">
{{cls}}
</div> </div>
</div> </div>
<div class="bg-black/50 rounded flex flex-col justify-end items-center p-4">
<div class="card-text">
<p>If highlight covers more than just the player area, that usually means the correct element is further down the list.</p>
<p>Move the mouse cursor down the list of elements, until you encounter the first element that covers the player area.</p>
</div> </div>
</td> <img src="/res/img/player-select-demo/uw_player_select___too_much.webp" />
<td> <div class="icon text-red-700">
<div class="flex flex-row"> <mdicon name="close-circle" size="64"></mdicon>
</div>
</div>
<div class="bg-black/50 rounded flex flex-col justify-end items-center p-4">
<div class="card-text">
<p>If highlight doesn't cover the whole player area, that usually means the correct element is further down the list.</p>
<p>Move your cursor up the list of elements, until you encounter something that highlights the entire player area.</p>
<p>If more than one element covers the player area, select the first (topmost) one on the list.</p>
</div>
<img src="/res/img/player-select-demo/uw_player_select___too_little.webp" />
<div class="icon text-red-700">
<mdicon name="close-circle" size="64"></mdicon>
</div> </div>
</td>
</tr>
</tbody>
</table>
<div class="element-config">
</div> </div>
</div> </div>
<!-- <div class="css-preview"> <div class="p-4 flex flex-row w-full gap-2 item-center justify-center">
{{cssStack}} <button @click="tutorialStep = 1">Previous</button>
</div> --> <button @click="tutorialStep = 0">Done</button>
</div>
</div>
</div> </div>
</div> </div>
</template> </template>
<template v-else>
<div class="text text-sm text-stone-500">
<p>If your video is not aligned correctly, video player was not detected correctly.</p>
<p>You need to help by selecting the video player. Hover over boxes below. This will highlight part of the screen.</p>
<p>Select the first box that highlights the video player on the page.</p>
<div class="flex flex-row justify-between">
<a @click="tutorialStep = 1">Click here for more details.</a>
<a @click="resetSettings()">Reset settings</a>
</div>
</div>
<!-- <div class="">
<div class="flex flex-row gap-4">
<button>How to use</button>
</div>
</div> -->
<!-- PLAYER ELEMENT SELECTOR FOR DUMMIES -->
<div class="flex flex-col-reverse gap-2">
<div
v-for="(element, index) of elementStack"
:key="index"
class="py-2 px-4 border border-stone-500 flex flex-col gap-2 text-sm cursor-pointer hover:bg-stone-800/50"
:class="{
'!border-blue-500 !hover:bg-blue-500/50': element.heuristics?.autoMatch,
'!border-teal-800 !hover:bg-teal-800/50': element.heuristics?.manualElementByParentIndex,
'!border-emerald-500 !hover:bg-emerald-500/50': element.heuristics?.qsMatch,
'!border-red-700 !bg-red-950/50': element.heuristics?.invalidSize,
'!border-primary-300 !hover:bg-primary-800/50': element.heuristics?.activePlayer,
'!pointer-events-none opacity-50': ['html', 'body', 'video'].includes(element.tagName.toLowerCase()),
}"
@mouseover="markElement(element.index, true)"
@mouseleave="markElement(element.index, false)"
@click="setPlayer(element, element.index)"
>
<div
v-if="element.heuristics?.activePlayer"
class="text-primary-300 flex flex-row gap-2"
>
<mdicon name="check-circle" /> This element is currently treated as player element.
</div>
<div>
<div class="tag">
<span class="text-monospace w-16">[{{element.index}}]</span> <b>&lt;{{element.tagName}}&gt;</b> <i class="text-red-800">{{element.id ? `#`:''}}{{element.id}}</i> @ <span class="text-blue-500">{{element.width}}</span>x<span class="text-blue-500">{{element.height}}</span>
</div>
<div v-if="element.classList" class="text-nowrap overflow-hidden text-ellipsis text-stone-500/50">
<span v-for="cls of element.classList" :key="cls">.{{cls}}&nbsp;</span>
</div>
</div>
<div
v-if="element.heuristics?.qsMatch"
class="text-emerald-600 flex flex-row gap-2"
>
<mdicon name="crosshairs" /> This element matches query string (advanced settings)
</div>
<div
v-if="element.heuristics?.manualElementByParentIndex"
class="text-teal-600 flex flex-row gap-2"
>
<mdicon name="bookmark" /> This element has been manually selected as player element.
</div>
<div
v-if="element.heuristics?.autoMatch"
class="text-blue-500 flex flex-row gap-2"
>
<mdicon name="refresh-auto" /> Automatic detections thinks this is the player.
</div>
<div
v-if="element.heuristics?.invalidSize"
class="text-red-700 flex flex-row gap-2"
>
<mdicon name="alert-remove" /> This element has invalid dimensions
</div>
</div>
</div>
</template>
</div>
</template>
<script lang="ts"> <script lang="ts">
import { PlayerDetectionMode } from '@src/common/enums/PlayerDetectionMode.enum';
import { SiteSupportLevel } from '@src/common/enums/SiteSupportLevel.enum';
import { SiteDOMSettingsInterface } from '@src/common/interfaces/SettingsInterface';
import { _cp } from '@src/common/utils/_cp';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
export default defineComponent({ export default defineComponent({
@ -145,22 +169,25 @@ export default defineComponent({
data() { data() {
return { return {
elementStack: [], elementStack: [],
elementStacks: {
requestId: undefined,
stacks: []
},
cssStack: [], cssStack: [],
showLegend: false, showLegend: false,
showAdvancedOptions: false, showAdvancedOptions: false,
tutorialVisible: false, tutorialVisible: false,
tutorialStep: 0 tutorialStep: 0,
lastTreeId: undefined,
}; };
}, },
computed: { computed: {
}, },
mixins: [], mixins: [],
props: [ props: [
'settings', // not used?
'siteSettings', 'siteSettings',
'frame',
'eventBus', 'eventBus',
'site',
'isPopup'
], ],
created() { created() {
this.eventBus.subscribe( this.eventBus.subscribe(
@ -183,40 +210,78 @@ export default defineComponent({
this.tutorialStep = 0; this.tutorialStep = 0;
}, },
getPlayerTree() { getPlayerTree() {
if (this.isPopup) { this.lastTreeId = crypto.randomUUID()
this.eventBus.send('get-player-tree'); this.eventBus.send('get-player-tree', {requestId: this.lastTreeId});
} else {
this.eventBus.sendToTunnel('get-player-tree');
}
}, },
handleElementStack(configBroadcast) { handleElementStack(configBroadcast) {
if (configBroadcast.type === 'player-tree') { if (configBroadcast.type === 'player-tree') {
this.elementStack = configBroadcast.config.reverse(); // reset tree if lastTreeId has changed
if (this.elementStacks.requestId !== this.lastTreeId) {
this.elementStacks = {
requestId: this.lastTreeId,
stacks: []
};
}
if (configBroadcast.requestId === this.lastTreeId) {
const stack = configBroadcast.elementStack.sort((a, b) => a.index - b.index);
this.elementStacks.stacks.push({
elementStack: stack
});
this.elementStack = stack;
this.$nextTick( () => this.$forceUpdate() ); this.$nextTick( () => this.$forceUpdate() );
} }
}
}, },
markElement(parentIndex, enable) { markElement(parentIndex, enable) {
this.eventBus.sendToTunnel('set-mark-element', {parentIndex, enable}); this.eventBus.send('set-mark-element', {parentIndex, enable});
}, },
async setPlayer(index) { async resetSettings() {
// yup. if (!this.siteSettings.raw?.activeDOMConfig) {
// this.siteSettings.getDOMConfig('modified', 'original'); console.warn('')
// await this.siteSettings.setUpdateFlags(['PlayerData']); return;
// await this.siteSettings.set('DOMConfig.modified.type', 'modified', {noSave: true}); }
// await this.siteSettings.set('activeDOMConfig', 'modified', {noSave: true});
// // if user agrees with ultrawidify on what element player should be, await this.siteSettings.setUpdateFlags(['PlayerData']);
// // we just unset our settings for this site await this.siteSettings.set(`DOMConfig.${this.siteSettings.data.activeDOMConfig}.player.detectionMode`, PlayerDetectionMode.Auto, {noSave: true});
// if (this.elementStack[index].heuristics?.autoMatch) { await this.siteSettings.set(`DOMConfig.${this.siteSettings.data.activeDOMConfig}.player.ancestorIndex`, undefined);
// await this.siteSettings.set('DOMConfig.modified.elements.player.manual', false);
// this.getPlayerTree(); this.getPlayerTree();
// } else { setTimeout( () => this.getPlayerTree(), 500);
// // ensure settings exist: setTimeout( () => this.getPlayerTree(), 1000);
// await this.siteSettings.set('DOMConfig.modified.elements.player.manual', true, {noSave: true}); },
// await this.siteSettings.set('DOMConfig.modified.elements.player.mode', 'index', {noSave: true}); /**
// await this.siteSettings.set('DOMConfig.modified.elements.player.index', index, true); * Designates new element as player element. Currently, we only need
// this.getPlayerTree(); * 'index', however at some point we might also set mode according
// } * to element flags.
*/
async setPlayer(element, index) {
await this.siteSettings.ensureSettings();
let domConfig: SiteDOMSettingsInterface = this.siteSettings.data.currentDOMConfig;
let domConfigName = this.siteSettings.data.activeDOMConfig.startsWith('@') ? 'custom' : this.siteSettings.data.activeDOMConfig;
switch (domConfig?.type) {
case SiteSupportLevel.UserModified:
case SiteSupportLevel.UserDefined:
break;
default:
domConfig = _cp(domConfig ?? this.settings.sites['@empty'].DOMConfig['@empty']);
domConfig.type = SiteSupportLevel.UserModified;
break;
}
domConfig.elements.player.detectionMode = index !== undefined ? PlayerDetectionMode.AncestorIndex : PlayerDetectionMode.Auto;
domConfig.elements.player.ancestorIndex = index;
await this.siteSettings.setUpdateFlags(['PlayerData']);
await this.siteSettings.set(`DOMConfig.${domConfigName}`, domConfig, {noSave: true});
await this.siteSettings.set('activeDOMConfig', domConfigName);
this.getPlayerTree();
setTimeout(() => this.getPlayerTree(), 500);
setTimeout(() => this.getPlayerTree(), 1000);
}, },
/** /**
* Toggles active CSS for element of certain parent index. * Toggles active CSS for element of certain parent index.

View File

@ -1,220 +0,0 @@
<template>
<div class="flex flex-col w-full">
<div class="flex flex-row">
<h1>Video player options</h1>
</div>
<div class="w-full">
<div v-if="tutorialVisible" class="w-full">
<button
class="info-button"
@click="tutorialVisible = false"
>
<mdicon name="arrow-left"></mdicon>
Back
</button>
<div v-if="tutorialStep == 0" class="flex flex-col w-full justify-center items-center tutorial-step">
<h3>1. Start hovering over elements on this list</h3>
<div class="flex flex-row w-full flex-wrap tutorial-list">
<div class="card">
<div class="card-text">
<p>This list contains all the elements on the webpage that could be a video player.</p>
<p>Icons to the left of the list indicate which element is currently selected as the player element, which element the extension thinks to be the player element, and which element should be the player according to manual settings.</p>
</div>
<img src="/res/img/player-select-demo/uw_player___element-list-1.webp" />
</div>
<div class="card">
<div class="card-text">
<p>Move your mouse over the first element on the list, but do not click it.</p>
<p>Hovering over elements on the list will highlight parts of the page.</p>
</div>
<img src="/res/img/player-select-demo/uw_player___element-list-hover.webp" />
</div>
</div>
<button @click="tutorialStep = 1">Next</button>
</div>
<div v-if="tutorialStep == 1" class="flex flex-col w-full justify-center items-center tutorial-step">
<h3>2. Observe highlight</h3>
<div class="flex flex-row w-full flex-wrap tutorial-list">
<div class="card">
<div class="card-text">
<p>Hovering over the elements will highlight part of the page. Highlighted area should cover the player area.</p>
<p>If the highlighted area covers the player, click the item on the list to select it and reload the page.</p>
<p>If more than one element covers the player area, select the first (topmost) one on the list.</p>
</div>
<img src="/res/img/player-select-demo/uw_player_select___just-right.webp" />
<div class="icon correct">
<mdicon name="check-circle" size="96"></mdicon>
</div>
</div>
<div class="card">
<div class="card-text">
<p>If highlight covers more than just the player area, that usually means the correct element is further down the list.</p>
<p>Move the mouse cursor down the list of elements, until you encounter the first element that covers the player area.</p>
</div>
<img src="/res/img/player-select-demo/uw_player_select___too_much.webp" />
<div class="icon wrong">
<mdicon name="close-circle" size="96"></mdicon>
</div>
</div>
<div class="card">
<div class="card-text">
<p>If highlight doesn't cover the whole player area, that usually means the correct element is further down the list.</p>
<p>Move your cursor up the list of elements, until you encounter something that highlights the entire player area.</p>
<p>If more than one element covers the player area, select the first (topmost) one on the list.</p>
</div>
<img src="/res/img/player-select-demo/uw_player_select___too_little.webp" />
<div class="icon wrong">
<mdicon name="close-circle" size="96"></mdicon>
</div>
</div>
</div>
<div>
<button @click="tutorialStep = 0">Previous</button>
<button @click="tutorialVisible = false">Done</button>
</div>
</div>
</div>
<div v-else class="w-full">
<button
class="info-button"
@click="showTutorial()"
>
<mdicon name="help"></mdicon>
How do I use this?
</button>
</div>
</div>
</div>
</template>
<script lang="ts">
export default({
components: {
},
data() {
return {
elementStack: [],
cssStack: [],
showLegend: false,
showAdvancedOptions: false,
tutorialVisible: false,
tutorialStep: 0
};
},
computed: {
},
mixins: [],
props: [
'siteSettings',
'frame',
'eventBus',
'site',
'isPopup'
],
created() {
this.eventBus.subscribe(
'uw-config-broadcast',
{
source: this,
function: (config) => this.handleElementStack(config)
}
);
},
mounted() {
this.getPlayerTree();
},
destroyed() {
this.eventBus.unsubscribeAll(this);
},
methods: {
showTutorial() {
this.tutorialVisible = true;
this.tutorialStep = 0;
},
getPlayerTree() {
if (this.isPopup) {
this.eventBus.send('get-player-tree');
} else {
this.eventBus.sendToTunnel('get-player-tree');
}
},
handleElementStack(configBroadcast) {
if (configBroadcast.type === 'player-tree') {
this.elementStack = configBroadcast.config.reverse();
this.$nextTick( () => this.$forceUpdate() );
}
},
markElement(parentIndex, enable) {
this.eventBus.sendToTunnel('set-mark-element', {parentIndex, enable});
},
async setPlayer(index) {
// yup.
// this.siteSettings.getDOMConfig('modified', 'original');
// await this.siteSettings.setUpdateFlags(['PlayerData']);
// await this.siteSettings.set('DOMConfig.modified.type', 'modified', {noSave: true});
// await this.siteSettings.set('activeDOMConfig', 'modified', {noSave: true});
// // if user agrees with ultrawidify on what element player should be,
// // we just unset our settings for this site
// if (this.elementStack[index].heuristics?.autoMatch) {
// await this.siteSettings.set('DOMConfig.modified.elements.player.manual', false);
// this.getPlayerTree();
// } else {
// // ensure settings exist:
// await this.siteSettings.set('DOMConfig.modified.elements.player.manual', true, {noSave: true});
// await this.siteSettings.set('DOMConfig.modified.elements.player.mode', 'index', {noSave: true});
// await this.siteSettings.set('DOMConfig.modified.elements.player.index', index, true);
// this.getPlayerTree();
// }
},
/**
* Toggles active CSS for element of certain parent index.
* cssValue is optional and can be included in cssRule argument
*/
toggleCssForElement(index, cssRule, cssValue) {
// we will handle elements that put cssValue as a separate argument elsewhere
if (cssValue) {
return this.toggleCssForElement_3arg(index,cssRule, cssValue);
}
// this rule applies to current element remove it!
if (this.cssStack[index]?.includes(cssRule)) {
this.cssStack[index] = this.cssStack[index].filter(x => ! x.includes(cssRule));
} else {
if (!this.cssStack[index]) {
this.cssStack[index] = [];
}
this.cssStack[index].push(cssRule)
}
//TODO: update settings!
},
toggleCssForElement_3arg(index, cssRule, cssValue) {
const matching = this.cssStack[index]?.find(x => x.includes(cssRule))
if (matching) {
this.cssStack[index] = this.cssStack[index].filter(x => ! x.includes(cssRule));
if (!matching.includes(cssValue)) {
this.cssStack[index].push(`${cssRule}: ${cssValue};`);
}
} else {
if (!this.cssStack[index]) {
this.cssStack[index] = [];
}
this.cssStack[index].push(`${cssRule}: ${cssValue};`);
}
//TODO: update settings!
}
}
})
</script>
<style lang="scss" scoped>
</style>

View File

@ -9,35 +9,6 @@
<div <div
class="flex flex-col field-group compact-form gap-2" class="flex flex-col field-group compact-form gap-2"
> >
<div class="field">
<div class="label">
Popup activator position:
</div>
<div class="select">
<select
v-model="settings.active.ui.inPlayer.activatorAlignment"
@change="saveSettings()"
>
<option value="left">Left</option>
<option value="right">Right</option>
</select>
</div>
</div>
<div class="field">
<div class="label">
Popup activator padding:
</div>
<div class="select">
<select
v-model="settings.active.ui.inPlayer.activatorAlignment"
@change="saveSettings()"
>
<option value="left">Left</option>
<option value="right">Right</option>
</select>
</div>
</div>
<div class="field"> <div class="field">
<div class="label"> <div class="label">
@ -48,14 +19,14 @@
v-model="settings.active.ui.inPlayer.activation" v-model="settings.active.ui.inPlayer.activation"
@change="saveSettings()" @change="saveSettings()"
> >
<option value="none">
Never activate UI with mouse movement alone
</option>
<option value="player"> <option value="player">
When mouse moves over player, always When mouse moves over player, always
</option> </option>
<option value="player-ctrl">
When mouse moves over player, while holding CTRL key
</option>
<option value="distance"> <option value="distance">
When mouse is close to the menu activator (experimental) When mouse is close to the menu activator
</option> </option>
<!-- <option value="trigger-zone"> <!-- <option value="trigger-zone">
When mouse moves over trigger zone When mouse moves over trigger zone
@ -64,6 +35,18 @@
</div> </div>
</div> </div>
<div class="field">
<div class="label">
<input type="checkbox"
v-model="settings.active.ui.inPlayer.activateWithCtrl"
@change="saveSettings()"
>
</div>
<div class="text-left">
Also show on CTRL + mouse move
</div>
</div>
<div v-show="settings.active.ui.inPlayer.activation === 'distance'" class="field"> <div v-show="settings.active.ui.inPlayer.activation === 'distance'" class="field">
<div class="label"> <div class="label">
Show menu when mouse is closer than: Show menu when mouse is closer than:
@ -85,7 +68,7 @@
> >
<select <select
class="unit-select !min-w-[72px]" class="unit-select !min-w-[72px]"
v-model="settings.active.ui.inPlayer.activationDistanceUnit" v-model="settings.active.ui.inPlayer.activationDistanceUnits"
@change="(event) => saveSettings(true)" @change="(event) => saveSettings(true)"
> >
<option value="%">%</option> <option value="%">%</option>
@ -99,6 +82,92 @@
<button @click="startTriggerZoneEdit()">Edit</button> <button @click="startTriggerZoneEdit()">Edit</button>
</div> </div>
<div class="field">
<div class="label">
Menu activator position:
</div>
<div class="select">
<select
v-model="settings.active.ui.inPlayer.activatorAlignment"
@change="saveSettings()"
>
<optgroup label="Edges">
<option :value="MenuPosition.Left">Left</option>
<option :value="MenuPosition.Right">Right</option>
<option :value="MenuPosition.Top">Top</option>
<option :value="MenuPosition.Bottom">Bottom</option>
</optgroup>
<optgroup label="corners">
<option :value="MenuPosition.TopLeft">Top left</option>
<option :value="MenuPosition.BottomLeft">Bottom left</option>
<option :value="MenuPosition.TopRight">Top right</option>
<option :value="MenuPosition.BottomRight">Bottom right</option>
</optgroup>
</select>
</div>
</div>
<div class="field">
<div class="label">
Menu activator horizontal padding:
</div>
<div class="input range-input">
<input
v-model="settings.active.ui.inPlayer.activatorPadding.x"
class="slider"
type="range"
min="0"
max="100"
step="1"
@change="(event) => saveSettings()"
>
<input
style="margin-right: 0.6rem;"
v-model="settings.active.ui.inPlayer.activatorPadding.x"
@change="(event) => saveSettings(true)"
>
<select
class="unit-select !min-w-[72px]"
v-model="settings.active.ui.inPlayer.activatorPaddingUnit.x"
@change="(event) => saveSettings(true)"
>
<option value="%">%</option>
<option value="px">px</option>
</select>
</div>
</div>
<div class="field">
<div class="label">
Menu activator vertical padding:
</div>
<div class="input range-input">
<input
v-model="settings.active.ui.inPlayer.activatorPadding.y"
class="slider"
type="range"
min="0"
max="100"
step="1"
@change="(event) => saveSettings()"
>
<input
style="margin-right: 0.6rem;"
v-model="settings.active.ui.inPlayer.activatorPadding.y"
@change="(event) => saveSettings(true)"
>
<select
class="unit-select !min-w-[72px]"
v-model="settings.active.ui.inPlayer.activatorPaddingUnit.y"
@change="(event) => saveSettings(true)"
>
<option value="%">%</option>
<option value="px">px</option>
</select>
</div>
</div>
<div class="field"> <div class="field">
<div class="label"> <div class="label">
Do not show in-player UI when video player is narrower than Do not show in-player UI when video player is narrower than
@ -159,6 +228,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { MenuPosition } from '@src/common/interfaces/ClientUiMenu.interface';
import BrowserDetect from '@src/ext/conf/BrowserDetect'; import BrowserDetect from '@src/ext/conf/BrowserDetect';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
@ -167,6 +237,7 @@ export default defineComponent({
}, },
data() { data() {
return { return {
MenuPosition,
ghettoComputed: { } ghettoComputed: { }
} }
}, },
@ -181,7 +252,12 @@ export default defineComponent({
this.ghettoComputed = { this.ghettoComputed = {
minEnabledWidth: this.optionalToFixed(this.settings.active.ui.inPlayer.minEnabledWidth * 100, 0), minEnabledWidth: this.optionalToFixed(this.settings.active.ui.inPlayer.minEnabledWidth * 100, 0),
minEnabledHeight: this.optionalToFixed(this.settings.active.ui.inPlayer.minEnabledHeight * 100, 0), minEnabledHeight: this.optionalToFixed(this.settings.active.ui.inPlayer.minEnabledHeight * 100, 0),
} };
this.eventBus?.send('force-menu-activator-state', {visibility: true});
},
unmounted() {
this.eventBus?.send('force-menu-activator-state', {visibility: false});
}, },
methods: { methods: {
forcePositiveNumber(value) { forcePositiveNumber(value) {
@ -213,11 +289,13 @@ export default defineComponent({
this.ghettoComputed[key] = this.optionalToFixed(value * 100, 0); this.ghettoComputed[key] = this.optionalToFixed(value * 100, 0);
} }
}, },
saveSettings(forceRefresh) { async saveSettings(forceRefresh) {
this.settings.saveWithoutReload(); await this.settings.saveWithoutReload();
this.eventBus.send('reload-menu');
if (forceRefresh) { if (forceRefresh) {
this.$nextTick( () => this.$forceRefresh() ); this.$nextTick( () => this.$forceUpdate() );
} }
}, },
startTriggerZoneEdit() { startTriggerZoneEdit() {

View File

@ -7,6 +7,9 @@
<ShortcutButton <ShortcutButton
v-for="(command, index) of settings?.active.commands.crop" v-for="(command, index) of settings?.active.commands.crop"
:key="index" :key="index"
:classList="{
'border !border-primary-700 text-primary-500 active-option': currentCropCommand === `${command.action}-${command.arguments.type}-${command.arguments.ratio ?? 'x'}`
}"
:label="command.label" :label="command.label"
:shortcut="getKeyboardShortcutLabel(command)" :shortcut="getKeyboardShortcutLabel(command)"
@click="execAction(command)" @click="execAction(command)"
@ -18,6 +21,9 @@
<ShortcutButton <ShortcutButton
v-for="(command, index) of settings?.active.commands.zoom" v-for="(command, index) of settings?.active.commands.zoom"
:key="index" :key="index"
:classList="{
'border !border-primary-700 text-primary-500 active-option': currentCropCommand === `${command.action}-${command.arguments.type}-${command.arguments.ratio ?? 'x'}`
}"
:label="command.label" :label="command.label"
:shortcut="getKeyboardShortcutLabel(command)" :shortcut="getKeyboardShortcutLabel(command)"
@click="execAction(command)" @click="execAction(command)"
@ -40,6 +46,10 @@
max="4" max="4"
:value="zoom.x" :value="zoom.x"
@input="changeZoom($event.target.value, 'x')" @input="changeZoom($event.target.value, 'x')"
@pointerdown="zoomUpdatesDisabled = true"
@pointerup="zoomUpdatesDisabled = false"
@pointercancel="zoomUpdatesDisabled = false"
@pointerleave="zoomUpdatesDisabled = false"
/> />
</div> </div>
</div> </div>
@ -55,6 +65,10 @@
max="4" max="4"
:value="zoom.y" :value="zoom.y"
@input="changeZoom($event.target.value, 'y')" @input="changeZoom($event.target.value, 'y')"
@pointerdown="zoomUpdatesDisabled = true"
@pointerup="zoomUpdatesDisabled = false"
@pointercancel="zoomUpdatesDisabled = false"
@pointerleave="zoomUpdatesDisabled = false"
/> />
</div> </div>
</div> </div>
@ -94,6 +108,9 @@
<ShortcutButton <ShortcutButton
v-for="(command, index) of settings?.active.commands.stretch" v-for="(command, index) of settings?.active.commands.stretch"
:key="index" :key="index"
:classList="{
'border border-primary-700 text-primary-500 active-option': currentStretchCommand === `${command.action}-${command.arguments.type}-${command.arguments.ratio ?? 'x'}`
}"
:label="command.label" :label="command.label"
:shortcut="getKeyboardShortcutLabel(command)" :shortcut="getKeyboardShortcutLabel(command)"
@click="execAction(command)" @click="execAction(command)"
@ -102,6 +119,7 @@
<h3>Align video</h3> <h3>Align video</h3>
<div <div
id="videoAlignmentController"
ref="alignmentSvgContainer" ref="alignmentSvgContainer"
class="w-full h-[12em] flex flex-row justify-center" class="w-full h-[12em] flex flex-row justify-center"
></div> ></div>
@ -119,6 +137,10 @@ import KeyboardShortcutParserMixin from '@ui/utils/mixins/KeyboardShortcutParser
import alignmentIndicatorSvg from '!!raw-loader!@ui/res/img/alignment-indicators.svg'; import alignmentIndicatorSvg from '!!raw-loader!@ui/res/img/alignment-indicators.svg';
import {setupVideoAlignmentIndicatorInteraction, setVideoAlignmentIndicatorState} from '@ui/utils/video-alignment-indicator-handling'; import {setupVideoAlignmentIndicatorInteraction, setVideoAlignmentIndicatorState} from '@ui/utils/video-alignment-indicator-handling';
import VideoAlignmentType from '@src/common/enums/VideoAlignmentType.enum'; import VideoAlignmentType from '@src/common/enums/VideoAlignmentType.enum';
import { ScalingParamsBroadcast } from '@src/common/interfaces/ScalingParamsBroadcast.interface';
import { ArVariant } from '@src/common/interfaces/ArInterface';
import AspectRatioType from '@src/common/enums/AspectRatioType.enum';
import StretchType from '@src/common/enums/StretchType.enum';
export default defineComponent({ export default defineComponent({
components: { components: {
@ -139,21 +161,26 @@ export default defineComponent({
return { return {
zoom: {x: 0, y: 0}, // zoom is logarithmic, so "100%" is represented as 0 instead of 1. zoom: {x: 0, y: 0}, // zoom is logarithmic, so "100%" is represented as 0 instead of 1.
zoomOptions: { zoomOptions: {
lockAr: false, lockAr: true,
}, },
alignmentSvgContainer: undefined as HTMLElement | undefined, alignmentSvgContainer: undefined as HTMLElement | undefined,
alignmentIndicatorSvg: undefined as SVGSVGElement | undefined,
currentCropCommand: '',
currentStretchCommand: '',
zoomUpdatesDisabled: false,
} }
}, },
created() { created() {
console.log('created ....'); this.eventBus.subscribeMulti({
this.eventBus.subscribe( 'broadcast-scaling-params': {
'uw-config-broadcast', function: (commandData: ScalingParamsBroadcast, context) => {
{ this.markActiveElements(commandData);
source: this,
function: (config) => this.handleConfigBroadcast(config)
} }
); }
});
}, },
mounted() { mounted() {
this.alignmentSvgContainer = this.$refs.alignmentSvgContainer as HTMLElement; this.alignmentSvgContainer = this.$refs.alignmentSvgContainer as HTMLElement;
@ -161,10 +188,10 @@ export default defineComponent({
if (this.alignmentSvgContainer) { if (this.alignmentSvgContainer) {
this.alignmentSvgContainer.innerHTML = alignmentIndicatorSvg; this.alignmentSvgContainer.innerHTML = alignmentIndicatorSvg;
const svgElement = this.alignmentSvgContainer.querySelector('svg') as SVGSVGElement; const svgElement = this.alignmentSvgContainer.querySelector('svg') as SVGSVGElement;
console.log('svg element:', svgElement); this.alignmentIndicatorSvg = svgElement;
if (svgElement) { if (svgElement) {
setupVideoAlignmentIndicatorInteraction(svgElement, (x: VideoAlignmentType, y: VideoAlignmentType) => { setupVideoAlignmentIndicatorInteraction(svgElement, (x: VideoAlignmentType, y: VideoAlignmentType) => {
console.log('clicked!');
// Update selection visually // Update selection visually
setVideoAlignmentIndicatorState(svgElement, x, y); setVideoAlignmentIndicatorState(svgElement, x, y);
@ -173,7 +200,8 @@ export default defineComponent({
} }
} }
this.eventBus.sendToTunnel('get-ar'); this.eventBus.send('get-ar');
this.eventBus.send('request-scaling-params');
}, },
destroyed() { destroyed() {
this.eventBus.unsubscribeAll(this); this.eventBus.unsubscribeAll(this);
@ -201,7 +229,7 @@ export default defineComponent({
// 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}); this.eventBus?.send('set-zoom', {zoom: 1});
}, },
changeZoom(logZoom: number, axis: 'x' | 'y') { changeZoom(logZoom: number, axis: 'x' | 'y') {
// we do not use logarithmic zoom elsewhere, therefore we need to convert // we do not use logarithmic zoom elsewhere, therefore we need to convert
@ -210,19 +238,81 @@ export default defineComponent({
if (this.zoomOptions.lockAr) { if (this.zoomOptions.lockAr) {
this.zoom.x = logZoom; this.zoom.x = logZoom;
this.zoom.y = logZoom; this.zoom.y = logZoom;
this.eventBus?.sendToTunnel('set-zoom', {zoom: linearZoom}); this.eventBus?.send('set-zoom', {zoom: linearZoom});
} else { } else {
this.zoom[axis] = logZoom; this.zoom[axis] = logZoom;
this.eventBus?.sendToTunnel('set-zoom', {zoom: {[axis]: linearZoom}}); this.eventBus?.send('set-zoom', {zoom: {[axis]: linearZoom}});
} }
}, },
isActiveZoom(command) { isActiveZoom(command) {
return false; return false;
}, },
align(alignmentX: VideoAlignmentType, alignmentY: VideoAlignmentType) { align(alignmentX: VideoAlignmentType, alignmentY: VideoAlignmentType) {
this.eventBus?.sendToTunnel('set-alignment', {x: alignmentX, y: alignmentY}) this.eventBus?.send('set-alignment', {x: alignmentX, y: alignmentY})
},
markActiveElements(scalingParams: ScalingParamsBroadcast) {
if (this.zoomUpdatesDisabled) {
return;
}
// we only set active options when manual zoom is NOT set
if (!scalingParams.manualZoom) {
const cropCommand = scalingParams.lastAr.variant === ArVariant.Zoom ? 'set-ar-zoom' : 'set-ar';
let typeCrop;
switch (scalingParams.lastAr.type) {
case AspectRatioType.Automatic:
case AspectRatioType.AutomaticUpdate:
typeCrop = `${AspectRatioType.Automatic}-x`;
break;
case AspectRatioType.Cover:
case AspectRatioType.FitWidth:
case AspectRatioType.FitHeight:
typeCrop = `${scalingParams.lastAr.type}-x`;
break;
case AspectRatioType.Cycle:
case AspectRatioType.Initial:
typeCrop = 'non-selectable';
break;
default:
typeCrop = `${scalingParams.lastAr.type}-${scalingParams.lastAr.ratio ?? 'x'}`;
} }
this.currentCropCommand = `${cropCommand}-${typeCrop}`;
let typeStretch;
switch (scalingParams.stretch.type) {
case StretchType.FixedSource:
case StretchType.Fixed:
typeStretch = `${scalingParams.stretch.type}-${scalingParams.stretch.ratio ?? 'x'}`;
break;
case StretchType.Default:
case StretchType.NoStretch:
typeStretch = `non-selectable`;
break;
default:
typeStretch = `${scalingParams.stretch.type}-x`;
}
this.currentStretchCommand = `set-stretch-${typeStretch}`;
} else {
this.currentCropCommand = '';
this.currentStretchCommand = '';
}
if (!this.zoomUpdatesDisabled) {
this.zoom = {
x: Math.log2(scalingParams.effectiveZoom.x),
y: Math.log2(scalingParams.effectiveZoom.y)
};
}
this.zoomOptions.lockAr = scalingParams.effectiveZoom.x === scalingParams.effectiveZoom.y;
if (this.alignmentIndicatorSvg) {
setVideoAlignmentIndicatorState(this.alignmentIndicatorSvg, scalingParams.videoAlignment.x, scalingParams.videoAlignment.y);
}
}
} }
}); });
</script> </script>

View File

@ -16,7 +16,7 @@ export default defineComponent({
props: { props: {
label: String, label: String,
shortcut: String, shortcut: String,
classList: String classList: Object
} }
}); });
</script> </script>

View File

@ -186,7 +186,7 @@ export default defineComponent({
this.site.host = config.site.host; this.site.host = config.site.host;
} }
} }
this.site = config.site; this.site = {...config.site, from: 'set-current-site, via setupPopup'};
// this.selectedSite = this.selectedSite || config.site.host; // this.selectedSite = this.selectedSite || config.site.host;
this.siteSettings = this.settings.getSiteSettings({site: this.site.host}); this.siteSettings = this.settings.getSiteSettings({site: this.site.host});
@ -271,7 +271,6 @@ export default defineComponent({
this.selectedTab = this.defaultTab; this.selectedTab = this.defaultTab;
} }
console.log('does event bus exist yet?', this.eventBus);
if (!this.eventBus) { if (!this.eventBus) {
// inter-frame communication should be set up by eventBus for free // inter-frame communication should be set up by eventBus for free
this.eventBus = new EventBus({name: 'ui-window'}); this.eventBus = new EventBus({name: 'ui-window'});
@ -280,7 +279,7 @@ export default defineComponent({
/** /**
* Subscribe to event bus commands. * Subscribe to event bus commands.
* Note that showing and hiding of the settings window is no longer handled by iframe * Note that showing and hiding of the settings window is no longer handled by iframe
* Instead, uw-show-ui should be handled by the content-script. * Instead, uw-show-settings-window should be handled by the content-script.
*/ */
this.eventBus.subscribeMulti({ this.eventBus.subscribeMulti({
'set-current-site': { 'set-current-site': {
@ -294,8 +293,7 @@ export default defineComponent({
this.site.host = config.site.host; this.site.host = config.site.host;
} }
} }
this.site = config.site; this.site = {...config.site, from: 'set-current-site, via setup iframe'};
this.siteSettings = this.settings.getSiteSettings({site: this.site.host}); this.siteSettings = this.settings.getSiteSettings({site: this.site.host});
console.log('set-site received:', this.site, this.siteSettings, 'current path:', this.initialPath); console.log('set-site received:', this.site, this.siteSettings, 'current path:', this.initialPath);
@ -336,6 +334,7 @@ export default defineComponent({
host: config.site, host: config.site,
frames: [], frames: [],
hostnames: [], hostnames: [],
from: 'has-video, via setup iframe. Settings: ' + !!this.settings + '; site settings: ' + !!this.settings.getSiteSettings({site: config.site.host}),
}; };
this.siteSettings = this.settings.getSiteSettings({site: this.site.host}); this.siteSettings = this.settings.getSiteSettings({site: this.site.host});
@ -350,7 +349,7 @@ export default defineComponent({
console.log('—————————————————————— polling from iframe window!!!!') console.log('—————————————————————— polling from iframe window!!!!')
this.startSitePolling(); this.startSitePolling();
// this.eventBus.subscribe( // this.eventBus.subscribe(
// 'uw-show-ui', // 'uw-show-settings-window',
// { // {
// source: this, // source: this,
// function: () => { // function: () => {
@ -376,7 +375,7 @@ export default defineComponent({
//#region EXTENSION POPUP //#region EXTENSION POPUP
showInPlayerUi() { showInPlayerUi() {
this.eventBus.send('uw-set-ui-state', {globalUiVisible: true}, {comms: {forwardTo: 'active'}}); this.eventBus.send('uw-show-settings-window', {initialState: undefined, allFrames: true}, {comms: {forwardTo: 'active'}});
}, },
async sleep(t) { async sleep(t) {
return new Promise<void>( (resolve,reject) => { return new Promise<void>( (resolve,reject) => {

View File

@ -8,13 +8,21 @@
transition: opacity 120ms ease; transition: opacity 120ms ease;
&:not(.uw-visible) { &:not(.uw-visible) {
&:not(.uw-force-visible) {
pointer-events: none; pointer-events: none;
opacity: 0; opacity: 0;
} }
}
&.uw-hidden { &.uw-hidden {
&:not(.uw-force-visible) {
pointer-events: none; pointer-events: none;
opacity: 0; opacity: 0;
} }
&.uw-force-visible {
@apply text-blue-500;
opacity: 0.5;
}
}
&.uw-visible { &.uw-visible {
/* pointer-events: auto; */ /* pointer-events: auto; */
opacity: 1; opacity: 1;
@ -68,13 +76,166 @@
} }
.uw-hidden { .uw-hidden {
&:not(.uw-force-visible) {
display: none !important; display: none !important;
} }
}
.uw-trigger, .uw-menu-item, .uw-menu-trigger { .uw-trigger, .uw-menu-item, .uw-menu-trigger {
pointer-events: auto; pointer-events: auto;
} }
/* Menu + active markers for left/right menus */
.uw-menu-left {
.uw-menu-item {
&.uw-active {
padding-right: 2rem;
&::after {
position: absolute;
display: block;
top: 50%;
right: 0.5rem;
content: '◈';
transform: translateY(calc(-50% + 0.25rem));
}
}
}
.uw-has-submenu {
padding-right: 2rem;
position: relative;
&.uw-active-within {
padding-right: 5rem;
&::after {
position: absolute;
display: block;
top: 50%;
right: 1.5rem;
content: '◈';
/* transform: translateY(-50%); */
transform: translateY(calc(-50% + 0.125rem));
}
}
&::before {
position: absolute;
display: block;
right: 0;
content: '▶';
transform: scale(0.5);
}
}
}
.uw-menu-right {
.uw-menu-item {
padding-left: 2rem;
&.uw-active {
padding-right: 2rem;
&::after {
position: absolute;
display: block;
top: 50%;
right: 0.5rem;
content: '◈';
transform: translateY(calc(-50% + 0.25rem));
}
}
}
.uw-has-submenu {
&.uw-active-within {
padding-right: 2rem;
&::after {
position: absolute;
display: block;
top: 50%;
right: 0.5rem;
content: '◈';
transform: translateY(calc(-50% + 0.125rem));
}
}
position: relative;
&::before {
position: absolute;
display: block;
left: 0;
content: '◀';
transform: scale(0.5);
}
}
}
/* Menus on top/center and bottom/center */
.uw-menu-center {
.uw-menu-top {
.uw-has-submenu {
position: relative;
&::before {
position: absolute;
display: block;
bottom: 0;
left: 50%;
content: '▼';
transform: scale(0.5) translateX(-50%);
}
/* We don't add :after marker, as it's already used for other purposes */
/* &.uw-active-within {
&::before {
position: absolute;
display: block;
bottom: 0;
left: 50%;
content: '◈';
transform: translateX(-50%);
}
} */
}
}
.uw-menu-bottom {
.uw-menu-item {
padding-top: 1.5rem;
}
.uw-has-submenu {
position: relative;
&::before {
position: absolute;
display: block;
top: 0;
left: 50%;
content: '▲';
transform: scale(0.5) translateX(-50%);
}
/* We don't add :after marker, as it's already used for other purposes */
/* &.uw-active-within {
&::after {
position: absolute;
display: block;
top: 0;
left: 50%;
content: '◈';
transform: translateX(-50%);
}
} */
}
}
}
.uw-submenu { .uw-submenu {
@apply absolute flex-col gap-0; @apply absolute flex-col gap-0;
@ -106,13 +267,28 @@
} }
&.uw-menu-center { &.uw-menu-center {
@apply left-1/2 -translate-x-1/2 flex-row; @apply flex-row;
left: 50%;
transform: translateX(-50%);
&.uw-menu-top { &.uw-menu-top {
@apply bottom-0; @apply top-full;
} }
&.uw-menu-bottom { &.uw-menu-bottom {
@apply top-0; @apply bottom-full;
}
.uw-menu-item {
@apply px-8 relative;
&:not(:first-child):after {
content: '·';
@apply absolute;
left: -1rem;
transform: translateX(50%);
}
} }
} }
@ -122,6 +298,7 @@
} }
.uw-menu-item, .uw-menu-trigger { .uw-menu-item, .uw-menu-trigger {
@apply cursor-pointer select-none;
&:hover > .uw-submenu { &:hover > .uw-submenu {
display: flex; display: flex;
@ -132,6 +309,12 @@
} }
} }
.uw-menu-item {
&.uw-active-within, &.uw-active {
@apply text-primary-500;
}
}
.uw-trigger { .uw-trigger {
@apply p-4 relative block @apply p-4 relative block
@ -265,8 +448,14 @@
stroke: transparent; stroke: transparent;
} }
.selected { .uw-active {
@apply fill-primary-400 stroke-primary-400; @apply fill-primary-400 stroke-primary-400 text-primary-400 bg-primary-400;
path {
@apply fill-primary-300 stroke-primary-100;
filter: drop-shadow(0 0 0.5rem theme('colors.primary.400'));
}
} }
} }
} }

View File

@ -1,13 +1,6 @@
<script lang="ts"> <script lang="ts">
export default { export default {
methods: { methods: {
handleConfigBroadcast(message) {
if (message.type === 'ar') {
this.resizerConfig.crop = message.config;
}
this.$nextTick( () => this.$forceUpdate() );
},
/** /**
* Sends commands to main content script in parent iframe * Sends commands to main content script in parent iframe
@ -15,7 +8,7 @@ export default {
*/ */
execAction(command) { execAction(command) {
const cmd = JSON.parse(JSON.stringify(command)); const cmd = JSON.parse(JSON.stringify(command));
this.eventBus?.sendToTunnel(cmd.action, cmd.arguments); this.eventBus?.send(cmd.action, cmd.arguments);
}, },
} }
} }

View File

@ -0,0 +1,25 @@
<script lang="ts">
import { SiteSettings } from '@src/ext/module/settings/SiteSettings'
export default {
data() {
return {
warnings: {
drm: false,
cors: false,
settingsUpdated: false,
extensionDisabled: {
allDisabled: false, // there's no way extension can run on this page
iframesAllowed: false, // page is blacklisted, but embedded content isn't
iframesBlacklisted: false, // page is enabled, but contains embedded content from blacklisted domains
}
}
}
},
methods: {
generateExtensionDisabledWarnings(site: {host: string, hostnames: string[]}) {
const siteSettings = new SiteSettings(this.settings, {site: site.host});
}
}
}
</script>

View File

@ -31,7 +31,7 @@ export function setVideoAlignmentIndicatorState(
y: VideoAlignmentType y: VideoAlignmentType
) { ) {
// reset all indicators // reset all indicators
svg.querySelectorAll<SVGGElement>('g').forEach(g => g.classList.remove('selected')); svg?.querySelectorAll<SVGGElement>('g').forEach(g => g.classList.remove('uw-active'));
// select the appropriate square // select the appropriate square
if (x === VideoAlignmentType.Default || y === VideoAlignmentType.Default) { if (x === VideoAlignmentType.Default || y === VideoAlignmentType.Default) {
@ -39,8 +39,10 @@ export function setVideoAlignmentIndicatorState(
} }
const gId = `${positionMap[y]}-${positionMap[x]}`; const gId = `${positionMap[y]}-${positionMap[x]}`;
const selected = svg.getElementById(gId); const selected = svg?.getElementById(gId);
if (selected) selected.classList.add('selected'); if (selected) {
selected.classList.add('uw-active');
}
} }
/** /**
@ -52,7 +54,7 @@ export function setupVideoAlignmentIndicatorInteraction(
svg: SVGSVGElement, svg: SVGSVGElement,
callback: (x: VideoAlignmentType, y: VideoAlignmentType) => void callback: (x: VideoAlignmentType, y: VideoAlignmentType) => void
) { ) {
svg.querySelectorAll<SVGGElement>('g').forEach(g => { svg?.querySelectorAll<SVGGElement>('g').forEach(g => {
g.addEventListener('click', () => { g.addEventListener('click', () => {
const [y, x] = g.id.split('-'); const [y, x] = g.id.split('-');

View File

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