Compare commits

...

13 Commits

17 changed files with 741 additions and 64 deletions

View File

@ -16,6 +16,7 @@
"decycle", "decycle",
"dinked", "dinked",
"dinks", "dinks",
"Discardable",
"disneyplus", "disneyplus",
"endregion", "endregion",
"equalish", "equalish",

43
package-lock.json generated
View File

@ -3243,8 +3243,7 @@
"asynckit": { "asynckit": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
"dev": true
}, },
"at-least-node": { "at-least-node": {
"version": "1.0.0", "version": "1.0.0",
@ -3375,6 +3374,28 @@
"integrity": "sha512-3AungXC4I8kKsS9PuS4JH2nc+0bVY/mjgrephHTIi8fpEeGsTHBUJeosp0Wc1myYMElmD0B3Oc4XL/HVJ4PV2g==", "integrity": "sha512-3AungXC4I8kKsS9PuS4JH2nc+0bVY/mjgrephHTIi8fpEeGsTHBUJeosp0Wc1myYMElmD0B3Oc4XL/HVJ4PV2g==",
"dev": true "dev": true
}, },
"axios": {
"version": "1.7.9",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
"integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
"requires": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
},
"dependencies": {
"form-data": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
"integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
}
}
}
},
"babel-code-frame": { "babel-code-frame": {
"version": "6.26.0", "version": "6.26.0",
"resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz",
@ -4849,7 +4870,6 @@
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"requires": { "requires": {
"delayed-stream": "~1.0.0" "delayed-stream": "~1.0.0"
} }
@ -5957,8 +5977,7 @@
"delayed-stream": { "delayed-stream": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
"dev": true
}, },
"delegates": { "delegates": {
"version": "1.0.0", "version": "1.0.0",
@ -6997,6 +7016,11 @@
"readable-stream": "^2.3.6" "readable-stream": "^2.3.6"
} }
}, },
"follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="
},
"for-each": { "for-each": {
"version": "0.3.3", "version": "0.3.3",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
@ -9375,14 +9399,12 @@
"mime-db": { "mime-db": {
"version": "1.52.0", "version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
"dev": true
}, },
"mime-types": { "mime-types": {
"version": "2.1.35", "version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dev": true,
"requires": { "requires": {
"mime-db": "1.52.0" "mime-db": "1.52.0"
} }
@ -12810,6 +12832,11 @@
"ipaddr.js": "1.9.1" "ipaddr.js": "1.9.1"
} }
}, },
"proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"prr": { "prr": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",

View File

@ -29,6 +29,7 @@
"@mdi/font": "^6.5.95", "@mdi/font": "^6.5.95",
"@mdi/js": "^6.4.95", "@mdi/js": "^6.4.95",
"@types/resize-observer-browser": "^0.1.6", "@types/resize-observer-browser": "^0.1.6",
"axios": "^1.7.9",
"concurrently": "^5.3.0", "concurrently": "^5.3.0",
"fs-extra": "^7.0.1", "fs-extra": "^7.0.1",
"gl-matrix": "^3.4.3", "gl-matrix": "^3.4.3",

View File

