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",
"version": "6.3.997",
"version": "6.3.998",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ultrawidify",
"version": "6.3.997",
"version": "6.3.998",
"dependencies": {
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@mdi/font": "^7.4.47",

View File

@ -1,6 +1,6 @@
{
"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.",
"author": "Tamius Han <tamius.han@gmail.com>",
"scripts": {

View File

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

View File

@ -4,7 +4,8 @@ enum VideoAlignmentType {
Right = 2,
Top = 3,
Bottom = 4,
Default = -1
Default = -1,
Custom = -2,
};
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 {
label: string;
export interface MenuItemBaseConfig {
label?: string;
subitems?: MenuItemConfig[];
command?: CommandInterface,
action?: () => void;
customHTML?: HTMLElement | string;
customId?: string;
customClassList?: string;
}
export type MenuItemConfig = MenuItemBaseConfig & ({label: string} | {customHTML: HTMLElement | string});
export enum MenuPosition {
TopLeft = 'top-left',
Left = 'left',
@ -23,6 +26,6 @@ export enum MenuPosition {
export interface MenuConfig {
isGlobal?: boolean;
ui: InPlayerUIOptions;
menuPosition: MenuPosition;
options?: {forceShow?: boolean};
items: MenuItemConfig[];
}

View File

@ -24,6 +24,7 @@ export interface EventBusContext {
// port?: string;
visitedBusses?: string[];
commandId?: string;
comms?: {
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 { PlayerDetectionMode } from '@src/common/enums/PlayerDetectionMode.enum';
import { InputHandlingMode } from '@src/common/enums/InputHandlingMode.enum';
import { MenuPosition } from '@src/common/interfaces/ClientUiMenu.interface';
export enum ExtensionEnvironment {
Normal = ExtensionMode.All,
@ -309,14 +310,15 @@ interface DevSettings {
}
export interface InPlayerUIOptions {
activatorAlignment: 'left' | 'right',
activatorAlignment: MenuPosition,
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
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,
activationDistanceUnits: '%' | 'px',
activatorPadding: 10,
activatorPaddingUnit: '%' | 'px',
activatorPadding: {x: number, y: number}
activatorPaddingUnit: {x: '%' | 'px', y: '%' | 'px'},
triggerZoneDimensions: { // how large the trigger zone is (relative to player size)
width: number
height: number,
@ -444,6 +446,8 @@ interface SettingsInterface {
}
export interface SiteSettingsInterface {
notes?: string; // any special things related to this site.
enable: ExtensionMode;
enableAard: ExtensionMode;
enableKeyboard: InputHandlingMode;

View File

@ -1,5 +1,5 @@
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 KeyboardHandler from '@src/ext/module/kbm/KeyboardHandler';
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.eventBus = new EventBus({name: 'content-script'});
this.eventBus = new EventBus({name: 'content-script', commsOrigin: CommsOrigin.ContentScript});
this.eventBus.subscribe(
'uw-restart',
{
@ -120,9 +120,12 @@ export default class UWContent {
this.logger.debug('setup', "KeyboardHandler initiated.");
this.globalUi = new UI('ultrawidify-global-ui', {eventBus: this.eventBus, isGlobal: true});
this.globalUi.enable();
this.globalUi.setUiVisibility(false);
if (this.globalUi) {
this.globalUi.destroy();
}
// this.globalUi = new UI('ultrawidify-global-ui', {eventBus: this.eventBus, isGlobal: true});
// this.globalUi.enable();
// this.globalUi.setUiVisibility(false);
} catch (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 { HostInfo } from '@src/common/interfaces/HostData.interface';
import { ExtensionEnvironment } from '@src/common/interfaces/SettingsInterface';
import { CommsOrigin } from '@src/ext/module/comms/CommsClient';
const BASE_LOGGING_STYLES = {
@ -81,7 +82,7 @@ export default class UWServer {
this.settings = new Settings({logAggregator: this.logAggregator});
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);
@ -105,6 +106,11 @@ export default class UWServer {
//#region CSS management
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)
if (!css) {
return;
@ -125,6 +131,11 @@ export default class UWServer {
}
}
async removeCss(css, sender) {
if (!sender?.tab) {
// console.warn('invalid removeCss received!');
return;
}
try {
await chrome.scripting.removeCSS({
target: {
@ -141,6 +152,10 @@ export default class UWServer {
}
}
async replaceCss(oldCss, newCss, sender) {
if (!sender?.tab) {
// console.warn('invalid replaceCss received!');
return;
}
if (oldCss !== newCss) {
this.removeCss(oldCss, sender);
this.injectCss(newCss, sender);
@ -197,6 +212,10 @@ export default class UWServer {
}
registerVideo(sender) {
if (!sender?.tab?.url) {
// console.warn('invalid registerVideo received!');
return;
}
this.logger.info('registerVideo', 'Registering video.\nsender:', sender);
const tabHostname = this.extractHostname(sender.tab.url);
@ -235,6 +254,11 @@ export default class UWServer {
}
unregisterVideo(sender) {
if (!sender?.tab) {
// console.warn('invalid unregisterVideo received!');
return;
}
this.logger.info('unregisterVideo', 'Unregistering video.\nsender:', sender);
if (this.videoTabs[sender.tab.id]) {
if ( Object.keys(this.videoTabs[sender.tab.id].frames).length <= 1) {
@ -254,8 +278,10 @@ export default class UWServer {
}
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();
// 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 { SiteSupportLevel } from '@src/common/enums/SiteSupportLevel.enum';
import SettingsInterface from '@src/common/interfaces/SettingsInterface';
import { _cp } from '@src/common/utils/_cp';
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 StretchType from '@src/common/enums/StretchType.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 Debug from '@src/ext/conf/Debug';
import { AardPollingOptions } from '@src/ext/module/aard/enums/aard-polling-options.enum';
@ -266,9 +267,9 @@ const ExtensionConf: SettingsInterface = {
activation: 'player',
activationDistance: 100,
activationDistanceUnits: '%',
activatorAlignment: 'left',
activatorPadding: 10,
activatorPaddingUnit: '%',
activatorAlignment: MenuPosition.Left,
activatorPadding: {x: 16, y: 16},
activatorPaddingUnit: {x: 'px', y: 'px'},
triggerZoneDimensions: {
width: 0.5,
height: 0.5,
@ -354,40 +355,6 @@ const ExtensionConf: SettingsInterface = {
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',
label: 'Cycle',
comment: 'Cycle through crop options',
@ -572,6 +539,23 @@ const ExtensionConf: SettingsInterface = {
onKeyUp: true,
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',
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": {
enable: ExtensionMode.All,
enableAard: ExtensionMode.All,

View File

@ -14,6 +14,8 @@ export default class EventBus {
private comms?: CommsClient | CommsServer;
private commsOrigin: CommsOrigin;
private lastExecutedCommandIds: string[] = new Array(32);
private lastExecutedCommandIndex: number = 0;
private disableTunnel: boolean = false;
private popupContext: any = {};
@ -22,7 +24,7 @@ export default class EventBus {
// private uiUri = window.location.href;
constructor(options?: {isUWServer?: boolean, name?: string, commsOrigin?: CommsOrigin}) {
constructor(options?: {isUWServer?: boolean, name?: string, commsOrigin: CommsOrigin}) {
if (!options?.isUWServer) {
this.setupIframeTunnelling();
}
@ -88,10 +90,38 @@ export default class EventBus {
}
}
send(command: string, commandData: any, context: EventBusContext = {}) {
context.visitedBusses = [...context.visitedBusses ?? [], this.uuid];
// execute commands we have subscriptions for
private cloneContext(context: EventBusContext = {}): EventBusContext {
return {
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]) {
for (const eventBusCommand of this.commands[command]) {
eventBusCommand.function(commandData, context);
@ -102,17 +132,30 @@ export default class EventBus {
// CommsServer's job. EventBus does not have enough data for this decision.
// We do, however, have enough data to prevent backflow of messages that
// 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 (
this.comms
&& context?.origin !== CommsOrigin.Server
&& !context?.borderCrossings?.commsServer
&& ( // ensure each message only enters commsServer once!
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 {
this.comms.sendMessage({command, config: commandData, context}, context);
} catch (e) {
if (command !== 'reload-required') {
// 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,
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) {
return;
@ -139,52 +191,6 @@ export default class EventBus {
}
//#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
private setupIframeTunnelling() {
// forward messages coming from iframe tunnels
@ -205,8 +211,8 @@ export default class EventBus {
const payload = event.data.payload as EventBusMessage;
console.info(this.name, 'received message from iframe. command:', payload);
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);
return;
}

View File

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

View File

@ -530,16 +530,26 @@ export class Aard {
// console.warn('DETECTED NOT LETTERBOX! (resetting)')
this.timer.arChanged();
this.updateAspectRatio(this.defaultAr, {forceReset: true});
this.testResults.activeLetterbox.width = 0;
this.testResults.activeLetterbox.offset = 0;
this.testResults.activeLetterbox.orientation = LetterboxOrientation.NotLetterbox;
break processUpdate;
}
if (this.testResults.subtitleDetected && arConf.subtitles.subtitleCropMode !== AardSubtitleCropMode.CropSubtitles) {
if (arConf.subtitles.subtitleCropMode === AardSubtitleCropMode.ResetAR) {
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;
} else if (arConf.subtitles.subtitleCropMode === AardSubtitleCropMode.ResetAndDisable) {
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;
}
@ -568,10 +578,13 @@ export class Aard {
this.testResults.guardLine.front = this.testResults.aspectRatioCheck.frontCandidate;
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();
if (finalAr > 0) {
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 {
this.testResults.aspectRatioInvalid = true;
this.testResults.aspectRatioInvalidReason = finalAr.toFixed(3);
@ -843,6 +856,15 @@ export class Aard {
* @returns
*/
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 maxOffset = ~~(crossDimension * this.settings.active.aard.edgeDetection.maxLetterboxOffset);
const diff = Math.abs(topCandidate - bottomDistance);
@ -858,6 +880,13 @@ export class Aard {
this.testResults.letterboxSize = candidateAvg;
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 {
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(
height,
ssrRegions.top.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;
}
@ -1370,9 +1419,17 @@ export class Aard {
if (this.testResults.letterboxOrientation === LetterboxOrientation.Pillarbox) {
const compensationFactor = compensatedWidth / this.canvasStore.main.width;
const pillarboxCompensated = (this.testResults.letterboxSize * 2 * compensationFactor);
return (compensatedWidth - pillarboxCompensated) / this.canvasStore.main.height;
} else {
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;
}
}

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/>
<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}
`;

View File

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

View File

@ -149,7 +149,11 @@ class CommsClient {
// send to server
if (!context?.borderCrossings?.commsServer) {
try {
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;
}
/**
* 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?.comms.forwardTo === 'all') {
return this.sendToAll(message);
this.sendToAll(message);
break forwardToContentScript;
}
if (context?.comms.forwardTo === 'active') {
return this.sendToActive(message);
this.sendToActive(message);
break forwardToContentScript;
}
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) {
if (context?.comms.forwardTo === 'popup') {
return this.sendToPopup(message);
this.sendToActive(message);
break forwardToContentScript;
}
}
if (context?.origin !== CommsOrigin.Popup) {
this.sendToPopup(message);
}
// okay I lied! Messages originating from content script can be forwarded to
// content scripts running in _other_ frames of the tab
let forwarded = false;
// content scripts running in _other_ frames of the tab.
if (context?.origin === CommsOrigin.ContentScript) {
if (context?.comms.forwardTo === 'all-frames') {
forwarded = true;
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
*/
private async sendToOtherFrames(message, context) {
const sender = context.comms.sourceFrame;
const sender = context.comms?.sourceFrame;
const enrichedMessage = {
message,
_sourceFrame: context.comms.sourceFrame,
_sourceFrame: context.comms?.sourceFrame,
_sourcePort: context.comms.port
}

View File

@ -451,6 +451,18 @@ export class SiteSettings {
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.
* @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 { ScalingParamsBroadcast } from '@src/common/interfaces/ScalingParamsBroadcast.interface';
import extensionCss from '@src/main.css?inline';
import { setVideoAlignmentIndicatorState } from '@src/ui/utils/video-alignment-indicator-handling';
export class ClientMenu {
@ -19,6 +25,7 @@ export class ClientMenu {
private isHovered = false;
private forceShow = false;
private isWithinActivation = false;
private lastMouseMove = performance.now();
private idleTimer?: number;
@ -29,7 +36,11 @@ export class ClientMenu {
private onDocumentMouseLeave?: () => void;
private idleIntervalId?: number;
constructor(private config: MenuConfig) {}
constructor(private config: MenuConfig) {
if (config.options?.forceShow !== undefined) {
this.forceShow = config.options.forceShow;
}
}
mount(anchorElement: HTMLElement) {
this.buildMenuPositionClassList();
@ -173,7 +184,8 @@ export class ClientMenu {
*/
private buildMenuPositionClassList() {
let classList;
switch (this.config.menuPosition) {
console.log('BUILDING MENU POS:', MenuPosition[this.config.ui.activatorAlignment]);
switch (this.config.ui.activatorAlignment) {
case MenuPosition.TopLeft:
classList = ['uw-menu-left','uw-menu-top'];
break;
@ -209,10 +221,14 @@ export class ClientMenu {
private createMenu() {
this.root = document.createElement('div');
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');
trigger.classList = 'uw-menu-trigger uw-trigger';
trigger.style = `margin: ${this.config.ui.activatorPadding ?? 10} ${this.config.ui.activatorPaddingUnit ?? '%'}`;
trigger.textContent = 'Ultrawidify';
this.trigger = trigger;
@ -272,6 +288,7 @@ export class ClientMenu {
}
if (item.subitems) {
el.classList.add('uw-has-submenu');
el.appendChild(this.buildSubmenu(item.subitems));
}
@ -287,11 +304,13 @@ export class ClientMenu {
}
private bindGlobalMouse(anchorEl: HTMLElement) {
// const forceRecheckAfter = 5000;
// let lastRecalculation = performance.now();
const playerRect = anchorEl.getBoundingClientRect();
let menuActivatorRect, cx, cy;
const activationRadius = this.getActivationRadius(anchorEl);
let activationRadius = this.getActivationRadius(anchorEl);
const recalculateActivator = () => {
menuActivatorRect = this.trigger.getBoundingClientRect();
@ -302,13 +321,20 @@ export class ClientMenu {
recalculateActivator();
this.onDocumentMouseMove = (e: MouseEvent) => {
this.lastMouseMove = performance.now();
const now = performance.now();
this.lastMouseMove = now;
if (activationRadius != null) {
if (! menuActivatorRect.width) {
recalculateActivator();
// activateWithCtrl is independent from other options.
// Depending on user settings, UI can be triggered even if we aren't holding CTRL
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);
this.isWithinActivation = d <= activationRadius;
@ -317,8 +343,8 @@ export class ClientMenu {
e.clientX >= playerRect.left &&
e.clientX <= playerRect.right &&
e.clientY >= playerRect.top &&
e.clientY <= playerRect.bottom &&
(this.config.ui.activation !== 'player-ctrl' || e.ctrlKey);
e.clientY <= playerRect.bottom;
}
}
this.updateVisibility();
@ -364,7 +390,7 @@ export class ClientMenu {
}
}
private show() {
show(options?: {forceShow?: boolean}) {
this.lastMouseMove = performance.now();
if (!this.visible) {
@ -372,13 +398,152 @@ export class ClientMenu {
this.root.classList.add('uw-visible');
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) {
this.visible = false;
this.root.classList.remove('uw-visible');
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 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'
if (process.env.CHANNEL !== 'stable'){
@ -39,6 +42,11 @@ class UI {
private extensionMenu: ClientMenu;
private logger: ComponentLogger;
private forwardedCommandIds: string[] = new Array(64);
private lastForwardedCommandIndex = 0;
private currentScalingParams?: ScalingParamsBroadcast;
private uiState = {
lockXY: true,
zoom: { // log2 scale — 100% is 0
@ -68,6 +76,38 @@ class UI {
// UI will be initialized when setUiVisibility is called
if (!this.isGlobal) {
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();
}
private messageListener?: (event?: MessageEvent) => void;
private initMessaging() {
window.addEventListener('message', (event: MessageEvent) => {
if (this.messageListener) {
this.destroyMessaging();
} else {
this.messageListener = (event: MessageEvent) => {
const data = event.data;
if (data?.action !== 'uw-bus-tunnel') {
return;
}
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
(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) {
@ -164,7 +238,7 @@ class UI {
}
}
createExtensionMenu() {
createExtensionMenu(options?: {forceShow?: boolean}) {
if (+this.siteSettings?.data.enableUI === ExtensionMode.Disabled || this.isGlobal) {
return; // don't
}
@ -185,6 +259,7 @@ class UI {
const menuConfig = {
isGlobal: this.isGlobal,
ui: this.settings.active.ui.inPlayer,
options,
items: [
{
customClassList: 'uw-site-info',
@ -223,28 +298,37 @@ class UI {
},
{
label: 'Crop',
customId: 'uw-crop',
subitems: this.settings.active.commands.crop.map((x: CommandInterface) => {
return {
label: x.label,
command: x,
customId: `uw-${x.action}-${x.arguments.type}-${x.arguments.ratio ?? 'x'}`.replaceAll('.', '_'),
action: () => this.executeCommand(x)
}
})
},
{
label: 'Stretch',
customId: 'uw-stretch',
subitems: this.settings.active.commands.stretch.map((x: CommandInterface) => {
return {
label: x.label,
command: x,
customId: `uw-${x.action}-${x.arguments.type}-${x.arguments.ratio ?? 'x'}`.replaceAll('.', '_'),
action: () => this.executeCommand(x)
}
})
},
{
label: 'Zoom (presets)',
customId: 'uw-zoom',
subitems: [
... this.settings.active.commands.zoom.map((x: CommandInterface) => {
return {
label: x.label,
command: x,
customId: `uw-${x.action}-${x.arguments.type}-${x.arguments.ratio ?? 'x'}`.replaceAll('.', '_'),
action: () => this.executeCommand(x)
}
}),
@ -339,18 +423,24 @@ class UI {
action: () => this.createSettingsWindow('about'),
}
]
}
};
this.extensionMenu = new ClientMenu(menuConfig);
this.extensionMenu.mount(this.uiConfig.parentElement);
if (this.currentScalingParams) {
this.updateMenuStatus(this.currentScalingParams);
}
/**
* SETUP MENU INTERACTIONS
*
* Interactions are needed for the following things:
* 0. [ ] 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
* 2. [ ] reset button just needs to reset, no icon changes necessary
* 0. [X] both sliders. Needs to read the state of the sliders and update the labels
* 1. [X] lock X/Y button needs to update icon state of the button and the linked bar
* 2. [X] reset button just needs to reset, no icon changes necessary
* 3. [X] alignment indicator also needs to update indicator state
* A
* |
* (yay done!)
*/
const menuElement = this.extensionMenu.root;
@ -360,6 +450,24 @@ class UI {
const zoomWidthLabel: HTMLDivElement = menuElement.querySelector('#zoomWidth');
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 = () => {
zoomWidthLabel.textContent = `${Math.round((Math.exp(this.uiState.zoom.x) * 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) {
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.setAttribute('allowtransparency', 'true');
Object.assign(iframe.style, {
@ -447,10 +574,7 @@ class UI {
new UwuiWindow({
title: `Ultrawidify settings (${window.location.host})`,
width: 1200,
height: 800,
x: 0,
y: 0,
...params,
content: iframe,
onClose: () => {
this.eventBus.cancelIframeForwarding(iframe)
@ -465,10 +589,6 @@ class UI {
});
}
setUiVisibility(visible) {
return;
}
async enable() {
// if root element is not present, we need to init the UI.
// if (!this.rootDiv) {
@ -489,6 +609,7 @@ class UI {
* @param {*} newUiConfig
*/
replace(newUiConfig) {
this.destroy();
this.uiConfig = newUiConfig;
this.init();
}
@ -497,6 +618,7 @@ class UI {
if (this.extensionMenu) {
this.extensionMenu.destroy();
}
this.destroyMessaging();
}

View File

@ -99,14 +99,14 @@ export default class IframeManager {
} else {
this.iframeList.push({
...data,
...context.comms.sourceFrame
...context.comms?.sourceFrame
});
}
}
private handleFrameDestroyed(context) {
// 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 markedElement: HTMLElement;
private markedElementIndex: number | undefined;
private ui: UI;
@ -114,7 +115,7 @@ class PlayerData {
//#region event bus configuration
private eventBusCommands = {
'get-player-tree': [{
function: () => this.handlePlayerTreeRequest()
function: (data) => this.handlePlayerTreeRequest(data)
}],
'get-player-dimensions': [{
function: () => {
@ -194,6 +195,10 @@ class PlayerData {
//#region lifecycle
constructor(videoData) {
if (!(window as any).uiCount) {
(window as any).uiCount = 1;
}
try {
// set all our helper objects
this.logger = new ComponentLogger(videoData.logAggregator, 'PlayerData', {styles: {}});
@ -530,6 +535,7 @@ class PlayerData {
private getElementStack(): ElementStack {
const elementStack: ElementStack = [{
index: 0,
element: this.videoElement,
type: 'video',
tagName: 'video',
@ -540,8 +546,10 @@ class PlayerData {
let element = this.videoElement.parentNode as HTMLElement;
// first pass to generate the element stack and translate it into array
let i = 1;
while (element) {
elementStack.push({
index: i,
element,
type: '',
tagName: element.tagName,
@ -552,6 +560,7 @@ class PlayerData {
heuristics: {},
});
element = element.parentElement;
i++;
}
this.elementStack = elementStack;
@ -610,6 +619,7 @@ class PlayerData {
// on verbose, get both qs and index player
if (options?.verbose) {
this.getPlayerAuto(elementStack, videoHeight, videoHeight, {listOnly: true});
if (playerIndex) {
playerCandidate = elementStack[playerIndex];
playerCandidate.heuristics['manualElementByParentIndex'] = true;
@ -622,6 +632,7 @@ class PlayerData {
if (detectionMode === PlayerDetectionMode.AncestorIndex) {
playerCandidate = elementStack[playerIndex];
playerCandidate.heuristics['manualElementByParentIndex'] = true;
playerCandidate.heuristics['activePlayer'] = true;
} else if (detectionMode === PlayerDetectionMode.QuerySelectors) {
playerCandidate = this.getPlayerQs(playerQs, elementStack, videoWidth, videoHeight);
}
@ -677,7 +688,7 @@ class PlayerData {
* @param videoHeight
* @returns
*/
private getPlayerAuto(elementStack: ElementStack, videoWidth, videoHeight) {
private getPlayerAuto(elementStack: ElementStack, videoWidth, videoHeight, options?: {listOnly?: boolean}) {
let penaltyMultiplier = 2;
const sizePenaltyMultiplier = 0.1;
const perLevelScorePenalty = 10;
@ -791,7 +802,7 @@ class PlayerData {
// 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.
// TODO: Ideally, observer should request a tick
if (bestCandidate) {
if (bestCandidate && !options?.listOnly) {
const observer = new MutationObserver(
(mutations) => {
mutations.forEach((mutation) => {
@ -868,30 +879,67 @@ class PlayerData {
bestCandidate.heuristics['qsMatch'] = true;
}
if (bestCandidate) {
bestCandidate.heuristics['activePlayer'] = true;
}
return bestCandidate;
}
/**
* 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.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}) {
if (data.enable === false) {
this.markedElement.remove();
this.elementStack[this.markedElementIndex]?.element.classList.remove('uw-mark-element');
this.markedElementIndex = undefined;
return;
}
if (this.markedElement) {
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);
@ -910,6 +958,8 @@ class PlayerData {
document.body.insertBefore(div, document.body.firstChild);
this.markedElement = div;
// 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;
}

View File

@ -7,6 +7,7 @@ import { RunLevel } from '@src/ext/enum/run-level.enum';
import { Aard } from '@src/ext/module/aard/Aard';
import { AardLegacy } from '@src/ext/module/aard/AardLegacy';
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 { ComponentLogger } from '@src/ext/module/logging/ComponentLogger';
import { LogAggregator } from '@src/ext/module/logging/LogAggregator';
@ -46,6 +47,7 @@ class VideoData {
videoLoaded: boolean = false;
videoDimensionsLoaded: boolean = false;
active: boolean = false;
private preventVideoOffsetValidation: boolean = false;
//#endregion
//#region misc stuff
@ -147,7 +149,7 @@ class VideoData {
};
if (!pageInfo.eventBus) {
this.eventBus = new EventBus({name: 'video-data'});
this.eventBus = new EventBus({name: 'video-data', commsOrigin: CommsOrigin.ContentScript});
} else {
this.eventBus = pageInfo.eventBus;
}
@ -298,10 +300,9 @@ class VideoData {
initializeObservers() {
try {
if (BrowserDetect.firefox) {
this.observer = new ResizeObserver(
_.debounce(
this.onVideoDimensionsChanged,
() => this.onVideoDimensionsChanged,
250,
{
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) {
console.error('[VideoData] Observer setup failed:', e);
}
@ -332,11 +317,13 @@ class VideoData {
}
setupMutationObserver() {
if (this.mutationObserver) {
this.mutationObserver.disconnect();
}
try {
if (BrowserDetect.firefox) {
this.mutationObserver = new MutationObserver(
_.debounce(
this.onVideoMutation,
() => this.onVideoMutation(),
250,
{
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) {
console.error('[VideoData] Observer setup failed:', e);
}
@ -372,6 +343,17 @@ class VideoData {
destroy() {
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) {
this.video.classList.remove(this.userCssClassName);
this.video.classList.remove('uw-ultrawidify-base-wide-screen');
@ -381,8 +363,6 @@ class VideoData {
this.video.removeEventListener('ontimeupdate', this.onTimeUpdate);
}
this.eventBus.send('set-run-level', RunLevel.Off);
this.destroyed = true;
this.eventBus.unsubscribeAll(this);
try {
@ -397,9 +377,6 @@ class VideoData {
try {
this.player.destroy();
} catch (e) {}
try {
this.observer.disconnect();
} catch (e) {}
this.player = undefined;
this.video = undefined;
}
@ -463,7 +440,7 @@ class VideoData {
this.runLevel = runLevel;
if (!options?.fromPlayer) {
this.player.setRunLevel(runLevel);
this.player?.setRunLevel(runLevel);
}
}
@ -532,6 +509,14 @@ class VideoData {
}
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.
let confirmAspectRatioRestore = false;
@ -550,7 +535,7 @@ class VideoData {
return;
}
for(const mutation of mutationList) {
for (const mutation of mutationList) {
if (mutation.type === 'attributes') {
if( mutation.attributeName === 'class'
&& mutation.oldValue.indexOf(this.baseCssName) !== -1
@ -610,7 +595,9 @@ class VideoData {
// sometimes something fucky wucky happens and mutations aren't detected correctly, so we
// try to get around that
this.preventVideoOffsetValidation = true;
setTimeout( () => {
this.preventVideoOffsetValidation = false;
this.validateVideoOffsets();
}, 100);
}
@ -631,6 +618,9 @@ class VideoData {
}
validateVideoOffsets() {
if (this.preventVideoOffsetValidation) {
return;
}
// validate if current video still exists. If not, we destroy current object
try {
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 VideoAlignmentType from '@src/common/enums/VideoAlignmentType.enum';
import { Ar, ArVariant } from '@src/common/interfaces/ArInterface';
import { ScalingParamsBroadcast } from '@src/common/interfaces/ScalingParamsBroadcast.interface';
import { Stretch } from '@src/common/interfaces/StretchInterface';
import getElementStyles from '@src/common/utils/getElementStyles';
import Debug from '@src/ext/conf/Debug';
@ -63,6 +64,16 @@ class Resizer {
currentVideoSettings: any;
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};
@ -100,6 +111,11 @@ class Resizer {
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': [{
function: (config: any) => {
this.manualZoom = false; // this only gets called from UI or keyboard shortcuts, making this action safe.
@ -149,6 +165,11 @@ class Resizer {
}],
'set-zoom': [{
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});
}
}],
@ -278,16 +299,21 @@ class Resizer {
let fileAr = this.getFileAr();
if (ar.type === AspectRatioType.FitWidth){
switch (ar.type) {
case AspectRatioType.FitWidth:
ar.ratio = ratioOut > fileAr ? ratioOut : fileAr;
}
else if(ar.type === AspectRatioType.FitHeight){
break;
case AspectRatioType.FitHeight:
ar.ratio = ratioOut < fileAr ? ratioOut : fileAr;
}
else if(ar.type === AspectRatioType.Reset){
break;
case AspectRatioType.Cover:
ar.ratio = Math.max(ratioOut, fileAr);
break;
case AspectRatioType.Reset:
this.logger.info('modeToAr', "Using original aspect ratio -", fileAr);
ar.ratio = fileAr;
} else {
break;
default:
return null;
}
@ -334,7 +360,7 @@ class Resizer {
return true;
}
async setAr(ar: Ar, lastAr?: Ar) {
async setAr(ar: Ar, lastAr?: Ar, flags?: {manualZoom?: boolean}) {
if (this.destroyed || ar == null) {
return;
}
@ -375,9 +401,13 @@ class Resizer {
return;
}
if (ar.type !== AspectRatioType.AutomaticUpdate) {
if (ar.type !== AspectRatioType.AutomaticUpdate && !flags?.manualZoom) {
if (flags?.manualZoom) {
this.manualZoom = true;
} else {
this.manualZoom = false;
}
}
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.`);
@ -424,7 +454,7 @@ class Resizer {
this.logger.info('setAr', `<rid:${this.resizerId}> Something wrong with ar or the player. Doing nothing.`);
return;
}
this.lastAr = {type: ar.type, ratio: ar.ratio};
this.lastAr = {type: ar.type, ratio: ar.ratio, variant: ar.variant};
}
if (! this.video) {
@ -495,19 +525,36 @@ class Resizer {
try {
const translate = this.computeOffsets(stretchFactors, options?.ar);
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) {
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
}
}
toFixedAr() {
toFixedAr(flags?: {manualZoom?: boolean}) {
// converting to fixed AR means we also turn off autoAR
this.setAr({
this.setAr(
{
ratio: this.lastAr.ratio ?? this.getFileAr(),
type: AspectRatioType.Fixed
});
},
undefined,
flags
);
if (flags?.manualZoom) {
this.manualZoom = true;
}
}
resetLastAr() {

View File

@ -71,7 +71,7 @@ class Zoom {
this.scale = Math.pow(2, this.logScale);
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.scaleY = Math.min(Math.max(scaleIn.y, MIN_SCALE), MAX_SCALE);
this.processZoom();
this.processZoom({manualZoom: true});
}
processZoom() {
this.conf.resizer.toFixedAr();
processZoom(flags?: {manualZoom?: boolean}) {
this.conf.resizer.toFixedAr(flags);
this.conf.resizer.applyScaling({xFactor: this.scale, yFactor: this.scaleY}, {noAnnounce: true});
}
}

View File

@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "Ultrawidify",
"description": "Removes black bars on ultrawide videos and offers advanced options to fix aspect ratio.",
"version": "6.3.997",
"version": "6.3.998",
"icons": {
"32":"res/icons/uw-32.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: {
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() {
chrome.runtime.openOptionsPage();

View File

@ -160,13 +160,35 @@
:settings="settings"
></OtherSiteSettings>
<!--
<PlayerElementSettings
v-if="selectedTab === 'settings.player-element-settings'"
:settings="settings"
:eventBus="eventBus"
>
</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
v-if="selectedTab === 'autodetectionSettings'"
@ -234,7 +256,6 @@ import { defineComponent } from 'vue';
import VideoSettings from '@components/segments/VideoSettings/VideoSettings.vue';
import OtherSiteSettings from '@components/segments/ExtensionSettings/Panels/OtherSiteSettings.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 UISettings from '@components/segments/UISettings/UISettings.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 Debugging from '@components/segments/Debugging/Debugging.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';
@ -250,6 +273,7 @@ import About from '@components/segments/ExtensionInfo/About.vue';
// not component:
import BrowserDetect from '@src/ext/conf/BrowserDetect'
import WarningsMixin from '../utils/mixins/WarningsMixin.vue';
const AVAILABLE_TABS = {
@ -281,11 +305,17 @@ const AVAILABLE_TABS = {
{ 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'},
'ui-settings': {id: 'ui-settings', label: 'UI settings', icon: 'movie-cog-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'},
'updated': { id: 'updated', label: 'Update completed', icon: 'update'},
@ -299,7 +329,7 @@ const AVAILABLE_TABS = {
const TAB_LOADOUT = {
'settings': [
'default-extension-settings',
'settings.player-element-settings',
// 'settings.player-element-settings',
'autodetectionSettings',
'ui-settings',
'keyboardShortcuts',
@ -343,7 +373,8 @@ export default defineComponent({
VideoSettings,
OtherSiteSettings,
PlayerElementSettings,
PlayerElementWindow,
PlayerSelectorAdvancedForm,
PlayerSelectorSimple,
AutodetectionSettings,
KeyboardShortcutSettings,
UISettings,
@ -357,7 +388,9 @@ export default defineComponent({
WhatsNew,
About,
},
mixins: [],
mixins: [
WarningsMixin
],
data() {
return {
statusFlags: {

View File

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

View File

@ -36,6 +36,21 @@
<li>
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>
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>
<b class="text-white">Other updates and fixes:</b>

View File

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

View File

@ -1,141 +1,165 @@
<template>
<div class="w-full flex flex-row" style="margin-top: 1rem;">
<div class="w-full flex flex-col" style="margin-top: 1rem;">
<h2 class="text-[1.75em]">Simple video player picker</h2>
<template v-if="tutorialStep">
<div v-if="tutorialStep == 1" class="flex flex-col w-full justify-center items-center tutorial-step">
<h3 class="mb-4">1. Start hovering over elements on this list</h3>
<div class="flex flex-row w-full flex-wrap justify-center">
<div class="bg-black/50 rounded flex flex-col justify-center items-center p-4">
<div class="">
<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>
<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" class="w-1/2 min-w-[420px]" />
</div>
</div>
<div class="p-4 flex flex-row w-full gap-2 item-center justify-center">
<button @click="tutorialStep = 2">Next</button>
</div>
</div>
<div v-if="tutorialStep == 2" class="flex flex-col w-full justify-center items-center tutorial-step">
<h3 class="mb-4">2. Observe highlight</h3>
<div class="grid grid-cols-2 gap-2 tutorial-list">
<div class="bg-black/50 rounded flex flex-col justify-end items-center p-4">
<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 text-teal-500">
<mdicon name="check-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 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 text-red-700">
<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>
</div>
<div class="p-4 flex flex-row w-full gap-2 item-center justify-center">
<button @click="tutorialStep = 1">Previous</button>
<button @click="tutorialStep = 0">Done</button>
</div>
</div>
</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 style="width: 48%">
<div class="sub-panel-content">
<div v-if="showAdvancedOptions" style="display: flex; flex-direction: row">
<div style="display: flex; flex-direction: column">
<div>
<input :checked="playerManualQs"
@change="togglePlayerManualQs"
type="checkbox"
/>
<div>
Use <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors" target="_blank">CSS selector</a> for player<br/>
<small>If defining multiple selectors, separate them with commas.</small>
</div>
</div>
<div>Selector</div>
<input type="text"
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 style="display: flex; flex-direction: row;">
<div class="element-tree">
<table>
<thead>
<tr>
<th>
<div class="status-relative">
Status <mdicon name="help-circle" @click="showLegend = !showLegend" />
<div v-if="showLegend" class="element-symbol-legend">
<b>Symbols:</b><br />
<mdicon name="alert-remove" class="invalid" /> Element of invalid dimensions<br />
<mdicon name="refresh-auto" class="auto-match" /> Ultrawidify's player detection thinks this should be the player<br />
<mdicon name="bookmark" class="parent-offset-match" /> Site settings say this should be the player (based on counting parents)<br />
<mdicon name="crosshairs" class="qs-match" /> Site settings say this should be the player (based on query selectors)<br />
<mdicon name="check-circle" class="activePlayer" /> Element that is actually the player
</div>
</div>
</th>
<th>Element</th>
<!-- <th>Actions</th> -->
<!-- <th>Quick fixes</th> -->
</tr>
</thead>
<tbody>
<tr
<div class="flex flex-col-reverse gap-2">
<div
v-for="(element, index) of elementStack"
:key="index"
class="element-row"
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)"
>
<td>
<div class="status">
<div
v-if="element.heuristics?.invalidSize"
class="invalid"
v-if="element.heuristics?.activePlayer"
class="text-primary-300 flex flex-row gap-2"
>
<mdicon name="alert-remove" />
<mdicon name="check-circle" /> This element is currently treated as player element.
</div>
<div
v-if="element.heuristics?.autoMatch"
class="auto-match"
>
<mdicon name="refresh-auto" />
<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="qs-match"
class="text-emerald-600 flex flex-row gap-2"
>
<mdicon name="crosshairs" />
<mdicon name="crosshairs" /> This element matches query string (advanced settings)
</div>
<div
v-if="element.heuristics?.manualElementByParentIndex"
class="parent-offset-match"
class="text-teal-600 flex flex-row gap-2"
>
<mdicon name="bookmark" />
<mdicon name="bookmark" /> This element has been manually selected as player element.
</div>
<div
v-if="element.heuristics?.activePlayer"
class="activePlayer"
v-if="element.heuristics?.autoMatch"
class="text-blue-500 flex flex-row gap-2"
>
<mdicon name="check-circle" />
<mdicon name="refresh-auto" /> Automatic detections thinks this is the player.
</div>
</div>
</td>
<td>
<div
class="element-data"
@mouseover="markElement(elementStack.length - index - 1, true)"
@mouseleave="markElement(elementStack.length - index - 1, false)"
@click="setPlayer(elementStack.length - index - 1)"
v-if="element.heuristics?.invalidSize"
class="text-red-700 flex flex-row gap-2"
>
<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>
</td>
<td>
<div class="flex flex-row">
</div>
</td>
</tr>
</tbody>
</table>
<div class="element-config">
<mdicon name="alert-remove" /> This element has invalid dimensions
</div>
</div>
<!-- <div class="css-preview">
{{cssStack}}
</div> -->
</div>
</div>
</div>
</template>
</div>
</template>
<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';
export default defineComponent({
@ -145,22 +169,25 @@ export default defineComponent({
data() {
return {
elementStack: [],
elementStacks: {
requestId: undefined,
stacks: []
},
cssStack: [],
showLegend: false,
showAdvancedOptions: false,
tutorialVisible: false,
tutorialStep: 0
tutorialStep: 0,
lastTreeId: undefined,
};
},
computed: {
},
mixins: [],
props: [
'settings', // not used?
'siteSettings',
'frame',
'eventBus',
'site',
'isPopup'
],
created() {
this.eventBus.subscribe(
@ -183,40 +210,78 @@ export default defineComponent({
this.tutorialStep = 0;
},
getPlayerTree() {
if (this.isPopup) {
this.eventBus.send('get-player-tree');
} else {
this.eventBus.sendToTunnel('get-player-tree');
}
this.lastTreeId = crypto.randomUUID()
this.eventBus.send('get-player-tree', {requestId: this.lastTreeId});
},
handleElementStack(configBroadcast) {
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() );
}
}
},
markElement(parentIndex, enable) {
this.eventBus.sendToTunnel('set-mark-element', {parentIndex, enable});
this.eventBus.send('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});
async resetSettings() {
if (!this.siteSettings.raw?.activeDOMConfig) {
console.warn('')
return;
}
// // 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();
// }
await this.siteSettings.setUpdateFlags(['PlayerData']);
await this.siteSettings.set(`DOMConfig.${this.siteSettings.data.activeDOMConfig}.player.detectionMode`, PlayerDetectionMode.Auto, {noSave: true});
await this.siteSettings.set(`DOMConfig.${this.siteSettings.data.activeDOMConfig}.player.ancestorIndex`, undefined);
this.getPlayerTree();
setTimeout( () => this.getPlayerTree(), 500);
setTimeout( () => this.getPlayerTree(), 1000);
},
/**
* Designates new element as player element. Currently, we only need
* '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.

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
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="label">
@ -48,14 +19,14 @@
v-model="settings.active.ui.inPlayer.activation"
@change="saveSettings()"
>
<option value="none">
Never activate UI with mouse movement alone
</option>
<option value="player">
When mouse moves over player, always
</option>
<option value="player-ctrl">
When mouse moves over player, while holding CTRL key
</option>
<option value="distance">
When mouse is close to the menu activator
(experimental) When mouse is close to the menu activator
</option>
<!-- <option value="trigger-zone">
When mouse moves over trigger zone
@ -64,6 +35,18 @@
</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 class="label">
Show menu when mouse is closer than:
@ -85,7 +68,7 @@
>
<select
class="unit-select !min-w-[72px]"
v-model="settings.active.ui.inPlayer.activationDistanceUnit"
v-model="settings.active.ui.inPlayer.activationDistanceUnits"
@change="(event) => saveSettings(true)"
>
<option value="%">%</option>
@ -99,6 +82,92 @@
<button @click="startTriggerZoneEdit()">Edit</button>
</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="label">
Do not show in-player UI when video player is narrower than
@ -159,6 +228,7 @@
</template>
<script lang="ts">
import { MenuPosition } from '@src/common/interfaces/ClientUiMenu.interface';
import BrowserDetect from '@src/ext/conf/BrowserDetect';
import { defineComponent } from 'vue';
@ -167,6 +237,7 @@ export default defineComponent({
},
data() {
return {
MenuPosition,
ghettoComputed: { }
}
},
@ -181,7 +252,12 @@ export default defineComponent({
this.ghettoComputed = {
minEnabledWidth: this.optionalToFixed(this.settings.active.ui.inPlayer.minEnabledWidth * 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: {
forcePositiveNumber(value) {
@ -213,11 +289,13 @@ export default defineComponent({
this.ghettoComputed[key] = this.optionalToFixed(value * 100, 0);
}
},
saveSettings(forceRefresh) {
this.settings.saveWithoutReload();
async saveSettings(forceRefresh) {
await this.settings.saveWithoutReload();
this.eventBus.send('reload-menu');
if (forceRefresh) {
this.$nextTick( () => this.$forceRefresh() );
this.$nextTick( () => this.$forceUpdate() );
}
},
startTriggerZoneEdit() {

View File

@ -7,6 +7,9 @@
<ShortcutButton
v-for="(command, index) of settings?.active.commands.crop"
: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"
:shortcut="getKeyboardShortcutLabel(command)"
@click="execAction(command)"
@ -18,6 +21,9 @@
<ShortcutButton
v-for="(command, index) of settings?.active.commands.zoom"
: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"
:shortcut="getKeyboardShortcutLabel(command)"
@click="execAction(command)"
@ -40,6 +46,10 @@
max="4"
:value="zoom.x"
@input="changeZoom($event.target.value, 'x')"
@pointerdown="zoomUpdatesDisabled = true"
@pointerup="zoomUpdatesDisabled = false"
@pointercancel="zoomUpdatesDisabled = false"
@pointerleave="zoomUpdatesDisabled = false"
/>
</div>
</div>
@ -55,6 +65,10 @@
max="4"
:value="zoom.y"
@input="changeZoom($event.target.value, 'y')"
@pointerdown="zoomUpdatesDisabled = true"
@pointerup="zoomUpdatesDisabled = false"
@pointercancel="zoomUpdatesDisabled = false"
@pointerleave="zoomUpdatesDisabled = false"
/>
</div>
</div>
@ -94,6 +108,9 @@
<ShortcutButton
v-for="(command, index) of settings?.active.commands.stretch"
: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"
:shortcut="getKeyboardShortcutLabel(command)"
@click="execAction(command)"
@ -102,6 +119,7 @@
<h3>Align video</h3>
<div
id="videoAlignmentController"
ref="alignmentSvgContainer"
class="w-full h-[12em] flex flex-row justify-center"
></div>
@ -119,6 +137,10 @@ import KeyboardShortcutParserMixin from '@ui/utils/mixins/KeyboardShortcutParser
import alignmentIndicatorSvg from '!!raw-loader!@ui/res/img/alignment-indicators.svg';
import {setupVideoAlignmentIndicatorInteraction, setVideoAlignmentIndicatorState} from '@ui/utils/video-alignment-indicator-handling';
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({
components: {
@ -139,21 +161,26 @@ export default defineComponent({
return {
zoom: {x: 0, y: 0}, // zoom is logarithmic, so "100%" is represented as 0 instead of 1.
zoomOptions: {
lockAr: false,
lockAr: true,
},
alignmentSvgContainer: undefined as HTMLElement | undefined,
alignmentIndicatorSvg: undefined as SVGSVGElement | undefined,
currentCropCommand: '',
currentStretchCommand: '',
zoomUpdatesDisabled: false,
}
},
created() {
console.log('created ....');
this.eventBus.subscribe(
'uw-config-broadcast',
{
source: this,
function: (config) => this.handleConfigBroadcast(config)
this.eventBus.subscribeMulti({
'broadcast-scaling-params': {
function: (commandData: ScalingParamsBroadcast, context) => {
this.markActiveElements(commandData);
}
);
}
});
},
mounted() {
this.alignmentSvgContainer = this.$refs.alignmentSvgContainer as HTMLElement;
@ -161,10 +188,10 @@ export default defineComponent({
if (this.alignmentSvgContainer) {
this.alignmentSvgContainer.innerHTML = alignmentIndicatorSvg;
const svgElement = this.alignmentSvgContainer.querySelector('svg') as SVGSVGElement;
console.log('svg element:', svgElement);
this.alignmentIndicatorSvg = svgElement;
if (svgElement) {
setupVideoAlignmentIndicatorInteraction(svgElement, (x: VideoAlignmentType, y: VideoAlignmentType) => {
console.log('clicked!');
// Update selection visually
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() {
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: 'x'});
this.eventBus?.sendToTunnel('set-zoom', {zoom: 1});
this.eventBus?.send('set-zoom', {zoom: 1});
},
changeZoom(logZoom: number, axis: 'x' | 'y') {
// we do not use logarithmic zoom elsewhere, therefore we need to convert
@ -210,19 +238,81 @@ export default defineComponent({
if (this.zoomOptions.lockAr) {
this.zoom.x = logZoom;
this.zoom.y = logZoom;
this.eventBus?.sendToTunnel('set-zoom', {zoom: linearZoom});
this.eventBus?.send('set-zoom', {zoom: linearZoom});
} else {
this.zoom[axis] = logZoom;
this.eventBus?.sendToTunnel('set-zoom', {zoom: {[axis]: linearZoom}});
this.eventBus?.send('set-zoom', {zoom: {[axis]: linearZoom}});
}
},
isActiveZoom(command) {
return false;
},
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>

View File

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

View File

@ -186,7 +186,7 @@ export default defineComponent({
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.siteSettings = this.settings.getSiteSettings({site: this.site.host});
@ -271,7 +271,6 @@ export default defineComponent({
this.selectedTab = this.defaultTab;
}
console.log('does event bus exist yet?', this.eventBus);
if (!this.eventBus) {
// inter-frame communication should be set up by eventBus for free
this.eventBus = new EventBus({name: 'ui-window'});
@ -280,7 +279,7 @@ export default defineComponent({
/**
* Subscribe to event bus commands.
* 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({
'set-current-site': {
@ -294,8 +293,7 @@ export default defineComponent({
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});
console.log('set-site received:', this.site, this.siteSettings, 'current path:', this.initialPath);
@ -336,6 +334,7 @@ export default defineComponent({
host: config.site,
frames: [],
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});
@ -350,7 +349,7 @@ export default defineComponent({
console.log('—————————————————————— polling from iframe window!!!!')
this.startSitePolling();
// this.eventBus.subscribe(
// 'uw-show-ui',
// 'uw-show-settings-window',
// {
// source: this,
// function: () => {
@ -376,7 +375,7 @@ export default defineComponent({
//#region EXTENSION POPUP
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) {
return new Promise<void>( (resolve,reject) => {

View File

@ -8,13 +8,21 @@
transition: opacity 120ms ease;
&:not(.uw-visible) {
&:not(.uw-force-visible) {
pointer-events: none;
opacity: 0;
}
}
&.uw-hidden {
&:not(.uw-force-visible) {
pointer-events: none;
opacity: 0;
}
&.uw-force-visible {
@apply text-blue-500;
opacity: 0.5;
}
}
&.uw-visible {
/* pointer-events: auto; */
opacity: 1;
@ -68,13 +76,166 @@
}
.uw-hidden {
&:not(.uw-force-visible) {
display: none !important;
}
}
.uw-trigger, .uw-menu-item, .uw-menu-trigger {
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 {
@apply absolute flex-col gap-0;
@ -106,13 +267,28 @@
}
&.uw-menu-center {
@apply left-1/2 -translate-x-1/2 flex-row;
@apply flex-row;
left: 50%;
transform: translateX(-50%);
&.uw-menu-top {
@apply bottom-0;
@apply top-full;
}
&.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 {
@apply cursor-pointer select-none;
&:hover > .uw-submenu {
display: flex;
@ -132,6 +309,12 @@
}
}
.uw-menu-item {
&.uw-active-within, &.uw-active {
@apply text-primary-500;
}
}
.uw-trigger {
@apply p-4 relative block
@ -265,8 +448,14 @@
stroke: transparent;
}
.selected {
@apply fill-primary-400 stroke-primary-400;
.uw-active {
@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">
export default {
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
@ -15,7 +8,7 @@ export default {
*/
execAction(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
) {
// 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
if (x === VideoAlignmentType.Default || y === VideoAlignmentType.Default) {
@ -39,8 +39,10 @@ export function setVideoAlignmentIndicatorState(
}
const gId = `${positionMap[y]}-${positionMap[x]}`;
const selected = svg.getElementById(gId);
if (selected) selected.classList.add('selected');
const selected = svg?.getElementById(gId);
if (selected) {
selected.classList.add('uw-active');
}
}
/**
@ -52,7 +54,7 @@ export function setupVideoAlignmentIndicatorInteraction(
svg: SVGSVGElement,
callback: (x: VideoAlignmentType, y: VideoAlignmentType) => void
) {
svg.querySelectorAll<SVGGElement>('g').forEach(g => {
svg?.querySelectorAll<SVGGElement>('g').forEach(g => {
g.addEventListener('click', () => {
const [y, x] = g.id.split('-');

View File

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