Compare commits

..

28 Commits

Author SHA1 Message Date
d3ef988af7 Fix problem with controller in Settings dialog 2025-01-16 21:50:23 +07:00
0bf4c289db Bump version to 6.2.1 2025-01-16 20:37:00 +07:00
c8865bd8a0 Re-arrange patches 2025-01-16 20:26:15 +07:00
a2f062d9d5 Lite: remove LocalCoOpManager 2025-01-16 20:05:51 +07:00
b6d4c51ca9 Update dists 2025-01-16 16:49:08 +07:00
785df72972 Lite: hide unsupported features 2025-01-16 16:37:18 +07:00
48da8bc527 Update Remote Play dialog's styling 2025-01-16 07:14:52 +07:00
f9cf02b2da Fix the Y button in default MKB preset 2025-01-16 06:46:12 +07:00
77e0f2d8ba Lite: disable navigating using gamepad in Settings dialog 2025-01-16 06:45:12 +07:00
d05a68c470 Fix exception when viewing deviceCode page 2025-01-15 21:30:50 +07:00
153873e034 Reduce Virtual Controller's input latency 2025-01-08 21:16:07 +07:00
8d7fbf2804 Bump version to 6.2.0 2025-01-04 19:39:40 +07:00
488b0dfef2 Show local co-op icon in settings 2025-01-04 18:43:24 +07:00
b3697df8dc Set background image's quality 2025-01-04 18:30:53 +07:00
de21549e0d Hide image quality's slider 2025-01-04 13:14:51 +07:00
097164b92e Set image quality 2025-01-04 12:33:47 +07:00
3fe6d97133 Update dists 2025-01-04 10:31:45 +07:00
328fdf46ea Don't render controller icon in game card 2025-01-04 10:31:13 +07:00
e4dbdea9a5 await requestPointerLock 2025-01-03 20:43:21 +07:00
f13ce94cf2 Update dists 2025-01-03 20:04:25 +07:00
a6c19fec15 Use Set() for local co-op list 2025-01-03 20:03:56 +07:00
6448a00271 Show local co-op icon in details page 2025-01-03 19:49:40 +07:00
68b29ecb50 Fix not applying class names to local co-op icon 2025-01-03 17:01:51 +07:00
90f89a0244 Show local co-op icon in game card 2025-01-02 21:39:27 +07:00
9862f794cf Update button's styling 2024-12-31 06:57:22 +07:00
e109cdec6a Attempt to fix problem with unadjustedMovement (#628) 2024-12-31 06:52:50 +07:00
40d1878fb2 Add icon to Better xCloud button 2024-12-29 15:41:35 +07:00
95f842d9f6 Update 02-feature-request.yml 2024-12-29 08:35:21 +07:00
47 changed files with 1075 additions and 710 deletions

View File

@ -13,7 +13,7 @@ body:
- type: dropdown
id: device_type
attributes:
label: Device
label: Device type
description: "Which device type is this feature for?"
options:
- All devices
@ -23,10 +23,20 @@ body:
multiple: false
validations:
required: true
- type: input
id: device_name
attributes:
label: "Device"
description: "Name of the device"
placeholder: "e.g., Google Pixel 8"
validations:
required: true
- type: textarea
id: suggestion
attributes:
label: "Suggestion"
description: "What do you want to suggest?"
description: "What do you want to suggest? Include (mockup) screenshot if possible."
validations:
required: true

7
.gitignore vendored
View File

@ -1,9 +1,4 @@
src/modules/patcher/patches/controller-customization.js
src/modules/patcher/patches/expose-stream-session.js
src/modules/patcher/patches/local-co-op-enable.js
src/modules/patcher/patches/poll-gamepad.js
src/modules/patcher/patches/remote-play-keep-alive.js
src/modules/patcher/patches/vibration-adjust.js
src/modules/patcher/patches/*.js
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore

View File

@ -3,6 +3,7 @@
"dist/**/*": true,
"src/modules/patcher/patches/controller-customization.js": true,
"src/modules/patcher/patches/expose-stream-session.js": true,
"src/modules/patcher/patches/game-card-icons.js": true,
"src/modules/patcher/patches/local-co-op-enable.js": true,
"src/modules/patcher/patches/poll-gamepad.js": true,
"src/modules/patcher/patches/remote-play-keep-alive.js": true,

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
// ==UserScript==
// @name Better xCloud
// @namespace https://github.com/redphx
// @version 6.1.1
// @version 6.2.2
// ==/UserScript==

File diff suppressed because one or more lines are too long

View File

@ -106,7 +106,6 @@
&.bx-frosted {
--button-alpha: 0.2;
background-color: unquote('rgba(var(--button-rgb), var(--button-alpha))');
backdrop-filter: blur(4px) brightness(1.5);
&:not([disabled]):not(:active) {
&:hover, &.bx-focusable:focus {
@ -145,15 +144,16 @@
overflow: hidden;
white-space: nowrap;
// Text with icon
&:not(:only-child) {
margin-left: 10px;
margin-inline-start: 8px;
}
}
&.bx-button-multi-lines {
height: auto;
text-align: left;
padding: 10px 0;
padding: 10px;
span {
line-height: unset;

View File

@ -1,3 +1,12 @@
.bx-product-details-icons {
padding: 8px;
border-radius: 4px;
svg {
margin-right: 8px;
}
}
.bx-product-details-buttons {
display: flex;
gap: 10px;

View File

@ -78,7 +78,7 @@
padding: 0;
margin: 0;
flex: 1;
font-size: 1.2rem;
font-size: 1.3rem;
font-weight: bold;
}

View File

@ -1,23 +1,3 @@
.bx-remote-play-container {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
background: #1a1b1e;
border-radius: 10px;
width: 420px;
max-width: calc(100vw - 20px);
margin: 0 0 0 auto;
padding: 16px;
> .bx-button {
display: table;
margin: 0 0 0 auto;
}
}
.bx-remote-play-settings {
margin-bottom: 12px;
padding-bottom: 12px;
@ -29,6 +9,7 @@
label {
flex: 1;
font-size: 14px;
p {
margin: 4px 0 0;
@ -63,23 +44,24 @@
.bx-remote-play-device-info {
flex: 1;
align-self: center;
padding: 4px 0;
}
.bx-remote-play-device-name {
font-size: 20px;
font-size: 14px;
font-weight: bold;
display: inline-block;
vertical-align: middle;
}
.bx-remote-play-console-type {
font-size: 12px;
font-size: 8px;
background: #004c87;
color: #fff;
display: inline-block;
border-radius: 14px;
padding: 2px 10px;
border-radius: 8px;
padding: 2px 6px;
margin-left: 8px;
vertical-align: middle;
}

View File

@ -149,6 +149,10 @@ div[class^=HUDButton-module__hiddenContainer] ~ div:not([class^=HUDButton-module
font-family: var(--bx-normal-font) !important;
}
.bx-frosted {
backdrop-filter: blur(4px) brightness(1.5);
}
select[multiple], select[multiple]:focus {
overflow: auto;
border: none;
@ -190,14 +194,6 @@ div[class*=NotFocusedDialog] {
visibility: hidden;
}
/* Hide Controller icon in Game tiles */
div[class*=SupportedInputsBadge] {
&:not(:has(:nth-child(2))), svg:first-of-type {
display: none;
}
}
.bx-game-tile-wait-time {
position: absolute;
top: 0;

View File

@ -190,6 +190,12 @@
margin-bottom: 0 !important;
flex: 1;
svg {
width: 20px;
height: 20px;
margin-inline-end: 8px;
}
+ * {
margin: 0 0 0 auto;
}

View File

@ -0,0 +1,7 @@
<svg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 32 32' fill-rule='evenodd' stroke-linecap='round' stroke-linejoin='round'>
<g>
<path d='M24.272 11.165h-3.294l-3.14 3.564c-.391.391-.922.611-1.476.611a2.1 2.1 0 0 1-2.087-2.088 2.09 2.09 0 0 1 .031-.362l1.22-6.274a3.89 3.89 0 0 1 3.81-3.206h6.57c1.834 0 3.439 1.573 3.833 3.295l1.205 6.185a2.09 2.09 0 0 1 .031.362 2.1 2.1 0 0 1-2.087 2.088c-.554 0-1.085-.22-1.476-.611l-3.14-3.564' fill='none' stroke='#fff' stroke-width='2'/>
<circle cx='22.625' cy='5.874' r='.879'/><path d='M11.022 24.415H7.728l-3.14 3.564c-.391.391-.922.611-1.476.611a2.1 2.1 0 0 1-2.087-2.088 2.09 2.09 0 0 1 .031-.362l1.22-6.274a3.89 3.89 0 0 1 3.81-3.206h6.57c1.834 0 3.439 1.573 3.833 3.295l1.205 6.185a2.09 2.09 0 0 1 .031.362 2.1 2.1 0 0 1-2.087 2.088c-.554 0-1.085-.22-1.476-.611l-3.14-3.564' fill='none' stroke='#fff' stroke-width='2'/>
<circle cx='9.375' cy='19.124' r='.879'/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 981 B

View File

@ -12,6 +12,7 @@ export const enum StorageKey {
GH_PAGES_COMMIT_HASH = 'BetterXcloud.GhPages.CommitHash',
LIST_CUSTOM_TOUCH_LAYOUTS = 'BetterXcloud.GhPages.CustomTouchLayouts',
LIST_FORCE_NATIVE_MKB = 'BetterXcloud.GhPages.ForceNativeMkb',
LIST_LOCAL_CO_OP = 'BetterXcloud.GhPages.LocalCoOp',
}
@ -85,6 +86,7 @@ export const enum PrefKey {
UI_SKIP_SPLASH_VIDEO = 'ui.splashVideo.skip',
UI_HIDE_SYSTEM_MENU_ICON = 'ui.systemMenu.hideHandle',
UI_REDUCE_ANIMATIONS = 'ui.reduceAnimations',
UI_IMAGE_QUALITY = 'ui.imageQuality',
VIDEO_PLAYER_TYPE = 'video.player.type',
VIDEO_POWER_PREFERENCE = 'video.player.powerPreference',
@ -171,6 +173,7 @@ export type PrefTypeMap = {
[PrefKey.UI_GAME_CARD_SHOW_WAIT_TIME]: boolean,
[PrefKey.UI_HIDE_SECTIONS]: UiSection[],
[PrefKey.UI_HIDE_SYSTEM_MENU_ICON]: boolean,
[PrefKey.UI_IMAGE_QUALITY]: number,
[PrefKey.UI_LAYOUT]: UiLayout,
[PrefKey.UI_REDUCE_ANIMATIONS]: boolean,
[PrefKey.UI_SCROLLBAR_HIDE]: boolean,

View File

@ -164,7 +164,9 @@ document.addEventListener('readystatechange', e => {
if (STATES.isSignedIn) {
// Preload Remote Play
RemotePlayManager.getInstance()?.initialize();
if (isFullVersion()) {
RemotePlayManager.getInstance()?.initialize();
}
} else {
// Show Settings button in the header when not signed in
window.setTimeout(HeaderSection.watchHeader, 2000);
@ -234,8 +236,10 @@ BxEventBus.Stream.on('state.starting', () => {
});
BxEventBus.Stream.on('state.playing', payload => {
window.BX_STREAM_SETTINGS = StreamSettings.settings;
StreamSettings.refreshAllSettings();
if (isFullVersion()) {
window.BX_STREAM_SETTINGS = StreamSettings.settings;
StreamSettings.refreshAllSettings();
}
STATES.isPlaying = true;
StreamUiHandler.observe();
@ -357,9 +361,11 @@ isFullVersion() && window.addEventListener(BxEvent.CAPTURE_SCREENSHOT, e => {
function main() {
GhPagesUtils.fetchLatestCommit();
if (getPref(PrefKey.NATIVE_MKB_MODE) !== NativeMkbMode.OFF) {
const customList = getPref(PrefKey.NATIVE_MKB_FORCED_GAMES);
BX_FLAGS.ForceNativeMkbTitles.push(...customList);
if (isFullVersion()) {
if (getPref(PrefKey.NATIVE_MKB_MODE) !== NativeMkbMode.OFF) {
const customList = getPref(PrefKey.NATIVE_MKB_FORCED_GAMES);
BX_FLAGS.ForceNativeMkbTitles.push(...customList);
}
}
StreamSettings.setup();

View File

@ -63,6 +63,11 @@ export class LoadingScreen {
// Limit max width to reduce image size
imageUrl = imageUrl + '?w=1920';
const imageQuality = getPref(PrefKey.UI_IMAGE_QUALITY);
if (imageQuality !== 90) {
imageUrl += '&q=' + imageQuality;
}
$bgStyle.textContent! += compressCss(`
#game-stream {
background-color: transparent !important;

View File

@ -19,6 +19,7 @@ import type { MkbConvertedPresetData } from "@/types/presets";
import { StreamSettings } from "@/utils/stream-settings";
import { ShortcutAction } from "@/enums/shortcut-actions";
import { BxEventBus } from "@/utils/bx-event-bus";
import { generateVirtualControllerMapping, toXcloudGamepadKey } from "@/utils/gamepad";
const PointerToMouseButton = {
1: 0,
@ -152,6 +153,8 @@ export class EmulatedMkbHandler extends MkbHandler {
};
private nativeGetGamepads: Navigator['getGamepads'];
private xCloudGamepad: XcloudGamepad = generateVirtualControllerMapping(0);
private initialized = false;
private enabled = false;
private mouseDataProvider: MouseDataProvider | undefined;
@ -171,16 +174,16 @@ export class EmulatedMkbHandler extends MkbHandler {
private popup: MkbPopup;
private STICK_MAP: { [key in GamepadKey]?: [GamepadKey[], number, number] } = {
[GamepadKey.LS_LEFT]: [this.LEFT_STICK_X, 0, -1],
[GamepadKey.LS_RIGHT]: [this.LEFT_STICK_X, 0, 1],
[GamepadKey.LS_UP]: [this.LEFT_STICK_Y, 1, -1],
[GamepadKey.LS_DOWN]: [this.LEFT_STICK_Y, 1, 1],
private STICK_MAP: { [key in GamepadKey]?: [GamepadKey[], number] } = {
[GamepadKey.LS_LEFT]: [this.LEFT_STICK_X, -1],
[GamepadKey.LS_RIGHT]: [this.LEFT_STICK_X, 1],
[GamepadKey.LS_UP]: [this.LEFT_STICK_Y, 1],
[GamepadKey.LS_DOWN]: [this.LEFT_STICK_Y, -1],
[GamepadKey.RS_LEFT]: [this.RIGHT_STICK_X, 2, -1],
[GamepadKey.RS_RIGHT]: [this.RIGHT_STICK_X, 2, 1],
[GamepadKey.RS_UP]: [this.RIGHT_STICK_Y, 3, -1],
[GamepadKey.RS_DOWN]: [this.RIGHT_STICK_Y, 3, 1],
[GamepadKey.RS_LEFT]: [this.RIGHT_STICK_X, -1],
[GamepadKey.RS_RIGHT]: [this.RIGHT_STICK_X, 1],
[GamepadKey.RS_UP]: [this.RIGHT_STICK_Y, 1],
[GamepadKey.RS_DOWN]: [this.RIGHT_STICK_Y, -1],
};
private constructor() {
@ -205,11 +208,16 @@ export class EmulatedMkbHandler extends MkbHandler {
private getVirtualGamepad = () => this.VIRTUAL_GAMEPAD;
private updateStick(stick: GamepadStick, x: number, y: number) {
const virtualGamepad = this.getVirtualGamepad();
virtualGamepad.axes[stick * 2] = x;
virtualGamepad.axes[stick * 2 + 1] = y;
const gamepad = this.xCloudGamepad;
if (stick === GamepadStick.LEFT) {
gamepad.LeftThumbXAxis = x;
gamepad.LeftThumbYAxis = -y;
} else {
gamepad.RightThumbXAxis = x;
gamepad.RightThumbYAxis = -y;
}
virtualGamepad.timestamp = performance.now();
window.BX_EXPOSED.inputChannel?.sendGamepadInput(performance.now(), [this.xCloudGamepad]);
}
/*
@ -224,29 +232,20 @@ export class EmulatedMkbHandler extends MkbHandler {
private vectorLength = (x: number, y: number): number => Math.sqrt(x ** 2 + y ** 2);
private resetGamepad() {
const gamepad = this.getVirtualGamepad();
resetXcloudGamepads() {
const index = getPref(PrefKey.MKB_P1_SLOT) - 1;
// Reset axes
gamepad.axes = [0, 0, 0, 0];
// Reset buttons
for (const button of gamepad.buttons) {
button.pressed = false;
button.value = 0;
}
gamepad.timestamp = performance.now();
this.xCloudGamepad = generateVirtualControllerMapping(0, {
GamepadIndex: getPref(PrefKey.LOCAL_CO_OP_ENABLED) ? index : 0,
Dirty: true,
});
this.VIRTUAL_GAMEPAD.index = index;
}
private pressButton(buttonIndex: GamepadKey, pressed: boolean) {
const virtualGamepad = this.getVirtualGamepad();
const xCloudKey = toXcloudGamepadKey(buttonIndex)!;
if (buttonIndex >= 100) {
let [valueArr, axisIndex] = this.STICK_MAP[buttonIndex]!;
valueArr = valueArr as number[];
axisIndex = axisIndex as number;
let [valueArr]: [GamepadKey[], number] = this.STICK_MAP[buttonIndex]!;
// Remove old index of the array
for (let i = valueArr.length - 1; i >= 0; i--) {
if (valueArr[i] === buttonIndex) {
@ -259,18 +258,19 @@ export class EmulatedMkbHandler extends MkbHandler {
let value;
if (valueArr.length) {
// Get value of the last key of the axis
value = this.STICK_MAP[valueArr[valueArr.length - 1]]![2] as number;
value = this.STICK_MAP[valueArr[valueArr.length - 1]]![1] as number;
} else {
value = 0;
}
virtualGamepad.axes[axisIndex] = value;
// @ts-ignore
this.xCloudGamepad[xCloudKey] = value;
} else {
virtualGamepad.buttons[buttonIndex].pressed = pressed;
virtualGamepad.buttons[buttonIndex].value = pressed ? 1 : 0;
// @ts-ignore
this.xCloudGamepad[xCloudKey] = pressed ? 1 : 0;
}
virtualGamepad.timestamp = performance.now();
window.BX_EXPOSED.inputChannel?.sendGamepadInput(performance.now(), [this.xCloudGamepad]);
}
private onKeyboardEvent = (e: KeyboardEvent) => {
@ -428,7 +428,7 @@ export class EmulatedMkbHandler extends MkbHandler {
return true;
}
toggle(force?: boolean) {
async toggle(force?: boolean) {
if (!this.initialized) {
return;
}
@ -440,9 +440,12 @@ export class EmulatedMkbHandler extends MkbHandler {
}
if (this.enabled) {
document.body.requestPointerLock({
unadjustedMovement: true,
});
try {
await document.body.requestPointerLock({ unadjustedMovement: true });
} catch (e) {
document.body.requestPointerLock();
console.log(e);
}
} else {
document.pointerLockElement && document.exitPointerLock();
}
@ -450,7 +453,7 @@ export class EmulatedMkbHandler extends MkbHandler {
refreshPresetData() {
this.PRESET = window.BX_STREAM_SETTINGS.mkbPreset;
this.resetGamepad();
this.resetXcloudGamepads();
}
waitForMouseData(showPopup: boolean) {
@ -578,11 +581,6 @@ export class EmulatedMkbHandler extends MkbHandler {
window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.onPollingModeChanged);
}
updateGamepadSlots() {
// Set gamepad slot
this.VIRTUAL_GAMEPAD.index = getPref(PrefKey.MKB_P1_SLOT) - 1;
}
start() {
if (!this.enabled) {
this.enabled = true;
@ -592,8 +590,8 @@ export class EmulatedMkbHandler extends MkbHandler {
this.isPolling = true;
this.escKeyDownTime = -1;
this.resetGamepad();
this.updateGamepadSlots();
window.BX_EXPOSED.toggleLocalCoOp(getPref(PrefKey.LOCAL_CO_OP_ENABLED));
this.resetXcloudGamepads();
window.navigator.getGamepads = this.patchedGetGamepads;
this.waitForMouseData(false);
@ -622,7 +620,7 @@ export class EmulatedMkbHandler extends MkbHandler {
const virtualGamepad = this.getVirtualGamepad();
if (virtualGamepad.connected) {
// Dispatch "gamepaddisconnected" event
this.resetGamepad();
this.resetXcloudGamepads();
virtualGamepad.connected = false;
virtualGamepad.timestamp = performance.now();

View File

@ -13,19 +13,7 @@ import { StreamSettings } from "@/utils/stream-settings";
import { ShortcutAction } from "@/enums/shortcut-actions";
import { NativeMkbMode } from "@/enums/pref-values";
import { BxEventBus } from "@/utils/bx-event-bus";
type NativeMouseData = {
X: number,
Y: number,
Buttons: number,
WheelX: number,
WheelY: number,
Type?: 0, // 0: Relative, 1: Absolute
}
type XcloudInputSink = {
onMouseInput: (data: NativeMouseData) => void;
}
import type { NativeMouseData, XcloudInputChannel } from "@/utils/gamepad";
export class NativeMkbHandler extends MkbHandler {
private static instance: NativeMkbHandler | null | undefined;
@ -54,7 +42,7 @@ export class NativeMkbHandler extends MkbHandler {
private mouseVerticalMultiply = 0;
private mouseHorizontalMultiply = 0;
private inputSink: XcloudInputSink | undefined;
private inputChannel: XcloudInputChannel | undefined;
private popup!: MkbPopup;
@ -114,7 +102,7 @@ export class NativeMkbHandler extends MkbHandler {
init() {
this.pointerClient = PointerClient.getInstance();
this.inputSink = window.BX_EXPOSED.inputSink;
this.inputChannel = window.BX_EXPOSED.inputChannel;
// Stop keyboard input at startup
this.updateInputConfigurationAsync(false);
@ -274,7 +262,7 @@ export class NativeMkbHandler extends MkbHandler {
private sendMouseInput(data: NativeMouseData) {
data.Type = 0; // Relative
this.inputSink?.onMouseInput(data);
this.inputChannel?.queueMouseInput(data);
}
private resetMouseInput() {

View File

@ -50,4 +50,50 @@ export class PatcherUtils {
return str;
}
private static isVarCharacter(char: string) {
const code = char.charCodeAt(0);
// Check for uppercase letters (A-Z)
const isUppercase = code >= 65 && code <= 90;
// Check for lowercase letters (a-z)
const isLowercase = code >= 97 && code <= 122;
// Check for digits (0-9)
const isDigit = code >= 48 && code <= 57;
// Check for special characters '_' and '$'
const isSpecial = char === '_' || char === '$';
return isUppercase || isLowercase || isDigit || isSpecial;
}
static getVariableNameBefore(str: string, index: number) {
if (index < 0) {
return null;
}
const end = index;
let start = end - 1;
while (PatcherUtils.isVarCharacter(str[start])) {
start -= 1;
}
return str.substring(start + 1, end);
}
static getVariableNameAfter(str: string, index: number) {
if (index < 0) {
return null;
}
const start = index;
let end = start + 1;
while (PatcherUtils.isVarCharacter(str[end])) {
end += 1;
}
return str.substring(start, end);
}
}

View File

@ -7,6 +7,7 @@ import { BxEvent } from "@/utils/bx-event";
import codeControllerCustomization from "./patches/controller-customization.js" with { type: "text" };
import codePollGamepad from "./patches/poll-gamepad.js" with { type: "text" };
import codeExposeStreamSession from "./patches/expose-stream-session.js" with { type: "text" };
import codeGameCardIcons from "./patches/game-card-icons.js" with { type: "text" };
import codeLocalCoOpEnable from "./patches/local-co-op-enable.js" with { type: "text" };
import codeRemotePlayKeepAlive from "./patches/remote-play-keep-alive.js" with { type: "text" };
import codeVibrationAdjust from "./patches/vibration-adjust.js" with { type: "text" };
@ -642,15 +643,14 @@ true` + text;
return str;
},
exposeInputSink(str: string) {
let text = 'this.controlChannel=null,this.inputChannel=null';
if (!str.includes(text)) {
exposeInputChannel(str: string) {
let index = str.indexOf('this.flushData=');
if (index < 0) {
return false;
}
const newCode = 'window.BX_EXPOSED.inputSink = this;';
str = str.replace(text, newCode + text);
const newCode = 'window.BX_EXPOSED.inputChannel = this,';
str = PatcherUtils.insertAt(str, index, newCode);
return str;
},
@ -1003,15 +1003,133 @@ ${subsVar} = subs;
str = PatcherUtils.insertAt(str, index, newCode);
return str;
},
exposeReactCreateComponent(str: string) {
let index = str.indexOf('.prototype.isReactComponent={}');
index > -1 && (index = PatcherUtils.indexOf(str, '.createElement=', index));
if (index < 0) {
return false;
}
const newCode = 'window.BX_EXPOSED.reactCreateElement=';
str = PatcherUtils.insertAt(str, index - 1, newCode);
return str;
},
// 27.0.6-hotfix.1, 73704.js
gameCardCustomIcons(str: string) {
let initialIndex = str.indexOf('const{supportedInputIcons:');
if (initialIndex < 0) {
return false;
}
const returnIndex = PatcherUtils.lastIndexOf(str, 'return ', str.indexOf('SupportedInputsBadge'));
if (returnIndex < 0) {
return false;
}
// Find function's parameter
const arrowIndex = PatcherUtils.lastIndexOf(str, '=>{', initialIndex, 300);
if (arrowIndex < 0) {
return false;
}
const paramVar = PatcherUtils.getVariableNameBefore(str, arrowIndex);
// Find supportedInputIcons and title var names
const supportedInputIconsVar = PatcherUtils.getVariableNameAfter(str, PatcherUtils.indexOf(str, 'supportedInputIcons:', initialIndex, 100, true));
if (!paramVar || !supportedInputIconsVar) {
return false;
}
const newCode = renderString(codeGameCardIcons, {
param: paramVar,
supportedInputIcons: supportedInputIconsVar,
});
str = PatcherUtils.insertAt(str, returnIndex, newCode);
return str;
},
/*
// 27.0.6-hotfix.1, 28444.js
gameCardPassTitle(str: string) {
// Pass gameTitle info to gameCardCustomIcons()
let index = str.indexOf('=["productId","showInputBadges","ownershipBadgeType"');
index > -1 && (index = PatcherUtils.indexOf(str, ',gameTitle:', index, 500, true));
if (index < 0) {
return false;
}
const gameTitleVar = PatcherUtils.getVariableNameAfter(str, index);
if (!gameTitleVar) {
return false;
}
index = PatcherUtils.indexOf(str, 'return', index);
index = PatcherUtils.indexOf(str, 'productId:', index);
if (index < 0) {
return false;
}
const newCode = `gameTitle: ${gameTitleVar},`;
str = PatcherUtils.insertAt(str, index, newCode);
return str;
},
*/
// 27.0.6-hotfix.1, 78831.js
setImageQuality(str: string) {
let index = str.indexOf('const{size:{width:');
index > -1 && (index = PatcherUtils.indexOf(str, '=new URLSearchParams', index, 500));
if (index < 0) {
return false;
}
const paramVar = PatcherUtils.getVariableNameBefore(str, index);
if (!paramVar) {
return false;
}
// Find "return" keyword
index = PatcherUtils.indexOf(str, 'return', index, 200);
const newCode = `${paramVar}.set('q', ${getPref(PrefKey.UI_IMAGE_QUALITY)});`;
str = PatcherUtils.insertAt(str, index, newCode);
return str;
},
setBackgroundImageQuality(str: string) {
let index = str.indexOf('}?w=${');
index > -1 && (index = PatcherUtils.indexOf(str, '}', index + 1, 10, true));
if (index < 0) {
return false;
}
str = PatcherUtils.insertAt(str, index, `&q=${getPref(PrefKey.UI_IMAGE_QUALITY)}`);
return str;
}
};
let PATCH_ORDERS = PatcherUtils.filterPatches([
...(AppInterface && getPref(PrefKey.NATIVE_MKB_MODE) === NativeMkbMode.ON ? [
'enableNativeMkb',
'exposeInputSink',
'disableAbsoluteMouse',
] : []),
'exposeReactCreateComponent',
'gameCardCustomIcons',
// 'gameCardPassTitle',
...(getPref(PrefKey.UI_IMAGE_QUALITY) < 90 ? [
'setImageQuality',
] : []),
'modifyPreloadedState',
'optimizeGameSlugGenerator',
@ -1068,10 +1186,6 @@ let PATCH_ORDERS = PatcherUtils.filterPatches([
'enableConsoleLogging',
'enableXcloudLogger',
] : []),
...(blockSomeNotifications() ? [
'changeNotificationsSubscription',
] : []),
]);
const hideSections = getPref(PrefKey.UI_HIDE_SECTIONS);
@ -1081,12 +1195,22 @@ let HOME_PAGE_PATCH_ORDERS = PatcherUtils.filterPatches([
hideSections.includes(UiSection.ALL_GAMES) && 'ignoreAllGamesSection',
STATES.browser.capabilities.touch && hideSections.includes(UiSection.TOUCH) && 'ignorePlayWithTouchSection',
hideSections.some(value => [UiSection.NATIVE_MKB, UiSection.MOST_POPULAR].includes(value)) && 'ignoreSiglSections',
...(getPref(PrefKey.UI_IMAGE_QUALITY) < 90 ? [
'setBackgroundImageQuality',
] : []),
...(blockSomeNotifications() ? [
'changeNotificationsSubscription',
] : []),
]);
// Only when playing
// TODO: check this
// @ts-ignore
let STREAM_PAGE_PATCH_ORDERS = PatcherUtils.filterPatches([
'exposeInputChannel',
'patchXcloudTitleInfo',
'disableGamepadDisconnectedScreen',
'patchStreamHud',
@ -1131,7 +1255,7 @@ let STREAM_PAGE_PATCH_ORDERS = PatcherUtils.filterPatches([
]);
let PRODUCT_DETAIL_PAGE_PATCH_ORDERS = PatcherUtils.filterPatches([
AppInterface && 'detectProductDetailPage',
'detectProductDetailPage',
]);
const ALL_PATCHES = [...PATCH_ORDERS, ...HOME_PAGE_PATCH_ORDERS, ...STREAM_PAGE_PATCH_ORDERS, ...PRODUCT_DETAIL_PAGE_PATCH_ORDERS];
@ -1256,6 +1380,7 @@ export class Patcher {
// Apply patched functions
if (modified) {
BX_FLAGS.Debug && console.time(LOG_TAG);
try {
chunkData[chunkId] = eval(patchedFuncStr);
} catch (e: unknown) {
@ -1263,6 +1388,7 @@ export class Patcher {
BxLogger.error(LOG_TAG, 'Error', appliedPatches, e.message, patchedFuncStr);
}
}
BX_FLAGS.Debug && console.timeEnd(LOG_TAG);
}
// Save to cache

View File

@ -0,0 +1,12 @@
declare const $supportedInputIcons$: Array<any>;
declare const $param$: { productId: string };
const supportedInputIcons = $supportedInputIcons$;
const { productId } = $param$;
// Remove controller icon
supportedInputIcons.shift();
if (window.BX_EXPOSED.localCoOpManager!.isSupported(productId)) {
supportedInputIcons.push(window.BX_EXPOSED.createReactLocalCoOpIcon);
}

View File

@ -7,8 +7,8 @@ export class VirtualControllerShortcut {
return;
}
const released = generateVirtualControllerMapping();
const pressed = generateVirtualControllerMapping({
const released = generateVirtualControllerMapping(0);
const pressed = generateVirtualControllerMapping(0, {
Nexus: 1,
VirtualPhysicality: 1024, // Home
});

View File

@ -1,3 +1,5 @@
import { isFullVersion } from "@macros/build" with { type: "macro" };
import { GamepadKey } from "@/enums/gamepad";
import { VIRTUAL_GAMEPAD_ID } from "@/modules/mkb/mkb-handler";
import { BxEvent } from "@/utils/bx-event";
@ -641,7 +643,9 @@ export class NavigationDialogManager {
private startGamepadPolling() {
this.stopGamepadPolling();
this.gamepadPollingIntervalId = window.setInterval(this.pollGamepad, NavigationDialogManager.GAMEPAD_POLLING_INTERVAL);
if (isFullVersion()) {
this.gamepadPollingIntervalId = window.setInterval(this.pollGamepad, NavigationDialogManager.GAMEPAD_POLLING_INTERVAL);
}
}
private stopGamepadPolling() {

View File

@ -32,7 +32,11 @@ export class RemotePlayDialog extends NavigationDialog {
}
private setupDialog() {
const $fragment = CE('div', { class: 'bx-remote-play-container' });
const $fragment = CE('div', { class: 'bx-centered-dialog' },
CE('div', { class: 'bx-dialog-title' },
CE('p', false, t('remote-play')),
),
);
const $settingNote = CE('p', {});

View File

@ -274,6 +274,7 @@ export class SettingsDialog extends NavigationDialog {
label: t('ui'),
items: [
PrefKey.UI_LAYOUT,
PrefKey.UI_IMAGE_QUALITY,
PrefKey.UI_GAME_CARD_SHOW_WAIT_TIME,
PrefKey.UI_CONTROLLER_SHOW_STATUS,
PrefKey.UI_SIMPLIFY_STREAM_MENU,
@ -312,7 +313,7 @@ export class SettingsDialog extends NavigationDialog {
items: [
PrefKey.BLOCK_TRACKING,
],
}, {
}, isFullVersion() && {
group: 'advanced',
label: t('advanced'),
items: [
@ -494,24 +495,23 @@ export class SettingsDialog extends NavigationDialog {
}],
}];
private readonly TAB_CONTROLLER_ITEMS: Array<SettingTabSection | HTMLElement | false> = [{
private readonly TAB_CONTROLLER_ITEMS: Array<SettingTabSection | HTMLElement | false> = isFullVersion() ? [{
group: 'controller',
label: t('controller'),
helpUrl: 'https://better-xcloud.github.io/ingame-features/#controller',
items: [
isFullVersion() && {
{
pref: PrefKey.LOCAL_CO_OP_ENABLED,
onChange: () => { BxExposed.toggleLocalCoOp(getPref(PrefKey.LOCAL_CO_OP_ENABLED)); },
},
isFullVersion() && {
}, {
pref: PrefKey.CONTROLLER_POLLING_RATE,
onChange: () => StreamSettings.refreshControllerSettings(),
}, isFullVersion() && ($parent => {
}, ($parent => {
$parent.appendChild(ControllerExtraSettings.renderSettings.apply(this));
})],
},
isFullVersion() && STATES.userAgent.capabilities.touch && {
STATES.userAgent.capabilities.touch && {
group: 'touch-control',
label: t('touch-controller'),
items: [{
@ -563,7 +563,9 @@ export class SettingsDialog extends NavigationDialog {
});
},
}],
}, isFullVersion() && STATES.browser.capabilities.deviceVibration && {
},
STATES.browser.capabilities.deviceVibration && {
group: 'device',
label: t('device'),
items: [{
@ -576,22 +578,22 @@ export class SettingsDialog extends NavigationDialog {
unsupported: !STATES.browser.capabilities.deviceVibration,
onChange: () => StreamSettings.refreshControllerSettings(),
}],
}];
}] : [];
private readonly TAB_MKB_ITEMS: (() => Array<SettingTabSection | false>) = () => [
isFullVersion() && {
private readonly TAB_MKB_ITEMS: (() => Array<SettingTabSection | false>) = isFullVersion() ? () => [
{
requiredVariants: 'full',
group: 'mkb',
label: t('mouse-and-keyboard'),
helpUrl: 'https://better-xcloud.github.io/mouse-and-keyboard/',
items: [
isFullVersion() && (($parent: HTMLElement) => {
($parent: HTMLElement) => {
$parent.appendChild(MkbExtraSettings.renderSettings.apply(this));
})
},
],
},
isFullVersion() && NativeMkbHandler.isAllowed() && {
NativeMkbHandler.isAllowed() && {
requiredVariants: 'full',
group: 'native-mkb',
label: t('native-mkb'),
@ -606,7 +608,7 @@ export class SettingsDialog extends NavigationDialog {
NativeMkbHandler.getInstance()?.setHorizontalScrollMultiplier(value / 100);
},
}] : [],
}];
}] : () => [];
private readonly TAB_STATS_ITEMS: Array<SettingTabSection | false> = [{
group: 'stats',
@ -1007,6 +1009,7 @@ export class SettingsDialog extends NavigationDialog {
const $row = createSettingRow(label, !prefDefinition?.unsupported && $control, {
$note,
multiLines: setting.multiLines,
icon: prefDefinition?.labelIcon,
});
if (pref) {
$row.htmlFor = `bx_setting_${escapeCssSelector(pref)}`;

View File

@ -69,7 +69,7 @@ export class MkbExtraSettings extends HTMLElement {
createSettingRow(
t('virtual-controller-slot'),
SettingElement.fromPref(PrefKey.MKB_P1_SLOT, STORAGE.Global, () => {
EmulatedMkbHandler.getInstance()?.updateGamepadSlots();
EmulatedMkbHandler.getInstance()?.resetXcloudGamepads();
}),
),
] : []),

View File

@ -41,6 +41,7 @@ export class GuideMenu {
const buttons = {
scriptSettings: createButton({
label: t('better-xcloud'),
icon: BxIcon.BETTER_XCLOUD,
style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE | ButtonStyle.PRIMARY,
onClick: () => {
// Wait until the Guide dialog is closed

View File

@ -1,3 +1,5 @@
import { isFullVersion } from "@macros/build" with { type: "macro" };
import { SCRIPT_VERSION } from "@utils/global";
import { createButton, ButtonStyle, CE, isElementVisible } from "@utils/html";
import { BxIcon } from "@utils/bx-icon";
@ -14,7 +16,7 @@ export class HeaderSection {
public static getInstance = () => HeaderSection.instance ?? (HeaderSection.instance = new HeaderSection());
private readonly LOG_TAG = 'HeaderSection';
private $btnRemotePlay: HTMLElement;
private $btnRemotePlay: HTMLElement | null;
private $btnSettings: HTMLElement;
private $buttonsWrapper: HTMLElement;
@ -24,13 +26,17 @@ export class HeaderSection {
constructor() {
BxLogger.info(this.LOG_TAG, 'constructor()');
this.$btnRemotePlay = createButton({
classes: ['bx-header-remote-play-button', 'bx-gone'],
icon: BxIcon.REMOTE_PLAY,
title: t('remote-play'),
style: ButtonStyle.GHOST | ButtonStyle.FOCUSABLE | ButtonStyle.CIRCULAR,
onClick: e => RemotePlayManager.getInstance()?.togglePopup(),
});
if (isFullVersion()) {
this.$btnRemotePlay = createButton({
classes: ['bx-header-remote-play-button', 'bx-gone'],
icon: BxIcon.REMOTE_PLAY,
title: t('remote-play'),
style: ButtonStyle.GHOST | ButtonStyle.FOCUSABLE | ButtonStyle.CIRCULAR,
onClick: e => RemotePlayManager.getInstance()?.togglePopup(),
});
} else {
this.$btnRemotePlay = null;
}
this.$btnSettings = createButton({
classes: ['bx-header-settings-button'],
@ -98,7 +104,7 @@ export class HeaderSection {
}
showRemotePlayButton() {
this.$btnRemotePlay.classList.remove('bx-gone');
this.$btnRemotePlay?.classList.remove('bx-gone');
}
static watchHeader() {

View File

@ -1,7 +1,8 @@
import { BX_FLAGS } from "@/utils/bx-flags";
import { BxIcon } from "@/utils/bx-icon";
import { AppInterface } from "@/utils/global";
import { ButtonStyle, CE, createButton } from "@/utils/html";
import { ButtonStyle, CE, createButton, createSvgIcon } from "@/utils/html";
import { LocalCoOpManager } from "@/utils/local-co-op-manager";
import { t } from "@/utils/translation";
import { parseDetailsPath } from "@/utils/utils";
@ -28,21 +29,33 @@ export class ProductDetailsPage {
private static injectTimeoutId: number | null = null;
static injectButtons() {
if (!AppInterface) {
return;
}
ProductDetailsPage.injectTimeoutId && clearTimeout(ProductDetailsPage.injectTimeoutId);
ProductDetailsPage.injectTimeoutId = window.setTimeout(() => {
// Find action buttons container
const $container = document.querySelector('div[class*=ActionButtons-module__container]');
if ($container && $container.parentElement) {
$container.parentElement.appendChild(CE('div', {
class: 'bx-product-details-buttons',
},
['android-handheld', 'android'].includes(BX_FLAGS.DeviceInfo.deviceType) && ProductDetailsPage.$btnShortcut,
ProductDetailsPage.$btnWallpaper,
));
// Inputs
const $inputsContainer = document.querySelector<HTMLElement>('div[class*="Header-module__gamePassAndInputsContainer"]');
if ($inputsContainer && !$inputsContainer.dataset.bxInjected) {
$inputsContainer.dataset.bxInjected = 'true';
const { productId } = parseDetailsPath(window.location.pathname);
if (LocalCoOpManager.getInstance().isSupported(productId || '')) {
$inputsContainer.insertAdjacentElement('afterend', CE('div', {
class: 'bx-product-details-icons bx-frosted',
}, createSvgIcon(BxIcon.LOCAL_CO_OP), t('local-co-op')));
}
}
// Inject buttons for Android app
if (AppInterface) {
// Find action buttons container
const $container = document.querySelector('div[class*=ActionButtons-module__container]');
if ($container && $container.parentElement) {
$container.parentElement.appendChild(CE('div', {
class: 'bx-product-details-buttons',
},
['android-handheld', 'android'].includes(BX_FLAGS.DeviceInfo.deviceType) && ProductDetailsPage.$btnShortcut,
ProductDetailsPage.$btnWallpaper,
));
}
}
}, 500);
}

View File

@ -5,6 +5,7 @@ import type { StreamSettings, type StreamSettingsData } from "@/utils/stream-set
import type { BxEvent } from "@/utils/bx-event";
import type { BxEventBus } from "@/utils/bx-event-bus";
import type { BxLogger } from "@/utils/bx-logger";
import type { XcloudInputChannel } from "@/utils/gamepad";
export {};
@ -20,7 +21,7 @@ declare global {
closeAll: () => void;
};
showStreamMenu: () => void;
inputSink: any;
inputChannel: XcloudInputChannel | undefined;
streamSession: any;
touchLayoutManager: any;
}>;

View File

@ -20,6 +20,7 @@ interface BaseSettingDefinition {
default: any;
label?: string;
labelIcon?: BxIconRaw,
note?: string | (() => HTMLElement) | HTMLElement;
experimental?: boolean;
unsupported?: boolean;

View File

@ -31,6 +31,10 @@ type ScriptEvents = {
data: any;
};
};
'list.localCoOp.updated': {
ids: Set<string>,
};
};
type StreamEvents = {

View File

@ -13,6 +13,7 @@ import { NativeMkbMode, TouchControllerMode } from "@/enums/pref-values";
import { Patcher, type PatchPage } from "@/modules/patcher/patcher";
import { BxEventBus } from "./bx-event-bus";
import { FeatureGates } from "./feature-gates";
import { LocalCoOpManager } from "./local-co-op-manager";
export enum SupportedInputType {
CONTROLLER = 'Controller',
@ -230,4 +231,25 @@ export const BxExposed = {
BxLogger.info('beforePageLoad', page);
Patcher.patchPage(page);
} : () => {},
localCoOpManager: isFullVersion() ? LocalCoOpManager.getInstance() : null,
reactCreateElement: function(...args: any[]) {},
createReactLocalCoOpIcon: isFullVersion() ? (attrs: any): any => {
const reactCE = window.BX_EXPOSED.reactCreateElement;
// local-co-op.svg
return reactCE(
'svg',
{ xmlns: 'http://www.w3.org/2000/svg', width: '1em', height: '1em', viewBox: '0 0 32 32', 'fill-rule': 'evenodd', 'stroke-linecap': 'round', 'stroke-linejoin': 'round', ...attrs },
reactCE(
'g',
null,
reactCE('path', { d: 'M24.272 11.165h-3.294l-3.14 3.564c-.391.391-.922.611-1.476.611a2.1 2.1 0 0 1-2.087-2.088 2.09 2.09 0 0 1 .031-.362l1.22-6.274a3.89 3.89 0 0 1 3.81-3.206h6.57c1.834 0 3.439 1.573 3.833 3.295l1.205 6.185a2.09 2.09 0 0 1 .031.362 2.1 2.1 0 0 1-2.087 2.088c-.554 0-1.085-.22-1.476-.611l-3.14-3.564', fill: 'none', stroke: '#fff', 'stroke-width': '2' }),
reactCE('circle', { cx: '22.625', cy: '5.874', r: '.879' }),
reactCE('path', { d: 'M11.022 24.415H7.728l-3.14 3.564c-.391.391-.922.611-1.476.611a2.1 2.1 0 0 1-2.087-2.088 2.09 2.09 0 0 1 .031-.362l1.22-6.274a3.89 3.89 0 0 1 3.81-3.206h6.57c1.834 0 3.439 1.573 3.833 3.295l1.205 6.185a2.09 2.09 0 0 1 .031.362 2.1 2.1 0 0 1-2.087 2.088c-.554 0-1.085-.22-1.476-.611l-3.14-3.564', fill: 'none', stroke: '#fff', 'stroke-width': '2' }),
reactCE('circle', { cx: '9.375', cy: '19.124', r: '.879' })
),
);
} : () => {},
};

View File

@ -10,6 +10,7 @@ import iconDisplay from "@assets/svg/display.svg" with { type: "text" };
import iconEye from "@assets/svg/eye.svg" with { type: "text" };
import iconEyeSlash from "@assets/svg/eye-slash.svg" with { type: "text" };
import iconHome from "@assets/svg/home.svg" with { type: "text" };
import iconLocalCoOp from "@assets/svg/local-co-op.svg" with { type: "text" };
import iconNativeMkb from "@assets/svg/native-mkb.svg" with { type: "text" };
import iconNew from "@assets/svg/new.svg" with { type: "text" };
import iconPencil from "@assets/svg/pencil-simple-line.svg" with { type: "text" };
@ -52,6 +53,7 @@ export const BxIcon = {
EYE: iconEye,
EYE_SLASH: iconEyeSlash,
HOME: iconHome,
LOCAL_CO_OP: iconLocalCoOp,
NATIVE_MKB: iconNativeMkb,
NEW: iconNew,
MANAGE: iconPencil,

View File

@ -1,5 +1,5 @@
import { CE } from "@utils/html";
import { compressCss, renderStylus } from "@macros/build" with { type: "macro" };
import { compressCss, isLiteVersion, renderStylus } from "@macros/build" with { type: "macro" };
import { BlockFeature, UiSection } from "@/enums/pref-values";
import { PrefKey } from "@/enums/pref-keys";
import { getPref } from "./settings-storages/global-settings-storage";
@ -12,6 +12,12 @@ export function addCss() {
const PREF_HIDE_SECTIONS = getPref(PrefKey.UI_HIDE_SECTIONS);
const selectorToHide = [];
if (isLiteVersion()) {
// Hide Controller icon in Game tiles
selectorToHide.push('div[class*=SupportedInputsBadge] svg:first-of-type');
selectorToHide.push('div[class*=SupportedInputsBadge]:not(:has(:nth-child(2)))');
}
// Hide "News" section
if (PREF_HIDE_SECTIONS.includes(UiSection.NEWS)) {
selectorToHide.push('#BodyContent > div[class*=CarouselRow-module]');

View File

@ -4,7 +4,21 @@ import { Toast } from "@utils/toast";
import { BxLogger } from "@utils/bx-logger";
import { PrefKey } from "@/enums/pref-keys";
import { getPref } from "./settings-storages/global-settings-storage";
import { GamepadKeyName, type GamepadKey } from "@/enums/gamepad";
import { GamepadKey, GamepadKeyName } from "@/enums/gamepad";
export type NativeMouseData = {
X: number,
Y: number,
Buttons: number,
WheelX: number,
WheelY: number,
Type?: 0, // 0: Relative, 1: Absolute
}
export type XcloudInputChannel = {
sendGamepadInput: (timestamp: number, gamepads: XcloudGamepad[]) => void;
queueMouseInput: (data: NativeMouseData) => void;
}
// Show a toast when connecting/disconecting controller
export function showGamepadToast(gamepad: Gamepad) {
@ -59,9 +73,9 @@ export function hasGamepad() {
return false;
}
export function generateVirtualControllerMapping(override: {}={}) {
export function generateVirtualControllerMapping(index: number, override: Partial<XcloudGamepad>={}) {
const mapping = {
GamepadIndex: 0,
GamepadIndex: index,
A: 0,
B: 0,
X: 0,
@ -95,3 +109,44 @@ export function generateVirtualControllerMapping(override: {}={}) {
export function getGamepadPrompt(gamepadKey: GamepadKey): string {
return GamepadKeyName[gamepadKey][1];
}
const XCLOUD_GAMEPAD_KEY_MAPPING: { [key in GamepadKey]?: keyof XcloudGamepad } = {
[GamepadKey.A]: 'A',
[GamepadKey.B]: 'B',
[GamepadKey.X]: 'X',
[GamepadKey.Y]: 'Y',
[GamepadKey.UP]: 'DPadUp',
[GamepadKey.RIGHT]: 'DPadRight',
[GamepadKey.DOWN]: 'DPadDown',
[GamepadKey.LEFT]: 'DPadLeft',
[GamepadKey.LB]: 'LeftShoulder',
[GamepadKey.RB]: 'RightShoulder',
[GamepadKey.LT]: 'LeftTrigger',
[GamepadKey.RT]: 'RightTrigger',
[GamepadKey.L3]: 'LeftThumb',
[GamepadKey.R3]: 'RightThumb',
[GamepadKey.LS]: 'LeftStickAxes',
[GamepadKey.RS]: 'RightStickAxes',
[GamepadKey.SELECT]: 'View',
[GamepadKey.START]: 'Menu',
[GamepadKey.HOME]: 'Nexus',
[GamepadKey.SHARE]: 'Share',
[GamepadKey.LS_LEFT]: 'LeftThumbXAxis',
[GamepadKey.LS_RIGHT]: 'LeftThumbXAxis',
[GamepadKey.LS_UP]: 'LeftThumbYAxis',
[GamepadKey.LS_DOWN]: 'LeftThumbYAxis',
[GamepadKey.RS_LEFT]: 'RightThumbXAxis',
[GamepadKey.RS_RIGHT]: 'RightThumbXAxis',
[GamepadKey.RS_UP]: 'RightThumbYAxis',
[GamepadKey.RS_DOWN]: 'RightThumbYAxis',
};
export function toXcloudGamepadKey(gamepadKey: GamepadKey) {
return XCLOUD_GAMEPAD_KEY_MAPPING[gamepadKey];
}

View File

@ -56,6 +56,8 @@ export class GhPagesUtils {
BxEventBus.Script.emit('list.forcedNativeMkb.updated', {
data: json,
});
} else {
window.localStorage.removeItem(key);
}
});
@ -70,6 +72,7 @@ export class GhPagesUtils {
}
static getTouchControlCustomList() {
// TODO: use Set()
const key = StorageKey.LIST_CUSTOM_TOUCH_LAYOUTS;
NATIVE_FETCH(GhPagesUtils.getUrl('touch-layouts/ids.json'))
@ -83,4 +86,31 @@ export class GhPagesUtils {
const customList = JSON.parse(window.localStorage.getItem(key) || '[]');
return customList;
}
static getLocalCoOpList(): Set<string> {
const supportedSchema = 1;
const key = StorageKey.LIST_LOCAL_CO_OP;
NATIVE_FETCH(GhPagesUtils.getUrl('local-co-op/ids.json'))
.then(response => response.json())
.then(json => {
if (json.$schemaVersion === supportedSchema) {
window.localStorage.setItem(key, JSON.stringify(json));
const ids = new Set(Object.keys(json.data));
BxEventBus.Script.emit('list.localCoOp.updated', { ids });
} else {
window.localStorage.removeItem(key);
BxEventBus.Script.emit('list.localCoOp.updated', { ids: new Set() });
}
});
const info = JSON.parse(window.localStorage.getItem(key) || '{}');
if (info.$schemaVersion !== supportedSchema) {
// Delete storage;
window.localStorage.removeItem(key);
return new Set();
}
return new Set(Object.keys(info.data || {}));
}
}

View File

@ -1,3 +1,5 @@
import { isFullVersion } from "@macros/build" with { type: "macro" };
import { BxEvent } from "@utils/bx-event";
import { LoadingScreen } from "@modules/loading-screen";
import { RemotePlayManager } from "@/modules/remote-play-manager";
@ -25,7 +27,9 @@ export function onHistoryChanged(e: PopStateEvent) {
return;
}
window.setTimeout(RemotePlayManager.detect, 10);
if (isFullVersion()) {
window.setTimeout(RemotePlayManager.detect, 10);
}
// Hide Navigation dialog
NavigationDialogManager.getInstance().hide();

View File

@ -54,6 +54,7 @@ export type BxButtonOptions = Partial<{
}>;
export type SettingsRowOptions = Partial<{
icon: BxIconRaw,
multiLines: boolean;
$note: HTMLElement;
}>;
@ -210,6 +211,7 @@ export function createSettingRow(label: string, $control: HTMLElement | false |
const $row = CE('label', { class: 'bx-settings-row' },
$label = CE('span', { class: 'bx-settings-label' },
options.icon && createSvgIcon(options.icon),
label,
options.$note,
),

View File

@ -0,0 +1,21 @@
import { BxEventBus } from "./bx-event-bus";
import { GhPagesUtils } from "./gh-pages";
export class LocalCoOpManager {
private static instance: LocalCoOpManager;
public static getInstance = () => LocalCoOpManager.instance ?? (LocalCoOpManager.instance = new LocalCoOpManager());
private supportedIds: Set<string>;
constructor() {
BxEventBus.Script.once('list.localCoOp.updated', e => {
this.supportedIds = e.ids;
});
this.supportedIds = GhPagesUtils.getLocalCoOpList();
console.log('this.supportedIds', this.supportedIds);
}
isSupported(productId: string) {
return this.supportedIds.has(productId);
}
}

View File

@ -46,7 +46,7 @@ export class MkbMappingPresetsTable extends BasePresetsTable<MkbPresetRecord> {
[GamepadKey.A]: ['Space', 'KeyE'],
[GamepadKey.X]: ['KeyR'],
[GamepadKey.B]: ['KeyC', 'Backspace'],
[GamepadKey.Y]: ['KeyE'],
[GamepadKey.Y]: ['KeyV'],
[GamepadKey.START]: ['Enter'],
[GamepadKey.SELECT]: ['Tab'],

View File

@ -221,8 +221,12 @@ export function interceptHttpRequests() {
}
// Ignore domains
const domain = (new URL(url)).hostname;
if (IGNORED_DOMAINS.includes(domain)) {
try {
const domain = (new URL(url)).hostname;
if (IGNORED_DOMAINS.includes(domain)) {
return NATIVE_FETCH(request, init);
}
} catch (e) {
return NATIVE_FETCH(request, init);
}

View File

@ -13,6 +13,7 @@ import { MkbMappingDefaultPresetId } from "../local-db/mkb-mapping-presets-table
import { KeyboardShortcutDefaultId } from "../local-db/keyboard-shortcuts-table";
import { GhPagesUtils } from "../gh-pages";
import { BxEventBus } from "../bx-event-bus";
import { BxIcon } from "../bx-icon";
function getSupportedCodecProfiles() {
@ -192,6 +193,25 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
label: t('hide-system-menu-icon'),
default: false,
},
[PrefKey.UI_IMAGE_QUALITY]: {
requiredVariants: 'full',
label: t('image-quality'),
default: 90,
min: 10,
max: 90,
params: {
steps: 10,
exactTicks: 20,
hideSlider: true,
customTextValue(value, min, max) {
if (value === 90) {
return t('default');
}
return value + '%';
},
},
},
[PrefKey.STREAM_COMBINE_SOURCES]: {
requiredVariants: 'full',
@ -320,6 +340,7 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
[PrefKey.LOCAL_CO_OP_ENABLED]: {
requiredVariants: 'full',
label: t('enable-local-co-op-support'),
labelIcon: BxIcon.LOCAL_CO_OP,
default: false,
note: () => CE('div', false,
CE('a', {
@ -436,7 +457,7 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
if (!setting.unsupported) {
(setting as any).multipleOptions = GhPagesUtils.getNativeMkbCustomList(true);
BxEventBus.Script.on('list.forcedNativeMkb.updated', payload => {
BxEventBus.Script.once('list.forcedNativeMkb.updated', payload => {
(setting as any).multipleOptions = payload.data.data;
});
}
@ -597,6 +618,7 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
default: false,
},
[PrefKey.BLOCK_FEATURES]: {
requiredVariants: 'full',
label: t('disable-features'),
default: [],
multipleOptions: {

View File

@ -6,7 +6,7 @@ import type { ControllerCustomizationConvertedPresetData, ControllerCustomizatio
import { STATES } from "./global";
import { DeviceVibrationMode } from "@/enums/pref-values";
import { VIRTUAL_GAMEPAD_ID } from "@/modules/mkb/mkb-handler";
import { hasGamepad } from "./gamepad";
import { hasGamepad, toXcloudGamepadKey } from "./gamepad";
import { MkbMappingPresetsTable } from "./local-db/mkb-mapping-presets-table";
import { GamepadKey } from "@/enums/gamepad";
import { MkbPresetKey, MouseConstant } from "@/enums/mkb";
@ -51,32 +51,6 @@ export class StreamSettings {
keyboardShortcuts: {},
};
private static CONTROLLER_CUSTOMIZATION_MAPPING: { [key in GamepadKey]?: keyof XcloudGamepad } = {
[GamepadKey.A]: 'A',
[GamepadKey.B]: 'B',
[GamepadKey.X]: 'X',
[GamepadKey.Y]: 'Y',
[GamepadKey.UP]: 'DPadUp',
[GamepadKey.RIGHT]: 'DPadRight',
[GamepadKey.DOWN]: 'DPadDown',
[GamepadKey.LEFT]: 'DPadLeft',
[GamepadKey.LB]: 'LeftShoulder',
[GamepadKey.RB]: 'RightShoulder',
[GamepadKey.LT]: 'LeftTrigger',
[GamepadKey.RT]: 'RightTrigger',
[GamepadKey.L3]: 'LeftThumb',
[GamepadKey.R3]: 'RightThumb',
[GamepadKey.LS]: 'LeftStickAxes',
[GamepadKey.RS]: 'RightStickAxes',
[GamepadKey.SELECT]: 'View',
[GamepadKey.START]: 'Menu',
[GamepadKey.SHARE]: 'Share',
};
static getPref<T extends keyof PrefTypeMap>(key: T) {
return getPref<T>(key);
}
@ -146,14 +120,14 @@ export class StreamSettings {
// Swap GamepadKey.A with "A"
let gamepadKey: unknown;
for (gamepadKey in customization.mapping) {
const gamepadStr = StreamSettings.CONTROLLER_CUSTOMIZATION_MAPPING[gamepadKey as GamepadKey];
const gamepadStr = toXcloudGamepadKey(gamepadKey as GamepadKey);
if (!gamepadStr) {
continue;
}
const mappedKey = customization.mapping[gamepadKey as GamepadKey];
if (typeof mappedKey === 'number') {
converted.mapping[gamepadStr] = StreamSettings.CONTROLLER_CUSTOMIZATION_MAPPING[mappedKey as GamepadKey];
converted.mapping[gamepadStr] = toXcloudGamepadKey(mappedKey as GamepadKey);
} else {
converted.mapping[gamepadStr] = false;
}

View File

@ -151,6 +151,7 @@ const Texts = {
"how-to-fix": "How to fix",
"how-to-improve-app-performance": "How to improve app's performance",
"ignore": "Ignore",
"image-quality": "Website's image quality",
"import": "Import",
"in-game-controller-customization": "In-game controller customization",
"in-game-controller-shortcuts": "In-game controller shortcuts",

View File

@ -123,7 +123,7 @@ export function productTitleToSlug(title: string): string {
export function parseDetailsPath(path: string) {
const matches = /\/games\/(?<titleSlug>[^\/]+)\/(?<productId>\w+)/.exec(path);
if (!matches?.groups) {
return;
return {};
}
const titleSlug = matches.groups.titleSlug!.replaceAll('\%' + '7C', '-');

View File

@ -59,7 +59,9 @@ export class XcloudInterceptor {
const obj = await response.clone().json();
// Store xCloud token
RemotePlayManager.getInstance()?.setXcloudToken(obj.gsToken);
if (isFullVersion()) {
RemotePlayManager.getInstance()?.setXcloudToken(obj.gsToken);
}
// Get server list
const serverRegex = /\/\/(\w+)\./;