@ -164,20 +164,7 @@ interface SettingsInterface {
arDetect: AardSettings, arDetect: AardSettings,
ui: { ui: {
inPlayer: { inPlayer: InPlayerUISettingsInterface
enabled: boolean,
enabledFullscreenOnly: boolean,
popupAlignment: 'left' | 'right',
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: 'trigger-zone' | 'player', // what needs to be hovered in order for UI to be visible
triggerZoneDimensions: { // how large the trigger zone is (relative to player size)
width: number
height: number,
offsetX: number, // fed to translateX(offsetX + '%'). Valid range [-100, 0]
offsetY: number // fed to translateY(offsetY + '%'). Valid range [-100, 100]
},
}
} }
restrictions?: RestrictionsSettings; restrictions?: RestrictionsSettings;
@ -305,13 +292,16 @@ interface SettingsInterface {
// //
sites: { sites: {
[x: string]: SiteSettingsInterface, [x: string]: SiteSettingsInterface,
} },
telemetry?: UwTelemetryInterface
} }
export interface SiteSettingsInterface { export interface SiteSettingsInterface {
enable: ExtensionEnvironmentSettingsInterface; enable: ExtensionEnvironmentSettingsInterface;
enableAard: ExtensionEnvironmentSettingsInterface; enableAard: ExtensionEnvironmentSettingsInterface;
enableKeyboard: ExtensionEnvironmentSettingsInterface; enableKeyboard: ExtensionEnvironmentSettingsInterface;
ui?: InPlayerUISettingsInterface,
type?: 'official' | 'community' | 'user-defined' | 'testing' | 'officially-disabled' | 'unknown' | 'modified'; type?: 'official' | 'community' | 'user-defined' | 'testing' | 'officially-disabled' | 'unknown' | 'modified';
defaultType: 'official' | 'community' | 'user-defined' | 'testing' | 'officially-disabled' | 'unknown' | 'modified'; defaultType: 'official' | 'community' | 'user-defined' | 'testing' | 'officially-disabled' | 'unknown' | 'modified';
@ -340,6 +330,13 @@ export interface SiteSettingsInterface {
// the following fields are for use with extension update script // the following fields are for use with extension update script
override?: boolean; // whether settings for this site will be overwritten by extension upgrade script override?: boolean; // whether settings for this site will be overwritten by extension upgrade script
workarounds?: {
iframeTransparencyWarningDismissed?: boolean;
disableColorSchemeAwareness?: boolean;
forceColorScheme?: 'normal' | 'light' | 'dark',
lastColorSchemeAwarenessCheck?: Date;
}
} }
export interface PlayerAutoConfigInterface { export interface PlayerAutoConfigInterface {
@ -372,4 +369,28 @@ export interface SiteDOMElementSettingsInterface {
nodeCss?: {[x: string]: string}; nodeCss?: {[x: string]: string};
} }
export interface InPlayerUISettingsInterface {
enabled: boolean,
enabledFullscreenOnly: boolean,
popupAlignment: 'left' | 'right',
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: 'trigger-zone' | 'player', // what needs to be hovered in order for UI to be visible
triggerZoneDimensions: { // how large the trigger zone is (relative to player size)
width: number
height: number,
offsetX: number, // fed to translateX(offsetX + '%'). Valid range [-100, 0]
offsetY: number // fed to translateY(offsetY + '%'). Valid range [-100, 100]
},
}
export interface UwTelemetryInterface {
iframeTransparency?: {
[siteName: string]: {
reportedWithColorSchemeAllowed?: boolean;
reportedWithColorSchemeDisabled?: boolean;
}
}
}
export default SettingsInterface; export default SettingsInterface;

View File

@ -36,6 +36,17 @@
</div> </div>
Ultrawidify Ultrawidify
<div
v-if="!siteSettings?.data.workarounds?.iframeTransparencyWarningDismissed"
class="absolute ui-warning small uw-clickable">
If all you see is white or black screen,<br/>disable UI and report your GPU to github.<br/>Open.<br/>
<br />
<br /> You can also <a href="" @click="iframeAutofix()">click here to attempt a fix</a>.
<br />
<br /> If you experience no issues, <a href="" @click="iframeDismiss()">click here to hide this warning.</a>
</div>
</div> </div>
</template> </template>
@ -452,6 +463,7 @@ export default {
this.sendToParentLowLevel('uw-bus-tunnel', { this.sendToParentLowLevel('uw-bus-tunnel', {
action: 'get-player-dimensions' action: 'get-player-dimensions'
}); });
this.sendToParentLowLevel('uw-get-page-stats', null);
}, },
destroyed() { destroyed() {
@ -585,6 +597,22 @@ export default {
handleBusTunnelIn(payload) { handleBusTunnelIn(payload) {
this.eventBus.send(payload.action, payload.config, payload.routingData); this.eventBus.send(payload.action, payload.config, payload.routingData);
},
async iframeAutofix() {
console.log('site:', this.site, this.siteSettings);
if (this.siteSettings?.data.workarounds?.disableColorSchemeAwareness === false) {
await this.settings.setProp(['sites', this.site, 'workarounds', 'disableColorSchemeAwareness'], true);
} else {
await this.settings.setProp(['sites', this.site, 'workarounds', 'disableColorSchemeAwareness'], false);
}
await this.settings.saveWithoutReload();
this.eventBus.sendToTunnel('uw-reload-window');
},
async iframeDismiss() {
await this.settings.setProp(['sites', this.site, 'workarounds', 'iframeTransparencyWarningDismissed'], true);
await this.settings.save();
this.eventBus.sendToTunnel('uw-reload-window');
} }
} }
} }
@ -656,10 +684,14 @@ export default {
.ui-warning { .ui-warning {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0;
transform: translateY(-100%); transform: translateY(-100%);
max-width: 16rem; max-width: 20rem;
width: 16rem; width: 20rem;
padding: 1rem;
box-sizing: border-box;
overflow: hidden; overflow: hidden;
overflow-wrap: break-word; overflow-wrap: break-word;
@ -667,6 +699,17 @@ export default {
white-space: normal; white-space: normal;
word-break: break-word; word-break: break-word;
word-wrap: break-word; word-wrap: break-word;
&.small {
font-size: 0.85rem;
color: #888;
background-color: rgba(0,0,0,0.69);
a {
color: #fa6;
opacity: 0.69;
}
}
} }

View File

@ -16,6 +16,27 @@
<li> <a target="_blank" href="https://github.com/tamius-han/ultrawidify/issues"><b>Github (preferred)</b></a><br/></li> <li> <a target="_blank" href="https://github.com/tamius-han/ultrawidify/issues"><b>Github (preferred)</b></a><br/></li>
<li>Email: <a target="_blank" :href="mailtoLink">tamius.han@gmail.com</a></li> <li>Email: <a target="_blank" :href="mailtoLink">tamius.han@gmail.com</a></li>
</ul> </ul>
<hr />
<b>Is your screen entirely white or entirely black?</b>
<p>This appears to be a rare issue that happens to some people. If you're experiencing this issue, please consider contacting me and sharing the following data:</p>
<ul>
<li>Which sites this problem appears on and whether it happens on youtube. If you use youtube premium, please try signing out of youtube (or use a new profile in Google Chrome) in order to see whether youtube premium is required.</li>
<li>your browser (get full version from <code>chrome://settings/help</code>). if using browsers other than Chrome, please try to reproduce this issue in Chrome</li>
<li>your operating system</li>
<li>your graphics card</li>
<li>whether you're using dark theme in your OS, and whether you're using website's built-in dark/light theme.</li>
</ul>
<p>Please post this info to <a href="https://github.com/tamius-han/ultrawidify/issues/262" target="_blank">this thread</a>, or message me via e-mail.</p>
<p>Then, disable the in-player UI.</p>
<hr />
<p> <p>
When reporting bugs, please include the following information: When reporting bugs, please include the following information:
</p> </p>
@ -67,6 +88,7 @@ export default {
mailtoLink: '', mailtoLink: '',
redditLink: '', redditLink: '',
showEasterEgg: false, showEasterEgg: false,
pageData: {}
} }
}, },
async created() { async created() {

View File

@ -32,7 +32,7 @@
<li>Half-fixed in-player UI on sites where player is not detected correctly</li> <li>Half-fixed in-player UI on sites where player is not detected correctly</li>
</ul> </ul>
<h3>6.2.3</h3> <h3>6.2.3 (current)</h3>
<ul> <ul>
<li>Fixed default persistent mode</li> <li>Fixed default persistent mode</li>
</ul> </ul>

View File

@ -62,7 +62,9 @@ export default {
'uw-page-stats': { 'uw-page-stats': {
function: (data) => { function: (data) => {
console.log('got page statss:', data); console.log('got page statss:', data);
this.pageData = data; this.pageData = JSON.parse(JSON.stringify(data));
this.$nextTick( () => this.$forceUpdate());
} }
} }
}, },

View File

@ -111,7 +111,7 @@ export default class UWContent {
this.logger.log('info', 'debug', "[uw.js::setup] KeyboardHandler initiated."); this.logger.log('info', 'debug', "[uw.js::setup] KeyboardHandler initiated.");
this.globalUi = new UI('ultrawidify-global-ui', {eventBus: this.eventBus, isGlobal: true}); this.globalUi = new UI('ultrawidify-global-ui', {eventBus: this.eventBus, isGlobal: true, siteSettings: this.siteSettings});
this.globalUi.enable(); this.globalUi.enable();
this.globalUi.setUiVisibility(false); this.globalUi.setUiVisibility(false);

View File

@ -5,6 +5,9 @@ import Settings from './lib/Settings';
import Logger, { baseLoggingOptions } from './lib/Logger'; import Logger, { baseLoggingOptions } from './lib/Logger';
import { sleep } from '../common/js/utils'; import { sleep } from '../common/js/utils';
import EventBus, { EventBusCommand } from './lib/EventBus'; import EventBus, { EventBusCommand } from './lib/EventBus';
import { IframeTransparencyVerifier, IframeVerificationPlayerData } from './lib/IframeTransparencyVerifier';
import { MessageSender } from './lib/comms/ChromeTabInterfaces';
import axios from 'axios';
export default class UWServer { export default class UWServer {
settings: Settings; settings: Settings;
@ -18,6 +21,8 @@ export default class UWServer {
videoTabs: any = {}; videoTabs: any = {};
currentTabId: number = 0; currentTabId: number = 0;
IframeTransparencyVerifier: IframeTransparencyVerifier = new IframeTransparencyVerifier()
selectedSubitem: any = { selectedSubitem: any = {
'siteSettings': undefined, 'siteSettings': undefined,
'videoSettings': undefined, 'videoSettings': undefined,
@ -44,6 +49,11 @@ export default class UWServer {
}, },
'get-current-site': { 'get-current-site': {
function: (message, context) => this.getCurrentSite() function: (message, context) => this.getCurrentSite()
},
'verify-iframe-transparency': {
function: (message, context) => {
this.verifyUiTransparency(message.playerData, context.comms.sender, message.telemetryData);
}
} }
}; };
@ -101,7 +111,7 @@ export default class UWServer {
}); });
} }
async injectCss(css, sender) { async injectCss(css, sender: MessageSender) {
if (!css) { if (!css) {
return; return;
} }
@ -133,7 +143,7 @@ export default class UWServer {
this.logger.log('error','debug', '[UwServer::injectCss] Error while injecting css:', {error: e, css, sender}); this.logger.log('error','debug', '[UwServer::injectCss] Error while injecting css:', {error: e, css, sender});
} }
} }
async removeCss(css, sender) { async removeCss(css, sender: MessageSender) {
try { try {
if (BrowserDetect.firefox) { if (BrowserDetect.firefox) {
chrome.scripting.removeCSS({ chrome.scripting.removeCSS({
@ -367,4 +377,39 @@ export default class UWServer {
return hostname; return hostname;
} }
async verifyUiTransparency(verificationData: IframeVerificationPlayerData, sender: MessageSender, telemetryData?: any) {
const transparencyVerificationResult = await this.IframeTransparencyVerifier.verifyUiTransparency(
sender.tab.windowId,
{
player: verificationData,
tab: {
width: sender.tab.width,
height: sender.tab.height,
}
}
);
console.log('Transparency confirmed.');
this.eventBus.send(
'iframe-transparency-results',
{
transparencyVerificationResult
},
{
comms: {
forwardTo: 'active'
}
}
);
// axios.post(
// 'https://uw-telemetry.tamius.net/iframe-transparency',
// {
// ...telemetryData,
// transparencyCheckResult: hasTransparency,
// }
// );
}
} }

View File

@ -1842,6 +1842,9 @@ const ExtensionConf: SettingsInterface = {
} }
} }
}, },
},
telemetry: {
} }
} }

