Compare commits

...

28 Commits

Author SHA1 Message Date
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
d691ea0cf6 Bump version to 6.1.1 2024-12-28 20:46:39 +07:00
3c05fdcb6d Update README.md 2024-12-28 20:42:51 +07:00
0cff0b3d3f Update README.md 2024-12-28 20:38:46 +07:00
6ea47aed48 Add logos 2024-12-28 20:36:56 +07:00
c8142e5079 Cleanup 2024-12-28 20:36:34 +07:00
ef85175a91 Update dists 2024-12-28 17:04:40 +07:00
116640eb32 Fix not releasing pointer lock after quitting the game 2024-12-28 17:04:17 +07:00
54e28ce350 Fix mouse wheel bug (contd) 2024-12-28 17:04:01 +07:00
0cd2c02ed6 Update dists 2024-12-28 16:33:25 +07:00
e585264e8c Fix mouse wheel bug (#600) 2024-12-28 16:33:10 +07:00
6a133186b8 Check MKB's protocol version 2024-12-28 16:32:52 +07:00
91b5434952 Update Bx icon 2024-12-27 19:45:37 +07:00
37 changed files with 759 additions and 205 deletions

View File

@ -13,7 +13,7 @@ body:
- type: dropdown - type: dropdown
id: device_type id: device_type
attributes: attributes:
label: Device label: Device type
description: "Which device type is this feature for?" description: "Which device type is this feature for?"
options: options:
- All devices - All devices
@ -23,10 +23,20 @@ body:
multiple: false multiple: false
validations: validations:
required: true 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 - type: textarea
id: suggestion id: suggestion
attributes: attributes:
label: "Suggestion" label: "Suggestion"
description: "What do you want to suggest?" description: "What do you want to suggest? Include (mockup) screenshot if possible."
validations: validations:
required: true required: true

7
.gitignore vendored
View File

@ -1,9 +1,4 @@
src/modules/patcher/patches/controller-customization.js src/modules/patcher/patches/*.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
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore

View File

@ -3,6 +3,7 @@
"dist/**/*": true, "dist/**/*": true,
"src/modules/patcher/patches/controller-customization.js": true, "src/modules/patcher/patches/controller-customization.js": true,
"src/modules/patcher/patches/expose-stream-session.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/local-co-op-enable.js": true,
"src/modules/patcher/patches/poll-gamepad.js": true, "src/modules/patcher/patches/poll-gamepad.js": true,
"src/modules/patcher/patches/remote-play-keep-alive.js": true, "src/modules/patcher/patches/remote-play-keep-alive.js": true,

View File

@ -1,10 +1,17 @@
# Better xCloud <div align="center">
Improve Xbox Cloud Gaming (xCloud) experience on [xbox.com/play](https://www.xbox.com/play). It also allows you to use Remote Play on the xCloud website. <img src="https://raw.githubusercontent.com/redphx/better-xcloud/refs/heads/typescript/resources/logos/better-xcloud.png" width="256"/>
<h1>Better xCloud</h1>
<!-- Latest Version Badge -->
<a href="https://github.com/redphx/better-xcloud/releases"><img src="https://img.shields.io/github/v/release/redphx/better-xcloud?label=latest" alt="Latest version" /></a>
<!-- Total Downloads Badge -->
<a href="https://github.com/redphx/better-xcloud/releases"><img src="https://img.shields.io/github/downloads/redphx/better-xcloud/total?color=%23e15f2c" alt="Total downloads" /></a>
<!-- Total Stars Badge -->
<a href="https://github.com/redphx/better-xcloud/stargazers"><img src="https://img.shields.io/github/stars/redphx/better-xcloud?color=%23cca400" alt="Total stars" /></a>
</div>
> [!TIP] ### Improve Xbox Cloud Gaming (xCloud) experience on [xbox.com/play](https://www.xbox.com/play). It also allows you to use Remote Play on the xCloud website.
> The Android app is in development at [redphx/better-xcloud-android](https://github.com/redphx/better-xcloud-android)
> [!IMPORTANT] > [!IMPORTANT]
> I only accept pull requests for: > I only accept pull requests for:
> - Custom touch controls > - Custom touch controls
> - Bug fixes > - Bug fixes
@ -13,16 +20,12 @@ Improve Xbox Cloud Gaming (xCloud) experience on [xbox.com/play](https://www.xbo
- Windows - Windows
- macOS - macOS
- Linux, SteamOS (including Steam Deck) - Linux, SteamOS (including Steam Deck)
- Android, Android TV (including Meta Quest VR Headsets) - Android, Android TV (including Meta Quest VR Headsets): [redphx/better-xcloud-android](https://github.com/redphx/better-xcloud-android)
- iOS, iPadOS - iOS, iPadOS
This script makes me spend more time with xCloud, and I hope the same thing happens to you. This script makes me spend more time with xCloud, and I hope the same thing happens to you.
If you like this project please give it a 🌟. Thank you 🙏. If you like this project please give it a 🌟. Thank you 🙏.
[![Latest version](https://img.shields.io/github/v/release/redphx/better-xcloud?label=latest)](https://github.com/redphx/better-xcloud/releases)
[![Total downloads](https://img.shields.io/github/downloads/redphx/better-xcloud/total?color=%23e15f2c)](https://github.com/redphx/better-xcloud/releases)
[![Total stars](https://img.shields.io/github/stars/redphx/better-xcloud?color=%23cca400)](https://github.com/redphx/better-xcloud/stargazers)
## How to install ## How to install
Visit the [home page](https://better-xcloud.github.io) to know how to install Better xCloud on your device. Visit the [home page](https://better-xcloud.github.io) to know how to install Better xCloud on your device.

File diff suppressed because one or more lines are too long

View File

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

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

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

View File

@ -149,6 +149,10 @@ div[class^=HUDButton-module__hiddenContainer] ~ div:not([class^=HUDButton-module
font-family: var(--bx-normal-font) !important; font-family: var(--bx-normal-font) !important;
} }
.bx-frosted {
backdrop-filter: blur(4px) brightness(1.5);
}
select[multiple], select[multiple]:focus { select[multiple], select[multiple]:focus {
overflow: auto; overflow: auto;
border: none; border: none;
@ -190,14 +194,6 @@ div[class*=NotFocusedDialog] {
visibility: hidden; 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 { .bx-game-tile-wait-time {
position: absolute; position: absolute;
top: 0; top: 0;

View File

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

View File

@ -1,4 +1,8 @@
<svg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='#fff' fill-rule='evenodd' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='2' stroke-width='2' viewBox='0 0 32 32'> <svg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='none' fill-rule='evenodd' viewBox='0 0 32 32'>
<path d='M16.001 7.236h-2.328c-.443 0-1.941-.851-2.357-.905-.824-.106-1.684 0-2.489.176a13.04 13.04 0 0 0-3.137 1.14c-.392.275-.677.668-.866 1.104v.03l-3.302 8.963-.015.015c-.288.867-.553 3.75-.5 4.279a4.89 4.89 0 0 0 1.022 2.55c.654.823 3.71 1.364 4.057 1.016l4.462-4.475c.185-.186 1.547-.706 2.01-.706h6.884c.463 0 1.825.52 2.01.706l4.462 4.475c.347.348 3.403-.193 4.057-1.016a4.89 4.89 0 0 0 1.022-2.55c.053-.529-.212-3.412-.5-4.279l-.015-.015-3.302-8.963v-.03c-.189-.436-.474-.829-.866-1.104a13.04 13.04 0 0 0-3.137-1.14c-.805-.176-1.665-.282-2.489-.176-.416.054-1.914.905-2.357.905h-2.328' fill='none' stroke='#fff'/> <clipPath id='svg-bx-logo'>
<path d='M8.172 12.914H6.519c-.235 0-.315.267-.335.452l-.052.578c0 .193.033.384.054.576.023.202.091.511.355.511h1.631l-.001 1.652c0 .234.266.315.452.335l.578.052c.193 0 .384-.033.576-.054.203-.023.511-.091.511-.355V15.03l1.652.001c.234 0 .315-.266.335-.452l.052-.578c-.001-.193-.033-.385-.055-.577-.022-.202-.09-.51-.354-.51h-1.632v-1.652c0-.234-.266-.315-.453-.335l-.577-.052c-.193 0-.385.033-.577.054-.202.023-.51.091-.51.355v1.631m16.546 2.994h-3.487c-.206 0-.413-.043-.604-.121-.177-.072-.339-.183-.476-.316-.149-.144-.259-.315-.341-.504-.156-.361-.172-.788-.032-1.157a1.57 1.57 0 0 1 .459-.641c.106-.089.223-.164.349-.222a1.52 1.52 0 0 1 .423-.123c.167-.024.338-.02.504.012a1.83 1.83 0 0 1 .455-.482 1.62 1.62 0 0 1 .522-.252c.307-.089.651-.09.959-.003a1.75 1.75 0 0 1 1.009.764 1.83 1.83 0 0 1 .251.721c.156 0 .312.031.456.09a1.24 1.24 0 0 1 .372.248c.091.087.165.19.221.302a1.19 1.19 0 0 1-.173 1.299c-.119.132-.276.239-.441.305a1.17 1.17 0 0 1-.426.08z' fill='#fff' stroke='none'/> <path d='M0 0h32v32H0z'/>
</clipPath>
<g clip-path='url(#svg-bx-logo)'>
<path d='M19.959 18.286l3.959 2.285-3.959 2.286V32L16 29.714v-9.143l3.959-2.285zM16 16V6.857l3.959-2.286 3.959 2.286-3.959 2.286v9.143L16 16zm-3.959-2.286L16 16l-3.959 2.286v9.143l-3.959-2.286V16l3.959-2.286zM8.082 2.286L12.041 0 16 2.286l-3.959 2.285v9.143l-3.959-2.285V2.286zm8.846 19.535c-.171-.098-.309-.018-.309.179s.138.437.309.536.309.018.309-.179-.138-.437-.309-.536zm0-13.714c-.171-.098-.309-.018-.309.179s.138.437.309.535.309.019.309-.178-.138-.437-.309-.536zM9.01 17.25c-.171-.099-.309-.019-.309.179s.138.437.309.535.309.019.309-.178-.138-.437-.309-.536zm0-13.714c-.171-.099-.309-.019-.309.178s.138.437.309.536.309.019.309-.179-.138-.437-.309-.535z' fill='#fff'/>
</g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 926 B

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', GH_PAGES_COMMIT_HASH = 'BetterXcloud.GhPages.CommitHash',
LIST_CUSTOM_TOUCH_LAYOUTS = 'BetterXcloud.GhPages.CustomTouchLayouts', LIST_CUSTOM_TOUCH_LAYOUTS = 'BetterXcloud.GhPages.CustomTouchLayouts',
LIST_FORCE_NATIVE_MKB = 'BetterXcloud.GhPages.ForceNativeMkb', 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_SKIP_SPLASH_VIDEO = 'ui.splashVideo.skip',
UI_HIDE_SYSTEM_MENU_ICON = 'ui.systemMenu.hideHandle', UI_HIDE_SYSTEM_MENU_ICON = 'ui.systemMenu.hideHandle',
UI_REDUCE_ANIMATIONS = 'ui.reduceAnimations', UI_REDUCE_ANIMATIONS = 'ui.reduceAnimations',
UI_IMAGE_QUALITY = 'ui.imageQuality',
VIDEO_PLAYER_TYPE = 'video.player.type', VIDEO_PLAYER_TYPE = 'video.player.type',
VIDEO_POWER_PREFERENCE = 'video.player.powerPreference', VIDEO_POWER_PREFERENCE = 'video.player.powerPreference',
@ -171,6 +173,7 @@ export type PrefTypeMap = {
[PrefKey.UI_GAME_CARD_SHOW_WAIT_TIME]: boolean, [PrefKey.UI_GAME_CARD_SHOW_WAIT_TIME]: boolean,
[PrefKey.UI_HIDE_SECTIONS]: UiSection[], [PrefKey.UI_HIDE_SECTIONS]: UiSection[],
[PrefKey.UI_HIDE_SYSTEM_MENU_ICON]: boolean, [PrefKey.UI_HIDE_SYSTEM_MENU_ICON]: boolean,
[PrefKey.UI_IMAGE_QUALITY]: number,
[PrefKey.UI_LAYOUT]: UiLayout, [PrefKey.UI_LAYOUT]: UiLayout,
[PrefKey.UI_REDUCE_ANIMATIONS]: boolean, [PrefKey.UI_REDUCE_ANIMATIONS]: boolean,
[PrefKey.UI_SCROLLBAR_HIDE]: boolean, [PrefKey.UI_SCROLLBAR_HIDE]: boolean,

View File

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

View File

@ -428,7 +428,7 @@ export class EmulatedMkbHandler extends MkbHandler {
return true; return true;
} }
toggle(force?: boolean) { async toggle(force?: boolean) {
if (!this.initialized) { if (!this.initialized) {
return; return;
} }
@ -440,9 +440,12 @@ export class EmulatedMkbHandler extends MkbHandler {
} }
if (this.enabled) { if (this.enabled) {
document.body.requestPointerLock({ try {
unadjustedMovement: true, await document.body.requestPointerLock({ unadjustedMovement: true });
}); } catch (e) {
document.body.requestPointerLock();
console.log(e);
}
} else { } else {
document.pointerLockElement && document.exitPointerLock(); document.pointerLockElement && document.exitPointerLock();
} }
@ -557,7 +560,7 @@ export class EmulatedMkbHandler extends MkbHandler {
this.stop(); this.stop();
this.waitForMouseData(false); this.waitForMouseData(false);
document.pointerLockElement && document.exitPointerLock(); document.exitPointerLock();
window.removeEventListener('keydown', this.onKeyboardEvent); window.removeEventListener('keydown', this.onKeyboardEvent);
window.removeEventListener('keyup', this.onKeyboardEvent); window.removeEventListener('keyup', this.onKeyboardEvent);

View File

@ -50,8 +50,6 @@ export class NativeMkbHandler extends MkbHandler {
private enabled = false; private enabled = false;
private mouseButtonsPressed = 0; private mouseButtonsPressed = 0;
private mouseWheelX = 0;
private mouseWheelY = 0;
private mouseVerticalMultiply = 0; private mouseVerticalMultiply = 0;
private mouseHorizontalMultiply = 0; private mouseHorizontalMultiply = 0;
@ -202,7 +200,7 @@ export class NativeMkbHandler extends MkbHandler {
BxEventBus.Script.off('dialog.shown', this.onDialogShown); BxEventBus.Script.off('dialog.shown', this.onDialogShown);
this.waitForMouseData(false); this.waitForMouseData(false);
document.pointerLockElement && document.exitPointerLock(); document.exitPointerLock();
} }
handleMouseMove(data: MkbMouseMove): void { handleMouseMove(data: MkbMouseMove): void {
@ -210,8 +208,8 @@ export class NativeMkbHandler extends MkbHandler {
X: data.movementX, X: data.movementX,
Y: data.movementY, Y: data.movementY,
Buttons: this.mouseButtonsPressed, Buttons: this.mouseButtonsPressed,
WheelX: this.mouseWheelX, WheelX: 0,
WheelY: this.mouseWheelY, WheelY: 0,
}); });
} }
@ -229,30 +227,30 @@ export class NativeMkbHandler extends MkbHandler {
X: 0, X: 0,
Y: 0, Y: 0,
Buttons: this.mouseButtonsPressed, Buttons: this.mouseButtonsPressed,
WheelX: this.mouseWheelX, WheelX: 0,
WheelY: this.mouseWheelY, WheelY: 0,
}); });
} }
handleMouseWheel(data: MkbMouseWheel): boolean { handleMouseWheel(data: MkbMouseWheel): boolean {
const { vertical, horizontal } = data; const { vertical, horizontal } = data;
this.mouseWheelX = horizontal; let mouseWheelX = horizontal;
if (this.mouseHorizontalMultiply && this.mouseHorizontalMultiply !== 1) { if (this.mouseHorizontalMultiply && this.mouseHorizontalMultiply !== 1) {
this.mouseWheelX *= this.mouseHorizontalMultiply; mouseWheelX *= this.mouseHorizontalMultiply;
} }
this.mouseWheelY = vertical; let mouseWheelY = vertical;
if (this.mouseVerticalMultiply && this.mouseVerticalMultiply !== 1) { if (this.mouseVerticalMultiply && this.mouseVerticalMultiply !== 1) {
this.mouseWheelY *= this.mouseVerticalMultiply; mouseWheelY *= this.mouseVerticalMultiply;
} }
this.sendMouseInput({ this.sendMouseInput({
X: 0, X: 0,
Y: 0, Y: 0,
Buttons: this.mouseButtonsPressed, Buttons: this.mouseButtonsPressed,
WheelX: this.mouseWheelX, WheelX: mouseWheelX,
WheelY: this.mouseWheelY, WheelY: mouseWheelY,
}); });
return true; return true;
@ -281,8 +279,6 @@ export class NativeMkbHandler extends MkbHandler {
private resetMouseInput() { private resetMouseInput() {
this.mouseButtonsPressed = 0; this.mouseButtonsPressed = 0;
this.mouseWheelX = 0;
this.mouseWheelY = 0;
this.sendMouseInput({ this.sendMouseInput({
X: 0, X: 0,

View File

@ -8,6 +8,7 @@ enum PointerAction {
BUTTON_RELEASE = 3, BUTTON_RELEASE = 3,
SCROLL = 4, SCROLL = 4,
POINTER_CAPTURE_CHANGED = 5, POINTER_CAPTURE_CHANGED = 5,
PROTOCOL_VERSION = 127,
} }
@ -15,6 +16,7 @@ export class PointerClient {
private static instance: PointerClient; private static instance: PointerClient;
public static getInstance = () => PointerClient.instance ?? (PointerClient.instance = new PointerClient()); public static getInstance = () => PointerClient.instance ?? (PointerClient.instance = new PointerClient());
private readonly LOG_TAG = 'PointerClient'; private readonly LOG_TAG = 'PointerClient';
private readonly REQUIRED_PROTOCOL_VERSION = 2;
private socket: WebSocket | undefined | null; private socket: WebSocket | undefined | null;
private mkbHandler: MkbHandler | undefined; private mkbHandler: MkbHandler | undefined;
@ -56,6 +58,15 @@ export class PointerClient {
let messageType = dataView.getInt8(0); let messageType = dataView.getInt8(0);
let offset = Int8Array.BYTES_PER_ELEMENT; let offset = Int8Array.BYTES_PER_ELEMENT;
switch (messageType) { switch (messageType) {
case PointerAction.PROTOCOL_VERSION:
const protocolVersion = this.onProtocolVersion(dataView, offset);
BxLogger.info(this.LOG_TAG, 'Protocol version', protocolVersion);
if (protocolVersion !== this.REQUIRED_PROTOCOL_VERSION) {
alert('Required MKB protocol: ' + protocolVersion);
this.stop();
}
break;
case PointerAction.MOVE: case PointerAction.MOVE:
this.onMove(dataView, offset); this.onMove(dataView, offset);
break; break;
@ -75,6 +86,10 @@ export class PointerClient {
}); });
} }
private onProtocolVersion(dataView: DataView, offset: number) {
return dataView.getUint16(offset);
}
onMove(dataView: DataView, offset: number) { onMove(dataView: DataView, offset: number) {
// [X, Y] // [X, Y]
const x = dataView.getInt16(offset); const x = dataView.getInt16(offset);

View File

@ -50,4 +50,50 @@ export class PatcherUtils {
return str; 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 codeControllerCustomization from "./patches/controller-customization.js" with { type: "text" };
import codePollGamepad from "./patches/poll-gamepad.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 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 codeLocalCoOpEnable from "./patches/local-co-op-enable.js" with { type: "text" };
import codeRemotePlayKeepAlive from "./patches/remote-play-keep-alive.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" }; import codeVibrationAdjust from "./patches/vibration-adjust.js" with { type: "text" };
@ -1003,6 +1004,117 @@ ${subsVar} = subs;
str = PatcherUtils.insertAt(str, index, newCode); str = PatcherUtils.insertAt(str, index, newCode);
return str; 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([ let PATCH_ORDERS = PatcherUtils.filterPatches([
@ -1012,6 +1124,15 @@ let PATCH_ORDERS = PatcherUtils.filterPatches([
'disableAbsoluteMouse', 'disableAbsoluteMouse',
] : []), ] : []),
'exposeReactCreateComponent',
'gameCardCustomIcons',
// 'gameCardPassTitle',
...(getPref(PrefKey.UI_IMAGE_QUALITY) < 90 ? [
'setImageQuality',
'setBackgroundImageQuality',
] : []),
'modifyPreloadedState', 'modifyPreloadedState',
'optimizeGameSlugGenerator', 'optimizeGameSlugGenerator',
@ -1131,7 +1252,7 @@ let STREAM_PAGE_PATCH_ORDERS = PatcherUtils.filterPatches([
]); ]);
let PRODUCT_DETAIL_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]; const ALL_PATCHES = [...PATCH_ORDERS, ...HOME_PAGE_PATCH_ORDERS, ...STREAM_PAGE_PATCH_ORDERS, ...PRODUCT_DETAIL_PAGE_PATCH_ORDERS];

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

@ -274,6 +274,7 @@ export class SettingsDialog extends NavigationDialog {
label: t('ui'), label: t('ui'),
items: [ items: [
PrefKey.UI_LAYOUT, PrefKey.UI_LAYOUT,
PrefKey.UI_IMAGE_QUALITY,
PrefKey.UI_GAME_CARD_SHOW_WAIT_TIME, PrefKey.UI_GAME_CARD_SHOW_WAIT_TIME,
PrefKey.UI_CONTROLLER_SHOW_STATUS, PrefKey.UI_CONTROLLER_SHOW_STATUS,
PrefKey.UI_SIMPLIFY_STREAM_MENU, PrefKey.UI_SIMPLIFY_STREAM_MENU,
@ -1007,6 +1008,7 @@ export class SettingsDialog extends NavigationDialog {
const $row = createSettingRow(label, !prefDefinition?.unsupported && $control, { const $row = createSettingRow(label, !prefDefinition?.unsupported && $control, {
$note, $note,
multiLines: setting.multiLines, multiLines: setting.multiLines,
icon: prefDefinition?.labelIcon,
}); });
if (pref) { if (pref) {
$row.htmlFor = `bx_setting_${escapeCssSelector(pref)}`; $row.htmlFor = `bx_setting_${escapeCssSelector(pref)}`;

View File

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

View File

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

View File

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

View File

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

View File

@ -13,6 +13,7 @@ import { NativeMkbMode, TouchControllerMode } from "@/enums/pref-values";
import { Patcher, type PatchPage } from "@/modules/patcher/patcher"; import { Patcher, type PatchPage } from "@/modules/patcher/patcher";
import { BxEventBus } from "./bx-event-bus"; import { BxEventBus } from "./bx-event-bus";
import { FeatureGates } from "./feature-gates"; import { FeatureGates } from "./feature-gates";
import { LocalCoOpManager } from "./local-co-op-manager";
export enum SupportedInputType { export enum SupportedInputType {
CONTROLLER = 'Controller', CONTROLLER = 'Controller',
@ -230,4 +231,25 @@ export const BxExposed = {
BxLogger.info('beforePageLoad', page); BxLogger.info('beforePageLoad', page);
Patcher.patchPage(page); Patcher.patchPage(page);
} : () => {}, } : () => {},
localCoOpManager: LocalCoOpManager.getInstance(),
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 iconEye from "@assets/svg/eye.svg" with { type: "text" };
import iconEyeSlash from "@assets/svg/eye-slash.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 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 iconNativeMkb from "@assets/svg/native-mkb.svg" with { type: "text" };
import iconNew from "@assets/svg/new.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" }; import iconPencil from "@assets/svg/pencil-simple-line.svg" with { type: "text" };
@ -52,6 +53,7 @@ export const BxIcon = {
EYE: iconEye, EYE: iconEye,
EYE_SLASH: iconEyeSlash, EYE_SLASH: iconEyeSlash,
HOME: iconHome, HOME: iconHome,
LOCAL_CO_OP: iconLocalCoOp,
NATIVE_MKB: iconNativeMkb, NATIVE_MKB: iconNativeMkb,
NEW: iconNew, NEW: iconNew,
MANAGE: iconPencil, MANAGE: iconPencil,

View File

@ -1,5 +1,5 @@
import { CE } from "@utils/html"; 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 { BlockFeature, UiSection } from "@/enums/pref-values";
import { PrefKey } from "@/enums/pref-keys"; import { PrefKey } from "@/enums/pref-keys";
import { getPref } from "./settings-storages/global-settings-storage"; import { getPref } from "./settings-storages/global-settings-storage";
@ -12,6 +12,11 @@ export function addCss() {
const PREF_HIDE_SECTIONS = getPref(PrefKey.UI_HIDE_SECTIONS); const PREF_HIDE_SECTIONS = getPref(PrefKey.UI_HIDE_SECTIONS);
const selectorToHide = []; const selectorToHide = [];
if (isLiteVersion()) {
// Hide Controller icon in Game tiles
selectorToHide.push('div[class*=SupportedInputsBadge] svg:first-of-type');
}
// Hide "News" section // Hide "News" section
if (PREF_HIDE_SECTIONS.includes(UiSection.NEWS)) { if (PREF_HIDE_SECTIONS.includes(UiSection.NEWS)) {
selectorToHide.push('#BodyContent > div[class*=CarouselRow-module]'); selectorToHide.push('#BodyContent > div[class*=CarouselRow-module]');

View File

@ -56,6 +56,8 @@ export class GhPagesUtils {
BxEventBus.Script.emit('list.forcedNativeMkb.updated', { BxEventBus.Script.emit('list.forcedNativeMkb.updated', {
data: json, data: json,
}); });
} else {
window.localStorage.removeItem(key);
} }
}); });
@ -70,6 +72,7 @@ export class GhPagesUtils {
} }
static getTouchControlCustomList() { static getTouchControlCustomList() {
// TODO: use Set()
const key = StorageKey.LIST_CUSTOM_TOUCH_LAYOUTS; const key = StorageKey.LIST_CUSTOM_TOUCH_LAYOUTS;
NATIVE_FETCH(GhPagesUtils.getUrl('touch-layouts/ids.json')) NATIVE_FETCH(GhPagesUtils.getUrl('touch-layouts/ids.json'))
@ -83,4 +86,31 @@ export class GhPagesUtils {
const customList = JSON.parse(window.localStorage.getItem(key) || '[]'); const customList = JSON.parse(window.localStorage.getItem(key) || '[]');
return customList; 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

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

@ -13,6 +13,7 @@ import { MkbMappingDefaultPresetId } from "../local-db/mkb-mapping-presets-table
import { KeyboardShortcutDefaultId } from "../local-db/keyboard-shortcuts-table"; import { KeyboardShortcutDefaultId } from "../local-db/keyboard-shortcuts-table";
import { GhPagesUtils } from "../gh-pages"; import { GhPagesUtils } from "../gh-pages";
import { BxEventBus } from "../bx-event-bus"; import { BxEventBus } from "../bx-event-bus";
import { BxIcon } from "../bx-icon";
function getSupportedCodecProfiles() { function getSupportedCodecProfiles() {
@ -192,6 +193,24 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
label: t('hide-system-menu-icon'), label: t('hide-system-menu-icon'),
default: false, default: false,
}, },
[PrefKey.UI_IMAGE_QUALITY]: {
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]: { [PrefKey.STREAM_COMBINE_SOURCES]: {
requiredVariants: 'full', requiredVariants: 'full',
@ -320,6 +339,7 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
[PrefKey.LOCAL_CO_OP_ENABLED]: { [PrefKey.LOCAL_CO_OP_ENABLED]: {
requiredVariants: 'full', requiredVariants: 'full',
label: t('enable-local-co-op-support'), label: t('enable-local-co-op-support'),
labelIcon: BxIcon.LOCAL_CO_OP,
default: false, default: false,
note: () => CE('div', false, note: () => CE('div', false,
CE('a', { CE('a', {
@ -436,7 +456,7 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
if (!setting.unsupported) { if (!setting.unsupported) {
(setting as any).multipleOptions = GhPagesUtils.getNativeMkbCustomList(true); (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; (setting as any).multipleOptions = payload.data.data;
}); });
} }

View File

@ -151,6 +151,7 @@ const Texts = {
"how-to-fix": "How to fix", "how-to-fix": "How to fix",
"how-to-improve-app-performance": "How to improve app's performance", "how-to-improve-app-performance": "How to improve app's performance",
"ignore": "Ignore", "ignore": "Ignore",
"image-quality": "Website's image quality",
"import": "Import", "import": "Import",
"in-game-controller-customization": "In-game controller customization", "in-game-controller-customization": "In-game controller customization",
"in-game-controller-shortcuts": "In-game controller shortcuts", "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) { export function parseDetailsPath(path: string) {
const matches = /\/games\/(?<titleSlug>[^\/]+)\/(?<productId>\w+)/.exec(path); const matches = /\/games\/(?<titleSlug>[^\/]+)\/(?<productId>\w+)/.exec(path);
if (!matches?.groups) { if (!matches?.groups) {
return; return {};
} }
const titleSlug = matches.groups.titleSlug!.replaceAll('\%' + '7C', '-'); const titleSlug = matches.groups.titleSlug!.replaceAll('\%' + '7C', '-');