mirror of
https://github.com/redphx/better-xcloud.git
synced 2025-07-06 06:11:43 +02:00
Compare commits
17 Commits
Author | SHA1 | Date | |
---|---|---|---|
6bd658e8a6 | |||
7e6b89b357 | |||
4271583a5a | |||
1b2cf70248 | |||
87447df7fd | |||
8664c1a60f | |||
602c31dc7f | |||
bbaea5f629 | |||
03efa528c8 | |||
63aaca7d61 | |||
15ae88e9e6 | |||
7578671cc3 | |||
82cfb11a6d | |||
15700e736d | |||
b27cfc8215 | |||
1e644504ec | |||
7206d11825 |
20
build.ts
20
build.ts
@ -20,6 +20,8 @@ enum BuildTarget {
|
|||||||
|
|
||||||
type BuildVariant = 'full' | 'lite';
|
type BuildVariant = 'full' | 'lite';
|
||||||
|
|
||||||
|
const MINIFY_SYNTAX = true;
|
||||||
|
|
||||||
const postProcess = (str: string): string => {
|
const postProcess = (str: string): string => {
|
||||||
// Unescape unicode charaters
|
// Unescape unicode charaters
|
||||||
str = unescape((str.replace(/\\u/g, '%u')));
|
str = unescape((str.replace(/\\u/g, '%u')));
|
||||||
@ -70,9 +72,6 @@ const postProcess = (str: string): string => {
|
|||||||
// Collapse empty brackets
|
// Collapse empty brackets
|
||||||
str = str.replaceAll(/\{[\s\n]+\}/g, '{}');
|
str = str.replaceAll(/\{[\s\n]+\}/g, '{}');
|
||||||
|
|
||||||
// Collapse if/else blocks without curly braces
|
|
||||||
str = str.replaceAll(/((if \(.*?\)|else)\n\s+)/g, '$2 ');
|
|
||||||
|
|
||||||
// Remove blank lines
|
// Remove blank lines
|
||||||
str = str.replaceAll(/\n([\s]*)\n/g, "\n");
|
str = str.replaceAll(/\n([\s]*)\n/g, "\n");
|
||||||
|
|
||||||
@ -91,10 +90,15 @@ const postProcess = (str: string): string => {
|
|||||||
// str = str.replaceAll(/ \(([^\s,.$()]+)\) =>/g, ' $1 =>');
|
// str = str.replaceAll(/ \(([^\s,.$()]+)\) =>/g, ' $1 =>');
|
||||||
|
|
||||||
// Set indent to 1 space
|
// Set indent to 1 space
|
||||||
str = str.replaceAll(/\n(\s+)/g, (match, p1) => {
|
if (MINIFY_SYNTAX) {
|
||||||
const len = p1.length / 2;
|
// Collapse if/else blocks without curly braces
|
||||||
return '\n' + ' '.repeat(len);
|
str = str.replaceAll(/((if \(.*?\)|else)\n\s+)/g, '$2 ');
|
||||||
});
|
|
||||||
|
str = str.replaceAll(/\n(\s+)/g, (match, p1) => {
|
||||||
|
const len = p1.length / 2;
|
||||||
|
return '\n' + ' '.repeat(len);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
assert(str.includes('/* ADDITIONAL CODE */'));
|
assert(str.includes('/* ADDITIONAL CODE */'));
|
||||||
assert(str.includes('window.BX_EXPOSED = BxExposed'));
|
assert(str.includes('window.BX_EXPOSED = BxExposed'));
|
||||||
@ -128,7 +132,7 @@ const build = async (target: BuildTarget, version: string, variant: BuildVariant
|
|||||||
outdir: outDir,
|
outdir: outDir,
|
||||||
naming: outputScriptName,
|
naming: outputScriptName,
|
||||||
minify: {
|
minify: {
|
||||||
syntax: true,
|
syntax: MINIFY_SYNTAX,
|
||||||
},
|
},
|
||||||
define: {
|
define: {
|
||||||
'Bun.env.BUILD_TARGET': JSON.stringify(target),
|
'Bun.env.BUILD_TARGET': JSON.stringify(target),
|
||||||
|
1075
dist/better-xcloud.lite.user.js
vendored
1075
dist/better-xcloud.lite.user.js
vendored
File diff suppressed because one or more lines are too long
2
dist/better-xcloud.meta.js
vendored
2
dist/better-xcloud.meta.js
vendored
@ -1,5 +1,5 @@
|
|||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name Better xCloud
|
// @name Better xCloud
|
||||||
// @namespace https://github.com/redphx
|
// @namespace https://github.com/redphx
|
||||||
// @version 5.8.5
|
// @version 5.8.6
|
||||||
// ==/UserScript==
|
// ==/UserScript==
|
||||||
|
1540
dist/better-xcloud.user.js
vendored
1540
dist/better-xcloud.user.js
vendored
File diff suppressed because one or more lines are too long
@ -10,14 +10,14 @@
|
|||||||
"build": "build.ts"
|
"build": "build.ts"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "^1.1.10",
|
"@types/bun": "^1.1.11",
|
||||||
"@types/node": "^22.7.5",
|
"@types/node": "^22.7.6",
|
||||||
"@types/stylus": "^0.48.43",
|
"@types/stylus": "^0.48.43",
|
||||||
"eslint": "^9.12.0",
|
"eslint": "^9.12.0",
|
||||||
"eslint-plugin-compat": "^6.0.1",
|
"eslint-plugin-compat": "^6.0.1",
|
||||||
"stylus": "^0.63.0"
|
"stylus": "^0.63.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "^5.6.2"
|
"typescript": "^5.6.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -69,7 +69,6 @@ export enum PrefKey {
|
|||||||
UI_SCROLLBAR_HIDE = 'ui_scrollbar_hide',
|
UI_SCROLLBAR_HIDE = 'ui_scrollbar_hide',
|
||||||
UI_HIDE_SECTIONS = 'ui_hide_sections',
|
UI_HIDE_SECTIONS = 'ui_hide_sections',
|
||||||
|
|
||||||
UI_HOME_CONTEXT_MENU_DISABLED = 'ui_home_context_menu_disabled',
|
|
||||||
UI_GAME_CARD_SHOW_WAIT_TIME = 'ui_game_card_show_wait_time',
|
UI_GAME_CARD_SHOW_WAIT_TIME = 'ui_game_card_show_wait_time',
|
||||||
|
|
||||||
VIDEO_PLAYER_TYPE = 'video_player_type',
|
VIDEO_PLAYER_TYPE = 'video_player_type',
|
||||||
|
55
src/index.ts
55
src/index.ts
@ -42,6 +42,7 @@ import { StreamUiHandler } from "./modules/stream/stream-ui";
|
|||||||
import { UserAgent } from "./utils/user-agent";
|
import { UserAgent } from "./utils/user-agent";
|
||||||
import { XboxApi } from "./utils/xbox-api";
|
import { XboxApi } from "./utils/xbox-api";
|
||||||
import { StreamStatsCollector } from "./utils/stream-stats-collector";
|
import { StreamStatsCollector } from "./utils/stream-stats-collector";
|
||||||
|
import { RootDialogObserver } from "./utils/root-dialog-observer";
|
||||||
|
|
||||||
// Handle login page
|
// Handle login page
|
||||||
if (window.location.pathname.includes('/auth/msa')) {
|
if (window.location.pathname.includes('/auth/msa')) {
|
||||||
@ -309,7 +310,8 @@ function unload() {
|
|||||||
window.BX_EXPOSED.stopTakRendering = false;
|
window.BX_EXPOSED.stopTakRendering = false;
|
||||||
|
|
||||||
NavigationDialogManager.getInstance().hide();
|
NavigationDialogManager.getInstance().hide();
|
||||||
StreamStats.getInstance().onStoppedPlaying();
|
StreamStats.getInstance().destroy();
|
||||||
|
StreamBadges.getInstance().destroy();
|
||||||
|
|
||||||
if (isFullVersion()) {
|
if (isFullVersion()) {
|
||||||
MouseCursorHider.stop();
|
MouseCursorHider.stop();
|
||||||
@ -328,55 +330,6 @@ isFullVersion() && window.addEventListener(BxEvent.CAPTURE_SCREENSHOT, e => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
function observeRootDialog($root: HTMLElement) {
|
|
||||||
let beingShown = false;
|
|
||||||
|
|
||||||
const observer = new MutationObserver(mutationList => {
|
|
||||||
for (const mutation of mutationList) {
|
|
||||||
if (mutation.type !== 'childList') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
BX_FLAGS.Debug && BxLogger.warning('RootDialog', 'added', mutation.addedNodes);
|
|
||||||
if (mutation.addedNodes.length === 1) {
|
|
||||||
const $addedElm = mutation.addedNodes[0];
|
|
||||||
if ($addedElm instanceof HTMLElement && $addedElm.className) {
|
|
||||||
// Make sure it's Guide dialog
|
|
||||||
if ($root.querySelector('div[class*=GuideDialog]')) {
|
|
||||||
GuideMenu.observe($addedElm);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const shown = !!($root.firstElementChild && $root.firstElementChild.childElementCount > 0);
|
|
||||||
if (shown !== beingShown) {
|
|
||||||
beingShown = shown;
|
|
||||||
BxEvent.dispatch(window, shown ? BxEvent.XCLOUD_DIALOG_SHOWN : BxEvent.XCLOUD_DIALOG_DISMISSED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
observer.observe($root, {subtree: true, childList: true});
|
|
||||||
}
|
|
||||||
|
|
||||||
function waitForRootDialog() {
|
|
||||||
const observer = new MutationObserver(mutationList => {
|
|
||||||
for (const mutation of mutationList) {
|
|
||||||
if (mutation.type !== 'childList') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const $target = mutation.target as HTMLElement;
|
|
||||||
if ($target.id && $target.id === 'gamepass-dialog-root') {
|
|
||||||
observer.disconnect();
|
|
||||||
observeRootDialog($target);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
observer.observe(document.documentElement, {subtree: true, childList: true});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function main() {
|
function main() {
|
||||||
if (getPref(PrefKey.GAME_MSFS2020_FORCE_NATIVE_MKB)) {
|
if (getPref(PrefKey.GAME_MSFS2020_FORCE_NATIVE_MKB)) {
|
||||||
BX_FLAGS.ForceNativeMkbTitles.push('9PMQDM08SNK9');
|
BX_FLAGS.ForceNativeMkbTitles.push('9PMQDM08SNK9');
|
||||||
@ -397,7 +350,7 @@ function main() {
|
|||||||
disableAdobeAudienceManager();
|
disableAdobeAudienceManager();
|
||||||
}
|
}
|
||||||
|
|
||||||
waitForRootDialog();
|
RootDialogObserver.waitForRootDialog();
|
||||||
|
|
||||||
// Setup UI
|
// Setup UI
|
||||||
addCss();
|
addCss();
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { CE, clearFocus, createSvgIcon } from "@utils/html";
|
import { CE, createSvgIcon } from "@utils/html";
|
||||||
import { ScreenshotAction } from "./action-screenshot";
|
import { ScreenshotAction } from "./action-screenshot";
|
||||||
import { TouchControlAction } from "./action-touch-control";
|
import { TouchControlAction } from "./action-touch-control";
|
||||||
import { BxEvent } from "@utils/bx-event";
|
import { BxEvent } from "@utils/bx-event";
|
||||||
@ -81,14 +81,11 @@ export class GameBar {
|
|||||||
|
|
||||||
// Enable/disable Game Bar when playing/pausing
|
// Enable/disable Game Bar when playing/pausing
|
||||||
getPref(PrefKey.GAME_BAR_POSITION) !== 'off' && window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, ((e: Event) => {
|
getPref(PrefKey.GAME_BAR_POSITION) !== 'off' && window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, ((e: Event) => {
|
||||||
if (!STATES.isPlaying) {
|
|
||||||
this.disable();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Toggle Game bar
|
// Toggle Game bar
|
||||||
const mode = (e as any).mode;
|
if (STATES.isPlaying) {
|
||||||
mode !== 'none' ? this.disable() : this.enable();
|
const mode = (e as any).mode;
|
||||||
|
mode !== 'none' ? this.disable() : this.enable();
|
||||||
|
}
|
||||||
}).bind(this));
|
}).bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,10 +121,6 @@ export class GameBar {
|
|||||||
|
|
||||||
hideBar() {
|
hideBar() {
|
||||||
this.clearHideTimeout();
|
this.clearHideTimeout();
|
||||||
|
|
||||||
// Stop focusing Game Bar
|
|
||||||
clearFocus();
|
|
||||||
|
|
||||||
this.$container.classList.replace('bx-show', 'bx-hide');
|
this.$container.classList.replace('bx-show', 'bx-hide');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -951,7 +951,20 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) {
|
|||||||
|
|
||||||
str = PatcherUtils.replaceWith(str, index, '.All', '.Locked');
|
str = PatcherUtils.replaceWith(str, index, '.All', '.Locked');
|
||||||
return str;
|
return str;
|
||||||
}
|
},
|
||||||
|
|
||||||
|
// Disable long touch activating context menu
|
||||||
|
disableTouchContextMenu(str: string) {
|
||||||
|
let index = str.indexOf('"ContextualCardActions-module__container');
|
||||||
|
index >= 0 && (index = str.indexOf('addEventListener("touchstart"', index));
|
||||||
|
index >= 0 && (index = PatcherUtils.lastIndexOf(str, 'return ', index, 50));
|
||||||
|
if (index < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
str = PatcherUtils.replaceWith(str, index, 'return', 'return () => {};');
|
||||||
|
return str;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let PATCH_ORDERS: PatchArray = [
|
let PATCH_ORDERS: PatchArray = [
|
||||||
@ -990,6 +1003,10 @@ let PATCH_ORDERS: PatchArray = [
|
|||||||
getPref(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.TOUCH) && 'ignorePlayWithTouchSection',
|
getPref(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.TOUCH) && 'ignorePlayWithTouchSection',
|
||||||
(getPref(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.NATIVE_MKB) || getPref(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.MOST_POPULAR)) && 'ignoreSiglSections',
|
(getPref(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.NATIVE_MKB) || getPref(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.MOST_POPULAR)) && 'ignoreSiglSections',
|
||||||
|
|
||||||
|
...(STATES.userAgent.capabilities.touch ? [
|
||||||
|
'disableTouchContextMenu',
|
||||||
|
] : []),
|
||||||
|
|
||||||
...(getPref(PrefKey.BLOCK_TRACKING) ? [
|
...(getPref(PrefKey.BLOCK_TRACKING) ? [
|
||||||
'disableAiTrack',
|
'disableAiTrack',
|
||||||
'disableTelemetry',
|
'disableTelemetry',
|
||||||
|
@ -94,20 +94,22 @@ export class WebGL2Player {
|
|||||||
gl.uniform1f(gl.getUniformLocation(program, 'saturation'), this.options.saturation);
|
gl.uniform1f(gl.getUniformLocation(program, 'saturation'), this.options.saturation);
|
||||||
}
|
}
|
||||||
|
|
||||||
drawFrame() {
|
drawFrame(force=false) {
|
||||||
// Don't draw when FPS is 0
|
if (!force) {
|
||||||
if (this.targetFps === 0) {
|
// Don't draw when FPS is 0
|
||||||
return;
|
if (this.targetFps === 0) {
|
||||||
}
|
|
||||||
|
|
||||||
// Limit FPS
|
|
||||||
if (this.targetFps < 60) {
|
|
||||||
const currentTime = performance.now();
|
|
||||||
const timeSinceLastFrame = currentTime - this.lastFrameTime;
|
|
||||||
if (timeSinceLastFrame < this.frameInterval) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.lastFrameTime = currentTime;
|
|
||||||
|
// Limit FPS
|
||||||
|
if (this.targetFps < 60) {
|
||||||
|
const currentTime = performance.now();
|
||||||
|
const timeSinceLastFrame = currentTime - this.lastFrameTime;
|
||||||
|
if (timeSinceLastFrame < this.frameInterval) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.lastFrameTime = currentTime;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const gl = this.gl!;
|
const gl = this.gl!;
|
||||||
|
@ -7,6 +7,7 @@ import { StreamPlayerType, StreamVideoProcessing } from "@enums/stream-player";
|
|||||||
import { STATES } from "@/utils/global";
|
import { STATES } from "@/utils/global";
|
||||||
import { PrefKey } from "@/enums/pref-keys";
|
import { PrefKey } from "@/enums/pref-keys";
|
||||||
import { getPref } from "@/utils/settings-storages/global-settings-storage";
|
import { getPref } from "@/utils/settings-storages/global-settings-storage";
|
||||||
|
import { BX_FLAGS } from "@/utils/bx-flags";
|
||||||
|
|
||||||
export type StreamPlayerOptions = Partial<{
|
export type StreamPlayerOptions = Partial<{
|
||||||
processing: string,
|
processing: string,
|
||||||
@ -173,6 +174,8 @@ export class StreamPlayer {
|
|||||||
|
|
||||||
setPlayerType(type: StreamPlayerType, refreshPlayer: boolean = false) {
|
setPlayerType(type: StreamPlayerType, refreshPlayer: boolean = false) {
|
||||||
if (this.playerType !== type) {
|
if (this.playerType !== type) {
|
||||||
|
const videoClass = BX_FLAGS.DeviceInfo.deviceType === 'android-tv' ? 'bx-pixel' : 'bx-gone';
|
||||||
|
|
||||||
// Switch from Video -> WebGL2
|
// Switch from Video -> WebGL2
|
||||||
if (type === StreamPlayerType.WEBGL2) {
|
if (type === StreamPlayerType.WEBGL2) {
|
||||||
// Initialize WebGL2 player
|
// Initialize WebGL2 player
|
||||||
@ -184,12 +187,12 @@ export class StreamPlayer {
|
|||||||
|
|
||||||
this.$videoCss!.textContent = '';
|
this.$videoCss!.textContent = '';
|
||||||
|
|
||||||
this.$video.classList.add('bx-pixel');
|
this.$video.classList.add(videoClass);
|
||||||
} else {
|
} else {
|
||||||
// Cleanup WebGL2 Player
|
// Cleanup WebGL2 Player
|
||||||
this.webGL2Player?.stop();
|
this.webGL2Player?.stop();
|
||||||
|
|
||||||
this.$video.classList.remove('bx-pixel');
|
this.$video.classList.remove(videoClass);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,7 +19,6 @@ type StreamBadgeInfo = {
|
|||||||
|
|
||||||
type StreamServerInfo = {
|
type StreamServerInfo = {
|
||||||
server?: {
|
server?: {
|
||||||
ipv6: boolean,
|
|
||||||
region?: string,
|
region?: string,
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -100,7 +99,6 @@ export class StreamBadges {
|
|||||||
setRegion(region: string) {
|
setRegion(region: string) {
|
||||||
this.serverInfo.server = {
|
this.serverInfo.server = {
|
||||||
region: region,
|
region: region,
|
||||||
ipv6: false,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -186,6 +184,11 @@ export class StreamBadges {
|
|||||||
this.intervalId = null;
|
this.intervalId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.serverInfo = {};
|
||||||
|
delete this.$container;
|
||||||
|
}
|
||||||
|
|
||||||
async render() {
|
async render() {
|
||||||
if (this.$container) {
|
if (this.$container) {
|
||||||
this.start();
|
this.start();
|
||||||
@ -205,7 +208,7 @@ export class StreamBadges {
|
|||||||
[StreamBadge.BATTERY, batteryLevel],
|
[StreamBadge.BATTERY, batteryLevel],
|
||||||
[StreamBadge.DOWNLOAD, humanFileSize(0)],
|
[StreamBadge.DOWNLOAD, humanFileSize(0)],
|
||||||
[StreamBadge.UPLOAD, humanFileSize(0)],
|
[StreamBadge.UPLOAD, humanFileSize(0)],
|
||||||
this.serverInfo.server ? this.badges.server.$element : [StreamBadge.SERVER, '?'],
|
this.badges.server.$element ?? [StreamBadge.SERVER, '?'],
|
||||||
this.serverInfo.video ? this.badges.video.$element : [StreamBadge.VIDEO, '?'],
|
this.serverInfo.video ? this.badges.video.$element : [StreamBadge.VIDEO, '?'],
|
||||||
this.serverInfo.audio ? this.badges.audio.$element : [StreamBadge.AUDIO, '?'],
|
this.serverInfo.audio ? this.badges.audio.$element : [StreamBadge.AUDIO, '?'],
|
||||||
];
|
];
|
||||||
@ -330,18 +333,16 @@ export class StreamBadges {
|
|||||||
BxLogger.info('candidate', candidateId, allCandidates);
|
BxLogger.info('candidate', candidateId, allCandidates);
|
||||||
|
|
||||||
// Server + Region
|
// Server + Region
|
||||||
|
let text = '';
|
||||||
|
const isIpv6 = allCandidates[candidateId].includes(':');
|
||||||
|
|
||||||
const server = this.serverInfo.server;
|
const server = this.serverInfo.server;
|
||||||
if (server) {
|
if (server && server.region) {
|
||||||
server.ipv6 = allCandidates[candidateId].includes(':');
|
text += server.region;
|
||||||
|
|
||||||
let text = '';
|
|
||||||
if (server.region) {
|
|
||||||
text += server.region;
|
|
||||||
}
|
|
||||||
|
|
||||||
text += '@' + (server.ipv6 ? 'IPv6' : 'IPv4');
|
|
||||||
this.badges.server.$element = this.renderBadge(StreamBadge.SERVER, text);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
text += '@' + (isIpv6 ? 'IPv6' : 'IPv4');
|
||||||
|
this.badges.server.$element = this.renderBadge(StreamBadge.SERVER, text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,7 +107,7 @@ export class StreamStats {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onStoppedPlaying() {
|
destroy() {
|
||||||
this.stop();
|
this.stop();
|
||||||
this.quickGlanceStop();
|
this.quickGlanceStop();
|
||||||
this.hideSettingsUi();
|
this.hideSettingsUi();
|
||||||
@ -156,7 +156,7 @@ export class StreamStats {
|
|||||||
|
|
||||||
private async update(forceUpdate=false) {
|
private async update(forceUpdate=false) {
|
||||||
if ((!forceUpdate && this.isHidden()) || !STATES.currentStream.peerConnection) {
|
if ((!forceUpdate && this.isHidden()) || !STATES.currentStream.peerConnection) {
|
||||||
this.onStoppedPlaying();
|
this.destroy();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -246,7 +246,6 @@ export class SettingsNavigationDialog extends NavigationDialog {
|
|||||||
items: [
|
items: [
|
||||||
PrefKey.UI_LAYOUT,
|
PrefKey.UI_LAYOUT,
|
||||||
PrefKey.UI_GAME_CARD_SHOW_WAIT_TIME,
|
PrefKey.UI_GAME_CARD_SHOW_WAIT_TIME,
|
||||||
PrefKey.UI_HOME_CONTEXT_MENU_DISABLED,
|
|
||||||
PrefKey.CONTROLLER_SHOW_CONNECTION_STATUS,
|
PrefKey.CONTROLLER_SHOW_CONNECTION_STATUS,
|
||||||
PrefKey.STREAM_SIMPLIFY_MENU,
|
PrefKey.STREAM_SIMPLIFY_MENU,
|
||||||
PrefKey.SKIP_SPLASH_VIDEO,
|
PrefKey.SKIP_SPLASH_VIDEO,
|
||||||
|
@ -3,6 +3,7 @@ import { BxIcon } from "@/utils/bx-icon";
|
|||||||
import { AppInterface } from "@/utils/global";
|
import { AppInterface } from "@/utils/global";
|
||||||
import { ButtonStyle, CE, createButton } from "@/utils/html";
|
import { ButtonStyle, CE, createButton } from "@/utils/html";
|
||||||
import { t } from "@/utils/translation";
|
import { t } from "@/utils/translation";
|
||||||
|
import { parseDetailsPath } from "@/utils/utils";
|
||||||
|
|
||||||
export class ProductDetailsPage {
|
export class ProductDetailsPage {
|
||||||
private static $btnShortcut = AppInterface && createButton({
|
private static $btnShortcut = AppInterface && createButton({
|
||||||
@ -20,17 +21,9 @@ export class ProductDetailsPage {
|
|||||||
label: t('wallpaper'),
|
label: t('wallpaper'),
|
||||||
style: ButtonStyle.FOCUSABLE,
|
style: ButtonStyle.FOCUSABLE,
|
||||||
tabIndex: 0,
|
tabIndex: 0,
|
||||||
onClick: async e => {
|
onClick: e => {
|
||||||
try {
|
const details = parseDetailsPath(window.location.pathname);
|
||||||
const matches = /\/games\/(?<titleSlug>[^\/]+)\/(?<productId>\w+)/.exec(window.location.pathname);
|
details && AppInterface.downloadWallpapers(details.titleSlug, details.productId);
|
||||||
if (!matches?.groups) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const titleSlug = matches.groups.titleSlug.replaceAll('\%' + '7C', '-');
|
|
||||||
const productId = matches.groups.productId;
|
|
||||||
AppInterface.downloadWallpapers(titleSlug, productId);
|
|
||||||
} catch (e) {}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
// Credit: https://phosphoricons.com
|
||||||
import iconBetterXcloud from "@assets/svg/better-xcloud.svg" with { type: "text" };
|
import iconBetterXcloud from "@assets/svg/better-xcloud.svg" with { type: "text" };
|
||||||
import iconTrueAchievements from "@assets/svg/true-achievements.svg" with { type: "text" };
|
import iconTrueAchievements from "@assets/svg/true-achievements.svg" with { type: "text" };
|
||||||
import iconClose from "@assets/svg/close.svg" with { type: "text" };
|
import iconClose from "@assets/svg/close.svg" with { type: "text" };
|
||||||
|
@ -9,11 +9,6 @@ export let FeatureGates: {[key: string]: boolean} = {
|
|||||||
'ShowForcedUpdateScreen': false,
|
'ShowForcedUpdateScreen': false,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Disable context menu in Home page
|
|
||||||
if (getPref(PrefKey.UI_HOME_CONTEXT_MENU_DISABLED)) {
|
|
||||||
FeatureGates['EnableHomeContextMenu'] = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disable chat feature
|
// Disable chat feature
|
||||||
if (getPref(PrefKey.BLOCK_SOCIAL_FEATURES)) {
|
if (getPref(PrefKey.BLOCK_SOCIAL_FEATURES)) {
|
||||||
FeatureGates['EnableGuideChatTab'] = false;
|
FeatureGates['EnableGuideChatTab'] = false;
|
||||||
|
@ -101,16 +101,14 @@ function createElement<T=HTMLElement>(elmName: string, props: CreateElementOptio
|
|||||||
|
|
||||||
export const CE = createElement;
|
export const CE = createElement;
|
||||||
|
|
||||||
// Credit: https://phosphoricons.com
|
const domParser = new DOMParser();
|
||||||
const svgParser = (svg: string) => new DOMParser().parseFromString(svg, 'image/svg+xml').documentElement;
|
export function createSvgIcon(icon: typeof BxIcon) {
|
||||||
|
return domParser.parseFromString(icon.toString(), 'image/svg+xml').documentElement;
|
||||||
export const createSvgIcon = (icon: typeof BxIcon) => {
|
|
||||||
return svgParser(icon.toString());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ButtonStyleIndices = Object.keys(ButtonStyleClass).map(i => parseInt(i));
|
const ButtonStyleIndices = Object.keys(ButtonStyleClass).map(i => parseInt(i));
|
||||||
|
|
||||||
export const createButton = <T=HTMLButtonElement>(options: BxButton): T => {
|
export function createButton<T=HTMLButtonElement>(options: BxButton): T {
|
||||||
let $btn;
|
let $btn;
|
||||||
if (options.url) {
|
if (options.url) {
|
||||||
$btn = CE('a', {'class': 'bx-button'}) as HTMLAnchorElement;
|
$btn = CE('a', {'class': 'bx-button'}) as HTMLAnchorElement;
|
||||||
|
@ -3,8 +3,6 @@ import { BxLogger } from "./bx-logger";
|
|||||||
import { TouchController } from "@modules/touch-controller";
|
import { TouchController } from "@modules/touch-controller";
|
||||||
import { GamePassCloudGallery } from "../enums/game-pass-gallery";
|
import { GamePassCloudGallery } from "../enums/game-pass-gallery";
|
||||||
import { BX_FLAGS } from "./bx-flags";
|
import { BX_FLAGS } from "./bx-flags";
|
||||||
import { PrefKey } from "@/enums/pref-keys";
|
|
||||||
import { getPref } from "./settings-storages/global-settings-storage";
|
|
||||||
|
|
||||||
const LOG_TAG = 'PreloadState';
|
const LOG_TAG = 'PreloadState';
|
||||||
|
|
||||||
@ -50,14 +48,6 @@ export function overridePreloadState() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getPref(PrefKey.UI_HOME_CONTEXT_MENU_DISABLED)) {
|
|
||||||
try {
|
|
||||||
state.experiments.experimentationInfo.data.treatments.EnableHomeContextMenu = false;
|
|
||||||
} catch (e) {
|
|
||||||
BxLogger.error(LOG_TAG, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
_state = state;
|
_state = state;
|
||||||
STATES.appContext = deepClone(state.appContext);
|
STATES.appContext = deepClone(state.appContext);
|
||||||
|
114
src/utils/root-dialog-observer.ts
Normal file
114
src/utils/root-dialog-observer.ts
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import { GuideMenu } from "@/modules/ui/guide-menu";
|
||||||
|
import { BxEvent } from "./bx-event";
|
||||||
|
import { BX_FLAGS } from "./bx-flags";
|
||||||
|
import { BxLogger } from "./bx-logger";
|
||||||
|
import { BxIcon } from "./bx-icon";
|
||||||
|
import { AppInterface } from "./global";
|
||||||
|
import { createButton, ButtonStyle } from "./html";
|
||||||
|
import { t } from "./translation";
|
||||||
|
import { parseDetailsPath } from "./utils";
|
||||||
|
|
||||||
|
|
||||||
|
export class RootDialogObserver {
|
||||||
|
private static $btnShortcut = AppInterface && createButton({
|
||||||
|
icon: BxIcon.CREATE_SHORTCUT,
|
||||||
|
label: t('create-shortcut'),
|
||||||
|
style: ButtonStyle.FOCUSABLE | ButtonStyle.GHOST | ButtonStyle.FULL_WIDTH | ButtonStyle.NORMAL_CASE | ButtonStyle.NORMAL_LINK,
|
||||||
|
tabIndex: 0,
|
||||||
|
onClick: e => {
|
||||||
|
window.BX_EXPOSED.dialogRoutes?.closeAll();
|
||||||
|
|
||||||
|
const $btn = (e.target as HTMLElement).closest('button');
|
||||||
|
AppInterface.createShortcut($btn?.dataset.path);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
private static $btnWallpaper = AppInterface && createButton({
|
||||||
|
icon: BxIcon.DOWNLOAD,
|
||||||
|
label: t('wallpaper'),
|
||||||
|
style: ButtonStyle.FOCUSABLE | ButtonStyle.GHOST | ButtonStyle.FULL_WIDTH | ButtonStyle.NORMAL_CASE | ButtonStyle.NORMAL_LINK,
|
||||||
|
tabIndex: 0,
|
||||||
|
onClick: e => {
|
||||||
|
window.BX_EXPOSED.dialogRoutes?.closeAll();
|
||||||
|
|
||||||
|
const $btn = (e.target as HTMLElement).closest('button');
|
||||||
|
const details = parseDetailsPath($btn!.dataset.path!);
|
||||||
|
details && AppInterface.downloadWallpapers(details.titleSlug, details.productId);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
private static handleGameCardMenu($root: HTMLElement) {
|
||||||
|
const $detail = $root.querySelector('a[href^="/play/"]') as HTMLAnchorElement;
|
||||||
|
if (!$detail) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = $detail.getAttribute('href')!;
|
||||||
|
RootDialogObserver.$btnShortcut.dataset.path = path;
|
||||||
|
RootDialogObserver.$btnWallpaper.dataset.path = path;
|
||||||
|
|
||||||
|
$root.append(RootDialogObserver.$btnShortcut, RootDialogObserver.$btnWallpaper);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static handleAddedElement($root: HTMLElement, $addedElm: HTMLElement): boolean {
|
||||||
|
if (AppInterface && $addedElm.className.startsWith('SlideSheet-module__container')) {
|
||||||
|
// Game card's context menu
|
||||||
|
const $gameCardMenu = $addedElm.querySelector<HTMLElement>('div[class^=MruContextMenu],div[class^=GameCardContextMenu]');
|
||||||
|
if ($gameCardMenu) {
|
||||||
|
RootDialogObserver.handleGameCardMenu($gameCardMenu);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else if ($root.querySelector('div[class*=GuideDialog]')) {
|
||||||
|
// Guide menu
|
||||||
|
GuideMenu.observe($addedElm);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static observe($root: HTMLElement) {
|
||||||
|
let beingShown = false;
|
||||||
|
|
||||||
|
const observer = new MutationObserver(mutationList => {
|
||||||
|
for (const mutation of mutationList) {
|
||||||
|
if (mutation.type !== 'childList') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
BX_FLAGS.Debug && BxLogger.warning('RootDialog', 'added', mutation.addedNodes);
|
||||||
|
if (mutation.addedNodes.length === 1) {
|
||||||
|
const $addedElm = mutation.addedNodes[0];
|
||||||
|
if ($addedElm instanceof HTMLElement && $addedElm.className) {
|
||||||
|
RootDialogObserver.handleAddedElement($root, $addedElm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const shown = !!($root.firstElementChild && $root.firstElementChild.childElementCount > 0);
|
||||||
|
if (shown !== beingShown) {
|
||||||
|
beingShown = shown;
|
||||||
|
BxEvent.dispatch(window, shown ? BxEvent.XCLOUD_DIALOG_SHOWN : BxEvent.XCLOUD_DIALOG_DISMISSED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
observer.observe($root, {subtree: true, childList: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static waitForRootDialog() {
|
||||||
|
const observer = new MutationObserver(mutationList => {
|
||||||
|
for (const mutation of mutationList) {
|
||||||
|
if (mutation.type !== 'childList') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const $target = mutation.target as HTMLElement;
|
||||||
|
if ($target.id && $target.id === 'gamepass-dialog-root') {
|
||||||
|
observer.disconnect();
|
||||||
|
RootDialogObserver.observe($target);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
observer.observe(document.documentElement, {subtree: true, childList: true});
|
||||||
|
}
|
||||||
|
}
|
@ -64,7 +64,7 @@ export class Screenshot {
|
|||||||
const canvasContext = Screenshot.#canvasContext;
|
const canvasContext = Screenshot.#canvasContext;
|
||||||
|
|
||||||
if ($player instanceof HTMLCanvasElement) {
|
if ($player instanceof HTMLCanvasElement) {
|
||||||
streamPlayer.getWebGL2Player().drawFrame();
|
streamPlayer.getWebGL2Player().drawFrame(true);
|
||||||
}
|
}
|
||||||
canvasContext.drawImage($player, 0, 0, $canvas.width, $canvas.height);
|
canvasContext.drawImage($player, 0, 0, $canvas.width, $canvas.height);
|
||||||
|
|
||||||
|
@ -533,12 +533,6 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
|
|||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
[PrefKey.UI_HOME_CONTEXT_MENU_DISABLED]: {
|
|
||||||
requiredVariants: 'full',
|
|
||||||
label: t('disable-home-context-menu'),
|
|
||||||
default: STATES.browser.capabilities.touch,
|
|
||||||
},
|
|
||||||
|
|
||||||
[PrefKey.UI_HIDE_SECTIONS]: {
|
[PrefKey.UI_HIDE_SECTIONS]: {
|
||||||
requiredVariants: 'full',
|
requiredVariants: 'full',
|
||||||
label: t('hide-sections'),
|
label: t('hide-sections'),
|
||||||
|
@ -28,7 +28,7 @@ export class UserAgent {
|
|||||||
static #USER_AGENTS: PartialRecord<UserAgentProfile, string> = {
|
static #USER_AGENTS: PartialRecord<UserAgentProfile, string> = {
|
||||||
[UserAgentProfile.WINDOWS_EDGE]: `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${CHROMIUM_VERSION} Safari/537.36 Edg/${CHROMIUM_VERSION}`,
|
[UserAgentProfile.WINDOWS_EDGE]: `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${CHROMIUM_VERSION} Safari/537.36 Edg/${CHROMIUM_VERSION}`,
|
||||||
[UserAgentProfile.MACOS_SAFARI]: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5.2 Safari/605.1.1',
|
[UserAgentProfile.MACOS_SAFARI]: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5.2 Safari/605.1.1',
|
||||||
[UserAgentProfile.SMART_TV_GENERIC]: `${window.navigator.userAgent} SmartTV`,
|
[UserAgentProfile.SMART_TV_GENERIC]: `${window.navigator.userAgent} Smart-TV`,
|
||||||
[UserAgentProfile.SMART_TV_TIZEN]: `Mozilla/5.0 (SMART-TV; LINUX; Tizen 7.0) AppleWebKit/537.36 (KHTML, like Gecko) ${CHROMIUM_VERSION}/7.0 TV Safari/537.36 ${SMART_TV_UNIQUE_ID}`,
|
[UserAgentProfile.SMART_TV_TIZEN]: `Mozilla/5.0 (SMART-TV; LINUX; Tizen 7.0) AppleWebKit/537.36 (KHTML, like Gecko) ${CHROMIUM_VERSION}/7.0 TV Safari/537.36 ${SMART_TV_UNIQUE_ID}`,
|
||||||
[UserAgentProfile.VR_OCULUS]: window.navigator.userAgent + ' OculusBrowser VR',
|
[UserAgentProfile.VR_OCULUS]: window.navigator.userAgent + ' OculusBrowser VR',
|
||||||
}
|
}
|
||||||
|
@ -120,3 +120,15 @@ export function productTitleToSlug(title: string): string {
|
|||||||
.replace(/ /g, '-')
|
.replace(/ /g, '-')
|
||||||
.toLowerCase();
|
.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parseDetailsPath(path: string) {
|
||||||
|
const matches = /\/games\/(?<titleSlug>[^\/]+)\/(?<productId>\w+)/.exec(path);
|
||||||
|
if (!matches?.groups) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const titleSlug = matches.groups.titleSlug.replaceAll('\%' + '7C', '-');
|
||||||
|
const productId = matches.groups.productId;
|
||||||
|
|
||||||
|
return {titleSlug, productId};
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user