View File

@ -0,0 +1,233 @@
export enum TransparencyVerificationResult {
Ok = 0,
Fail = 1,
NoVisibleElements = 2,
Error = 3
}
export interface IframeVerificationData {
tab: {
width: number,
height: number,
},
player: IframeVerificationPlayerData
}
export interface IframeVerificationPlayerData {
x: number,
y: number,
width: number,
height: number,
}
interface IframeCheckItem {
position: 'top' | 'center' | 'bottom' | 'left' | 'right';
offset: number;
}
interface IframeCheckPosition {
positionX: 'top' | 'center' | 'bottom';
positionY: 'left' | 'center' | 'right';
offsetX: number;
offsetY: number;
}
export class IframeTransparencyVerifier {
async verifyUiTransparency(windowId: number, tabDimensions: IframeVerificationData): Promise<{result: TransparencyVerificationResult, dataUrl?: string}> { console.info('Verifying UI transparency:', tabDimensions);
// const {visibleX, visibleY} = this.getVisibleMarkers(tabDimensions);
// if (!visibleX.length || !visibleY.length) {
// console.warn('[transparency check] No visible elements.');
return {result: TransparencyVerificationResult.NoVisibleElements};
// }
// const checkPositions = this.processMarkers(visibleX, visibleY);
// const dataUrl = await chrome.tabs.captureVisibleTab(
// undefined, // windowId,
// {
// format: "png"
// }
// );
// try {
// const canvas = new OffscreenCanvas(tabDimensions.tab.width, tabDimensions.tab.height);
// const ctx = canvas.getContext('2d')!;
// const res = await fetch(dataUrl);
// const blob = await res.blob();
// const bitmap = createImageBitmap(blob);
// (ctx as any).drawImage(bitmap, 0, 0);
// const imageData = (ctx as any).getImageData(0, 0, tabDimensions.tab.width, tabDimensions.tab.height).data;
// if (this.detectMarkers(checkPositions, tabDimensions.tab.width, imageData)) {
// console.info('Verified transparency');
// return {
// result: TransparencyVerificationResult.Ok,
// dataUrl: dataUrl
// };
// } else {
// console.info('Transparency checks came back negative');
// return {
// result: TransparencyVerificationResult.Fail,
// dataUrl: dataUrl
// };
// }
// } catch (e) {
// console.error('[transparency check] Error while checking for transparency:', e);
// return {result: TransparencyVerificationResult.Error};
// }
}
private getVisibleMarkers({tab, player}: IframeVerificationData) {
const visibleX: IframeCheckItem[] = [];
const visibleY: IframeCheckItem[] = [];
// Determine which markers should be visible.
// Visibility: TOP ROW
console.log('player:', player.y, tab.height);
if (player.y >= 0 && player.y < tab.height) {
visibleY.push({
position: 'top',
offset: player.y,
});
}
// Visibility: CENTER
const yHalfPosition = Math.floor(player.y + player.height / 2);
if (player.y + yHalfPosition - 2 > 0 && player.y + yHalfPosition + 2 < tab.height) {
visibleY.push({
position: 'center',
offset: player.y + yHalfPosition,
});
}
// Visibility: BOTTOM ROW
if (player.y + player.height - 1 > 0 && player.y + player.height + 1 < tab.height) {
visibleY.push({
position: 'bottom',
offset: player.y + player.height - 1,
});
}
// Visibility: LEFT SIDE
if (player.x >= 0 && player.x < tab.width) {
visibleX.push({
position: 'left',
offset: player.x,
});
}
// Visibility: X CENTER
const xHalfPosition = Math.floor(player.x + player.width / 2);
if (player.x + xHalfPosition - 2 > 0 && player.x + xHalfPosition + 2 < tab.width) {
visibleX.push({
position: 'center',
offset: player.x + xHalfPosition,
});
}
// Visibility: RIGHT SIDE
if (player.x + player.width - 1 > 0 && player.x + player.width + 1 < tab.width) {
visibleX.push({
position: 'right',
offset: player.x + player.width - 1,
});
}
console.log('visible candidates', visibleX, visibleY);
return {
visibleX,
visibleY,
};
}
private processMarkers(candidatesX: IframeCheckItem[], candidatesY: IframeCheckItem[]): IframeCheckPosition[] {
if (!candidatesX.length || !candidatesY.length) {
return [] as IframeCheckPosition[];
}
const checkPositions: IframeCheckPosition[] = [];
for (const row of candidatesY) {
for (const col of candidatesX) {
// 'center center' is not valid position.
if (row.position !== col.position) {
checkPositions.push({
positionX: row.position as 'top' | 'center' | 'bottom',
positionY: col.position as 'left' | 'center' | 'right',
offsetX: col.offset,
offsetY: row.offset,
});
}
}
}
return checkPositions;
}
private detectMarkers(checkPositions: IframeCheckPosition[], rowLength: number, imageData: Uint8ClampedArray): boolean {
for (const position of checkPositions) {
if (position.positionY === 'center') {
if (this.detectColumnMarker(position.offsetX, position.offsetY, rowLength, imageData)) {
return true;
}
} else {
if (this.detectRowMarker(position.offsetX, position.offsetY, rowLength, imageData)) {
return true;
}
}
}
return false;
}
// Checks if our magic sequence is present in a row configuration
private detectRowMarker(x, y, rowLength, imageData): boolean {
const start = (y * rowLength + x) * 4;
return imageData[start] === 0
&& imageData[start + 1] === 1
&& imageData[start + 2] === 2
&& imageData[start + 4] === 3
&& imageData[start + 5] === 4
&& imageData[start + 6] === 5
&& imageData[start + 8] === 5
&& imageData[start + 9] === 4
&& imageData[start + 10] === 3
&& imageData[start + 12] === 2
&& imageData[start + 13] === 1
&& imageData[start + 15] === 0;
}
// Checks if our magic sequence is present in a column configuration
private detectColumnMarker(x, y, rowLength, imageData) {
const rowOffset = rowLength * 4;
const r1 = (y * rowLength + x) * 4;
const r2 = r1 + rowOffset;
const r3 = r2 + rowOffset;
const r4 = r3 + rowOffset;
return imageData[r1] === 0
&& imageData[r1 + 1] === 1
&& imageData[r1 + 2] === 2
&& imageData[r2 ] === 3
&& imageData[r2 + 1] === 4
&& imageData[r2 + 2] === 5
&& imageData[r3 ] === 5
&& imageData[r3 + 1] === 4
&& imageData[r3 + 2] === 3
&& imageData[r4 ] === 2
&& imageData[r4 + 1] === 1
&& imageData[r4 + 2] === 0;
}
}

