mirror of
https://github.com/redphx/better-xcloud.git
synced 2025-07-05 22:01:43 +02:00
Compare commits
16 Commits
Author | SHA1 | Date | |
---|---|---|---|
488b0dfef2 | |||
b3697df8dc | |||
de21549e0d | |||
097164b92e | |||
3fe6d97133 | |||
328fdf46ea | |||
e4dbdea9a5 | |||
f13ce94cf2 | |||
a6c19fec15 | |||
6448a00271 | |||
68b29ecb50 | |||
90f89a0244 | |||
9862f794cf | |||
e109cdec6a | |||
40d1878fb2 | |||
95f842d9f6 |
14
.github/ISSUE_TEMPLATE/02-feature-request.yml
vendored
14
.github/ISSUE_TEMPLATE/02-feature-request.yml
vendored
@ -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
7
.gitignore
vendored
@ -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
|
||||
|
||||
|
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -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,
|
||||
|
158
dist/better-xcloud.lite.user.js
vendored
158
dist/better-xcloud.lite.user.js
vendored
File diff suppressed because one or more lines are too long
238
dist/better-xcloud.user.js
vendored
238
dist/better-xcloud.user.js
vendored
File diff suppressed because one or more lines are too long
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -190,6 +190,12 @@
|
||||
margin-bottom: 0 !important;
|
||||
flex: 1;
|
||||
|
||||
svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
|
||||
+ * {
|
||||
margin: 0 0 0 auto;
|
||||
}
|
||||
|
7
src/assets/svg/local-co-op.svg
Normal file
7
src/assets/svg/local-co-op.svg
Normal 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 |
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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" };
|
||||
@ -1003,6 +1004,117 @@ ${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([
|
||||
@ -1012,6 +1124,15 @@ let PATCH_ORDERS = PatcherUtils.filterPatches([
|
||||
'disableAbsoluteMouse',
|
||||
] : []),
|
||||
|
||||
'exposeReactCreateComponent',
|
||||
'gameCardCustomIcons',
|
||||
// 'gameCardPassTitle',
|
||||
|
||||
...(getPref(PrefKey.UI_IMAGE_QUALITY) < 90 ? [
|
||||
'setImageQuality',
|
||||
'setBackgroundImageQuality',
|
||||
] : []),
|
||||
|
||||
'modifyPreloadedState',
|
||||
|
||||
'optimizeGameSlugGenerator',
|
||||
@ -1131,7 +1252,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];
|
||||
|
12
src/modules/patcher/patches/src/game-card-icons.ts
Normal file
12
src/modules/patcher/patches/src/game-card-icons.ts
Normal 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);
|
||||
}
|
@ -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,
|
||||
@ -1007,6 +1008,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)}`;
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
1
src/types/setting-definition.d.ts
vendored
1
src/types/setting-definition.d.ts
vendored
@ -20,6 +20,7 @@ interface BaseSettingDefinition {
|
||||
default: any;
|
||||
|
||||
label?: string;
|
||||
labelIcon?: BxIconRaw,
|
||||
note?: string | (() => HTMLElement) | HTMLElement;
|
||||
experimental?: boolean;
|
||||
unsupported?: boolean;
|
||||
|
@ -31,6 +31,10 @@ type ScriptEvents = {
|
||||
data: any;
|
||||
};
|
||||
};
|
||||
|
||||
'list.localCoOp.updated': {
|
||||
ids: Set<string>,
|
||||
};
|
||||
};
|
||||
|
||||
type StreamEvents = {
|
||||
|
@ -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: 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' })
|
||||
),
|
||||
);
|
||||
} : () => {},
|
||||
};
|
||||
|
@ -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,
|
||||
|
@ -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,11 @@ 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');
|
||||
}
|
||||
|
||||
// Hide "News" section
|
||||
if (PREF_HIDE_SECTIONS.includes(UiSection.NEWS)) {
|
||||
selectorToHide.push('#BodyContent > div[class*=CarouselRow-module]');
|
||||
|
@ -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 || {}));
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
),
|
||||
|
21
src/utils/local-co-op-manager.ts
Normal file
21
src/utils/local-co-op-manager.ts
Normal 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);
|
||||
}
|
||||
}
|
@ -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,24 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
|
||||
label: t('hide-system-menu-icon'),
|
||||
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]: {
|
||||
requiredVariants: 'full',
|
||||
@ -320,6 +339,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 +456,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;
|
||||
});
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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', '-');
|
||||
|
Reference in New Issue
Block a user