View File

@ -323,8 +323,38 @@ class Settings {
this.active = activeSettings; this.active = activeSettings;
} }
async setProp(prop, value) { /**
this.active[prop] = value; * Sets value of a prop at given path.
* @param propPath path to property we want to set. If prop path does not exist,
* this function will recursively create it. It is assumed that uninitialized properties
* are objects.
* @param value
*/
async setProp(propPath: string | string[], value: any, options?: {forceReload?: boolean}, currentPath?: any) {
if (!Array.isArray(propPath)) {
propPath = propPath.split('.');
}
if (!currentPath) {
currentPath = this.active;
}
const currentProp = propPath.shift();
if (propPath.length) {
if (!currentPath[currentProp]) {
currentPath[currentProp] = {};
}
return this.setProp(propPath, value, options, currentPath[currentProp]);
} else {
currentPath[currentProp] = value;
if (options?.forceReload) {
return this.save();
} else {
return this.saveWithoutReload();
}
}
} }
async save(options?) { async save(options?) {

View File

@ -0,0 +1,31 @@
export interface MessageSender {
documentId?: string,
frameId?: number,
id: string,
origin: string,
tab: Tab,
url: string,
}
export interface Tab {
active: boolean,
audible?: boolean,
autoDiscardable: boolean,
discarded: boolean,
favIconUrl?: string,
frozen?: boolean,
groupId: number,
height: number,
highlighted: boolean,
id?: number,
incognito: boolean,
index: number,
lastAccessed: number,
openerTabId?: number,
pendingUrl?: string,
pinned: boolean,
sessionId: string,
url?: string;
width: number;
windowId?: number;
}

View File

@ -117,6 +117,29 @@ class CommsClient {
this.commsId = (Math.random() * 20).toFixed(0); this.commsId = (Math.random() * 20).toFixed(0);
this.eventBus.subscribeMulti(
{
'uw-get-page-stats': {
function: (config, routingData) => {
this.eventBus.send(
'uw-page-stats',
{
pcsDark: window.matchMedia('(prefers-color-scheme: dark)').matches,
pcsLight: window.matchMedia('(prefers-color-scheme: light)').matches,
colorScheme: window.getComputedStyle( document.body ,null).getPropertyValue('color-scheme')
},
{
comms: {
forwardTo: 'popup'
}
}
);
}
},
},
this
);
} catch (e) { } catch (e) {
console.error("CONSTRUCOTR FAILED:", e) console.error("CONSTRUCOTR FAILED:", e)
} }

View File

@ -15,8 +15,8 @@ if (process.env.CHANNEL !== 'stable'){
// As of 1. 1. 2025, 'light' and 'dark' are commented out in order to force 'csui-overlay-normal' everywhere. // As of 1. 1. 2025, 'light' and 'dark' are commented out in order to force 'csui-overlay-normal' everywhere.
const csuiVersions = { const csuiVersions = {
'normal': 'csui', // csui-overlay-normal.html, maps to csui.html 'normal': 'csui', // csui-overlay-normal.html, maps to csui.html
// 'light': 'csui-light', // csui-overlay-light.html, maps to csui-light.html 'light': 'csui-light', // csui-overlay-light.html, maps to csui-light.html
// 'dark': 'csui-dark' // csui-overlay-dark.html, maps to csui-dark.html 'dark': 'csui-dark' // csui-overlay-dark.html, maps to csui-dark.html
}; };
const MAX_IFRAME_ERROR_COUNT = 5; const MAX_IFRAME_ERROR_COUNT = 5;
@ -29,14 +29,10 @@ class UI {
this.interfaceId = interfaceId; this.interfaceId = interfaceId;
this.uiConfig = uiConfig; this.uiConfig = uiConfig;
this.lastProbeResponseTs = null; this.lastProbeResponseTs = null;
this.isVisible = false;
this.isOpaque = false;
this.isGlobal = uiConfig.isGlobal ?? false; this.isGlobal = uiConfig.isGlobal ?? false;
// TODO: at some point, UI should be different for global popup and in-player UI
const preferredScheme = window.getComputedStyle( document.body ,null).getPropertyValue('color-scheme');
const csuiVersion = csuiVersions[preferredScheme] ?? csuiVersions.normal;
this.uiURI = chrome.runtime.getURL(`/csui/${csuiVersion}.html`);
this.extensionBase = chrome.runtime.getURL('').replace(/\/$/, "");
this.eventBus = uiConfig.eventBus; this.eventBus = uiConfig.eventBus;
this.disablePointerEvents = false; this.disablePointerEvents = false;
@ -44,15 +40,44 @@ class UI {
this.saveState = undefined; this.saveState = undefined;
this.playerData = uiConfig.playerData; this.playerData = uiConfig.playerData;
this.uiSettings = uiConfig.uiSettings; this.uiSettings = uiConfig.uiSettings;
this.siteSettings = uiConfig.siteSettings;
this.settings = uiConfig.settings;
this.iframeErrorCount = 0; this.iframeErrorCount = 0;
this.iframeConfirmed = false; this.iframeConfirmed = false;
this.iframeRejected = false; this.iframeRejected = false;
this.preventHiding = false;
this.wantsToHide = false;
this.wantsToTransparent = false;
this.performedTransparencyCheck = false;
// TODO: at some point, UI should be different for global popup and in-player UI
const csuiVersion = this.getCsuiVersion();
this.uiURI = chrome.runtime.getURL(`/csui/${csuiVersion}.html`);
this.extensionBase = chrome.runtime.getURL('').replace(/\/$/, "");
} }
async init() { async init() {
try {
this.initIframes(); this.initIframes();
this.initMessaging(); this.initMessaging();
} catch (e) {
console.error('failed to init ui:', e)
}
}
getCsuiVersion() {
if (this.siteSettings?.workarounds?.forceColorScheme) {
return csuiVersions[this.siteSettings.workarounds.forceColorScheme];
}
if (this.siteSettings?.data?.workarounds?.disableColorSchemeAwareness !== false) {
return csuiVersions.normal;
}
const preferredScheme = window.getComputedStyle( document.body ,null).getPropertyValue('color-scheme');
return csuiVersions[preferredScheme] ?? csuiVersions.normal;
} }
initIframes() { initIframes() {
@ -94,8 +119,8 @@ class UI {
iframe.style.position = "absolute"; iframe.style.position = "absolute";
iframe.style.zIndex = this.isGlobal ? '90009' : '90000'; iframe.style.zIndex = this.isGlobal ? '90009' : '90000';
iframe.style.border = 0; iframe.style.border = 0;
iframe.style.pointerEvents = 'none';
iframe.style.opacity = 0; iframe.style.opacity = 0;
iframe.style.pointerEvents = 'none';
iframe.style.backgroundColor = 'transparent !important'; iframe.style.backgroundColor = 'transparent !important';
/* so we have a problem: we want iframe to be clickthrough everywhere except /* so we have a problem: we want iframe to be clickthrough everywhere except
@ -158,6 +183,18 @@ class UI {
document.addEventListener('mousemove', fn, true); document.addEventListener('mousemove', fn, true);
} }
// Add some squares to the page.
// Sets up checks for conditions that cause these two mutually exclusive issues:
// * https://github.com/tamius-han/ultrawidify/issues/262
// * https://github.com/tamius-han/ultrawidify/issues/259
for (const x of ['left', 'center', 'right']) {
for (const y of ['top', 'center', 'bottom']) {
if (x !== y) {
rootDiv.appendChild(this.generateDebugMarker(x, y));
}
}
}
rootDiv.appendChild(iframe); rootDiv.appendChild(iframe);
} }
@ -173,6 +210,11 @@ class UI {
this.eventBus.subscribeMulti( this.eventBus.subscribeMulti(
{ {
'uw-reload-window': {
function: () => {
window.location.reload();
}
},
'uw-config-broadcast': { 'uw-config-broadcast': {
function: (config, routingData) => { function: (config, routingData) => {
this.sendToIframe('uw-config-broadcast', config, routingData); this.sendToIframe('uw-config-broadcast', config, routingData);
@ -190,24 +232,6 @@ class UI {
this.sendToIframe('uw-set-ui-state', {...config, isGlobal: this.isGlobal}, routingData); this.sendToIframe('uw-set-ui-state', {...config, isGlobal: this.isGlobal}, routingData);
} }
}, },
'uw-get-page-stats': {
function: (config, routingData) => {
console.log('got get page stats!');
this.eventBus.send(
'uw-page-stats',
{
pcsDark: window.matchMedia('(prefers-color-scheme: dark)').matches,
pcsLight: window.matchMedia('(prefers-color-scheme: light)').matches,
colorScheme: window.getComputedStyle( document.body ,null).getPropertyValue('color-scheme')
},
{
comms: {
forwardTo: 'popup'
}
}
);
}
},
'uw-restore-ui-state': { 'uw-restore-ui-state': {
function: (config, routingData) => { function: (config, routingData) => {
if (!this.isGlobal) { if (!this.isGlobal) {
@ -215,8 +239,26 @@ class UI {
this.sendToIframe('uw-restore-ui-state', config, routingData); this.sendToIframe('uw-restore-ui-state', config, routingData);
} }
} }
},
'iframe-transparency-results': {
function: (data, routingData) => {
console.log('——————————— iframe transparency results are back!', data);
} }
}, },
'uw-get-page-stats': {
function: (config, routingData) => {
console.log('uw:Č Got page stats request')
this.sendToIframeLowLevel(
'uw-page-stats',
{
pcsDark: window.matchMedia('(prefers-color-scheme: dark)').matches,
pcsLight: window.matchMedia('(prefers-color-scheme: light)').matches,
colorScheme: window.getComputedStyle( document.body ,null).getPropertyValue('color-scheme')
},
);
}
},
},
this this
); );
} }
@ -229,7 +271,151 @@ class UI {
this.handleMessage(message); this.handleMessage(message);
} }
verifyIframeTransparency() {
if (this.isGlobal) {
return;
}
if (!this.siteSettings || !this.settings) {
console.warn('settings not provided, not verifying transparency');
return;
}
if (this.performedTransparencyCheck || !this.isOpaque || !this.uiConfig?.parentElement) {
// console.warn('transparency was already checked, opacity is zero, or parent element isnt a thing:', this.performedTransparencyCheck, 'is opaque:', this.isOpaque, this.uiConfig?.parentElement);
return;
}
let reportTelemetry = true;
// if (this.siteSettings.data.workarounds?.disableSchemeAwareness) {
// if (this.settings.active.telemetry?.iframeTransparency?.[window.location.hostname]?.reportedWithColorSchemeDisabled) {
// reportTelemetry = false;
// } else {
// this.settings.setProp(['telemetry', window.location.hostname, 'reportedWithColorSchemaDisabled'], true)
// }
// } else {
// if (this.settings.active.telemetry?.iframeTransparency?.[window.location.hostname]?.reportedWithColorSchemeAllowed) {
// reportTelemetry = false;
// } else {
// this.settings.setProp(['telemetry', window.location.hostname, 'reportedWithColorSchemeAllowed'], true)
// }
// }
const rect = this.uiConfig.parentElement.getBoundingClientRect();
this.preventHiding = true;
setTimeout( () => {
this.eventBus.send(
'verify-iframe-transparency',
{
playerData: {
y: rect.top,
x: rect.left,
width: rect.width,
height: rect.height,
},
telemetryData: {
reportTelemetry,
}
}
);
}, 50);
this.performedTransparencyCheck = true;
setTimeout(() => {
this.preventHiding = false;
if (this.wantsToHide) {
this.setUiVisibility(false);
}
if (this.wantsToTransparent) {
this.setUiOpacity(false);
}
this.wantsToHide = false;
this.wantsToTransparent = false;
});
}
/**
* Generates marker positions for bug mitigations
*/
generateDebugMarker(x, y) {
const [parentMainDimension, parentCrossDimension] = y === 'center' ? ['height', 'width'] : ['width', 'height'];
let anchorStyle;
if (x === 'center' && x === y) {
anchorStyle = 'left: 50%; top: 50%; transform: translate(-50%, -50%);';
} else {
switch (x) {
case 'left':
anchorStyle = 'left: 0px;';
break;
case 'center':
anchorStyle = 'left: 50%; transform: translateX(-50%);';
break;
case 'right':
anchorStyle = 'right: 0px;';
break;
}
switch (y) {
case 'top':
anchorStyle = `${anchorStyle} top: 0px;`;
break;
case 'center':
anchorStyle = `${anchorStyle} top: 50%; transform: translateY(-50%);`;
break;
case 'bottom':
anchorStyle = `${anchorStyle} bottom: 0px;`;
break;
}
}
let [mainAxis, crossAxis] = y === 'center' ? ['left', 'top'] : ['top', 'left'];
const template = document.createElement('template');
template.innerHTML = `
<div style="position: absolute; ${anchorStyle} ${parentMainDimension}: 4px; ${parentCrossDimension}: 1px; pointer-events: none;">
<div style="position: relative; width: 100%; height: 100%">
<div style="position: absolute; ${mainAxis}: 0px; ${crossAxis}: 0px; width: 1px; height: 1px; background-color: #000102"></div>
<div style="position: absolute; ${mainAxis}: 1px; ${crossAxis}: 0px; width: 1px; height: 1px; background-color: #030405"></div>
<div style="position: absolute; ${mainAxis}: 2px; ${crossAxis}: 0px; width: 1px; height: 1px; background-color: #050403"></div>
<div style="position: absolute; ${mainAxis}: 3px; ${crossAxis}: 0px; width: 1px; height: 1px; background-color: #020100"></div>
</div>
<div style="top: 5px; left: 5px; opacity: 0">
This marker is Chrome Shitiness Mitigation mechanism for Ultrawidify. It turns out that as of 2025-01, Chrome does not correctly respect
allowTransparency property on certain iframes, and will force white or black background across the entire element. It is unclear what's
causing the issue — so far, it seems to appear randomly.
</div>
</div>
`.replace(/\s+/g, ' ').trim();
return template.content.firstChild;
}
setUiOpacity(visible) {
if (!visible && this.isVisible && this.preventHiding) {
this.wantsToTransparent = true;
return;
}
this.uiIframe.style.opacity = visible ? '100' : '0';
this.isOpaque = visible;
if (visible) {
this.verifyIframeTransparency();
}
}
setUiVisibility(visible) { setUiVisibility(visible) {
if (!visible && this.isVisible && this.preventHiding) {
this.wantsToHide = true;
return;
}
this.isVisible = visible;
if (visible) { if (visible) {
this.element.style.width = '100%'; this.element.style.width = '100%';
this.element.style.height = '100%'; this.element.style.height = '100%';
@ -241,6 +427,7 @@ class UI {
this.uiIframe.style.width = '0px'; this.uiIframe.style.width = '0px';
this.uiIframe.style.height = '0px'; this.uiIframe.style.height = '0px';
} }
} }
async enable() { async enable() {
@ -342,7 +529,8 @@ class UI {
} }
this.uiIframe.style.pointerEvents = event.data.clickable ? 'auto' : 'none'; this.uiIframe.style.pointerEvents = event.data.clickable ? 'auto' : 'none';
this.uiIframe.style.opacity = event.data.opacity || this.isGlobal ? '100' : '0'; this.setUiOpacity(event.data.opacity || this.isGlobal);
// this.setUiVisibility( event.data.opacity || this.isGlobal );
break; break;
case 'uw-bus-tunnel': case 'uw-bus-tunnel':
const busCommand = event.data.payload; const busCommand = event.data.payload;
@ -355,7 +543,8 @@ class UI {
this.setUiVisibility(!this.isGlobal); this.setUiVisibility(!this.isGlobal);
break; break;
case 'uwui-hidden': case 'uwui-hidden':
this.uiIframe.style.opacity = event.data.opacity || this.isGlobal ? '100' : '0'; this.setUiOpacity(event.data.opacity || this.isGlobal);
// this.setUiVisibility(event.data.opacity || this.isGlobal);
break; break;
case 'uwui-global-window-hidden': case 'uwui-global-window-hidden':
if (!this.isGlobal) { if (!this.isGlobal) {

View File

@ -248,7 +248,11 @@ class PlayerData {
//#endregion //#endregion
deferredUiInitialization(playerDimensions) { deferredUiInitialization(playerDimensions) {
if (this.ui || ! this.videoData.settings.active.ui?.inPlayer?.enabled) { if (
this.ui
|| ! this.videoData.settings.active.ui?.inPlayer?.enabled
|| (this.siteSettings.data.ui && !this.siteSettings.data.ui.enabled)
) {
return; return;
} }
@ -265,7 +269,9 @@ class PlayerData {
parentElement: this.element, parentElement: this.element,
eventBus: this.eventBus, eventBus: this.eventBus,
playerData: this, playerData: this,
uiSettings: this.videoData.settings.active.ui uiSettings: this.videoData.settings.active.ui,
settings: this.videoData.settings,
siteSettings: this.siteSettings
} }
); );