diff --git a/.gitignore b/.gitignore index 2c8942c..5ada0f1 100755 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,10 @@ +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 + # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore # Logs diff --git a/.vscode/settings.json b/.vscode/settings.json index 58c648f..0ca1742 100755 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,11 @@ { "files.readonlyInclude": { - "dist/**/*": true + "dist/**/*": true, + "src/modules/patcher/patches/controller-customization.js": true, + "src/modules/patcher/patches/expose-stream-session.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, + "src/modules/patcher/patches/vibration-adjust.js": true } } diff --git a/build.ts b/build.ts index 595643b..aa4dbff 100755 --- a/build.ts +++ b/build.ts @@ -1,5 +1,5 @@ #!/usr/bin/env bun -import { readFile } from "node:fs/promises"; +import { readFile, readdir } from "node:fs/promises"; import { parseArgs } from "node:util"; import { sys } from "typescript"; // @ts-ignore @@ -56,7 +56,23 @@ function minifyCodeImports(str: string): string { return str; } -const postProcess = (str: string): string => { +function minifyIfElse(str: string): string { + // Collapse if/else blocks without curly braces + return str.replaceAll(/((if \(.*?\)|else)\n\s+)/g, '$2 '); +} + +function removeComments(str: string): string { + // Remove enum's inlining comments + str = str.replaceAll(/ \/\* [A-Z0-9_:]+ \*\//g, ''); + str = str.replaceAll('/* @__PURE__ */ ', ''); + + // Remove comments from import + str = str.replaceAll(/\/\/ src.*\n/g, ''); + + return str; +} + +function postProcess(str: string): string { // Unescape unicode charaters str = unescape((str.replace(/\\u/g, '%u'))); // Replace \x00 to normal character @@ -65,12 +81,7 @@ const postProcess = (str: string): string => { // Replace "globalThis." with "var"; str = str.replaceAll('globalThis.', 'var '); - // Remove enum's inlining comments - str = str.replaceAll(/ \/\* [A-Z0-9_:]+ \*\//g, ''); - str = str.replaceAll('/* @__PURE__ */ ', ''); - - // Remove comments from import - str = str.replaceAll(/\/\/ src.*\n/g, ''); + str = removeComments(str); // Add ADDITIONAL CODE block str = str.replace('var DEFAULT_FLAGS', '\n/* ADDITIONAL CODE */\n\nvar DEFAULT_FLAGS'); @@ -114,8 +125,7 @@ const postProcess = (str: string): string => { // Set indent to 1 space if (MINIFY_SYNTAX) { - // Collapse if/else blocks without curly braces - str = str.replaceAll(/((if \(.*?\)|else)\n\s+)/g, '$2 '); + str = minifyIfElse(str); str = str.replaceAll(/\n(\s+)/g, (match, p1) => { const len = p1.length / 2; @@ -134,7 +144,47 @@ const postProcess = (str: string): string => { return str; } -const build = async (target: BuildTarget, version: string, variant: BuildVariant, config: any={}) => { +async function buildPatches() { + const inputDir = './src/modules/patcher/patches/src'; + const outputDir = './src/modules/patcher/patches'; + + const files = await readdir(inputDir); + const tsFiles = files.filter(file => file.endsWith('.ts')); + + tsFiles.forEach(async file => { + // You can perform any operation with each TypeScript file + console.log(`Building patch: ${file}`); + const filePath = `${inputDir}/${file}`; + + await Bun.build({ + entrypoints: [filePath], + outdir: outputDir, + target: 'browser', + format: 'esm', + minify: { + syntax: true, + whitespace: true, + }, + }); + + const outputFile = `${outputDir}/${file.replace('.ts', '.js')}`; + + let code = await readFile(outputFile, 'utf-8'); + + // Replace "$this$" to "this" + code = code.replaceAll('$this$', 'this'); + + // Minify code + code = removeComments(code); + code = minifyIfElse(code); + + // Save + await Bun.write(outputFile, code); + console.log(`Patch built successfully: ${file}`) + }); +} + +async function build(target: BuildTarget, version: string, variant: BuildVariant, config: any={}) { console.log('-- Target:', target); const startTime = performance.now(); @@ -153,6 +203,8 @@ const build = async (target: BuildTarget, version: string, variant: BuildVariant const outDir = './dist'; + await buildPatches(); + let output = await Bun.build({ entrypoints: ['src/index.ts'], outdir: outDir, diff --git a/bun.lock b/bun.lock new file mode 100755 index 0000000..40bef75 --- /dev/null +++ b/bun.lock @@ -0,0 +1,315 @@ +{ + "lockfileVersion": 0, + "workspaces": { + "": { + "devDependencies": { + "@types/bun": "^1.1.14", + "@types/node": "^22.10.2", + "@types/stylus": "^0.48.43", + "eslint": "^9.17.0", + "eslint-plugin-compat": "^6.0.2", + "stylus": "^0.64.0", + }, + "peerDependencies": { + "typescript": "^5.7.2", + }, + }, + }, + "packages": { + "@adobe/css-tools": ["@adobe/css-tools@4.3.3", "", {}, "sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ=="], + + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.4.0", "", { "dependencies": { "eslint-visitor-keys": "^3.3.0" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="], + + "@eslint/config-array": ["@eslint/config-array@0.19.0", "", { "dependencies": { "@eslint/object-schema": "^2.1.4", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-zdHg2FPIFNKPdcHWtiNT+jEFCHYVplAXRDlQDyqy0zGx/q2parwh7brGJSiTxRk/TSMkbM//zt/f5CHgyTyaSQ=="], + + "@eslint/core": ["@eslint/core@0.9.0", "", {}, "sha512-7ATR9F0e4W85D/0w7cU0SNj7qkAexMG+bAHEZOjo9akvGuhHE2m7umzWzfnpa0XAg5Kxc1BWmtPMV67jJ+9VUg=="], + + "@eslint/eslintrc": ["@eslint/eslintrc@3.2.0", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w=="], + + "@eslint/js": ["@eslint/js@9.17.0", "", {}, "sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w=="], + + "@eslint/object-schema": ["@eslint/object-schema@2.1.4", "", {}, "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ=="], + + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.2.3", "", { "dependencies": { "levn": "^0.4.1" } }, "sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA=="], + + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], + + "@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.1", "", {}, "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA=="], + + "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], + + "@mdn/browser-compat-data": ["@mdn/browser-compat-data@5.5.42", "", {}, "sha512-qhHVgb2dxaFNT00Z1upHaDCstUEjjrgtIkrk4tr+YnDSGbTIKncbdydIpSed+RCXz0f6nb4UDD4eKEWokNom6g=="], + + "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], + + "@types/bun": ["@types/bun@1.1.14", "", { "dependencies": { "bun-types": "1.1.37" } }, "sha512-opVYiFGtO2af0dnWBdZWlioLBoxSdDO5qokaazLhq8XQtGZbY4pY3/JxY8Zdf/hEwGubbp7ErZXoN1+h2yesxA=="], + + "@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="], + + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/node": ["@types/node@22.10.2", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ=="], + + "@types/stylus": ["@types/stylus@0.48.43", "", { "dependencies": { "@types/node": "*" } }, "sha512-72dv/zdhuyXWVHUXG2VTPEQdOG+oen95/DNFx2aMFFaY6LoITI6PwEqf5x31JF49kp2w9hvUzkNfTGBIeg61LQ=="], + + "@types/ws": ["@types/ws@8.5.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A=="], + + "acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + + "ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "ast-metadata-inferer": ["ast-metadata-inferer@0.8.1", "", { "dependencies": { "@mdn/browser-compat-data": "^5.6.19" } }, "sha512-ht3Dm6Zr7SXv6t1Ra6gFo0+kLDglHGrEbYihTkcycrbHw7WCcuhBzPlJYHEsIpycaUwzsJHje+vUcxXUX4ztTA=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], + + "browserslist": ["browserslist@4.24.3", "", { "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.1" }, "bin": { "browserslist": "cli.js" } }, "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA=="], + + "bun-types": ["bun-types@1.1.37", "", { "dependencies": { "@types/node": "~20.12.8", "@types/ws": "~8.5.10" } }, "sha512-C65lv6eBr3LPJWFZ2gswyrGZ82ljnH8flVE03xeXxKhi2ZGtFiO4isRKTKnitbSqtRAcaqYSR6djt1whI66AbA=="], + + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001690", "", {}, "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w=="], + + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "debug": ["debug@4.3.5", "", { "dependencies": { "ms": "2.1.2" } }, "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg=="], + + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + + "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.75", "", {}, "sha512-Lf3++DumRE/QmweGjU+ZcKqQ+3bKkU/qjaKYhIJKEOhgIO9Xs6IiAQFkfFoj+RhgDk4LUeNsLo6plExHqSyu6Q=="], + + "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "eslint": ["eslint@9.17.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.19.0", "@eslint/core": "^0.9.0", "@eslint/eslintrc": "^3.2.0", "@eslint/js": "9.17.0", "@eslint/plugin-kit": "^0.2.3", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.1", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.2.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-evtlNcpJg+cZLcnVKwsai8fExnqjGPicK7gnUtlNuzu+Fv9bI0aLpND5T44VLQtoMEnI57LoXO9XAkIXwohKrA=="], + + "eslint-plugin-compat": ["eslint-plugin-compat@6.0.2", "", { "dependencies": { "@mdn/browser-compat-data": "^5.5.35", "ast-metadata-inferer": "^0.8.1", "browserslist": "^4.24.2", "caniuse-lite": "^1.0.30001687", "find-up": "^5.0.0", "globals": "^15.7.0", "lodash.memoize": "^4.1.2", "semver": "^7.6.2" }, "peerDependencies": { "eslint": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" } }, "sha512-1ME+YfJjmOz1blH0nPZpHgjMGK4kjgEeoYqGCqoBPQ/mGu/dJzdoP0f1C8H2jcWZjzhZjAMccbM/VdXhPORIfA=="], + + "eslint-scope": ["eslint-scope@8.2.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="], + + "espree": ["espree@10.3.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.0" } }, "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg=="], + + "esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], + + "flatted": ["flatted@3.3.1", "", {}, "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw=="], + + "foreground-child": ["foreground-child@3.3.0", "", { "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" } }, "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg=="], + + "glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], + + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "globals": ["globals@15.8.0", "", {}, "sha512-VZAJ4cewHTExBWDHR6yptdIBlx9YSSZuwojj9Nt5mBRXQzrKakDsVKQ1J63sklLvzAJm0X5+RpO4i3Y2hcOnFw=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "ignore": ["ignore@5.3.1", "", {}, "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw=="], + + "import-fresh": ["import-fresh@3.3.0", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], + + "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], + + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + + "lodash.memoize": ["lodash.memoize@4.1.2", "", {}, "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag=="], + + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + + "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + + "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + + "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + + "ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="], + + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + + "node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="], + + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + + "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], + + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "sax": ["sax@1.4.1", "", {}, "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg=="], + + "semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "source-map": ["source-map@0.7.4", "", {}, "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA=="], + + "string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + + "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="], + + "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + + "stylus": ["stylus@0.64.0", "", { "dependencies": { "@adobe/css-tools": "~4.3.3", "debug": "^4.3.2", "glob": "^10.4.5", "sax": "~1.4.1", "source-map": "^0.7.3" }, "bin": { "stylus": "bin/stylus" } }, "sha512-ZIdT8eUv8tegmqy1tTIdJv9We2DumkNZFdCF5mz/Kpq3OcTaxSuCAYZge6HKK2CmNC02G1eJig2RV7XTw5hQrA=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + + "typescript": ["typescript@5.7.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg=="], + + "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + + "update-browserslist-db": ["update-browserslist-db@1.1.1", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.0" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A=="], + + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + + "wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + + "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + + "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="], + + "@types/stylus/@types/node": ["@types/node@22.5.5", "", { "dependencies": { "undici-types": "~6.19.2" } }, "sha512-Xjs4y5UPO/CLdzpgR6GirZJx36yScjh73+2NlLlkFRSoQN8B0DpfXPdZGnvVmLRLOsqDpOfTNv7D9trgGhmOIA=="], + + "@types/ws/@types/node": ["@types/node@20.14.2", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q=="], + + "ast-metadata-inferer/@mdn/browser-compat-data": ["@mdn/browser-compat-data@5.6.26", "", {}, "sha512-7NdgdOR7lkzrN70zGSULmrcvKyi/aJjpTJRCbuy8IZuHiLkPTvsr10jW0MJgWzK2l2wTmhdQvegTw6yNU5AVNQ=="], + + "bun-types/@types/node": ["@types/node@20.12.14", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-scnD59RpYD91xngrQQLGkE+6UrHUPzeKZWhhjBSa3HSkwjbQc38+q3RoIVEwxQGRw3M+j5hpNAM+lgV3cVormg=="], + + "foreground-child/cross-spawn": ["cross-spawn@7.0.3", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w=="], + + "glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="], + + "wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "@types/stylus/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="], + + "@types/ws/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "bun-types/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + + "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + } +} diff --git a/bun.lockb b/bun.lockb deleted file mode 100755 index 0caf3b7..0000000 Binary files a/bun.lockb and /dev/null differ diff --git a/dist/better-xcloud.lite.user.js b/dist/better-xcloud.lite.user.js index 17dc19e..3b5b9b2 100755 --- a/dist/better-xcloud.lite.user.js +++ b/dist/better-xcloud.lite.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name Better xCloud (Lite) // @namespace https://github.com/redphx -// @version 6.0.7 +// @version 6.1.0-beta // @description Improve Xbox Cloud Gaming (xCloud) experience // @author redphx // @license MIT @@ -105,7 +105,7 @@ class UserAgent { }); } } -var SCRIPT_VERSION = "6.0.7", SCRIPT_VARIANT = "lite", AppInterface = window.AppInterface; +var SCRIPT_VERSION = "6.1.0-beta", SCRIPT_VARIANT = "lite", AppInterface = window.AppInterface; UserAgent.init(); var userAgent = window.navigator.userAgent.toLowerCase(), isTv = userAgent.includes("smart-tv") || userAgent.includes("smarttv") || /\baft.*\b/.test(userAgent), isVr = window.navigator.userAgent.includes("VR") && window.navigator.userAgent.includes("OculusBrowser"), browserHasTouchSupport = "ontouchstart" in window || navigator.maxTouchPoints > 0, userAgentHasTouchSupport = !isTv && !isVr && browserHasTouchSupport, STATES = { supportedRegion: !0, @@ -269,6 +269,7 @@ var SUPPORTED_LANGUAGES = { "zh-CN": "中文(简体)", "zh-TW": "中文(繁體)" }, Texts = { + "slightly-increase-input-latency": "Slightly increase input latency", achievements: "Achievements", activate: "Activate", activated: "Activated", @@ -328,12 +329,10 @@ var SUPPORTED_LANGUAGES = { "continent-south-america": "South America", contrast: "Contrast", controller: "Controller", + "controller-customization": "Controller customization", "controller-friendly-ui": "Controller-friendly UI", - "controller-mapping": "Controller mapping", - "controller-mapping-in-game": "In-game controller mapping", "controller-shortcuts": "Controller shortcuts", "controller-shortcuts-connect-note": "Connect a controller to use this feature", - "controller-shortcuts-in-game": "In-game controller shortcuts", "controller-shortcuts-xbox-note": "Button to open the Guide menu", "controller-vibration": "Controller vibration", copy: "Copy", @@ -344,12 +343,12 @@ var SUPPORTED_LANGUAGES = { default: "Default", "default-preset-note": "You can't modify default presets. Create a new one to customize it.", delete: "Delete", + "detect-controller-button": "Detect controller button", device: "Device", "device-unsupported-touch": "Your device doesn't have touch support", "device-vibration": "Device vibration", "device-vibration-not-using-gamepad": "On when not using gamepad", disable: "Disable", - "disable-byog-feature": 'Disable "Stream your own game" feature', "disable-features": "Disable features", "disable-home-context-menu": "Disable context menu in Home page", "disable-post-stream-feedback-dialog": "Disable post-stream feedback dialog", @@ -395,6 +394,10 @@ var SUPPORTED_LANGUAGES = { "how-to-improve-app-performance": "How to improve app's performance", ignore: "Ignore", import: "Import", + "in-game-controller-customization": "In-game controller customization", + "in-game-controller-shortcuts": "In-game controller shortcuts", + "in-game-keyboard-shortcuts": "In-game keyboard shortcuts", + "in-game-shortcuts": "In-game shortcuts", increase: "Increase", "install-android": "Better xCloud app for Android", invites: "Invites", @@ -402,12 +405,13 @@ var SUPPORTED_LANGUAGES = { jitter: "Jitter", "keyboard-key": "Keyboard key", "keyboard-shortcuts": "Keyboard shortcuts", - "keyboard-shortcuts-in-game": "In-game keyboard shortcuts", korea: "Korea", language: "Language", large: "Large", layout: "Layout", "left-stick": "Left stick", + "left-stick-deadzone": "Left stick deadzone", + "left-trigger-range": "Left trigger range", "limit-fps": "Limit FPS", "load-failed-message": "Failed to run Better xCloud", "loading-screen": "Loading screen", @@ -415,7 +419,6 @@ var SUPPORTED_LANGUAGES = { "lowest-quality": "Lowest quality", manage: "Manage", "map-mouse-to": "Map mouse to", - mapping: "Mapping", "may-not-work-properly": "May not work properly!", menu: "Menu", microphone: "Microphone", @@ -472,6 +475,7 @@ var SUPPORTED_LANGUAGES = { "preferred-game-language": "Preferred game's language", preset: "Preset", press: "Press", + "press-any-button": "Press any button...", "press-esc-to-cancel": "Press Esc to cancel", "press-key-to-toggle-mkb": [ e => `Press ${e.key} to toggle this feature`, @@ -527,6 +531,8 @@ var SUPPORTED_LANGUAGES = { "renderer-configuration": "Renderer configuration", "right-click-to-unbind": "Right-click on a key to unbind it", "right-stick": "Right stick", + "right-stick-deadzone": "Right stick deadzone", + "right-trigger-range": "Right trigger range", "rocket-always-hide": "Always hide", "rocket-always-show": "Always show", "rocket-animation": "Rocket animation", @@ -536,7 +542,6 @@ var SUPPORTED_LANGUAGES = { screen: "Screen", "screenshot-apply-filters": "Apply video filters to screenshots", "section-all-games": "All games", - "section-byog": "Stream your own game", "section-most-popular": "Most popular", "section-native-mkb": "Play with mouse & keyboard", "section-news": "News", @@ -626,7 +631,6 @@ var SUPPORTED_LANGUAGES = { e => `觸控遊玩佈局由 ${e.name} 提供` ], "touch-controller": "Touch controller", - "transparent-background": "Transparent background", "true-achievements": "TrueAchievements", ui: "UI", "unexpected-behavior": "May cause unexpected behavior", @@ -746,10 +750,11 @@ var ButtonStyleClass = { 64: "bx-focusable", 128: "bx-full-width", 256: "bx-full-height", - 512: "bx-tall", - 1024: "bx-circular", - 2048: "bx-normal-case", - 4096: "bx-normal-link" + 512: "bx-auto-height", + 1024: "bx-tall", + 2048: "bx-circular", + 4096: "bx-normal-case", + 8192: "bx-normal-link" }; function createElement(elmName, props = {}, ..._) { let $elm, hasNs = "xmlns" in props; @@ -831,6 +836,23 @@ function clearDataSet($elm) { delete $elm.dataset[key]; }); } +function calculateSelectBoxes($root) { + let selects = Array.from($root.querySelectorAll("div.bx-select:not([data-calculated]) select")); + for (let $select of selects) { + let $parent = $select.parentElement; + if ($parent.classList.contains("bx-full-width")) { + $parent.dataset.calculated = "true"; + continue; + } + let rect = $select.getBoundingClientRect(), $label, width = Math.ceil(rect.width); + if (!width) continue; + if ($label = $parent.querySelector($select.multiple ? ".bx-select-value" : "div"), $parent.isControllerFriendly) { + if ($select.multiple) width += 20; + if ($select.querySelector("optgroup")) width -= 15; + } else width += 10; + $select.style.left = "0", $label.style.minWidth = width + "px", $parent.dataset.calculated = "true"; + } +} var FILE_SIZE_UNITS = ["B", "KB", "MB", "GB", "TB"]; function humanFileSize(size) { let i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024)); @@ -1001,9 +1023,10 @@ class LocalDb { static instance; static getInstance = () => LocalDb.instance ?? (LocalDb.instance = new LocalDb); static DB_NAME = "BetterXcloud"; - static DB_VERSION = 3; + static DB_VERSION = 4; static TABLE_VIRTUAL_CONTROLLERS = "virtual_controllers"; static TABLE_CONTROLLER_SHORTCUTS = "controller_shortcuts"; + static TABLE_CONTROLLER_CUSTOMIZATIONS = "controller_customizations"; static TABLE_CONTROLLER_SETTINGS = "controller_settings"; static TABLE_KEYBOARD_SHORTCUTS = "keyboard_shortcuts"; db; @@ -1028,6 +1051,10 @@ class LocalDb { if (!db.objectStoreNames.contains(LocalDb.TABLE_CONTROLLER_SETTINGS)) db.createObjectStore(LocalDb.TABLE_CONTROLLER_SETTINGS, { keyPath: "id" }); + if (!db.objectStoreNames.contains(LocalDb.TABLE_CONTROLLER_CUSTOMIZATIONS)) db.createObjectStore(LocalDb.TABLE_CONTROLLER_CUSTOMIZATIONS, { + keyPath: "id", + autoIncrement: !0 + }); if (!db.objectStoreNames.contains(LocalDb.TABLE_KEYBOARD_SHORTCUTS)) db.createObjectStore(LocalDb.TABLE_KEYBOARD_SHORTCUTS, { keyPath: "id", autoIncrement: !0 @@ -2077,27 +2104,27 @@ class StreamStatsCollector { current: -1, grades: [40, 75, 100], toString() { - return this.current === -1 ? "???" : this.current.toString().padStart(3, " "); + return this.current === -1 ? "???" : this.current.toString().padStart(3); } }, jit: { current: 0, grades: [30, 40, 60], toString() { - return `${this.current.toFixed(1)}ms`.padStart(6, " "); + return `${this.current.toFixed(1)}ms`.padStart(6); } }, fps: { current: 0, toString() { let maxFps = getPref("video.maxFps"); - return maxFps < 60 ? `${maxFps}/${this.current}`.padStart(5, " ") : this.current.toString(); + return maxFps < 60 ? `${maxFps}/${this.current}`.padStart(5) : this.current.toString(); } }, btr: { current: 0, toString() { - return `${this.current.toFixed(1)} Mbps`.padStart(9, " "); + return `${this.current.toFixed(1)} Mbps`.padStart(9); } }, fl: { @@ -2121,13 +2148,13 @@ class StreamStatsCollector { total: 0, grades: [6, 9, 12], toString() { - return isNaN(this.current) ? "??ms" : `${this.current.toFixed(1)}ms`.padStart(6, " "); + return isNaN(this.current) ? "??ms" : `${this.current.toFixed(1)}ms`.padStart(6); } }, dl: { total: 0, toString() { - return humanFileSize(this.total).padStart(8, " "); + return humanFileSize(this.total).padStart(8); } }, ul: { @@ -2594,12 +2621,39 @@ class ControllerShortcutsTable extends BasePresetsTable { BxLogger.info(this.LOG_TAG, "constructor()"); } } +class ControllerCustomizationsTable extends BasePresetsTable { + static instance; + static getInstance = () => ControllerCustomizationsTable.instance ?? (ControllerCustomizationsTable.instance = new ControllerCustomizationsTable(LocalDb.TABLE_CONTROLLER_CUSTOMIZATIONS)); + TABLE_PRESETS = LocalDb.TABLE_CONTROLLER_CUSTOMIZATIONS; + DEFAULT_PRESETS = { + [-1]: { + id: -1, + name: "ABXY ⇄ BAYX", + data: { + mapping: { + 0: 1, + 1: 0, + 2: 3, + 3: 2 + }, + settings: { + leftStickDeadzone: [0, 100], + rightStickDeadzone: [0, 100], + leftTriggerRange: [0, 100], + rightTriggerRange: [0, 100], + vibrationIntensity: 100 + } + } + } + }; + DEFAULT_PRESET_ID = 0; +} class ControllerSettingsTable extends BaseLocalTable { static instance; static getInstance = () => ControllerSettingsTable.instance ?? (ControllerSettingsTable.instance = new ControllerSettingsTable(LocalDb.TABLE_CONTROLLER_SETTINGS)); static DEFAULT_DATA = { shortcutPresetId: -1, - vibrationIntensity: 50 + customizationPresetId: 0 }; async getControllerData(id) { let setting = await this.get(id); @@ -2610,8 +2664,8 @@ class ControllerSettingsTable extends BaseLocalTable { let all = await this.getAll(), results = {}; for (let key in all) { if (!all[key]) continue; - let settings = all[key].data; - settings.vibrationIntensity /= 100, results[key] = settings; + let settings = Object.assign(all[key].data, ControllerSettingsTable.DEFAULT_DATA); + results[key] = settings; } return results; } @@ -2644,24 +2698,65 @@ class StreamSettings { mkbPreset: null, keyboardShortcuts: {} }; + static CONTROLLER_CUSTOMIZATION_MAPPING = { + 0: "A", + 1: "B", + 2: "X", + 3: "Y", + 12: "DPadUp", + 15: "DPadRight", + 13: "DPadDown", + 14: "DPadLeft", + 4: "LeftShoulder", + 5: "RightShoulder", + 6: "LeftTrigger", + 7: "RightTrigger", + 10: "LeftThumb", + 11: "RightThumb", + 104: "LeftStickAxes", + 204: "RightStickAxes", + 8: "View", + 9: "Menu", + 17: "Share" + }; static getPref(key) { return getPref(key); } static async refreshControllerSettings() { - let settings = StreamSettings.settings, controllers = {}, settingsTable = ControllerSettingsTable.getInstance(), shortcutsTable = ControllerShortcutsTable.getInstance(), gamepads = window.navigator.getGamepads(); + let settings = StreamSettings.settings, controllers = {}, settingsTable = ControllerSettingsTable.getInstance(), shortcutsTable = ControllerShortcutsTable.getInstance(), mappingTable = ControllerCustomizationsTable.getInstance(), gamepads = window.navigator.getGamepads(); for (let gamepad of gamepads) { if (!gamepad?.connected) continue; if (gamepad.id === VIRTUAL_GAMEPAD_ID) continue; - let settingsData = await settingsTable.getControllerData(gamepad.id), shortcutsMapping, preset = await shortcutsTable.getPreset(settingsData.shortcutPresetId); - if (!preset) shortcutsMapping = null; - else shortcutsMapping = preset.data.mapping; + let settingsData = await settingsTable.getControllerData(gamepad.id), shortcutsPreset = await shortcutsTable.getPreset(settingsData.shortcutPresetId), shortcutsMapping = !shortcutsPreset ? null : shortcutsPreset.data.mapping, customizationPreset = await mappingTable.getPreset(settingsData.customizationPresetId), customizationData = StreamSettings.convertControllerCustomization(customizationPreset?.data); controllers[gamepad.id] = { - vibrationIntensity: settingsData.vibrationIntensity, - shortcuts: shortcutsMapping + shortcuts: shortcutsMapping, + customization: customizationData }; } settings.controllers = controllers, settings.controllerPollingRate = StreamSettings.getPref("controller.pollingRate"), await StreamSettings.refreshDeviceVibration(); } + static preCalculateControllerRange(obj, target, values) { + if (values && Array.isArray(values)) { + let [from, to] = values; + if (from > 1 || to < 100) obj[target] = [from / 100, to / 100]; + } + } + static convertControllerCustomization(customization) { + if (!customization) return null; + let converted = { + mapping: {}, + ranges: {}, + vibrationIntensity: 1 + }, gamepadKey; + for (gamepadKey in customization.mapping) { + let gamepadStr = StreamSettings.CONTROLLER_CUSTOMIZATION_MAPPING[gamepadKey]; + if (!gamepadStr) continue; + let mappedKey = customization.mapping[gamepadKey]; + if (typeof mappedKey === "number") converted.mapping[gamepadStr] = StreamSettings.CONTROLLER_CUSTOMIZATION_MAPPING[mappedKey]; + else converted.mapping[gamepadStr] = !1; + } + return StreamSettings.preCalculateControllerRange(converted.ranges, "LeftTrigger", customization.settings.leftTriggerRange), StreamSettings.preCalculateControllerRange(converted.ranges, "RightTrigger", customization.settings.rightTriggerRange), StreamSettings.preCalculateControllerRange(converted.ranges, "LeftThumb", customization.settings.leftStickDeadzone), StreamSettings.preCalculateControllerRange(converted.ranges, "RightThumb", customization.settings.rightStickDeadzone), converted.vibrationIntensity = customization.settings.vibrationIntensity / 100, converted; + } static async refreshDeviceVibration() { if (!STATES.browser.capabilities.deviceVibration) return; let mode = StreamSettings.getPref("deviceVibration.mode"), intensity = 0; @@ -2711,6 +2806,43 @@ class StreamSettings { window.addEventListener("gamepadconnected", listener), window.addEventListener("gamepaddisconnected", listener), StreamSettings.refreshAllSettings(); } } +var BxIcon = { + BETTER_XCLOUD: "", + TRUE_ACHIEVEMENTS: "", + STREAM_SETTINGS: "", + STREAM_STATS: "", + CLOSE: "", + CONTROLLER: "", + CREATE_SHORTCUT: "", + DISPLAY: "", + EYE: "", + EYE_SLASH: "", + HOME: "", + NATIVE_MKB: "", + NEW: "", + MANAGE: "", + COPY: "", + TRASH: "", + CURSOR_TEXT: "", + POWER: "", + QUESTION: "", + REFRESH: "", + REMOTE_PLAY: "", + CARET_LEFT: "", + CARET_RIGHT: "", + SCREENSHOT: "", + SPEAKER_MUTED: "", + TOUCH_CONTROL_ENABLE: "", + TOUCH_CONTROL_DISABLE: "", + MICROPHONE: "", + MICROPHONE_MUTED: "", + BATTERY: "", + PLAYTIME: "", + SERVER: "", + DOWNLOAD: "", + UPLOAD: "", + AUDIO: "" +}; class MkbPopup { static instance; static getInstance = () => MkbPopup.instance ?? (MkbPopup.instance = new MkbPopup); @@ -2736,7 +2868,7 @@ class MkbPopup { } createActivateButton() { let options = { - style: 1 | 512 | 128, + style: 1 | 1024 | 128, label: t("activate"), onClick: this.onActivate }, shortcutKey = StreamSettings.findKeyboardShortcut("mkb.toggle"); @@ -2755,6 +2887,7 @@ class MkbPopup { } }), createButton({ label: t("manage"), + icon: BxIcon.MANAGE, style: 64, onClick: () => { let dialog = SettingsDialog.getInstance(); @@ -3259,16 +3392,22 @@ class NavigationDialogManager { LOG_TAG = "NavigationDialogManager"; static GAMEPAD_POLLING_INTERVAL = 50; static GAMEPAD_KEYS = [ - 12, - 13, - 14, - 15, 0, 1, + 2, + 3, + 12, + 15, + 13, + 14, 4, 5, 6, - 7 + 7, + 10, + 11, + 8, + 9 ]; static GAMEPAD_DIRECTION_MAP = { 12: 1, @@ -3298,31 +3437,14 @@ class NavigationDialogManager { dialog = null; dialogsStack = []; constructor() { - if (BxLogger.info(this.LOG_TAG, "constructor()"), this.$overlay = CE("div", { class: "bx-navigation-dialog-overlay bx-gone" }), this.$overlay.addEventListener("click", (e) => { + BxLogger.info(this.LOG_TAG, "constructor()"), this.$overlay = CE("div", { class: "bx-navigation-dialog-overlay bx-gone" }), this.$overlay.addEventListener("click", (e) => { e.preventDefault(), e.stopPropagation(), this.dialog?.isCancellable() && this.hide(); - }), document.documentElement.appendChild(this.$overlay), this.$container = CE("div", { class: "bx-navigation-dialog bx-gone" }), document.documentElement.appendChild(this.$container), window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, (e) => this.hide()), getPref("ui.controllerFriendly")) - new MutationObserver((mutationList) => { - if (mutationList.length === 0 || mutationList[0].addedNodes.length === 0) return; - let $dialog = mutationList[0].addedNodes[0]; - if (!$dialog || !($dialog instanceof HTMLElement)) return; - this.calculateSelectBoxes($dialog); - }).observe(this.$container, { childList: !0 }); - } - calculateSelectBoxes($root) { - let selects = Array.from($root.querySelectorAll(".bx-select:not([data-calculated]) select")); - for (let $select of selects) { - let $parent = $select.parentElement; - if ($parent.classList.contains("bx-full-width")) { - $parent.dataset.calculated = "true"; - return; - } - let rect = $select.getBoundingClientRect(), $label, width = Math.ceil(rect.width); - if (!width) return; - if ($select.multiple) $label = $parent.querySelector(".bx-select-value"), width += 20; - else $label = $parent.querySelector("div"); - if ($select.querySelector("optgroup")) width -= 15; - $label.style.minWidth = width + "px", $parent.dataset.calculated = "true"; - } + }), document.documentElement.appendChild(this.$overlay), this.$container = CE("div", { class: "bx-navigation-dialog bx-gone" }), document.documentElement.appendChild(this.$container), window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, (e) => this.hide()), new MutationObserver((mutationList) => { + if (mutationList.length === 0 || mutationList[0].addedNodes.length === 0) return; + let $dialog = mutationList[0].addedNodes[0]; + if (!$dialog || !($dialog instanceof HTMLElement)) return; + calculateSelectBoxes($dialog); + }).observe(this.$container, { childList: !0 }); } updateActiveInput(input) { document.documentElement.dataset.activeInput = input; @@ -3385,7 +3507,7 @@ class NavigationDialogManager { } } this.clearGamepadHoldingInterval(); - }, 200); + }, 100); continue; } if (releasedButton === null) { @@ -3455,7 +3577,12 @@ class NavigationDialogManager { if (!$focusing || $focusing === this.$container) return null; if (checked.includes($focusing)) return null; checked.push($focusing); - let $target = $focusing, $parent = $target.parentElement, nearby = $target.nearby || {}, orientation = this.getOrientation($target), siblingProperty = NavigationDialogManager.SIBLING_PROPERTY_MAP[orientation][direction]; + let $target = $focusing, $parent = $target.parentElement, nearby = $target.nearby || {}, orientation = this.getOrientation($target); + if (nearby[1] && direction === 1) return nearby[1]; + else if (nearby[3] && direction === 3) return nearby[3]; + else if (nearby[4] && direction === 4) return nearby[4]; + else if (nearby[2] && direction === 2) return nearby[2]; + let siblingProperty = NavigationDialogManager.SIBLING_PROPERTY_MAP[orientation][direction]; if (siblingProperty) { let $sibling = $target; while ($sibling[siblingProperty]) { @@ -3510,43 +3637,8 @@ class NavigationDialogManager { dialog && dialog.onBeforeUnmount(), this.$container.firstChild?.remove(), dialog && dialog.onUnmounted(), this.dialog = null; } } -var BxIcon = { - BETTER_XCLOUD: "", - TRUE_ACHIEVEMENTS: "", - STREAM_SETTINGS: "", - STREAM_STATS: "", - CLOSE: "", - CONTROLLER: "", - CREATE_SHORTCUT: "", - DISPLAY: "", - EYE: "", - EYE_SLASH: "", - HOME: "", - NATIVE_MKB: "", - NEW: "", - COPY: "", - TRASH: "", - CURSOR_TEXT: "", - POWER: "", - QUESTION: "", - REFRESH: "", - REMOTE_PLAY: "", - CARET_LEFT: "", - CARET_RIGHT: "", - SCREENSHOT: "", - SPEAKER_MUTED: "", - TOUCH_CONTROL_ENABLE: "", - TOUCH_CONTROL_DISABLE: "", - MICROPHONE: "", - MICROPHONE_MUTED: "", - BATTERY: "", - PLAYTIME: "", - SERVER: "", - DOWNLOAD: "", - UPLOAD: "", - AUDIO: "" -}; class BxSelectElement extends HTMLSelectElement { + isControllerFriendly; optionsList; indicatorsList; $indicators; @@ -3558,21 +3650,36 @@ class BxSelectElement extends HTMLSelectElement { $label; $checkBox; static create($select, forceFriendly = !1) { - if (!forceFriendly && !getPref("ui.controllerFriendly")) return $select.classList.add("bx-select"), $select; + let isControllerFriendly = forceFriendly || getPref("ui.controllerFriendly"); + if ($select.multiple && !isControllerFriendly) return $select.classList.add("bx-select"), $select; $select.removeAttribute("tabindex"); - let $wrapper = CE("div", { class: "bx-select" }), $btnPrev = createButton({ - label: "<", - style: 64 - }), $btnNext = createButton({ - label: ">", - style: 64 - }); - setNearby($wrapper, { - orientation: "horizontal", - focus: $btnNext + let $wrapper = CE("div", { + class: "bx-select", + _dataset: { + controllerFriendly: isControllerFriendly + } }); + if ($select.classList.contains("bx-full-width")) $wrapper.classList.add("bx-full-width"); let $content, self = $wrapper; - if (self.isMultiple = $select.multiple, self.visibleIndex = $select.selectedIndex, self.$select = $select, self.optionsList = Array.from($select.querySelectorAll("option")), self.$indicators = CE("div", { class: "bx-select-indicators" }), self.indicatorsList = [], self.$btnNext = $btnNext, self.$btnPrev = $btnPrev, self.isMultiple) $content = CE("button", { + self.isControllerFriendly = isControllerFriendly, self.isMultiple = $select.multiple, self.visibleIndex = $select.selectedIndex, self.$select = $select, self.optionsList = Array.from($select.querySelectorAll("option")), self.$indicators = CE("div", { class: "bx-select-indicators" }), self.indicatorsList = []; + let $btnPrev, $btnNext; + if (isControllerFriendly) { + $btnPrev = createButton({ + label: "<", + style: 64 + }), $btnNext = createButton({ + label: ">", + style: 64 + }), setNearby($wrapper, { + orientation: "horizontal", + focus: $btnNext + }), self.$btnNext = $btnNext, self.$btnPrev = $btnPrev; + let boundOnPrevNext = BxSelectElement.onPrevNext.bind(self); + $btnPrev.addEventListener("click", boundOnPrevNext), $btnNext.addEventListener("click", boundOnPrevNext); + } else $select.addEventListener("change", (e) => { + self.visibleIndex = $select.selectedIndex, BxSelectElement.resetIndicators.call(self), BxSelectElement.render.call(self); + }); + if (self.isMultiple) $content = CE("button", { class: "bx-select-value bx-focusable", tabindex: 0 }, CE("div", {}, self.$checkBox = CE("input", { type: "checkbox" }), self.$label = CE("span", {}, "")), self.$indicators), $content.addEventListener("click", (e) => { @@ -3582,8 +3689,7 @@ class BxSelectElement extends HTMLSelectElement { $option && ($option.selected = e.target.checked), BxEvent.dispatch($select, "input"); }); else $content = CE("div", {}, self.$label = CE("label", { for: $select.id + "_checkbox" }, ""), self.$indicators); - let boundOnPrevNext = BxSelectElement.onPrevNext.bind(self); - return $select.addEventListener("input", BxSelectElement.render.bind(self)), $btnPrev.addEventListener("click", boundOnPrevNext), $btnNext.addEventListener("click", boundOnPrevNext), new MutationObserver((mutationList, observer2) => { + return $select.addEventListener("input", BxSelectElement.render.bind(self)), new MutationObserver((mutationList, observer2) => { mutationList.forEach((mutation) => { if (mutation.type === "childList" || mutation.type === "attributes") self.visibleIndex = $select.selectedIndex, self.optionsList = Array.from($select.querySelectorAll("option")), BxSelectElement.resetIndicators.call(self), BxSelectElement.render.call(self); }); @@ -3591,7 +3697,7 @@ class BxSelectElement extends HTMLSelectElement { subtree: !0, childList: !0, attributes: !0 - }), self.append($select, $btnPrev, $content, $btnNext), BxSelectElement.resetIndicators.call(self), BxSelectElement.render.call(self), Object.defineProperty(self, "value", { + }), self.append($select, $btnPrev || "", $content, $btnNext || ""), BxSelectElement.resetIndicators.call(self), BxSelectElement.render.call(self), Object.defineProperty(self, "value", { get() { return $select.value; }, @@ -3640,7 +3746,6 @@ class BxSelectElement extends HTMLSelectElement { $btnNext, $btnPrev, $checkBox, - visibleIndex, optionsList, indicatorsList } = this; @@ -3649,7 +3754,7 @@ class BxSelectElement extends HTMLSelectElement { let $option = BxSelectElement.getOptionAtIndex.call(this, this.visibleIndex), content = ""; if ($option) { let $parent = $option.parentElement, hasLabel = $parent instanceof HTMLOptGroupElement || this.$select.querySelector("optgroup"); - if (content = $option.textContent || "", content && hasLabel) { + if (content = $option.dataset.label || $option.textContent || "", content && hasLabel) { let groupLabel = $parent instanceof HTMLOptGroupElement ? $parent.label : " "; $label.innerHTML = ""; let fragment = document.createDocumentFragment(); @@ -3658,12 +3763,12 @@ class BxSelectElement extends HTMLSelectElement { } else $label.textContent = content; if ($label.classList.toggle("bx-line-through", $option && $option.disabled), this.isMultiple) $checkBox.checked = $option?.selected || !1, $checkBox.classList.toggle("bx-gone", !content); let disableButtons = optionsList.length <= 1; - $btnPrev.classList.toggle("bx-inactive", disableButtons), $btnNext.classList.toggle("bx-inactive", disableButtons); + $btnPrev?.classList.toggle("bx-inactive", disableButtons), $btnNext?.classList.toggle("bx-inactive", disableButtons); for (let i = 0;i < optionsList.length; i++) { let $option2 = optionsList[i], $indicator = indicatorsList[i]; if (!$option2 || !$indicator) continue; if (clearDataSet($indicator), $option2.selected) $indicator.dataset.selected = "true"; - if ($option2.index === visibleIndex) $indicator.dataset.highlighted = "true"; + if ($option2.index === this.visibleIndex) $indicator.dataset.highlighted = "true"; } } static normalizeIndex(index) { @@ -4006,7 +4111,7 @@ class SuggestionsSetting { _nearby: { orientation: "vertical" } - }, BxSelectElement.create($select, !0), $suggestedSettings, $btnApply, BX_FLAGS.DeviceInfo.deviceType.includes("android") && CE("a", { + }, BxSelectElement.create($select), $suggestedSettings, $btnApply, BX_FLAGS.DeviceInfo.deviceType.includes("android") && CE("a", { class: "bx-suggest-link bx-focusable", href: "https://better-xcloud.github.io/guide/android-webview-tweaks/", target: "_blank", @@ -4385,7 +4490,6 @@ class SettingsDialog extends NavigationDialog { }] }]; TAB_CONTROLLER_ITEMS = [ - !1, { group: "controller", label: t("controller"), @@ -4396,6 +4500,7 @@ class SettingsDialog extends NavigationDialog { !1 ] }, + !1, !1 ]; TAB_MKB_ITEMS = () => [ @@ -4505,9 +4610,8 @@ class SettingsDialog extends NavigationDialog { } let $child, children = Array.from(this.$tabContents.children); for ($child of children) - if ($child.dataset.tabGroup === $svg.dataset.group) { - if ($child.classList.remove("bx-gone"), getPref("ui.controllerFriendly")) this.dialogManager.calculateSelectBoxes($child); - } else $child.classList.add("bx-gone"); + if ($child.dataset.tabGroup === $svg.dataset.group) $child.classList.remove("bx-gone"), calculateSelectBoxes($child); + else $child.classList.add("bx-gone"); for (let $child2 of Array.from(this.$tabs.children)) $child2.classList.remove("bx-active"); $svg.classList.add("bx-active"); @@ -4541,7 +4645,6 @@ class SettingsDialog extends NavigationDialog { } }, $control = CE("select", { id: `bx_setting_${escapeCssSelector(setting.pref)}`, - title: setting.label, tabindex: 0 }); $control.name = $control.id, $control.addEventListener("input", (e) => { @@ -4640,7 +4743,7 @@ class SettingsDialog extends NavigationDialog { label = createButton({ label, url: "https://github.com/redphx/better-xcloud/releases", - style: 2048 | 16 | 64 + style: 4096 | 16 | 64 }); } if (label) { @@ -4873,8 +4976,8 @@ var BxExposed = { BxLogger.error("setupGainNode", e), STATES.currentStream.audioGainNode = null; } }, - handleControllerShortcut: !1, - resetControllerShortcut: !1, + handleControllerShortcut: () => {}, + resetControllerShortcut: () => {}, overrideSettings: { Tv_settings: { hasCompletedOnboarding: !0 @@ -4942,7 +5045,7 @@ class HeaderSection { classes: ["bx-header-remote-play-button", "bx-gone"], icon: BxIcon.REMOTE_PLAY, title: t("remote-play"), - style: 8 | 64 | 1024, + style: 8 | 64 | 2048, onClick: (e) => RemotePlayManager.getInstance()?.togglePopup() }), this.$btnSettings = createButton({ classes: ["bx-header-settings-button"], @@ -5772,7 +5875,7 @@ function getOsNameFromResolution(resolution) { return osName; } function addCss() { - let css = ':root{--bx-title-font:Bahnschrift,Arial,Helvetica,sans-serif;--bx-title-font-semibold:Bahnschrift Semibold,Arial,Helvetica,sans-serif;--bx-normal-font:"Segoe UI",Arial,Helvetica,sans-serif;--bx-monospaced-font:Consolas,"Courier New",Courier,monospace;--bx-promptfont-font:promptfont;--bx-button-height:40px;--bx-default-button-color:#2d3036;--bx-default-button-rgb:45,48,54;--bx-default-button-hover-color:#515863;--bx-default-button-hover-rgb:81,88,99;--bx-default-button-active-color:#222428;--bx-default-button-active-rgb:34,36,40;--bx-default-button-disabled-color:#8e8e8e;--bx-default-button-disabled-rgb:142,142,142;--bx-primary-button-color:#008746;--bx-primary-button-rgb:0,135,70;--bx-primary-button-hover-color:#04b358;--bx-primary-button-hover-rgb:4,179,88;--bx-primary-button-active-color:#044e2a;--bx-primary-button-active-rgb:4,78,42;--bx-primary-button-disabled-color:#448262;--bx-primary-button-disabled-rgb:68,130,98;--bx-warning-button-color:#c16e04;--bx-warning-button-rgb:193,110,4;--bx-warning-button-hover-color:#fa9005;--bx-warning-button-hover-rgb:250,144,5;--bx-warning-button-active-color:#965603;--bx-warning-button-active-rgb:150,86,3;--bx-warning-button-disabled-color:#a2816c;--bx-warning-button-disabled-rgb:162,129,108;--bx-danger-button-color:#c10404;--bx-danger-button-rgb:193,4,4;--bx-danger-button-hover-color:#e61d1d;--bx-danger-button-hover-rgb:230,29,29;--bx-danger-button-active-color:#a26c6c;--bx-danger-button-active-rgb:162,108,108;--bx-danger-button-disabled-color:#df5656;--bx-danger-button-disabled-rgb:223,86,86;--bx-fullscreen-text-z-index:9999;--bx-toast-z-index:6000;--bx-key-binding-dialog-z-index:5010;--bx-key-binding-dialog-overlay-z-index:5000;--bx-stats-bar-z-index:4010;--bx-navigation-dialog-z-index:3010;--bx-navigation-dialog-overlay-z-index:3000;--bx-mkb-pointer-lock-msg-z-index:2000;--bx-game-bar-z-index:1000;--bx-screenshot-animation-z-index:200;--bx-wait-time-box-z-index:100}@font-face{font-family:\'promptfont\';src:url("https://redphx.github.io/better-xcloud/fonts/promptfont.otf")}div[class^=HUDButton-module__hiddenContainer] ~ div:not([class^=HUDButton-module__hiddenContainer]){opacity:0;pointer-events:none !important;position:absolute;top:-9999px;left:-9999px}@media screen and (max-width:640px){header a[href="/play"]{display:none}}.bx-full-width{width:100% !important}.bx-full-height{height:100% !important}.bx-no-scroll{overflow:hidden !important}.bx-hide-scroll-bar{scrollbar-width:none}.bx-hide-scroll-bar::-webkit-scrollbar{display:none}.bx-gone{display:none !important}.bx-offscreen{position:absolute !important;top:-9999px !important;left:-9999px !important;visibility:hidden !important}.bx-hidden{visibility:hidden !important}.bx-invisible{opacity:0}.bx-unclickable{pointer-events:none}.bx-pixel{width:1px !important;height:1px !important}.bx-no-margin{margin:0 !important}.bx-no-padding{padding:0 !important}.bx-prompt{font-family:var(--bx-promptfont-font) !important}.bx-line-through{text-decoration:line-through !important}.bx-normal-case{text-transform:none !important}.bx-normal-link{text-transform:none !important;text-align:left !important;font-weight:400 !important;font-family:var(--bx-normal-font) !important}select[multiple],select[multiple]:focus{overflow:auto;border:none}select[multiple] option,select[multiple]:focus option{padding:4px 6px}select[multiple] option:checked,select[multiple]:focus option:checked{background:#1a7bc0 linear-gradient(0deg,#1a7bc0 0%,#1a7bc0 100%)}select[multiple] option:checked::before,select[multiple]:focus option:checked::before{content:\'☑️\';font-size:12px;display:inline-block;margin-right:6px;height:100%;line-height:100%;vertical-align:middle}#headerArea,#uhfSkipToMain,.uhf-footer{display:none}div[class*=NotFocusedDialog]{position:absolute !important;top:-9999px !important;left:-9999px !important;width:0 !important;height:0 !important}#game-stream video:not([src]){visibility:hidden}div[class*=SupportedInputsBadge]:not(:has(:nth-child(2))),div[class*=SupportedInputsBadge] svg:first-of-type{display:none}.bx-game-tile-wait-time{position:absolute;top:0;left:0;z-index:1;background:rgba(0,0,0,0.5);display:flex;border-radius:4px 0 4px 0;align-items:center;padding:4px 8px}.bx-game-tile-wait-time svg{width:14px;height:16px;margin-right:2px}.bx-game-tile-wait-time span{display:inline-block;height:16px;line-height:16px;font-size:12px;font-weight:bold;margin-left:2px}.bx-game-tile-wait-time[data-duration=short]{background-color:rgba(0,133,133,0.75)}.bx-game-tile-wait-time[data-duration=medium]{background-color:rgba(213,133,0,0.75)}.bx-game-tile-wait-time[data-duration=long]{background-color:rgba(150,0,0,0.75)}.bx-fullscreen-text{position:fixed;top:0;bottom:0;left:0;right:0;background:rgba(0,0,0,0.8);z-index:var(--bx-fullscreen-text-z-index);line-height:100vh;color:#fff;text-align:center;font-weight:400;font-family:var(--bx-normal-font);font-size:1.3rem;user-select:none;-webkit-user-select:none}#root section[class*=DeviceCodePage-module__page]{margin-left:20px !important;margin-right:20px !important;margin-top:20px !important;max-width:800px !important}#root div[class*=DeviceCodePage-module__back]{display:none}.bx-blink-me{animation:bx-blinker 1s linear infinite}@-moz-keyframes bx-blinker{100%{opacity:0}}@-webkit-keyframes bx-blinker{100%{opacity:0}}@-o-keyframes bx-blinker{100%{opacity:0}}@keyframes bx-blinker{100%{opacity:0}}.bx-button{--button-rgb:var(--bx-default-button-rgb);--button-hover-rgb:var(--bx-default-button-hover-rgb);--button-active-rgb:var(--bx-default-button-active-rgb);--button-disabled-rgb:var(--bx-default-button-disabled-rgb);background-color:rgb(var(--button-rgb));user-select:none;-webkit-user-select:none;color:#fff;font-family:var(--bx-title-font-semibold);font-size:14px;border:none;font-weight:400;height:var(--bx-button-height);border-radius:4px;padding:0 8px;text-transform:uppercase;cursor:pointer;overflow:hidden}.bx-button:not([disabled]):active{background-color:rgb(var(--button-active-rgb))}.bx-button:focus{outline:none !important}.bx-button:not([disabled]):not(:active):hover,.bx-button:not([disabled]):not(:active).bx-focusable:focus{background-color:rgb(var(--button-hover-rgb))}.bx-button:disabled{cursor:default;background-color:rgb(var(--button-disabled-rgb))}.bx-button.bx-ghost{background-color:transparent}.bx-button.bx-ghost:not([disabled]):not(:active):hover,.bx-button.bx-ghost:not([disabled]):not(:active).bx-focusable:focus{background-color:rgb(var(--button-hover-rgb))}.bx-button.bx-primary{--button-rgb:var(--bx-primary-button-rgb)}.bx-button.bx-primary:not([disabled]):active{--button-active-rgb:var(--bx-primary-button-active-rgb)}.bx-button.bx-primary:not([disabled]):not(:active):hover,.bx-button.bx-primary:not([disabled]):not(:active).bx-focusable:focus{--button-hover-rgb:var(--bx-primary-button-hover-rgb)}.bx-button.bx-primary:disabled{--button-disabled-rgb:var(--bx-primary-button-disabled-rgb)}.bx-button.bx-warning{--button-rgb:var(--bx-warning-button-rgb)}.bx-button.bx-warning:not([disabled]):active{--button-active-rgb:var(--bx-warning-button-active-rgb)}.bx-button.bx-warning:not([disabled]):not(:active):hover,.bx-button.bx-warning:not([disabled]):not(:active).bx-focusable:focus{--button-hover-rgb:var(--bx-warning-button-hover-rgb)}.bx-button.bx-warning:disabled{--button-disabled-rgb:var(--bx-warning-button-disabled-rgb)}.bx-button.bx-danger{--button-rgb:var(--bx-danger-button-rgb)}.bx-button.bx-danger:not([disabled]):active{--button-active-rgb:var(--bx-danger-button-active-rgb)}.bx-button.bx-danger:not([disabled]):not(:active):hover,.bx-button.bx-danger:not([disabled]):not(:active).bx-focusable:focus{--button-hover-rgb:var(--bx-danger-button-hover-rgb)}.bx-button.bx-danger:disabled{--button-disabled-rgb:var(--bx-danger-button-disabled-rgb)}.bx-button.bx-frosted{--button-alpha:.2;background-color:rgba(var(--button-rgb), var(--button-alpha));backdrop-filter:blur(4px) brightness(1.5)}.bx-button.bx-frosted:not([disabled]):not(:active):hover,.bx-button.bx-frosted:not([disabled]):not(:active).bx-focusable:focus{background-color:rgba(var(--button-hover-rgb), var(--button-alpha))}.bx-button.bx-drop-shadow{box-shadow:0 0 4px rgba(0,0,0,0.502)}.bx-button.bx-tall{height:calc(var(--bx-button-height) * 1.5) !important}.bx-button.bx-circular{border-radius:var(--bx-button-height);width:var(--bx-button-height);height:var(--bx-button-height)}.bx-button svg{display:inline-block;width:16px;height:var(--bx-button-height)}.bx-button span{display:inline-block;line-height:var(--bx-button-height);vertical-align:middle;color:#fff;overflow:hidden;white-space:nowrap}.bx-button span:not(:only-child){margin-left:10px}.bx-button.bx-button-multi-lines{height:auto;text-align:left;padding:10px 0}.bx-button.bx-button-multi-lines span{line-height:unset;display:block}.bx-button.bx-button-multi-lines span:last-of-type{text-transform:none;font-weight:normal;font-family:"Segoe Sans Variable Text";font-size:12px;margin-top:4px}.bx-focusable{position:relative;overflow:visible}.bx-focusable::after{border:2px solid transparent;border-radius:10px}.bx-focusable:focus::after{content:\'\';border-color:#fff;position:absolute;top:-6px;left:-6px;right:-6px;bottom:-6px}html[data-active-input=touch] .bx-focusable:focus::after,html[data-active-input=mouse] .bx-focusable:focus::after{border-color:transparent !important}.bx-focusable.bx-circular::after{border-radius:var(--bx-button-height)}a.bx-button{display:inline-block}a.bx-button.bx-full-width{text-align:center}button.bx-inactive{pointer-events:none;opacity:.2;background:transparent !important}.bx-header-remote-play-button{height:auto;margin-right:8px !important}.bx-header-remote-play-button svg{width:24px;height:24px}.bx-header-settings-button{line-height:30px;font-size:14px;text-transform:uppercase;position:relative}.bx-header-settings-button[data-update-available]::before{content:\'🌟\' !important;line-height:var(--bx-button-height);display:inline-block;margin-left:4px}.bx-key-binding-dialog-overlay{position:fixed;inset:0;z-index:var(--bx-key-binding-dialog-overlay-z-index);background:#000;opacity:50%}.bx-key-binding-dialog{display:flex;flex-flow:column;max-height:90vh;position:fixed;top:50%;left:50%;margin-right:-50%;transform:translate(-50%,-50%);min-width:420px;padding:16px;border-radius:8px;z-index:var(--bx-key-binding-dialog-z-index);background:#1a1b1e;color:#fff;font-weight:400;font-size:16px;font-family:var(--bx-normal-font);box-shadow:0 0 6px #000;user-select:none;-webkit-user-select:none}.bx-key-binding-dialog *:focus{outline:none !important}.bx-key-binding-dialog h2{margin-bottom:12px;color:#fff;display:block;font-family:var(--bx-title-font);font-size:32px;font-weight:400;line-height:var(--bx-button-height)}.bx-key-binding-dialog > div{overflow:auto;padding:2px 0}.bx-key-binding-dialog > button{padding:8px 32px;margin:10px auto 0;border:none;border-radius:4px;display:block;background-color:#2d3036;text-align:center;color:#fff;text-transform:uppercase;font-family:var(--bx-title-font);font-weight:400;line-height:18px;font-size:14px}@media (hover:hover){.bx-key-binding-dialog > button:hover{background-color:#515863}}.bx-key-binding-dialog > button:focus{background-color:#515863}.bx-key-binding-dialog ul{margin-bottom:1rem}.bx-key-binding-dialog ul li{display:none}.bx-key-binding-dialog ul[data-flags*="[1]"] > li[data-flag="1"],.bx-key-binding-dialog ul[data-flags*="[2]"] > li[data-flag="2"],.bx-key-binding-dialog ul[data-flags*="[4]"] > li[data-flag="4"],.bx-key-binding-dialog ul[data-flags*="[8]"] > li[data-flag="8"]{display:list-item}@media screen and (max-width:450px){.bx-key-binding-dialog{min-width:100%}}.bx-navigation-dialog{position:absolute;z-index:var(--bx-navigation-dialog-z-index);font-family:var(--bx-title-font)}.bx-navigation-dialog *:focus{outline:none !important}.bx-navigation-dialog select:disabled{-webkit-appearance:none;text-align-last:right;text-align:right;color:#fff;background:#131416;border:none;border-radius:4px;padding:0 5px}.bx-navigation-dialog-overlay{position:fixed;background:rgba(11,11,11,0.89);top:0;left:0;right:0;bottom:0;z-index:var(--bx-navigation-dialog-overlay-z-index)}.bx-navigation-dialog-overlay[data-is-playing="true"]{background:transparent}.bx-centered-dialog{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);color:#fff;background:#1a1b1e;border-radius:10px;width:450px;max-width:calc(100vw - 20px);margin:0 0 0 auto;padding:16px;max-height:95vh;flex-direction:column;overflow:hidden;display:flex;flex-direction:column}.bx-centered-dialog .bx-dialog-title{display:flex;flex-direction:row;align-items:center;margin-bottom:10px}.bx-centered-dialog .bx-dialog-title p{padding:0;margin:0;flex:1;font-size:1.2rem;font-weight:bold}.bx-centered-dialog .bx-dialog-title button{flex-shrink:0}.bx-centered-dialog .bx-dialog-content{flex:1;overflow:auto;overflow-x:hidden}.bx-centered-dialog .bx-dialog-preset-tools{display:flex;margin-bottom:12px;gap:6px}.bx-centered-dialog .bx-dialog-preset-tools select{flex:1}.bx-centered-dialog .bx-default-preset-note{font-size:12px;font-style:italic;text-align:center;margin-bottom:10px}.bx-centered-dialog input,.bx-settings-dialog input{accent-color:var(--bx-primary-button-color)}.bx-centered-dialog input:focus,.bx-settings-dialog input:focus{accent-color:var(--bx-danger-button-color)}.bx-centered-dialog select:disabled,.bx-settings-dialog select:disabled{-webkit-appearance:none;background:transparent;text-align-last:right;border:none;color:#fff}.bx-centered-dialog select option:disabled,.bx-settings-dialog select option:disabled{display:none}.bx-centered-dialog input[type=checkbox]:focus,.bx-settings-dialog input[type=checkbox]:focus,.bx-centered-dialog select:focus,.bx-settings-dialog select:focus{filter:drop-shadow(1px 0 0 #fff) drop-shadow(-1px 0 0 #fff) drop-shadow(0 1px 0 #fff) drop-shadow(0 -1px 0 #fff)}.bx-centered-dialog a,.bx-settings-dialog a{color:#1c9d1c;text-decoration:none}.bx-centered-dialog a:hover,.bx-settings-dialog a:hover,.bx-centered-dialog a:focus,.bx-settings-dialog a:focus{color:#5dc21e}.bx-centered-dialog label,.bx-settings-dialog label{margin:0}.bx-controller-shortcuts-manager-container .bx-shortcut-note{margin-top:10px;font-size:14px;text-align:center}.bx-controller-shortcuts-manager-container .bx-shortcut-row{display:flex;gap:10px;margin-bottom:10px;align-items:center}.bx-controller-shortcuts-manager-container .bx-shortcut-row label.bx-prompt{flex-shrink:0;font-size:32px;margin:0}.bx-controller-shortcuts-manager-container .bx-shortcut-row label.bx-prompt::first-letter{letter-spacing:6px}.bx-controller-shortcuts-manager-container .bx-shortcut-row .bx-shortcut-actions{flex:1;position:relative}.bx-controller-shortcuts-manager-container .bx-shortcut-row .bx-shortcut-actions select{width:100%;height:100%;min-height:38px;display:block}.bx-controller-shortcuts-manager-container .bx-shortcut-row .bx-shortcut-actions select:first-of-type{position:absolute;top:0;left:0}.bx-controller-shortcuts-manager-container .bx-shortcut-row .bx-shortcut-actions select:last-of-type{opacity:0;z-index:calc(var(--bx-settings-z-index) + 1)}.bx-controller-shortcuts-manager-container select:disabled{text-align:left;text-align-last:left}.bx-keyboard-shortcuts-manager-container{display:flex;flex-direction:column;gap:16px}.bx-keyboard-shortcuts-manager-container fieldset{background:#2a2a2a;border:1px solid #2a2a2a;border-radius:4px;padding:4px}.bx-keyboard-shortcuts-manager-container legend{width:auto;padding:4px 8px;margin:0 4px 4px;background:#004f87;box-shadow:0 2px 0 #071e3d;border-radius:4px;font-size:14px;font-weight:bold;text-transform:uppercase}.bx-keyboard-shortcuts-manager-container .bx-settings-row{background:none;padding:10px}.bx-settings-dialog{display:flex;position:fixed;top:0;right:0;bottom:0;opacity:.98;user-select:none;-webkit-user-select:none}.bx-settings-dialog .bx-focusable::after{border-radius:4px}.bx-settings-dialog .bx-focusable:focus::after{top:0;left:0;right:0;bottom:0}.bx-settings-dialog .bx-settings-reload-note{font-size:.8rem;display:block;padding:8px;font-style:italic;font-weight:normal;height:var(--bx-button-height)}.bx-settings-tabs-container{position:fixed;width:48px;max-height:100vh;display:flex;flex-direction:column}.bx-settings-tabs-container > div:last-of-type{display:flex;flex-direction:column;align-items:end}.bx-settings-tabs-container > div:last-of-type button{flex-shrink:0;border-top-right-radius:0;border-bottom-right-radius:0;margin-top:8px;height:unset;padding:8px 10px}.bx-settings-tabs-container > div:last-of-type button svg{width:16px;height:16px}.bx-settings-tabs{display:flex;flex-direction:column;border-radius:0 0 0 8px;box-shadow:0 0 6px #000;overflow:overlay;flex:1}.bx-settings-tabs svg{width:24px;height:24px;padding:10px;flex-shrink:0;box-sizing:content-box;background:#131313;cursor:pointer;border-left:4px solid #1e1e1e}.bx-settings-tabs svg.bx-active{background:#222;border-color:#008746}.bx-settings-tabs svg:not(.bx-active):hover{background:#2f2f2f;border-color:#484848}.bx-settings-tabs svg:focus{border-color:#fff}.bx-settings-tabs svg[data-group=global][data-need-refresh=true]{background:var(--bx-danger-button-color) !important}.bx-settings-tabs svg[data-group=global][data-need-refresh=true]:hover{background:var(--bx-danger-button-hover-color) !important}.bx-settings-tab-contents{flex-direction:column;padding:10px;margin-left:48px;width:450px;max-width:calc(100vw - tabsWidth);background:#1a1b1e;color:#fff;font-weight:400;font-size:16px;font-family:var(--bx-title-font);text-align:center;box-shadow:0 0 6px #000;overflow:overlay;z-index:1}.bx-settings-tab-contents > div[data-tab-group=mkb]{display:flex;flex-direction:column;height:100%;overflow:hidden}.bx-settings-tab-contents .bx-top-buttons{display:flex;flex-direction:column;gap:8px;margin-bottom:8px}.bx-settings-tab-contents .bx-top-buttons .bx-button{display:block}.bx-settings-tab-contents h2{margin:16px 0 8px 0;display:flex;align-items:center}.bx-settings-tab-contents h2:first-of-type{margin-top:0}.bx-settings-tab-contents h2 span{display:inline-block;font-size:20px;font-weight:bold;text-align:left;flex:1;text-overflow:ellipsis;overflow:hidden;white-space:nowrap;min-height:var(--bx-button-height);align-content:center}@media (max-width:500px){.bx-settings-tab-contents{width:calc(100vw - 48px)}}.bx-settings-row{display:flex;gap:10px;padding:16px 10px;margin:0;background:#2a2a2a;border-bottom:1px solid #343434}.bx-settings-row:hover,.bx-settings-row:focus-within{background-color:#242424}.bx-settings-row:not(:has(> input[type=checkbox])){flex-wrap:wrap}.bx-settings-row > span.bx-settings-label{font-size:14px;display:block;text-align:left;align-self:center;margin-bottom:0 !important;flex:1}.bx-settings-row > span.bx-settings-label + *{margin:0 0 0 auto}.bx-settings-row[data-multi-lines="true"]{flex-direction:column}.bx-settings-row[data-multi-lines="true"] > span.bx-settings-label{align-self:start}.bx-settings-row[data-multi-lines="true"] > span.bx-settings-label + *{margin:unset}.bx-settings-dialog-note{display:block;color:#afafb0;font-size:12px;font-weight:lighter;font-style:italic}.bx-settings-dialog-note:not(:has(a)){margin-top:4px}.bx-settings-dialog-note a{display:inline-block;padding:4px}.bx-settings-custom-user-agent{display:block;width:100%;padding:6px}.bx-donation-link{display:block;text-align:center;text-decoration:none;height:20px;line-height:20px;font-size:14px;margin-top:10px;margin-bottom:10px}.bx-debug-info button{margin-top:10px}.bx-debug-info pre{margin-top:10px;cursor:copy;color:#fff;padding:8px;border:1px solid #2d2d2d;background:#212121;white-space:break-spaces;text-align:left}.bx-debug-info pre:hover{background:#272727}.bx-settings-app-version{margin-top:10px;text-align:center;color:#747474;font-size:12px}.bx-note-unsupported{display:block;font-size:12px;font-style:italic;font-weight:normal;color:#828282}.bx-settings-tab-contents > div *:not(.bx-settings-row):has(+ .bx-settings-row) + .bx-settings-row:has(+ .bx-settings-row){border-top-left-radius:6px;border-top-right-radius:6px}.bx-settings-tab-contents > div .bx-settings-row:not(:has(+ .bx-settings-row)){border:none;border-bottom-left-radius:6px;border-bottom-right-radius:6px}.bx-settings-tab-contents > div *:not(.bx-settings-row):has(+ .bx-settings-row) + .bx-settings-row:not(:has(+ .bx-settings-row)){border:none;border-radius:6px}.bx-suggest-toggler{text-align:left;display:flex;border-radius:4px;overflow:hidden;background:#003861;height:45px;align-items:center}.bx-suggest-toggler label{flex:1;align-content:center;padding:0 10px;background:#004f87;height:100%}.bx-suggest-toggler span{display:inline-block;align-self:center;padding:10px;width:45px;text-align:center}.bx-suggest-toggler:hover,.bx-suggest-toggler:focus{cursor:pointer;background:#005da1}.bx-suggest-toggler:hover label,.bx-suggest-toggler:focus label{background:#006fbe}.bx-suggest-toggler[bx-open] span{transform:rotate(90deg)}.bx-suggest-toggler[bx-open]+ .bx-suggest-box{display:block}.bx-suggest-box{display:none}.bx-suggest-wrapper{display:flex;flex-direction:column;gap:10px;margin:10px}.bx-suggest-note{font-size:11px;color:#8c8c8c;font-style:italic;font-weight:100}.bx-suggest-link{font-size:14px;display:inline-block;margin-top:4px;padding:4px}.bx-suggest-row{display:flex;flex-direction:row;gap:10px}.bx-suggest-row label{flex:1;overflow:overlay;border-radius:4px}.bx-suggest-row label .bx-suggest-label{background:#323232;padding:4px 10px;font-size:12px;text-align:left}.bx-suggest-row label .bx-suggest-value{padding:6px;font-size:14px}.bx-suggest-row label .bx-suggest-value.bx-suggest-change{background-color:var(--bx-warning-color)}.bx-suggest-row.bx-suggest-ok input{visibility:hidden}.bx-suggest-row.bx-suggest-ok .bx-suggest-label{background-color:#008114}.bx-suggest-row.bx-suggest-ok .bx-suggest-value{background-color:#13a72a}.bx-suggest-row.bx-suggest-change .bx-suggest-label{background-color:#a65e08}.bx-suggest-row.bx-suggest-change .bx-suggest-value{background-color:#d57f18}.bx-suggest-row.bx-suggest-change:hover label{cursor:pointer}.bx-suggest-row.bx-suggest-change:hover .bx-suggest-label{background-color:#995707}.bx-suggest-row.bx-suggest-change:hover .bx-suggest-value{background-color:#bd7115}.bx-suggest-row.bx-suggest-change input:not(:checked) + label{opacity:.5}.bx-suggest-row.bx-suggest-change input:not(:checked) + label .bx-suggest-label{background-color:#2a2a2a}.bx-suggest-row.bx-suggest-change input:not(:checked) + label .bx-suggest-value{background-color:#393939}.bx-suggest-row.bx-suggest-change:hover input:not(:checked) + label{opacity:1}.bx-suggest-row.bx-suggest-change:hover input:not(:checked) + label .bx-suggest-label{background-color:#202020}.bx-suggest-row.bx-suggest-change:hover input:not(:checked) + label .bx-suggest-value{background-color:#303030}.bx-sub-content-box{background:#161616;padding:10px;box-shadow:0 0 12px #0f0f0f inset;border-radius:10px}.bx-settings-row .bx-sub-content-box{background:#202020;padding:12px;box-shadow:0 0 4px #000 inset;border-radius:6px}.bx-controller-extra-settings[data-has-gamepad=true] > :first-child{display:none}.bx-controller-extra-settings[data-has-gamepad=true] > :last-child{display:block}.bx-controller-extra-settings[data-has-gamepad=false] > :first-child{display:block}.bx-controller-extra-settings[data-has-gamepad=false] > :last-child{display:none}.bx-controller-extra-settings .bx-controller-extra-wrapper{flex:1;min-width:1px}.bx-controller-extra-settings .bx-sub-content-box{flex:1;text-align:left;display:flex;flex-direction:column;margin-top:10px}.bx-controller-extra-settings .bx-sub-content-box > label{font-size:14px}.bx-preset-row{display:flex;gap:8px}.bx-preset-row .bx-select{flex:1}.bx-toast{user-select:none;-webkit-user-select:none;position:fixed;left:50%;top:24px;transform:translate(-50%,0);background:#000;border-radius:16px;color:#fff;z-index:var(--bx-toast-z-index);font-family:var(--bx-normal-font);border:2px solid #fff;display:flex;align-items:center;opacity:0;overflow:clip;transition:opacity .2s ease-in}.bx-toast.bx-show{opacity:.85}.bx-toast.bx-hide{opacity:0;pointer-events:none}.bx-toast-msg{font-size:14px;display:inline-block;padding:12px 16px;white-space:pre}.bx-toast-status{font-weight:bold;font-size:14px;text-transform:uppercase;display:inline-block;background:#515863;padding:12px 16px;color:#fff;white-space:pre}.bx-wait-time-box{position:fixed;top:0;right:0;background-color:rgba(0,0,0,0.8);color:#fff;z-index:var(--bx-wait-time-box-z-index);padding:12px;border-radius:0 0 0 8px}.bx-wait-time-box label{display:block;text-transform:uppercase;text-align:right;font-size:12px;font-weight:bold;margin:0}.bx-wait-time-box span{display:block;font-family:var(--bx-monospaced-font);text-align:right;font-size:16px;margin-bottom:10px}.bx-wait-time-box span:last-of-type{margin-bottom:0}.bx-remote-play-container{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);color:#fff;background:#1a1b1e;border-radius:10px;width:420px;max-width:calc(100vw - 20px);margin:0 0 0 auto;padding:16px}.bx-remote-play-container > .bx-button{display:table;margin:0 0 0 auto}.bx-remote-play-settings{margin-bottom:12px;padding-bottom:12px;border-bottom:1px solid #2d2d2d}.bx-remote-play-settings > div{display:flex}.bx-remote-play-settings label{flex:1}.bx-remote-play-settings label p{margin:4px 0 0;padding:0;color:#888;font-size:12px}.bx-remote-play-resolution{display:block}.bx-remote-play-resolution input[type="radio"]{accent-color:var(--bx-primary-button-color);margin-right:6px}.bx-remote-play-resolution input[type="radio"]:focus{accent-color:var(--bx-primary-button-hover-color)}.bx-remote-play-device-wrapper{display:flex;margin-bottom:12px}.bx-remote-play-device-wrapper:last-child{margin-bottom:2px}.bx-remote-play-device-info{flex:1;padding:4px 0}.bx-remote-play-device-name{font-size:20px;font-weight:bold;display:inline-block;vertical-align:middle}.bx-remote-play-console-type{font-size:12px;background:#004c87;color:#fff;display:inline-block;border-radius:14px;padding:2px 10px;margin-left:8px;vertical-align:middle}.bx-remote-play-power-state{color:#888;font-size:12px}.bx-remote-play-connect-button{min-height:100%;margin:4px 0}.bx-remote-play-buttons{display:flex;justify-content:space-between}select.bx-select{min-height:30px}div.bx-select{display:flex;align-items:center;flex:0 1 auto;gap:8px}div.bx-select select{position:absolute !important;top:-9999px !important;left:-9999px !important;visibility:hidden !important}div.bx-select select:disabled ~ button{display:none}div.bx-select select:disabled ~ div{background:#131416;color:#fff;pointer-events:none}div.bx-select select:disabled ~ div .bx-select-indicators{visibility:hidden}div.bx-select > div,div.bx-select button.bx-select-value{min-width:120px;text-align:left;line-height:24px;vertical-align:middle;background:#fff;color:#000;border-radius:4px;padding:2px 8px;display:flex;flex:1;flex-direction:column}div.bx-select > div{min-height:24px;box-sizing:content-box}div.bx-select > div input{display:inline-block;margin-right:8px}div.bx-select > div label{margin-bottom:0;font-size:14px;width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;min-height:15px}div.bx-select > div label span{display:block;font-size:10px;font-weight:bold;text-align:left;line-height:initial;white-space:pre;min-height:15px;align-content:center}div.bx-select button.bx-select-value{border:none;cursor:pointer;min-height:30px;font-size:.9rem;align-items:center}div.bx-select button.bx-select-value > div{display:flex;width:100%}div.bx-select button.bx-select-value span{flex:1;text-align:left;display:inline-block}div.bx-select button.bx-select-value input{margin:0 4px;accent-color:var(--bx-primary-button-color);pointer-events:none}div.bx-select button.bx-select-value:hover input,div.bx-select button.bx-select-value:focus input{accent-color:var(--bx-danger-button-color)}div.bx-select button.bx-select-value:hover::after,div.bx-select button.bx-select-value:focus::after{border-color:#4d4d4d !important}div.bx-select button.bx-button{border:none;height:24px;width:24px;padding:0;line-height:24px;color:#fff;border-radius:4px;font-weight:bold;font-size:12px;font-family:var(--bx-monospaced-font);flex-shrink:0}div.bx-select button.bx-button span{line-height:unset}.bx-select-indicators{display:flex;height:4px;gap:2px;margin-bottom:2px}.bx-select-indicators span{content:\' \';display:inline-block;flex:1;background:#cfcfcf;border-radius:4px}.bx-select-indicators span[data-highlighted]{background:#9c9c9c}.bx-select-indicators span[data-selected]{background:#aacfe7}.bx-select-indicators span[data-highlighted][data-selected]{background:#5fa3d0}.bx-guide-home-achievements-progress{display:flex;gap:10px;flex-direction:row}.bx-guide-home-achievements-progress .bx-button{margin-bottom:0 !important}body[data-bx-media-type=tv] .bx-guide-home-achievements-progress{flex-direction:column}body:not([data-bx-media-type=tv]) .bx-guide-home-achievements-progress{flex-direction:row}body:not([data-bx-media-type=tv]) .bx-guide-home-achievements-progress > button:first-of-type{flex:1}body:not([data-bx-media-type=tv]) .bx-guide-home-achievements-progress > button:last-of-type{width:40px}body:not([data-bx-media-type=tv]) .bx-guide-home-achievements-progress > button:last-of-type span{display:none}.bx-guide-home-buttons > div{display:flex;flex-direction:row;gap:12px}body[data-bx-media-type=tv] .bx-guide-home-buttons > div{flex-direction:column}body[data-bx-media-type=tv] .bx-guide-home-buttons > div button{margin-bottom:0 !important}body:not([data-bx-media-type=tv]) .bx-guide-home-buttons > div button span{display:none}.bx-guide-home-buttons[data-is-playing="true"] button[data-state=\'normal\']{display:none}.bx-guide-home-buttons[data-is-playing="false"] button[data-state=\'playing\']{display:none}div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module]{overflow:visible}.bx-stream-menu-button-on{fill:#000 !important;background-color:#2d2d2d !important;color:#000 !important}.bx-stream-refresh-button{top:calc(env(safe-area-inset-top, 0px) + 10px + 50px) !important}body[data-media-type=default] .bx-stream-refresh-button{left:calc(env(safe-area-inset-left, 0px) + 11px) !important}body[data-media-type=tv] .bx-stream-refresh-button{top:calc(var(--gds-focus-borderSize) + 80px) !important}.bx-stream-home-button{top:calc(env(safe-area-inset-top, 0px) + 10px + 50px * 2) !important}body[data-media-type=default] .bx-stream-home-button{left:calc(env(safe-area-inset-left, 0px) + 12px) !important}body[data-media-type=tv] .bx-stream-home-button{top:calc(var(--gds-focus-borderSize) + 80px * 2) !important}div[data-testid=media-container][data-position=center]{display:flex}div[data-testid=media-container][data-position=top] video,div[data-testid=media-container][data-position=top] canvas{top:0}div[data-testid=media-container][data-position=bottom] video,div[data-testid=media-container][data-position=bottom] canvas{bottom:0}#game-stream video{margin:auto;align-self:center;background:#000;position:absolute;left:0;right:0}#game-stream canvas{align-self:center;margin:auto;position:absolute;left:0;right:0}#game-stream.bx-taking-screenshot:before{animation:bx-anim-taking-screenshot .5s ease;content:\' \';position:absolute;width:100%;height:100%;z-index:var(--bx-screenshot-animation-z-index)}#gamepass-dialog-root div[class^=Guide-module__guide] .bx-button{overflow:visible;margin-bottom:12px}@-moz-keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}@-webkit-keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}@-o-keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}@keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}.bx-number-stepper{text-align:center}.bx-number-stepper > div{display:flex;align-items:center}.bx-number-stepper > div span{flex:1;display:inline-block;min-width:40px;font-family:var(--bx-monospaced-font);font-size:13px;margin:0 4px}.bx-number-stepper > div button{flex-shrink:0;border:none;width:24px;height:24px;margin:0;line-height:24px;background-color:var(--bx-default-button-color);color:#fff;border-radius:4px;font-weight:bold;font-size:14px;font-family:var(--bx-monospaced-font)}@media (hover:hover){.bx-number-stepper > div button:hover{background-color:var(--bx-default-button-hover-color)}}.bx-number-stepper > div button:active{background-color:var(--bx-default-button-hover-color)}.bx-number-stepper > div button:disabled + span{font-family:var(--bx-title-font)}.bx-number-stepper input[type="range"]{display:block;margin:8px 0 2px auto;min-width:180px;width:100%;color:#959595 !important}.bx-number-stepper input[type=range]:disabled,.bx-number-stepper button:disabled{display:none}.bx-number-stepper[data-disabled=true] input[type=range],.bx-number-stepper[disabled=true] input[type=range],.bx-number-stepper[data-disabled=true] button,.bx-number-stepper[disabled=true] button{display:none}#bx-game-bar{z-index:var(--bx-game-bar-z-index);position:fixed;bottom:0;width:40px;height:90px;overflow:visible;cursor:pointer}#bx-game-bar > svg{display:none;pointer-events:none;position:absolute;height:28px;margin-top:16px}@media (hover:hover){#bx-game-bar:hover > svg{display:block}}#bx-game-bar .bx-game-bar-container{opacity:0;position:absolute;display:flex;overflow:hidden;background:rgba(26,27,30,0.91);box-shadow:0 0 6px #1c1c1c;transition:opacity .1s ease-in}#bx-game-bar .bx-game-bar-container.bx-show{opacity:.9}#bx-game-bar .bx-game-bar-container.bx-show + svg{display:none !important}#bx-game-bar .bx-game-bar-container.bx-hide{opacity:0;pointer-events:none}#bx-game-bar .bx-game-bar-container button{width:60px;height:60px;border-radius:0}#bx-game-bar .bx-game-bar-container button svg{width:28px;height:28px;transition:transform .08s ease 0s}#bx-game-bar .bx-game-bar-container button:hover{border-radius:0}#bx-game-bar .bx-game-bar-container button:active svg{transform:scale(.75)}#bx-game-bar .bx-game-bar-container button.bx-activated{background-color:#fff}#bx-game-bar .bx-game-bar-container button.bx-activated svg{filter:invert(1)}#bx-game-bar .bx-game-bar-container div[data-activated] button{display:none}#bx-game-bar .bx-game-bar-container div[data-activated=\'false\'] button:first-of-type{display:block}#bx-game-bar .bx-game-bar-container div[data-activated=\'true\'] button:last-of-type{display:block}#bx-game-bar[data-position="bottom-left"]{left:0;direction:ltr}#bx-game-bar[data-position="bottom-left"] .bx-game-bar-container{border-radius:0 10px 10px 0}#bx-game-bar[data-position="bottom-right"]{right:0;direction:rtl}#bx-game-bar[data-position="bottom-right"] .bx-game-bar-container{direction:ltr;border-radius:10px 0 0 10px}.bx-badges{margin-left:0;user-select:none;-webkit-user-select:none}.bx-badge{border:none;display:inline-block;line-height:24px;color:#fff;font-family:var(--bx-title-font-semibold);font-size:14px;font-weight:400;margin:0 8px 8px 0;box-shadow:0 0 6px #000;border-radius:4px}.bx-badge-name{background-color:#2d3036;border-radius:4px 0 0 4px}.bx-badge-name svg{width:16px;height:16px}.bx-badge-value{background-color:#808080;border-radius:0 4px 4px 0}.bx-badge-name,.bx-badge-value{display:inline-block;padding:0 8px;line-height:30px;vertical-align:bottom}.bx-badge-battery[data-charging=true] span:first-of-type::after{content:\' ⚡️\'}div[class^=StreamMenu-module__container] .bx-badges{position:absolute;max-width:500px}#gamepass-dialog-root .bx-badges{position:fixed;top:60px;left:460px;max-width:500px}@media (min-width:568px) and (max-height:480px){#gamepass-dialog-root .bx-badges{position:unset;top:unset;left:unset;margin:8px 0}}.bx-stats-bar{display:flex;flex-direction:row;gap:8px;user-select:none;-webkit-user-select:none;position:fixed;top:0;background-color:#000;color:#fff;font-family:var(--bx-monospaced-font);font-size:.9rem;padding-left:8px;z-index:var(--bx-stats-bar-z-index);text-wrap:nowrap}.bx-stats-bar[data-stats*="[time]"] > .bx-stat-time,.bx-stats-bar[data-stats*="[play]"] > .bx-stat-play,.bx-stats-bar[data-stats*="[batt]"] > .bx-stat-batt,.bx-stats-bar[data-stats*="[fps]"] > .bx-stat-fps,.bx-stats-bar[data-stats*="[ping]"] > .bx-stat-ping,.bx-stats-bar[data-stats*="[jit]"] > .bx-stat-jit,.bx-stats-bar[data-stats*="[btr]"] > .bx-stat-btr,.bx-stats-bar[data-stats*="[dt]"] > .bx-stat-dt,.bx-stats-bar[data-stats*="[pl]"] > .bx-stat-pl,.bx-stats-bar[data-stats*="[fl]"] > .bx-stat-fl,.bx-stats-bar[data-stats*="[dl]"] > .bx-stat-dl,.bx-stats-bar[data-stats*="[ul]"] > .bx-stat-ul{display:inline-flex;align-items:baseline}.bx-stats-bar[data-stats$="[time]"] > .bx-stat-time,.bx-stats-bar[data-stats$="[play]"] > .bx-stat-play,.bx-stats-bar[data-stats$="[batt]"] > .bx-stat-batt,.bx-stats-bar[data-stats$="[fps]"] > .bx-stat-fps,.bx-stats-bar[data-stats$="[ping]"] > .bx-stat-ping,.bx-stats-bar[data-stats$="[jit]"] > .bx-stat-jit,.bx-stats-bar[data-stats$="[btr]"] > .bx-stat-btr,.bx-stats-bar[data-stats$="[dt]"] > .bx-stat-dt,.bx-stats-bar[data-stats$="[pl]"] > .bx-stat-pl,.bx-stats-bar[data-stats$="[fl]"] > .bx-stat-fl,.bx-stats-bar[data-stats$="[dl]"] > .bx-stat-dl,.bx-stats-bar[data-stats$="[ul]"] > .bx-stat-ul{border-right:none}.bx-stats-bar::before{display:none;content:\'👀\';vertical-align:middle;margin-right:8px}.bx-stats-bar[data-display=glancing]::before{display:inline-block}.bx-stats-bar[data-position=top-left]{left:0;border-radius:0 0 4px 0}.bx-stats-bar[data-position=top-right]{right:0;border-radius:0 0 0 4px}.bx-stats-bar[data-position=top-center]{transform:translate(-50%,0);left:50%;border-radius:0 0 4px 4px}.bx-stats-bar[data-shadow=true]{background:none;filter:drop-shadow(1px 0 0 rgba(0,0,0,0.941)) drop-shadow(-1px 0 0 rgba(0,0,0,0.941)) drop-shadow(0 1px 0 rgba(0,0,0,0.941)) drop-shadow(0 -1px 0 rgba(0,0,0,0.941))}.bx-stats-bar > div{display:none;border-right:1px solid #fff;padding-right:8px}.bx-stats-bar label{margin:0 8px 0 0;font-family:var(--bx-title-font);font-size:70%;font-weight:bold;vertical-align:middle;cursor:help}.bx-stats-bar span{display:inline-block;text-align:right;vertical-align:middle;white-space:pre}.bx-stats-bar span[data-grade=good]{color:#6bffff}.bx-stats-bar span[data-grade=ok]{color:#fff16b}.bx-stats-bar span[data-grade=bad]{color:#ff5f5f}.bx-mkb-settings{display:flex;flex-direction:column;flex:1;padding-bottom:10px;overflow:hidden}.bx-mkb-pointer-lock-msg{user-select:none;-webkit-user-select:none;position:fixed;left:50%;bottom:40px;transform:translateX(-50%);margin:auto;background:#151515;z-index:var(--bx-mkb-pointer-lock-msg-z-index);color:#fff;font-weight:400;font-family:"Segoe UI",Arial,Helvetica,sans-serif;font-size:1.3rem;padding:12px;border-radius:8px;align-items:center;box-shadow:0 0 6px #000;min-width:300px;opacity:.9;display:flex;flex-direction:column;gap:10px}.bx-mkb-pointer-lock-msg:hover{opacity:1}.bx-mkb-pointer-lock-msg > p{margin:0;width:100%;font-size:22px;margin-bottom:4px;font-weight:bold;text-align:left}.bx-mkb-pointer-lock-msg > div{width:100%;display:flex;flex-direction:row;gap:10px}.bx-mkb-pointer-lock-msg > div button:first-of-type{flex-shrink:1}.bx-mkb-pointer-lock-msg > div button:last-of-type{flex-grow:1}.bx-mkb-key-row{display:flex;margin-bottom:10px;align-items:center;gap:20px}.bx-mkb-key-row label{margin-bottom:0;font-family:var(--bx-promptfont-font);font-size:32px;text-align:center}.bx-mkb-settings.bx-editing .bx-mkb-key-row button{background:#393939;border-radius:4px;border:none}.bx-mkb-settings.bx-editing .bx-mkb-key-row button:hover{background:#333;cursor:pointer}.bx-mkb-action-buttons > div{text-align:right;display:none}.bx-mkb-action-buttons button{margin-left:8px}.bx-mkb-settings:not(.bx-editing) .bx-mkb-action-buttons > div:first-child{display:block}.bx-mkb-settings.bx-editing .bx-mkb-action-buttons > div:last-child{display:block}.bx-mkb-note{display:block;margin:0 0 10px;font-size:12px;text-align:center}button.bx-binding-button{flex:1;min-height:38px;border:none;border-radius:4px;font-size:14px;color:#fff;display:flex;align-items:center;align-self:center;padding:0 6px}button.bx-binding-button:disabled{background:#131416;padding:0 8px}button.bx-binding-button:not(:disabled){border:2px solid transparent;border-top:none;border-bottom:4px solid #252525;background:#3b3b3b;cursor:pointer}button.bx-binding-button:not(:disabled):hover,button.bx-binding-button:not(:disabled).bx-focusable:focus{background:#20b217;border-bottom-color:#186c13}button.bx-binding-button:not(:disabled):active{background:#16900f;border-bottom:3px solid #0c4e08;border-left-width:2px;border-right-width:2px}button.bx-binding-button:not(:disabled).bx-focusable:focus::after{top:-6px;left:-8px;right:-8px;bottom:-10px}.bx-settings-row .bx-binding-button-wrapper button.bx-binding-button{min-width:60px}.bx-product-details-buttons{display:flex;gap:10px;flex-direction:row}.bx-product-details-buttons button{max-width:max-content;margin:10px 0 0 0;display:flex}@media (min-width:568px) and (max-height:480px){.bx-product-details-buttons{flex-direction:column}.bx-product-details-buttons button{margin:8px 0 0 10px}}', PREF_HIDE_SECTIONS = getPref("ui.hideSections"), selectorToHide = []; + let css = ':root{--bx-title-font:Bahnschrift,Arial,Helvetica,sans-serif;--bx-title-font-semibold:Bahnschrift Semibold,Arial,Helvetica,sans-serif;--bx-normal-font:"Segoe UI",Arial,Helvetica,sans-serif;--bx-monospaced-font:Consolas,"Courier New",Courier,monospace;--bx-promptfont-font:promptfont;--bx-button-height:40px;--bx-default-button-color:#2d3036;--bx-default-button-rgb:45,48,54;--bx-default-button-hover-color:#515863;--bx-default-button-hover-rgb:81,88,99;--bx-default-button-active-color:#222428;--bx-default-button-active-rgb:34,36,40;--bx-default-button-disabled-color:#8e8e8e;--bx-default-button-disabled-rgb:142,142,142;--bx-primary-button-color:#008746;--bx-primary-button-rgb:0,135,70;--bx-primary-button-hover-color:#04b358;--bx-primary-button-hover-rgb:4,179,88;--bx-primary-button-active-color:#044e2a;--bx-primary-button-active-rgb:4,78,42;--bx-primary-button-disabled-color:#448262;--bx-primary-button-disabled-rgb:68,130,98;--bx-warning-button-color:#c16e04;--bx-warning-button-rgb:193,110,4;--bx-warning-button-hover-color:#fa9005;--bx-warning-button-hover-rgb:250,144,5;--bx-warning-button-active-color:#965603;--bx-warning-button-active-rgb:150,86,3;--bx-warning-button-disabled-color:#a2816c;--bx-warning-button-disabled-rgb:162,129,108;--bx-danger-button-color:#c10404;--bx-danger-button-rgb:193,4,4;--bx-danger-button-hover-color:#e61d1d;--bx-danger-button-hover-rgb:230,29,29;--bx-danger-button-active-color:#a26c6c;--bx-danger-button-active-rgb:162,108,108;--bx-danger-button-disabled-color:#df5656;--bx-danger-button-disabled-rgb:223,86,86;--bx-fullscreen-text-z-index:9999;--bx-toast-z-index:6000;--bx-key-binding-dialog-z-index:5010;--bx-key-binding-dialog-overlay-z-index:5000;--bx-stats-bar-z-index:4010;--bx-navigation-dialog-z-index:3010;--bx-navigation-dialog-overlay-z-index:3000;--bx-mkb-pointer-lock-msg-z-index:2000;--bx-game-bar-z-index:1000;--bx-screenshot-animation-z-index:200;--bx-wait-time-box-z-index:100}@font-face{font-family:\'promptfont\';src:url("https://redphx.github.io/better-xcloud/fonts/promptfont.otf");unicode-range:U+2196-E011}div[class^=HUDButton-module__hiddenContainer] ~ div:not([class^=HUDButton-module__hiddenContainer]){opacity:0;pointer-events:none !important;position:absolute;top:-9999px;left:-9999px}@media screen and (max-width:640px){header a[href="/play"]{display:none}}.bx-full-width{width:100% !important}.bx-full-height{height:100% !important}.bx-auto-height{height:auto !important}.bx-no-scroll{overflow:hidden !important}.bx-hide-scroll-bar{scrollbar-width:none}.bx-hide-scroll-bar::-webkit-scrollbar{display:none}.bx-gone{display:none !important}.bx-offscreen{position:absolute !important;top:-9999px !important;left:-9999px !important;visibility:hidden !important}.bx-hidden{visibility:hidden !important}.bx-invisible{opacity:0}.bx-unclickable{pointer-events:none}.bx-pixel{width:1px !important;height:1px !important}.bx-no-margin{margin:0 !important}.bx-no-padding{padding:0 !important}.bx-prompt{font-family:var(--bx-promptfont-font) !important}.bx-monospaced{font-family:var(--bx-monospaced-font) !important}.bx-line-through{text-decoration:line-through !important}.bx-normal-case{text-transform:none !important}.bx-normal-link{text-transform:none !important;text-align:left !important;font-weight:400 !important;font-family:var(--bx-normal-font) !important}select[multiple],select[multiple]:focus{overflow:auto;border:none}select[multiple] option,select[multiple]:focus option{padding:4px 6px}select[multiple] option:checked,select[multiple]:focus option:checked{background:#1a7bc0 linear-gradient(0deg,#1a7bc0 0%,#1a7bc0 100%)}select[multiple] option:checked::before,select[multiple]:focus option:checked::before{content:\'☑️\';font-size:12px;display:inline-block;margin-right:6px;height:100%;line-height:100%;vertical-align:middle}#headerArea,#uhfSkipToMain,.uhf-footer{display:none}div[class*=NotFocusedDialog]{position:absolute !important;top:-9999px !important;left:-9999px !important;width:0 !important;height:0 !important}#game-stream video:not([src]){visibility:hidden}div[class*=SupportedInputsBadge]:not(:has(:nth-child(2))),div[class*=SupportedInputsBadge] svg:first-of-type{display:none}.bx-game-tile-wait-time{position:absolute;top:0;left:0;z-index:1;background:rgba(0,0,0,0.5);display:flex;border-radius:4px 0 4px 0;align-items:center;padding:4px 8px}.bx-game-tile-wait-time svg{width:14px;height:16px;margin-right:2px}.bx-game-tile-wait-time span{display:inline-block;height:16px;line-height:16px;font-size:12px;font-weight:bold;margin-left:2px}.bx-game-tile-wait-time[data-duration=short]{background-color:rgba(0,133,133,0.75)}.bx-game-tile-wait-time[data-duration=medium]{background-color:rgba(213,133,0,0.75)}.bx-game-tile-wait-time[data-duration=long]{background-color:rgba(150,0,0,0.75)}.bx-fullscreen-text{position:fixed;top:0;bottom:0;left:0;right:0;background:rgba(0,0,0,0.8);z-index:var(--bx-fullscreen-text-z-index);line-height:100vh;color:#fff;text-align:center;font-weight:400;font-family:var(--bx-normal-font);font-size:1.3rem;user-select:none;-webkit-user-select:none}#root section[class*=DeviceCodePage-module__page]{margin-left:20px !important;margin-right:20px !important;margin-top:20px !important;max-width:800px !important}#root div[class*=DeviceCodePage-module__back]{display:none}.bx-blink-me{animation:bx-blinker 1s linear infinite}.bx-horizontal-shaking{animation:bx-horizontal-shaking .4s ease-in-out 2}@-moz-keyframes bx-blinker{100%{opacity:0}}@-webkit-keyframes bx-blinker{100%{opacity:0}}@-o-keyframes bx-blinker{100%{opacity:0}}@keyframes bx-blinker{100%{opacity:0}}@-moz-keyframes bx-horizontal-shaking{0%{transform:translateX(0)}25%{transform:translateX(5px)}50%{transform:translateX(-5px)}75%{transform:translateX(5px)}100%{transform:translateX(0)}}@-webkit-keyframes bx-horizontal-shaking{0%{transform:translateX(0)}25%{transform:translateX(5px)}50%{transform:translateX(-5px)}75%{transform:translateX(5px)}100%{transform:translateX(0)}}@-o-keyframes bx-horizontal-shaking{0%{transform:translateX(0)}25%{transform:translateX(5px)}50%{transform:translateX(-5px)}75%{transform:translateX(5px)}100%{transform:translateX(0)}}@keyframes bx-horizontal-shaking{0%{transform:translateX(0)}25%{transform:translateX(5px)}50%{transform:translateX(-5px)}75%{transform:translateX(5px)}100%{transform:translateX(0)}}.bx-button{--button-rgb:var(--bx-default-button-rgb);--button-hover-rgb:var(--bx-default-button-hover-rgb);--button-active-rgb:var(--bx-default-button-active-rgb);--button-disabled-rgb:var(--bx-default-button-disabled-rgb);background-color:rgb(var(--button-rgb));user-select:none;-webkit-user-select:none;color:#fff;font-family:var(--bx-title-font-semibold);font-size:14px;border:none;font-weight:400;height:var(--bx-button-height);border-radius:4px;padding:0 8px;text-transform:uppercase;cursor:pointer;overflow:hidden}.bx-button:not([disabled]):active{background-color:rgb(var(--button-active-rgb))}.bx-button:focus{outline:none !important}.bx-button:not([disabled]):not(:active):hover,.bx-button:not([disabled]):not(:active).bx-focusable:focus{background-color:rgb(var(--button-hover-rgb))}.bx-button:disabled{cursor:default;background-color:rgb(var(--button-disabled-rgb))}.bx-button.bx-ghost{background-color:transparent}.bx-button.bx-ghost:not([disabled]):not(:active):hover,.bx-button.bx-ghost:not([disabled]):not(:active).bx-focusable:focus{background-color:rgb(var(--button-hover-rgb))}.bx-button.bx-primary{--button-rgb:var(--bx-primary-button-rgb)}.bx-button.bx-primary:not([disabled]):active{--button-active-rgb:var(--bx-primary-button-active-rgb)}.bx-button.bx-primary:not([disabled]):not(:active):hover,.bx-button.bx-primary:not([disabled]):not(:active).bx-focusable:focus{--button-hover-rgb:var(--bx-primary-button-hover-rgb)}.bx-button.bx-primary:disabled{--button-disabled-rgb:var(--bx-primary-button-disabled-rgb)}.bx-button.bx-warning{--button-rgb:var(--bx-warning-button-rgb)}.bx-button.bx-warning:not([disabled]):active{--button-active-rgb:var(--bx-warning-button-active-rgb)}.bx-button.bx-warning:not([disabled]):not(:active):hover,.bx-button.bx-warning:not([disabled]):not(:active).bx-focusable:focus{--button-hover-rgb:var(--bx-warning-button-hover-rgb)}.bx-button.bx-warning:disabled{--button-disabled-rgb:var(--bx-warning-button-disabled-rgb)}.bx-button.bx-danger{--button-rgb:var(--bx-danger-button-rgb)}.bx-button.bx-danger:not([disabled]):active{--button-active-rgb:var(--bx-danger-button-active-rgb)}.bx-button.bx-danger:not([disabled]):not(:active):hover,.bx-button.bx-danger:not([disabled]):not(:active).bx-focusable:focus{--button-hover-rgb:var(--bx-danger-button-hover-rgb)}.bx-button.bx-danger:disabled{--button-disabled-rgb:var(--bx-danger-button-disabled-rgb)}.bx-button.bx-frosted{--button-alpha:.2;background-color:rgba(var(--button-rgb), var(--button-alpha));backdrop-filter:blur(4px) brightness(1.5)}.bx-button.bx-frosted:not([disabled]):not(:active):hover,.bx-button.bx-frosted:not([disabled]):not(:active).bx-focusable:focus{background-color:rgba(var(--button-hover-rgb), var(--button-alpha))}.bx-button.bx-drop-shadow{box-shadow:0 0 4px rgba(0,0,0,0.502)}.bx-button.bx-tall{height:calc(var(--bx-button-height) * 1.5) !important}.bx-button.bx-circular{border-radius:var(--bx-button-height);width:var(--bx-button-height);height:var(--bx-button-height)}.bx-button svg{display:inline-block;width:16px;height:var(--bx-button-height)}.bx-button span{display:inline-block;line-height:var(--bx-button-height);vertical-align:middle;color:#fff;overflow:hidden;white-space:nowrap}.bx-button span:not(:only-child){margin-left:10px}.bx-button.bx-button-multi-lines{height:auto;text-align:left;padding:10px 0}.bx-button.bx-button-multi-lines span{line-height:unset;display:block}.bx-button.bx-button-multi-lines span:last-of-type{text-transform:none;font-weight:normal;font-family:"Segoe Sans Variable Text";font-size:12px;margin-top:4px}.bx-focusable{position:relative;overflow:visible}.bx-focusable::after{border:2px solid transparent;border-radius:10px}.bx-focusable:focus::after{content:\'\';border-color:#fff;position:absolute;top:-6px;left:-6px;right:-6px;bottom:-6px}html[data-active-input=touch] .bx-focusable:focus::after,html[data-active-input=mouse] .bx-focusable:focus::after{border-color:transparent !important}.bx-focusable.bx-circular::after{border-radius:var(--bx-button-height)}a.bx-button{display:inline-block}a.bx-button.bx-full-width{text-align:center}button.bx-inactive{pointer-events:none;opacity:.2;background:transparent !important}.bx-header-remote-play-button{height:auto;margin-right:8px !important}.bx-header-remote-play-button svg{width:24px;height:24px}.bx-header-settings-button{line-height:30px;font-size:14px;text-transform:uppercase;position:relative}.bx-header-settings-button[data-update-available]::before{content:\'🌟\' !important;line-height:var(--bx-button-height);display:inline-block;margin-left:4px}.bx-key-binding-dialog-overlay{position:fixed;inset:0;z-index:var(--bx-key-binding-dialog-overlay-z-index);background:#000;opacity:50%}.bx-key-binding-dialog{display:flex;flex-flow:column;max-height:90vh;position:fixed;top:50%;left:50%;margin-right:-50%;transform:translate(-50%,-50%);min-width:420px;padding:16px;border-radius:8px;z-index:var(--bx-key-binding-dialog-z-index);background:#1a1b1e;color:#fff;font-weight:400;font-size:16px;font-family:var(--bx-normal-font);box-shadow:0 0 6px #000;user-select:none;-webkit-user-select:none}.bx-key-binding-dialog *:focus{outline:none !important}.bx-key-binding-dialog h2{margin-bottom:12px;color:#fff;display:block;font-family:var(--bx-title-font);font-size:32px;font-weight:400;line-height:var(--bx-button-height)}.bx-key-binding-dialog > div{overflow:auto;padding:2px 0}.bx-key-binding-dialog > button{padding:8px 32px;margin:10px auto 0;border:none;border-radius:4px;display:block;background-color:#2d3036;text-align:center;color:#fff;text-transform:uppercase;font-family:var(--bx-title-font);font-weight:400;line-height:18px;font-size:14px}@media (hover:hover){.bx-key-binding-dialog > button:hover{background-color:#515863}}.bx-key-binding-dialog > button:focus{background-color:#515863}.bx-key-binding-dialog ul{margin-bottom:1rem}.bx-key-binding-dialog ul li{display:none}.bx-key-binding-dialog ul[data-flags*="[1]"] > li[data-flag="1"],.bx-key-binding-dialog ul[data-flags*="[2]"] > li[data-flag="2"],.bx-key-binding-dialog ul[data-flags*="[4]"] > li[data-flag="4"],.bx-key-binding-dialog ul[data-flags*="[8]"] > li[data-flag="8"]{display:list-item}@media screen and (max-width:450px){.bx-key-binding-dialog{min-width:100%}}.bx-navigation-dialog{position:absolute;z-index:var(--bx-navigation-dialog-z-index);font-family:var(--bx-title-font)}.bx-navigation-dialog *:focus{outline:none !important}.bx-navigation-dialog select:disabled{-webkit-appearance:none;text-align-last:right;text-align:right;color:#fff;background:#131416;border:none;border-radius:4px;padding:0 5px}.bx-navigation-dialog .bx-focusable::after{border-radius:4px}.bx-navigation-dialog .bx-focusable:focus::after{top:0;left:0;right:0;bottom:0}.bx-navigation-dialog-overlay{position:fixed;background:rgba(11,11,11,0.89);top:0;left:0;right:0;bottom:0;z-index:var(--bx-navigation-dialog-overlay-z-index)}.bx-navigation-dialog-overlay[data-is-playing="true"]{background:transparent}.bx-centered-dialog{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);color:#fff;background:#1a1b1e;border-radius:10px;min-width:min(calc(100vw - 20px), 500px);max-width:calc(100vw - 20px);margin:0 0 0 auto;padding:16px;max-height:95vh;flex-direction:column;overflow:hidden;display:flex;flex-direction:column}.bx-centered-dialog .bx-dialog-title{display:flex;flex-direction:row;align-items:center;margin-bottom:10px}.bx-centered-dialog .bx-dialog-title p{padding:0;margin:0;flex:1;font-size:1.2rem;font-weight:bold}.bx-centered-dialog .bx-dialog-title button{flex-shrink:0}.bx-centered-dialog .bx-dialog-content{flex:1;padding:6px;overflow:auto;overflow-x:hidden}.bx-centered-dialog .bx-dialog-preset-tools{display:flex;margin-bottom:12px;gap:6px}.bx-centered-dialog .bx-dialog-preset-tools button{align-self:center;min-height:50px}.bx-centered-dialog .bx-default-preset-note{font-size:12px;font-style:italic;text-align:center;margin-bottom:10px}.bx-centered-dialog input,.bx-settings-dialog input{accent-color:var(--bx-primary-button-color)}.bx-centered-dialog input:focus,.bx-settings-dialog input:focus{accent-color:var(--bx-danger-button-color)}.bx-centered-dialog select:disabled,.bx-settings-dialog select:disabled{-webkit-appearance:none;background:transparent;text-align-last:right;border:none;color:#fff}.bx-centered-dialog select option:disabled,.bx-settings-dialog select option:disabled{display:none}.bx-centered-dialog input[type=checkbox]:focus,.bx-settings-dialog input[type=checkbox]:focus,.bx-centered-dialog select:focus,.bx-settings-dialog select:focus{filter:drop-shadow(1px 0 0 #fff) drop-shadow(-1px 0 0 #fff) drop-shadow(0 1px 0 #fff) drop-shadow(0 -1px 0 #fff)}.bx-centered-dialog a,.bx-settings-dialog a{color:#1c9d1c;text-decoration:none}.bx-centered-dialog a:hover,.bx-settings-dialog a:hover,.bx-centered-dialog a:focus,.bx-settings-dialog a:focus{color:#5dc21e}.bx-centered-dialog label,.bx-settings-dialog label{margin:0}.bx-controller-shortcuts-manager-container .bx-shortcut-note{margin-top:10px;font-size:14px;text-align:center}.bx-controller-shortcuts-manager-container .bx-shortcut-row{display:flex;gap:10px;margin-bottom:10px;align-items:center}.bx-controller-shortcuts-manager-container .bx-shortcut-row label.bx-prompt{flex-shrink:0;font-size:32px;margin:0}.bx-controller-shortcuts-manager-container .bx-shortcut-row label.bx-prompt::first-letter{letter-spacing:6px}.bx-controller-shortcuts-manager-container select:disabled{text-align:left;text-align-last:left}.bx-keyboard-shortcuts-manager-container{display:flex;flex-direction:column;gap:16px}.bx-keyboard-shortcuts-manager-container fieldset{background:#2a2a2a;border:1px solid #2a2a2a;border-radius:4px;padding:4px}.bx-keyboard-shortcuts-manager-container legend{width:auto;padding:4px 8px;margin:0 4px 4px;background:#004f87;box-shadow:0 2px 0 #071e3d;border-radius:4px;font-size:14px;font-weight:bold;text-transform:uppercase}.bx-keyboard-shortcuts-manager-container .bx-settings-row{background:none;padding:10px}.bx-settings-dialog{display:flex;position:fixed;top:0;right:0;bottom:0;opacity:.98;user-select:none;-webkit-user-select:none}.bx-settings-dialog .bx-settings-reload-note{font-size:.8rem;display:block;padding:8px;font-style:italic;font-weight:normal;height:var(--bx-button-height)}.bx-settings-tabs-container{position:fixed;width:48px;max-height:100vh;display:flex;flex-direction:column}.bx-settings-tabs-container > div:last-of-type{display:flex;flex-direction:column;align-items:end}.bx-settings-tabs-container > div:last-of-type button{flex-shrink:0;border-top-right-radius:0;border-bottom-right-radius:0;margin-top:8px;height:unset;padding:8px 10px}.bx-settings-tabs-container > div:last-of-type button svg{width:16px;height:16px}.bx-settings-tabs{display:flex;flex-direction:column;border-radius:0 0 0 8px;box-shadow:0 0 6px #000;overflow:overlay;flex:1}.bx-settings-tabs svg{width:24px;height:24px;padding:10px;flex-shrink:0;box-sizing:content-box;background:#131313;cursor:pointer;border-left:4px solid #1e1e1e}.bx-settings-tabs svg.bx-active{background:#222;border-color:#008746}.bx-settings-tabs svg:not(.bx-active):hover{background:#2f2f2f;border-color:#484848}.bx-settings-tabs svg:focus{border-color:#fff}.bx-settings-tabs svg[data-group=global][data-need-refresh=true]{background:var(--bx-danger-button-color) !important}.bx-settings-tabs svg[data-group=global][data-need-refresh=true]:hover{background:var(--bx-danger-button-hover-color) !important}.bx-settings-tab-contents{flex-direction:column;padding:10px;margin-left:48px;width:450px;max-width:calc(100vw - tabsWidth);background:#1a1b1e;color:#fff;font-weight:400;font-size:16px;font-family:var(--bx-title-font);text-align:center;box-shadow:0 0 6px #000;overflow:overlay;z-index:1}.bx-settings-tab-contents > div[data-tab-group=mkb]{display:flex;flex-direction:column;height:100%;overflow:hidden}.bx-settings-tab-contents .bx-top-buttons{display:flex;flex-direction:column;gap:8px;margin-bottom:8px}.bx-settings-tab-contents .bx-top-buttons .bx-button{display:block}.bx-settings-tab-contents h2{margin:16px 0 8px 0;display:flex;align-items:center}.bx-settings-tab-contents h2:first-of-type{margin-top:0}.bx-settings-tab-contents h2 span{display:inline-block;font-size:20px;font-weight:bold;text-align:left;flex:1;text-overflow:ellipsis;overflow:hidden;white-space:nowrap;min-height:var(--bx-button-height);align-content:center}@media (max-width:500px){.bx-settings-tab-contents{width:calc(100vw - 48px)}}.bx-settings-row{display:flex;gap:10px;padding:16px 10px;margin:0;background:#2a2a2a;border-bottom:1px solid #343434}.bx-settings-row:hover,.bx-settings-row:focus-within{background-color:#242424}.bx-settings-row:not(:has(> input[type=checkbox])){flex-wrap:wrap}.bx-settings-row > span.bx-settings-label{font-size:14px;display:block;text-align:left;align-self:center;margin-bottom:0 !important;flex:1}.bx-settings-row > span.bx-settings-label + *{margin:0 0 0 auto}.bx-settings-row[data-multi-lines="true"]{flex-direction:column}.bx-settings-row[data-multi-lines="true"] > span.bx-settings-label{align-self:start}.bx-settings-row[data-multi-lines="true"] > span.bx-settings-label + *{margin:unset}.bx-settings-dialog-note{display:block;color:#afafb0;font-size:12px;font-weight:lighter;font-style:italic}.bx-settings-dialog-note:not(:has(a)){margin-top:4px}.bx-settings-dialog-note a{display:inline-block;padding:4px}.bx-settings-custom-user-agent{display:block;width:100%;padding:6px}.bx-donation-link{display:block;text-align:center;text-decoration:none;height:20px;line-height:20px;font-size:14px;margin-top:10px;margin-bottom:10px}.bx-debug-info button{margin-top:10px}.bx-debug-info pre{margin-top:10px;cursor:copy;color:#fff;padding:8px;border:1px solid #2d2d2d;background:#212121;white-space:break-spaces;text-align:left}.bx-debug-info pre:hover{background:#272727}.bx-settings-app-version{margin-top:10px;text-align:center;color:#747474;font-size:12px}.bx-note-unsupported{display:block;font-size:12px;font-style:italic;font-weight:normal;color:#828282}.bx-settings-tab-contents > div *:not(.bx-settings-row):has(+ .bx-settings-row) + .bx-settings-row:has(+ .bx-settings-row){border-top-left-radius:6px;border-top-right-radius:6px}.bx-settings-tab-contents > div .bx-settings-row:not(:has(+ .bx-settings-row)){border:none;border-bottom-left-radius:6px;border-bottom-right-radius:6px}.bx-settings-tab-contents > div *:not(.bx-settings-row):has(+ .bx-settings-row) + .bx-settings-row:not(:has(+ .bx-settings-row)){border:none;border-radius:6px}.bx-suggest-toggler{text-align:left;display:flex;border-radius:4px;overflow:hidden;background:#003861;height:45px;align-items:center}.bx-suggest-toggler label{flex:1;align-content:center;padding:0 10px;background:#004f87;height:100%}.bx-suggest-toggler span{display:inline-block;align-self:center;padding:10px;width:45px;text-align:center}.bx-suggest-toggler:hover,.bx-suggest-toggler:focus{cursor:pointer;background:#005da1}.bx-suggest-toggler:hover label,.bx-suggest-toggler:focus label{background:#006fbe}.bx-suggest-toggler[bx-open] span{transform:rotate(90deg)}.bx-suggest-toggler[bx-open]+ .bx-suggest-box{display:block}.bx-suggest-box{display:none}.bx-suggest-wrapper{display:flex;flex-direction:column;gap:10px;margin:10px}.bx-suggest-note{font-size:11px;color:#8c8c8c;font-style:italic;font-weight:100}.bx-suggest-link{font-size:14px;display:inline-block;margin-top:4px;padding:4px}.bx-suggest-row{display:flex;flex-direction:row;gap:10px}.bx-suggest-row label{flex:1;overflow:overlay;border-radius:4px}.bx-suggest-row label .bx-suggest-label{background:#323232;padding:4px 10px;font-size:12px;text-align:left}.bx-suggest-row label .bx-suggest-value{padding:6px;font-size:14px}.bx-suggest-row label .bx-suggest-value.bx-suggest-change{background-color:var(--bx-warning-color)}.bx-suggest-row.bx-suggest-ok input{visibility:hidden}.bx-suggest-row.bx-suggest-ok .bx-suggest-label{background-color:#008114}.bx-suggest-row.bx-suggest-ok .bx-suggest-value{background-color:#13a72a}.bx-suggest-row.bx-suggest-change .bx-suggest-label{background-color:#a65e08}.bx-suggest-row.bx-suggest-change .bx-suggest-value{background-color:#d57f18}.bx-suggest-row.bx-suggest-change:hover label{cursor:pointer}.bx-suggest-row.bx-suggest-change:hover .bx-suggest-label{background-color:#995707}.bx-suggest-row.bx-suggest-change:hover .bx-suggest-value{background-color:#bd7115}.bx-suggest-row.bx-suggest-change input:not(:checked) + label{opacity:.5}.bx-suggest-row.bx-suggest-change input:not(:checked) + label .bx-suggest-label{background-color:#2a2a2a}.bx-suggest-row.bx-suggest-change input:not(:checked) + label .bx-suggest-value{background-color:#393939}.bx-suggest-row.bx-suggest-change:hover input:not(:checked) + label{opacity:1}.bx-suggest-row.bx-suggest-change:hover input:not(:checked) + label .bx-suggest-label{background-color:#202020}.bx-suggest-row.bx-suggest-change:hover input:not(:checked) + label .bx-suggest-value{background-color:#303030}.bx-sub-content-box{background:#161616;padding:10px;box-shadow:0 0 12px #0f0f0f inset;border-radius:10px}.bx-settings-row .bx-sub-content-box{background:#202020;padding:12px;box-shadow:0 0 4px #000 inset;border-radius:6px}.bx-controller-extra-settings[data-has-gamepad=true] > :first-child{display:none}.bx-controller-extra-settings[data-has-gamepad=true] > :last-child{display:block}.bx-controller-extra-settings[data-has-gamepad=false] > :first-child{display:block}.bx-controller-extra-settings[data-has-gamepad=false] > :last-child{display:none}.bx-controller-extra-settings .bx-controller-extra-wrapper{flex:1;min-width:1px}.bx-controller-extra-settings .bx-sub-content-box{flex:1;text-align:left;display:flex;flex-direction:column;margin-top:10px}.bx-controller-extra-settings .bx-sub-content-box > label{font-size:14px}.bx-preset-row{display:flex;gap:8px}.bx-preset-row .bx-select{flex:1}.bx-toast{user-select:none;-webkit-user-select:none;position:fixed;left:50%;top:24px;transform:translate(-50%,0);background:#000;border-radius:16px;color:#fff;z-index:var(--bx-toast-z-index);font-family:var(--bx-normal-font);border:2px solid #fff;display:flex;align-items:center;opacity:0;overflow:clip;transition:opacity .2s ease-in}.bx-toast.bx-show{opacity:.85}.bx-toast.bx-hide{opacity:0;pointer-events:none}.bx-toast-msg{font-size:14px;display:inline-block;padding:12px 16px;white-space:pre}.bx-toast-status{font-weight:bold;font-size:14px;text-transform:uppercase;display:inline-block;background:#515863;padding:12px 16px;color:#fff;white-space:pre}.bx-wait-time-box{position:fixed;top:0;right:0;background-color:rgba(0,0,0,0.8);color:#fff;z-index:var(--bx-wait-time-box-z-index);padding:12px;border-radius:0 0 0 8px}.bx-wait-time-box label{display:block;text-transform:uppercase;text-align:right;font-size:12px;font-weight:bold;margin:0}.bx-wait-time-box span{display:block;font-family:var(--bx-monospaced-font);text-align:right;font-size:16px;margin-bottom:10px}.bx-wait-time-box span:last-of-type{margin-bottom:0}.bx-remote-play-container{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);color:#fff;background:#1a1b1e;border-radius:10px;width:420px;max-width:calc(100vw - 20px);margin:0 0 0 auto;padding:16px}.bx-remote-play-container > .bx-button{display:table;margin:0 0 0 auto}.bx-remote-play-settings{margin-bottom:12px;padding-bottom:12px;border-bottom:1px solid #2d2d2d}.bx-remote-play-settings > div{display:flex}.bx-remote-play-settings label{flex:1}.bx-remote-play-settings label p{margin:4px 0 0;padding:0;color:#888;font-size:12px}.bx-remote-play-resolution{display:block}.bx-remote-play-resolution input[type="radio"]{accent-color:var(--bx-primary-button-color);margin-right:6px}.bx-remote-play-resolution input[type="radio"]:focus{accent-color:var(--bx-primary-button-hover-color)}.bx-remote-play-device-wrapper{display:flex;margin-bottom:12px}.bx-remote-play-device-wrapper:last-child{margin-bottom:2px}.bx-remote-play-device-info{flex:1;padding:4px 0}.bx-remote-play-device-name{font-size:20px;font-weight:bold;display:inline-block;vertical-align:middle}.bx-remote-play-console-type{font-size:12px;background:#004c87;color:#fff;display:inline-block;border-radius:14px;padding:2px 10px;margin-left:8px;vertical-align:middle}.bx-remote-play-power-state{color:#888;font-size:12px}.bx-remote-play-connect-button{min-height:100%;margin:4px 0}.bx-remote-play-buttons{display:flex;justify-content:space-between}select.bx-select{min-height:30px}div.bx-select{display:flex;align-items:stretch;flex:0 1 auto;gap:8px}div.bx-select select:disabled ~ button{display:none}div.bx-select select:disabled ~ div{background:#131416;color:#fff;pointer-events:none}div.bx-select select:disabled ~ div .bx-select-indicators{visibility:hidden}div.bx-select > div,div.bx-select button.bx-select-value{min-width:120px;text-align:left;line-height:24px;vertical-align:middle;background:#fff;color:#000;border-radius:4px;padding:2px 8px;display:flex;flex:1;flex-direction:column}div.bx-select > div{min-height:24px}div.bx-select > div input{display:inline-block;margin-right:8px}div.bx-select > div label{margin-bottom:0;font-size:14px;width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;min-height:15px}div.bx-select > div label span{display:block;font-size:10px;font-weight:bold;text-align:left;line-height:20px;white-space:pre;min-height:15px;align-content:center}div.bx-select button.bx-select-value{border:none;cursor:pointer;min-height:30px;font-size:.9rem;align-items:center}div.bx-select button.bx-select-value > div{display:flex;width:100%}div.bx-select button.bx-select-value span{flex:1;text-align:left;display:inline-block}div.bx-select button.bx-select-value input{margin:0 4px;accent-color:var(--bx-primary-button-color);pointer-events:none}div.bx-select button.bx-select-value:hover input,div.bx-select button.bx-select-value:focus input{accent-color:var(--bx-danger-button-color)}div.bx-select button.bx-select-value:hover::after,div.bx-select button.bx-select-value:focus::after{border-color:#4d4d4d !important}div.bx-select button.bx-button{border:none;width:24px;height:auto;padding:0;color:#fff;border-radius:4px;font-weight:bold;font-size:12px;font-family:var(--bx-monospaced-font);flex-shrink:0}div.bx-select button.bx-button span{line-height:unset}div.bx-select[data-controller-friendly=true] > div{box-sizing:content-box}div.bx-select[data-controller-friendly=true] select{position:absolute !important;top:-9999px !important;left:-9999px !important;visibility:hidden !important}div.bx-select[data-controller-friendly=false]{position:relative}div.bx-select[data-controller-friendly=false] > div{box-sizing:border-box}div.bx-select[data-controller-friendly=false] > div label{margin-right:24px}div.bx-select[data-controller-friendly=false] select:disabled{display:none}div.bx-select[data-controller-friendly=false] select:not(:disabled){cursor:pointer;position:absolute;top:0;right:0;bottom:0;display:block;opacity:0;z-index:calc(var(--bx-settings-z-index) + 1)}div.bx-select[data-controller-friendly=false] select:not(:disabled):hover + div{background:#f0f0f0}div.bx-select[data-controller-friendly=false] select:not(:disabled) + div label::after{content:\'▾\';font-size:14px;position:absolute;right:8px;pointer-events:none}.bx-select-indicators{display:flex;height:4px;gap:2px;margin-bottom:2px}.bx-select-indicators span{content:\' \';display:inline-block;flex:1;background:#cfcfcf;border-radius:4px;min-width:1px}.bx-select-indicators span[data-highlighted]{background:#9c9c9c;min-width:6px}.bx-select-indicators span[data-selected]{background:#aacfe7}.bx-select-indicators span[data-highlighted][data-selected]{background:#5fa3d0}.bx-guide-home-achievements-progress{display:flex;gap:10px;flex-direction:row}.bx-guide-home-achievements-progress .bx-button{margin-bottom:0 !important}body[data-bx-media-type=tv] .bx-guide-home-achievements-progress{flex-direction:column}body:not([data-bx-media-type=tv]) .bx-guide-home-achievements-progress{flex-direction:row}body:not([data-bx-media-type=tv]) .bx-guide-home-achievements-progress > button:first-of-type{flex:1}body:not([data-bx-media-type=tv]) .bx-guide-home-achievements-progress > button:last-of-type{width:40px}body:not([data-bx-media-type=tv]) .bx-guide-home-achievements-progress > button:last-of-type span{display:none}.bx-guide-home-buttons > div{display:flex;flex-direction:row;gap:12px}body[data-bx-media-type=tv] .bx-guide-home-buttons > div{flex-direction:column}body[data-bx-media-type=tv] .bx-guide-home-buttons > div button{margin-bottom:0 !important}body:not([data-bx-media-type=tv]) .bx-guide-home-buttons > div button span{display:none}.bx-guide-home-buttons[data-is-playing="true"] button[data-state=\'normal\']{display:none}.bx-guide-home-buttons[data-is-playing="false"] button[data-state=\'playing\']{display:none}div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module]{overflow:visible}.bx-stream-menu-button-on{fill:#000 !important;background-color:#2d2d2d !important;color:#000 !important}.bx-stream-refresh-button{top:calc(env(safe-area-inset-top, 0px) + 10px + 50px) !important}body[data-media-type=default] .bx-stream-refresh-button{left:calc(env(safe-area-inset-left, 0px) + 11px) !important}body[data-media-type=tv] .bx-stream-refresh-button{top:calc(var(--gds-focus-borderSize) + 80px) !important}.bx-stream-home-button{top:calc(env(safe-area-inset-top, 0px) + 10px + 50px * 2) !important}body[data-media-type=default] .bx-stream-home-button{left:calc(env(safe-area-inset-left, 0px) + 12px) !important}body[data-media-type=tv] .bx-stream-home-button{top:calc(var(--gds-focus-borderSize) + 80px * 2) !important}div[data-testid=media-container][data-position=center]{display:flex}div[data-testid=media-container][data-position=top] video,div[data-testid=media-container][data-position=top] canvas{top:0}div[data-testid=media-container][data-position=bottom] video,div[data-testid=media-container][data-position=bottom] canvas{bottom:0}#game-stream video{margin:auto;align-self:center;background:#000;position:absolute;left:0;right:0}#game-stream canvas{align-self:center;margin:auto;position:absolute;left:0;right:0}#game-stream.bx-taking-screenshot:before{animation:bx-anim-taking-screenshot .5s ease;content:\' \';position:absolute;width:100%;height:100%;z-index:var(--bx-screenshot-animation-z-index)}#gamepass-dialog-root div[class^=Guide-module__guide] .bx-button{overflow:visible;margin-bottom:12px}@-moz-keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}@-webkit-keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}@-o-keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}@keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}.bx-number-stepper{text-align:center}.bx-number-stepper > div{display:flex;align-items:center}.bx-number-stepper > div span{flex:1;display:inline-block;min-width:40px;font-family:var(--bx-monospaced-font);white-space:pre;font-size:13px;margin:0 4px}.bx-number-stepper > div button{flex-shrink:0;border:none;width:24px;height:24px;margin:0;line-height:24px;background-color:var(--bx-default-button-color);color:#fff;border-radius:4px;font-weight:bold;font-size:14px;font-family:var(--bx-monospaced-font)}@media (hover:hover){.bx-number-stepper > div button:hover{background-color:var(--bx-default-button-hover-color)}}.bx-number-stepper > div button:active{background-color:var(--bx-default-button-hover-color)}.bx-number-stepper > div button:disabled + span{font-family:var(--bx-title-font)}.bx-number-stepper input[type=range]{display:block;margin:8px 0 2px auto;min-width:180px;width:100%;color:#959595 !important}.bx-number-stepper input[type=range]:disabled,.bx-number-stepper button:disabled{display:none}.bx-number-stepper[data-disabled=true] input[type=range],.bx-number-stepper[disabled=true] input[type=range],.bx-number-stepper[data-disabled=true] button,.bx-number-stepper[disabled=true] button{display:none}.bx-dual-number-stepper > span{display:block;font-family:var(--bx-monospaced-font);font-size:13px;white-space:pre;margin:0 4px;text-align:center}.bx-dual-number-stepper > div input[type=range]{display:block;width:100%;min-width:180px;background:transparent;color:#959595 !important;appearance:none;padding:8px 0}.bx-dual-number-stepper > div input[type=range]::-webkit-slider-runnable-track{background:linear-gradient(90deg,#fff var(--from),var(--bx-primary-button-color) var(--from) var(--to),#fff var(--to) 100%);height:8px;border-radius:2px}.bx-dual-number-stepper > div input[type=range]::-moz-range-track{background:linear-gradient(90deg,#fff var(--from),var(--bx-primary-button-color) var(--from) var(--to),#fff var(--to) 100%);height:8px;border-radius:2px}.bx-dual-number-stepper > div input[type=range]::-webkit-slider-thumb{margin-top:-4px;appearance:none;width:4px;height:16px;background:#00b85f;border:none;border-radius:2px}.bx-dual-number-stepper > div input[type=range]::-moz-range-thumb{margin-top:-4px;appearance:none;width:4px;height:16px;background:#00b85f;border:none;border-radius:2px}.bx-dual-number-stepper > div input[type=range]:hover::-webkit-slider-runnable-track,.bx-dual-number-stepper > div input[type=range].bx-dual-number-stepper > div input[type=range]:active::-webkit-slider-runnable-track,.bx-dual-number-stepper > div input[type=range]:focus::-webkit-slider-runnable-track{background:linear-gradient(90deg,#fff var(--from),#006635 var(--from) var(--to),#fff var(--to) 100%)}.bx-dual-number-stepper > div input[type=range]:hover::-moz-range-track,.bx-dual-number-stepper > div input[type=range].bx-dual-number-stepper > div input[type=range]:active::-moz-range-track,.bx-dual-number-stepper > div input[type=range]:focus::-moz-range-track{background:linear-gradient(90deg,#fff var(--from),#006635 var(--from) var(--to),#fff var(--to) 100%)}.bx-dual-number-stepper > div input[type=range]:hover::-webkit-slider-thumb,.bx-dual-number-stepper > div input[type=range].bx-dual-number-stepper > div input[type=range]:active::-webkit-slider-thumb,.bx-dual-number-stepper > div input[type=range]:focus::-webkit-slider-thumb{background:#fb3232}.bx-dual-number-stepper > div input[type=range]:hover::-moz-range-thumb,.bx-dual-number-stepper > div input[type=range].bx-dual-number-stepper > div input[type=range]:active::-moz-range-thumb,.bx-dual-number-stepper > div input[type=range]:focus::-moz-range-thumb{background:#fb3232}.bx-dual-number-stepper[data-disabled=true] input[type=range],.bx-dual-number-stepper[disabled=true] input[type=range]{display:none}#bx-game-bar{z-index:var(--bx-game-bar-z-index);position:fixed;bottom:0;width:40px;height:90px;overflow:visible;cursor:pointer}#bx-game-bar > svg{display:none;pointer-events:none;position:absolute;height:28px;margin-top:16px}@media (hover:hover){#bx-game-bar:hover > svg{display:block}}#bx-game-bar .bx-game-bar-container{opacity:0;position:absolute;display:flex;overflow:hidden;background:rgba(26,27,30,0.91);box-shadow:0 0 6px #1c1c1c;transition:opacity .1s ease-in}#bx-game-bar .bx-game-bar-container.bx-show{opacity:.9}#bx-game-bar .bx-game-bar-container.bx-show + svg{display:none !important}#bx-game-bar .bx-game-bar-container.bx-hide{opacity:0;pointer-events:none}#bx-game-bar .bx-game-bar-container button{width:60px;height:60px;border-radius:0}#bx-game-bar .bx-game-bar-container button svg{width:28px;height:28px;transition:transform .08s ease 0s}#bx-game-bar .bx-game-bar-container button:hover{border-radius:0}#bx-game-bar .bx-game-bar-container button:active svg{transform:scale(.75)}#bx-game-bar .bx-game-bar-container button.bx-activated{background-color:#fff}#bx-game-bar .bx-game-bar-container button.bx-activated svg{filter:invert(1)}#bx-game-bar .bx-game-bar-container div[data-activated] button{display:none}#bx-game-bar .bx-game-bar-container div[data-activated=\'false\'] button:first-of-type{display:block}#bx-game-bar .bx-game-bar-container div[data-activated=\'true\'] button:last-of-type{display:block}#bx-game-bar[data-position="bottom-left"]{left:0;direction:ltr}#bx-game-bar[data-position="bottom-left"] .bx-game-bar-container{border-radius:0 10px 10px 0}#bx-game-bar[data-position="bottom-right"]{right:0;direction:rtl}#bx-game-bar[data-position="bottom-right"] .bx-game-bar-container{direction:ltr;border-radius:10px 0 0 10px}.bx-badges{margin-left:0;user-select:none;-webkit-user-select:none}.bx-badge{border:none;display:inline-block;line-height:24px;color:#fff;font-family:var(--bx-title-font-semibold);font-size:14px;font-weight:400;margin:0 8px 8px 0;box-shadow:0 0 6px #000;border-radius:4px}.bx-badge-name{background-color:#2d3036;border-radius:4px 0 0 4px}.bx-badge-name svg{width:16px;height:16px}.bx-badge-value{background-color:#808080;border-radius:0 4px 4px 0}.bx-badge-name,.bx-badge-value{display:inline-block;padding:0 8px;line-height:30px;vertical-align:bottom}.bx-badge-battery[data-charging=true] span:first-of-type::after{content:\' ⚡️\'}div[class^=StreamMenu-module__container] .bx-badges{position:absolute;max-width:500px}#gamepass-dialog-root .bx-badges{position:fixed;top:60px;left:460px;max-width:500px}@media (min-width:568px) and (max-height:480px){#gamepass-dialog-root .bx-badges{position:unset;top:unset;left:unset;margin:8px 0}}.bx-stats-bar{display:flex;flex-direction:row;gap:8px;user-select:none;-webkit-user-select:none;position:fixed;top:0;background-color:#000;color:#fff;font-family:var(--bx-monospaced-font);font-size:.9rem;padding-left:8px;z-index:var(--bx-stats-bar-z-index);text-wrap:nowrap}.bx-stats-bar[data-stats*="[time]"] > .bx-stat-time,.bx-stats-bar[data-stats*="[play]"] > .bx-stat-play,.bx-stats-bar[data-stats*="[batt]"] > .bx-stat-batt,.bx-stats-bar[data-stats*="[fps]"] > .bx-stat-fps,.bx-stats-bar[data-stats*="[ping]"] > .bx-stat-ping,.bx-stats-bar[data-stats*="[jit]"] > .bx-stat-jit,.bx-stats-bar[data-stats*="[btr]"] > .bx-stat-btr,.bx-stats-bar[data-stats*="[dt]"] > .bx-stat-dt,.bx-stats-bar[data-stats*="[pl]"] > .bx-stat-pl,.bx-stats-bar[data-stats*="[fl]"] > .bx-stat-fl,.bx-stats-bar[data-stats*="[dl]"] > .bx-stat-dl,.bx-stats-bar[data-stats*="[ul]"] > .bx-stat-ul{display:inline-flex;align-items:baseline}.bx-stats-bar[data-stats$="[time]"] > .bx-stat-time,.bx-stats-bar[data-stats$="[play]"] > .bx-stat-play,.bx-stats-bar[data-stats$="[batt]"] > .bx-stat-batt,.bx-stats-bar[data-stats$="[fps]"] > .bx-stat-fps,.bx-stats-bar[data-stats$="[ping]"] > .bx-stat-ping,.bx-stats-bar[data-stats$="[jit]"] > .bx-stat-jit,.bx-stats-bar[data-stats$="[btr]"] > .bx-stat-btr,.bx-stats-bar[data-stats$="[dt]"] > .bx-stat-dt,.bx-stats-bar[data-stats$="[pl]"] > .bx-stat-pl,.bx-stats-bar[data-stats$="[fl]"] > .bx-stat-fl,.bx-stats-bar[data-stats$="[dl]"] > .bx-stat-dl,.bx-stats-bar[data-stats$="[ul]"] > .bx-stat-ul{border-right:none}.bx-stats-bar::before{display:none;content:\'👀\';vertical-align:middle;margin-right:8px}.bx-stats-bar[data-display=glancing]::before{display:inline-block}.bx-stats-bar[data-position=top-left]{left:0;border-radius:0 0 4px 0}.bx-stats-bar[data-position=top-right]{right:0;border-radius:0 0 0 4px}.bx-stats-bar[data-position=top-center]{transform:translate(-50%,0);left:50%;border-radius:0 0 4px 4px}.bx-stats-bar[data-shadow=true]{background:none;filter:drop-shadow(1px 0 0 rgba(0,0,0,0.941)) drop-shadow(-1px 0 0 rgba(0,0,0,0.941)) drop-shadow(0 1px 0 rgba(0,0,0,0.941)) drop-shadow(0 -1px 0 rgba(0,0,0,0.941))}.bx-stats-bar > div{display:none;border-right:1px solid #fff;padding-right:8px}.bx-stats-bar label{margin:0 8px 0 0;font-family:var(--bx-title-font);font-size:70%;font-weight:bold;vertical-align:middle;cursor:help}.bx-stats-bar span{display:inline-block;text-align:right;vertical-align:middle;white-space:pre}.bx-stats-bar span[data-grade=good]{color:#6bffff}.bx-stats-bar span[data-grade=ok]{color:#fff16b}.bx-stats-bar span[data-grade=bad]{color:#ff5f5f}.bx-mkb-settings{display:flex;flex-direction:column;flex:1;padding-bottom:10px;overflow:hidden}.bx-mkb-pointer-lock-msg{user-select:none;-webkit-user-select:none;position:fixed;left:50%;bottom:40px;transform:translateX(-50%);margin:auto;background:#151515;z-index:var(--bx-mkb-pointer-lock-msg-z-index);color:#fff;font-weight:400;font-family:"Segoe UI",Arial,Helvetica,sans-serif;font-size:1.3rem;padding:12px;border-radius:8px;align-items:center;box-shadow:0 0 6px #000;min-width:300px;opacity:.9;display:flex;flex-direction:column;gap:10px}.bx-mkb-pointer-lock-msg:hover{opacity:1}.bx-mkb-pointer-lock-msg > p{margin:0;width:100%;font-size:22px;margin-bottom:4px;font-weight:bold;text-align:left}.bx-mkb-pointer-lock-msg > div{width:100%;display:flex;flex-direction:row;gap:10px}.bx-mkb-pointer-lock-msg > div button:first-of-type{flex-shrink:1}.bx-mkb-pointer-lock-msg > div button:last-of-type{flex-grow:1}.bx-mkb-key-row{display:flex;margin-bottom:10px;align-items:center;gap:20px}.bx-mkb-key-row label{margin-bottom:0;font-family:var(--bx-promptfont-font);font-size:32px;text-align:center}.bx-mkb-settings.bx-editing .bx-mkb-key-row button{background:#393939;border-radius:4px;border:none}.bx-mkb-settings.bx-editing .bx-mkb-key-row button:hover{background:#333;cursor:pointer}.bx-mkb-action-buttons > div{text-align:right;display:none}.bx-mkb-action-buttons button{margin-left:8px}.bx-mkb-settings:not(.bx-editing) .bx-mkb-action-buttons > div:first-child{display:block}.bx-mkb-settings.bx-editing .bx-mkb-action-buttons > div:last-child{display:block}.bx-mkb-note{display:block;margin:0 0 10px;font-size:12px;text-align:center}button.bx-binding-button{flex:1;min-height:38px;border:none;border-radius:4px;font-size:14px;color:#fff;display:flex;align-items:center;align-self:center;padding:0 6px}button.bx-binding-button:disabled{background:#131416;padding:0 8px}button.bx-binding-button:not(:disabled){border:2px solid transparent;border-top:none;border-bottom:4px solid #252525;background:#3b3b3b;cursor:pointer}button.bx-binding-button:not(:disabled):hover,button.bx-binding-button:not(:disabled).bx-focusable:focus{background:#20b217;border-bottom-color:#186c13}button.bx-binding-button:not(:disabled):active{background:#16900f;border-bottom:3px solid #0c4e08;border-left-width:2px;border-right-width:2px}button.bx-binding-button:not(:disabled).bx-focusable:focus::after{top:-6px;left:-8px;right:-8px;bottom:-10px}.bx-settings-row .bx-binding-button-wrapper button.bx-binding-button{min-width:60px}.bx-controller-customizations-container .bx-btn-detect{display:block;margin-bottom:20px}.bx-controller-customizations-container .bx-btn-detect.bx-monospaced{background:none;font-weight:bold;font-size:12px}.bx-controller-customizations-container .bx-buttons-grid{display:grid;grid-template-columns:auto auto;column-gap:20px;row-gap:10px;margin-bottom:20px}.bx-controller-key-row{display:flex;align-items:stretch}.bx-controller-key-row > label{margin-bottom:0;font-family:var(--bx-promptfont-font);font-size:32px;text-align:center;min-width:50px;flex-shrink:0;display:flex;align-self:center}.bx-controller-key-row > label::after{content:\'❯\';margin:0 12px;font-size:16px;align-self:center}.bx-controller-key-row .bx-select{width:100% !important}.bx-controller-key-row .bx-select > div{min-width:50px}.bx-controller-key-row .bx-select label{font-family:var(--bx-promptfont-font),var(--bx-normal-font);font-size:32px;text-align:center;margin-bottom:6px;height:40px;line-height:40px}.bx-controller-key-row:hover > label{color:#ffe64b}.bx-controller-key-row:hover > label::after{color:#fff}.bx-product-details-buttons{display:flex;gap:10px;flex-direction:row}.bx-product-details-buttons button{max-width:max-content;margin:10px 0 0 0;display:flex}@media (min-width:568px) and (max-height:480px){.bx-product-details-buttons{flex-direction:column}.bx-product-details-buttons button{margin:8px 0 0 10px}}', PREF_HIDE_SECTIONS = getPref("ui.hideSections"), selectorToHide = []; if (PREF_HIDE_SECTIONS.includes("news")) selectorToHide.push("#BodyContent > div[class*=CarouselRow-module]"); if (getPref("block.features").includes("byog")) selectorToHide.push("#BodyContent > div[class*=ByogRow-module__container___]"); if (PREF_HIDE_SECTIONS.includes("all-games")) selectorToHide.push("#BodyContent div[class*=AllGamesRow-module__gridContainer]"), selectorToHide.push("#BodyContent div[class*=AllGamesRow-module__rowHeader]"); @@ -6396,7 +6499,7 @@ class RootDialogObserver { static $btnShortcut = AppInterface && createButton({ icon: BxIcon.CREATE_SHORTCUT, label: t("create-shortcut"), - style: 64 | 8 | 128 | 2048 | 4096, + style: 64 | 8 | 128 | 4096 | 8192, onClick: (e) => { window.BX_EXPOSED.dialogRoutes?.closeAll(); let $btn = e.target.closest("button"); @@ -6406,7 +6509,7 @@ class RootDialogObserver { static $btnWallpaper = AppInterface && createButton({ icon: BxIcon.DOWNLOAD, label: t("wallpaper"), - style: 64 | 8 | 128 | 2048 | 4096, + style: 64 | 8 | 128 | 4096 | 8192, onClick: (e) => { window.BX_EXPOSED.dialogRoutes?.closeAll(); let $btn = e.target.closest("button"), details = parseDetailsPath($btn.dataset.path); diff --git a/dist/better-xcloud.user.js b/dist/better-xcloud.user.js index ef32ab3..8b4cca8 100755 --- a/dist/better-xcloud.user.js +++ b/dist/better-xcloud.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name Better xCloud // @namespace https://github.com/redphx -// @version 6.0.7 +// @version 6.1.0-beta // @description Improve Xbox Cloud Gaming (xCloud) experience // @author redphx // @license MIT @@ -107,7 +107,7 @@ class UserAgent { }); } } -var SCRIPT_VERSION = "6.0.7", SCRIPT_VARIANT = "full", AppInterface = window.AppInterface; +var SCRIPT_VERSION = "6.1.0-beta", SCRIPT_VARIANT = "full", AppInterface = window.AppInterface; UserAgent.init(); var userAgent = window.navigator.userAgent.toLowerCase(), isTv = userAgent.includes("smart-tv") || userAgent.includes("smarttv") || /\baft.*\b/.test(userAgent), isVr = window.navigator.userAgent.includes("VR") && window.navigator.userAgent.includes("OculusBrowser"), browserHasTouchSupport = "ontouchstart" in window || navigator.maxTouchPoints > 0, userAgentHasTouchSupport = !isTv && !isVr && browserHasTouchSupport, STATES = { supportedRegion: !0, @@ -179,11 +179,14 @@ var GamepadKeyName = { 101: ["Left Stick Down", "⇂"], 102: ["Left Stick Left", "↼"], 103: ["Left Stick Right", "⇀"], + 104: ["Left Stick", "⇱"], 11: ["R3", "↻"], 200: ["Right Stick Up", "↿"], 201: ["Right Stick Down", "⇃"], 202: ["Right Stick Left", "↽"], - 203: ["Right Stick Right", "⇁"] + 203: ["Right Stick Right", "⇁"], + 204: ["Right Stick", "⇲"], + 17: ["Screenshot", "📸"] }; class BxEventBus { listeners = new Map; @@ -298,6 +301,7 @@ var SUPPORTED_LANGUAGES = { "zh-CN": "中文(简体)", "zh-TW": "中文(繁體)" }, Texts = { + "slightly-increase-input-latency": "Slightly increase input latency", achievements: "Achievements", activate: "Activate", activated: "Activated", @@ -357,12 +361,10 @@ var SUPPORTED_LANGUAGES = { "continent-south-america": "South America", contrast: "Contrast", controller: "Controller", + "controller-customization": "Controller customization", "controller-friendly-ui": "Controller-friendly UI", - "controller-mapping": "Controller mapping", - "controller-mapping-in-game": "In-game controller mapping", "controller-shortcuts": "Controller shortcuts", "controller-shortcuts-connect-note": "Connect a controller to use this feature", - "controller-shortcuts-in-game": "In-game controller shortcuts", "controller-shortcuts-xbox-note": "Button to open the Guide menu", "controller-vibration": "Controller vibration", copy: "Copy", @@ -373,12 +375,12 @@ var SUPPORTED_LANGUAGES = { default: "Default", "default-preset-note": "You can't modify default presets. Create a new one to customize it.", delete: "Delete", + "detect-controller-button": "Detect controller button", device: "Device", "device-unsupported-touch": "Your device doesn't have touch support", "device-vibration": "Device vibration", "device-vibration-not-using-gamepad": "On when not using gamepad", disable: "Disable", - "disable-byog-feature": 'Disable "Stream your own game" feature', "disable-features": "Disable features", "disable-home-context-menu": "Disable context menu in Home page", "disable-post-stream-feedback-dialog": "Disable post-stream feedback dialog", @@ -424,6 +426,10 @@ var SUPPORTED_LANGUAGES = { "how-to-improve-app-performance": "How to improve app's performance", ignore: "Ignore", import: "Import", + "in-game-controller-customization": "In-game controller customization", + "in-game-controller-shortcuts": "In-game controller shortcuts", + "in-game-keyboard-shortcuts": "In-game keyboard shortcuts", + "in-game-shortcuts": "In-game shortcuts", increase: "Increase", "install-android": "Better xCloud app for Android", invites: "Invites", @@ -431,12 +437,13 @@ var SUPPORTED_LANGUAGES = { jitter: "Jitter", "keyboard-key": "Keyboard key", "keyboard-shortcuts": "Keyboard shortcuts", - "keyboard-shortcuts-in-game": "In-game keyboard shortcuts", korea: "Korea", language: "Language", large: "Large", layout: "Layout", "left-stick": "Left stick", + "left-stick-deadzone": "Left stick deadzone", + "left-trigger-range": "Left trigger range", "limit-fps": "Limit FPS", "load-failed-message": "Failed to run Better xCloud", "loading-screen": "Loading screen", @@ -444,7 +451,6 @@ var SUPPORTED_LANGUAGES = { "lowest-quality": "Lowest quality", manage: "Manage", "map-mouse-to": "Map mouse to", - mapping: "Mapping", "may-not-work-properly": "May not work properly!", menu: "Menu", microphone: "Microphone", @@ -501,6 +507,7 @@ var SUPPORTED_LANGUAGES = { "preferred-game-language": "Preferred game's language", preset: "Preset", press: "Press", + "press-any-button": "Press any button...", "press-esc-to-cancel": "Press Esc to cancel", "press-key-to-toggle-mkb": [ e => `Press ${e.key} to toggle this feature`, @@ -556,6 +563,8 @@ var SUPPORTED_LANGUAGES = { "renderer-configuration": "Renderer configuration", "right-click-to-unbind": "Right-click on a key to unbind it", "right-stick": "Right stick", + "right-stick-deadzone": "Right stick deadzone", + "right-trigger-range": "Right trigger range", "rocket-always-hide": "Always hide", "rocket-always-show": "Always show", "rocket-animation": "Rocket animation", @@ -565,7 +574,6 @@ var SUPPORTED_LANGUAGES = { screen: "Screen", "screenshot-apply-filters": "Apply video filters to screenshots", "section-all-games": "All games", - "section-byog": "Stream your own game", "section-most-popular": "Most popular", "section-native-mkb": "Play with mouse & keyboard", "section-news": "News", @@ -655,7 +663,6 @@ var SUPPORTED_LANGUAGES = { e => `觸控遊玩佈局由 ${e.name} 提供` ], "touch-controller": "Touch controller", - "transparent-background": "Transparent background", "true-achievements": "TrueAchievements", ui: "UI", "unexpected-behavior": "May cause unexpected behavior", @@ -775,10 +782,11 @@ var ButtonStyleClass = { 64: "bx-focusable", 128: "bx-full-width", 256: "bx-full-height", - 512: "bx-tall", - 1024: "bx-circular", - 2048: "bx-normal-case", - 4096: "bx-normal-link" + 512: "bx-auto-height", + 1024: "bx-tall", + 2048: "bx-circular", + 4096: "bx-normal-case", + 8192: "bx-normal-link" }; function createElement(elmName, props = {}, ..._) { let $elm, hasNs = "xmlns" in props; @@ -888,6 +896,23 @@ function renderPresetsList($select, allPresets, selectedValue, options = {}) { if ($optGroup.hasChildNodes()) $select.appendChild($optGroup); } } +function calculateSelectBoxes($root) { + let selects = Array.from($root.querySelectorAll("div.bx-select:not([data-calculated]) select")); + for (let $select of selects) { + let $parent = $select.parentElement; + if ($parent.classList.contains("bx-full-width")) { + $parent.dataset.calculated = "true"; + continue; + } + let rect = $select.getBoundingClientRect(), $label, width = Math.ceil(rect.width); + if (!width) continue; + if ($label = $parent.querySelector($select.multiple ? ".bx-select-value" : "div"), $parent.isControllerFriendly) { + if ($select.multiple) width += 20; + if ($select.querySelector("optgroup")) width -= 15; + } else width += 10; + $select.style.left = "0", $label.style.minWidth = width + "px", $parent.dataset.calculated = "true"; + } +} var FILE_SIZE_UNITS = ["B", "KB", "MB", "GB", "TB"]; function humanFileSize(size) { let i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024)); @@ -1070,9 +1095,10 @@ class LocalDb { static instance; static getInstance = () => LocalDb.instance ?? (LocalDb.instance = new LocalDb); static DB_NAME = "BetterXcloud"; - static DB_VERSION = 3; + static DB_VERSION = 4; static TABLE_VIRTUAL_CONTROLLERS = "virtual_controllers"; static TABLE_CONTROLLER_SHORTCUTS = "controller_shortcuts"; + static TABLE_CONTROLLER_CUSTOMIZATIONS = "controller_customizations"; static TABLE_CONTROLLER_SETTINGS = "controller_settings"; static TABLE_KEYBOARD_SHORTCUTS = "keyboard_shortcuts"; db; @@ -1097,6 +1123,10 @@ class LocalDb { if (!db.objectStoreNames.contains(LocalDb.TABLE_CONTROLLER_SETTINGS)) db.createObjectStore(LocalDb.TABLE_CONTROLLER_SETTINGS, { keyPath: "id" }); + if (!db.objectStoreNames.contains(LocalDb.TABLE_CONTROLLER_CUSTOMIZATIONS)) db.createObjectStore(LocalDb.TABLE_CONTROLLER_CUSTOMIZATIONS, { + keyPath: "id", + autoIncrement: !0 + }); if (!db.objectStoreNames.contains(LocalDb.TABLE_KEYBOARD_SHORTCUTS)) db.createObjectStore(LocalDb.TABLE_KEYBOARD_SHORTCUTS, { keyPath: "id", autoIncrement: !0 @@ -2080,10 +2110,9 @@ function hashCode(str) { return hash; } function renderString(str, obj) { - return str.replace(/\$\{.+?\}/g, (match) => { - let key = match.substring(2, match.length - 1); - if (key in obj) return obj[key]; - return match; + return str.replace(/\$\{([A-Za-z0-9_$]+)\}|\$([A-Za-z0-9_$]+)\$/g, (match, p1, p2) => { + let name = p1 || p2; + return name in obj ? obj[name] : match; }); } function ceilToNearest(value, interval) { @@ -2185,27 +2214,27 @@ class StreamStatsCollector { current: -1, grades: [40, 75, 100], toString() { - return this.current === -1 ? "???" : this.current.toString().padStart(3, " "); + return this.current === -1 ? "???" : this.current.toString().padStart(3); } }, jit: { current: 0, grades: [30, 40, 60], toString() { - return `${this.current.toFixed(1)}ms`.padStart(6, " "); + return `${this.current.toFixed(1)}ms`.padStart(6); } }, fps: { current: 0, toString() { let maxFps = getPref("video.maxFps"); - return maxFps < 60 ? `${maxFps}/${this.current}`.padStart(5, " ") : this.current.toString(); + return maxFps < 60 ? `${maxFps}/${this.current}`.padStart(5) : this.current.toString(); } }, btr: { current: 0, toString() { - return `${this.current.toFixed(1)} Mbps`.padStart(9, " "); + return `${this.current.toFixed(1)} Mbps`.padStart(9); } }, fl: { @@ -2229,13 +2258,13 @@ class StreamStatsCollector { total: 0, grades: [6, 9, 12], toString() { - return isNaN(this.current) ? "??ms" : `${this.current.toFixed(1)}ms`.padStart(6, " "); + return isNaN(this.current) ? "??ms" : `${this.current.toFixed(1)}ms`.padStart(6); } }, dl: { total: 0, toString() { - return humanFileSize(this.total).padStart(8, " "); + return humanFileSize(this.total).padStart(8); } }, ul: { @@ -2702,12 +2731,39 @@ class ControllerShortcutsTable extends BasePresetsTable { BxLogger.info(this.LOG_TAG, "constructor()"); } } +class ControllerCustomizationsTable extends BasePresetsTable { + static instance; + static getInstance = () => ControllerCustomizationsTable.instance ?? (ControllerCustomizationsTable.instance = new ControllerCustomizationsTable(LocalDb.TABLE_CONTROLLER_CUSTOMIZATIONS)); + TABLE_PRESETS = LocalDb.TABLE_CONTROLLER_CUSTOMIZATIONS; + DEFAULT_PRESETS = { + [-1]: { + id: -1, + name: "ABXY ⇄ BAYX", + data: { + mapping: { + 0: 1, + 1: 0, + 2: 3, + 3: 2 + }, + settings: { + leftStickDeadzone: [0, 100], + rightStickDeadzone: [0, 100], + leftTriggerRange: [0, 100], + rightTriggerRange: [0, 100], + vibrationIntensity: 100 + } + } + } + }; + DEFAULT_PRESET_ID = 0; +} class ControllerSettingsTable extends BaseLocalTable { static instance; static getInstance = () => ControllerSettingsTable.instance ?? (ControllerSettingsTable.instance = new ControllerSettingsTable(LocalDb.TABLE_CONTROLLER_SETTINGS)); static DEFAULT_DATA = { shortcutPresetId: -1, - vibrationIntensity: 50 + customizationPresetId: 0 }; async getControllerData(id) { let setting = await this.get(id); @@ -2718,8 +2774,8 @@ class ControllerSettingsTable extends BaseLocalTable { let all = await this.getAll(), results = {}; for (let key in all) { if (!all[key]) continue; - let settings = all[key].data; - settings.vibrationIntensity /= 100, results[key] = settings; + let settings = Object.assign(all[key].data, ControllerSettingsTable.DEFAULT_DATA); + results[key] = settings; } return results; } @@ -2788,24 +2844,65 @@ class StreamSettings { mkbPreset: null, keyboardShortcuts: {} }; + static CONTROLLER_CUSTOMIZATION_MAPPING = { + 0: "A", + 1: "B", + 2: "X", + 3: "Y", + 12: "DPadUp", + 15: "DPadRight", + 13: "DPadDown", + 14: "DPadLeft", + 4: "LeftShoulder", + 5: "RightShoulder", + 6: "LeftTrigger", + 7: "RightTrigger", + 10: "LeftThumb", + 11: "RightThumb", + 104: "LeftStickAxes", + 204: "RightStickAxes", + 8: "View", + 9: "Menu", + 17: "Share" + }; static getPref(key) { return getPref(key); } static async refreshControllerSettings() { - let settings = StreamSettings.settings, controllers = {}, settingsTable = ControllerSettingsTable.getInstance(), shortcutsTable = ControllerShortcutsTable.getInstance(), gamepads = window.navigator.getGamepads(); + let settings = StreamSettings.settings, controllers = {}, settingsTable = ControllerSettingsTable.getInstance(), shortcutsTable = ControllerShortcutsTable.getInstance(), mappingTable = ControllerCustomizationsTable.getInstance(), gamepads = window.navigator.getGamepads(); for (let gamepad of gamepads) { if (!gamepad?.connected) continue; if (gamepad.id === VIRTUAL_GAMEPAD_ID) continue; - let settingsData = await settingsTable.getControllerData(gamepad.id), shortcutsMapping, preset = await shortcutsTable.getPreset(settingsData.shortcutPresetId); - if (!preset) shortcutsMapping = null; - else shortcutsMapping = preset.data.mapping; + let settingsData = await settingsTable.getControllerData(gamepad.id), shortcutsPreset = await shortcutsTable.getPreset(settingsData.shortcutPresetId), shortcutsMapping = !shortcutsPreset ? null : shortcutsPreset.data.mapping, customizationPreset = await mappingTable.getPreset(settingsData.customizationPresetId), customizationData = StreamSettings.convertControllerCustomization(customizationPreset?.data); controllers[gamepad.id] = { - vibrationIntensity: settingsData.vibrationIntensity, - shortcuts: shortcutsMapping + shortcuts: shortcutsMapping, + customization: customizationData }; } settings.controllers = controllers, settings.controllerPollingRate = StreamSettings.getPref("controller.pollingRate"), await StreamSettings.refreshDeviceVibration(); } + static preCalculateControllerRange(obj, target, values) { + if (values && Array.isArray(values)) { + let [from, to] = values; + if (from > 1 || to < 100) obj[target] = [from / 100, to / 100]; + } + } + static convertControllerCustomization(customization) { + if (!customization) return null; + let converted = { + mapping: {}, + ranges: {}, + vibrationIntensity: 1 + }, gamepadKey; + for (gamepadKey in customization.mapping) { + let gamepadStr = StreamSettings.CONTROLLER_CUSTOMIZATION_MAPPING[gamepadKey]; + if (!gamepadStr) continue; + let mappedKey = customization.mapping[gamepadKey]; + if (typeof mappedKey === "number") converted.mapping[gamepadStr] = StreamSettings.CONTROLLER_CUSTOMIZATION_MAPPING[mappedKey]; + else converted.mapping[gamepadStr] = !1; + } + return StreamSettings.preCalculateControllerRange(converted.ranges, "LeftTrigger", customization.settings.leftTriggerRange), StreamSettings.preCalculateControllerRange(converted.ranges, "RightTrigger", customization.settings.rightTriggerRange), StreamSettings.preCalculateControllerRange(converted.ranges, "LeftThumb", customization.settings.leftStickDeadzone), StreamSettings.preCalculateControllerRange(converted.ranges, "RightThumb", customization.settings.rightStickDeadzone), converted.vibrationIntensity = customization.settings.vibrationIntensity / 100, converted; + } static async refreshDeviceVibration() { if (!STATES.browser.capabilities.deviceVibration) return; let mode = StreamSettings.getPref("deviceVibration.mode"), intensity = 0; @@ -2855,6 +2952,43 @@ class StreamSettings { window.addEventListener("gamepadconnected", listener), window.addEventListener("gamepaddisconnected", listener), StreamSettings.refreshAllSettings(); } } +var BxIcon = { + BETTER_XCLOUD: "", + TRUE_ACHIEVEMENTS: "", + STREAM_SETTINGS: "", + STREAM_STATS: "", + CLOSE: "", + CONTROLLER: "", + CREATE_SHORTCUT: "", + DISPLAY: "", + EYE: "", + EYE_SLASH: "", + HOME: "", + NATIVE_MKB: "", + NEW: "", + MANAGE: "", + COPY: "", + TRASH: "", + CURSOR_TEXT: "", + POWER: "", + QUESTION: "", + REFRESH: "", + REMOTE_PLAY: "", + CARET_LEFT: "", + CARET_RIGHT: "", + SCREENSHOT: "", + SPEAKER_MUTED: "", + TOUCH_CONTROL_ENABLE: "", + TOUCH_CONTROL_DISABLE: "", + MICROPHONE: "", + MICROPHONE_MUTED: "", + BATTERY: "", + PLAYTIME: "", + SERVER: "", + DOWNLOAD: "", + UPLOAD: "", + AUDIO: "" +}; class MkbPopup { static instance; static getInstance = () => MkbPopup.instance ?? (MkbPopup.instance = new MkbPopup); @@ -2880,7 +3014,7 @@ class MkbPopup { } createActivateButton() { let options = { - style: 1 | 512 | 128, + style: 1 | 1024 | 128, label: t("activate"), onClick: this.onActivate }, shortcutKey = StreamSettings.findKeyboardShortcut("mkb.toggle"); @@ -2899,6 +3033,7 @@ class MkbPopup { } }), createButton({ label: t("manage"), + icon: BxIcon.MANAGE, style: 64, onClick: () => { let dialog = SettingsDialog.getInstance(); @@ -3411,16 +3546,22 @@ class NavigationDialogManager { LOG_TAG = "NavigationDialogManager"; static GAMEPAD_POLLING_INTERVAL = 50; static GAMEPAD_KEYS = [ - 12, - 13, - 14, - 15, 0, 1, + 2, + 3, + 12, + 15, + 13, + 14, 4, 5, 6, - 7 + 7, + 10, + 11, + 8, + 9 ]; static GAMEPAD_DIRECTION_MAP = { 12: 1, @@ -3450,31 +3591,14 @@ class NavigationDialogManager { dialog = null; dialogsStack = []; constructor() { - if (BxLogger.info(this.LOG_TAG, "constructor()"), this.$overlay = CE("div", { class: "bx-navigation-dialog-overlay bx-gone" }), this.$overlay.addEventListener("click", (e) => { + BxLogger.info(this.LOG_TAG, "constructor()"), this.$overlay = CE("div", { class: "bx-navigation-dialog-overlay bx-gone" }), this.$overlay.addEventListener("click", (e) => { e.preventDefault(), e.stopPropagation(), this.dialog?.isCancellable() && this.hide(); - }), document.documentElement.appendChild(this.$overlay), this.$container = CE("div", { class: "bx-navigation-dialog bx-gone" }), document.documentElement.appendChild(this.$container), window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, (e) => this.hide()), getPref("ui.controllerFriendly")) - new MutationObserver((mutationList) => { - if (mutationList.length === 0 || mutationList[0].addedNodes.length === 0) return; - let $dialog = mutationList[0].addedNodes[0]; - if (!$dialog || !($dialog instanceof HTMLElement)) return; - this.calculateSelectBoxes($dialog); - }).observe(this.$container, { childList: !0 }); - } - calculateSelectBoxes($root) { - let selects = Array.from($root.querySelectorAll(".bx-select:not([data-calculated]) select")); - for (let $select of selects) { - let $parent = $select.parentElement; - if ($parent.classList.contains("bx-full-width")) { - $parent.dataset.calculated = "true"; - return; - } - let rect = $select.getBoundingClientRect(), $label, width = Math.ceil(rect.width); - if (!width) return; - if ($select.multiple) $label = $parent.querySelector(".bx-select-value"), width += 20; - else $label = $parent.querySelector("div"); - if ($select.querySelector("optgroup")) width -= 15; - $label.style.minWidth = width + "px", $parent.dataset.calculated = "true"; - } + }), document.documentElement.appendChild(this.$overlay), this.$container = CE("div", { class: "bx-navigation-dialog bx-gone" }), document.documentElement.appendChild(this.$container), window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, (e) => this.hide()), new MutationObserver((mutationList) => { + if (mutationList.length === 0 || mutationList[0].addedNodes.length === 0) return; + let $dialog = mutationList[0].addedNodes[0]; + if (!$dialog || !($dialog instanceof HTMLElement)) return; + calculateSelectBoxes($dialog); + }).observe(this.$container, { childList: !0 }); } updateActiveInput(input) { document.documentElement.dataset.activeInput = input; @@ -3537,7 +3661,7 @@ class NavigationDialogManager { } } this.clearGamepadHoldingInterval(); - }, 200); + }, 100); continue; } if (releasedButton === null) { @@ -3607,7 +3731,12 @@ class NavigationDialogManager { if (!$focusing || $focusing === this.$container) return null; if (checked.includes($focusing)) return null; checked.push($focusing); - let $target = $focusing, $parent = $target.parentElement, nearby = $target.nearby || {}, orientation = this.getOrientation($target), siblingProperty = NavigationDialogManager.SIBLING_PROPERTY_MAP[orientation][direction]; + let $target = $focusing, $parent = $target.parentElement, nearby = $target.nearby || {}, orientation = this.getOrientation($target); + if (nearby[1] && direction === 1) return nearby[1]; + else if (nearby[3] && direction === 3) return nearby[3]; + else if (nearby[4] && direction === 4) return nearby[4]; + else if (nearby[2] && direction === 2) return nearby[2]; + let siblingProperty = NavigationDialogManager.SIBLING_PROPERTY_MAP[orientation][direction]; if (siblingProperty) { let $sibling = $target; while ($sibling[siblingProperty]) { @@ -3848,43 +3977,8 @@ class TouchController { }); } } -var BxIcon = { - BETTER_XCLOUD: "", - TRUE_ACHIEVEMENTS: "", - STREAM_SETTINGS: "", - STREAM_STATS: "", - CLOSE: "", - CONTROLLER: "", - CREATE_SHORTCUT: "", - DISPLAY: "", - EYE: "", - EYE_SLASH: "", - HOME: "", - NATIVE_MKB: "", - NEW: "", - COPY: "", - TRASH: "", - CURSOR_TEXT: "", - POWER: "", - QUESTION: "", - REFRESH: "", - REMOTE_PLAY: "", - CARET_LEFT: "", - CARET_RIGHT: "", - SCREENSHOT: "", - SPEAKER_MUTED: "", - TOUCH_CONTROL_ENABLE: "", - TOUCH_CONTROL_DISABLE: "", - MICROPHONE: "", - MICROPHONE_MUTED: "", - BATTERY: "", - PLAYTIME: "", - SERVER: "", - DOWNLOAD: "", - UPLOAD: "", - AUDIO: "" -}; class BxSelectElement extends HTMLSelectElement { + isControllerFriendly; optionsList; indicatorsList; $indicators; @@ -3896,21 +3990,36 @@ class BxSelectElement extends HTMLSelectElement { $label; $checkBox; static create($select, forceFriendly = !1) { - if (!forceFriendly && !getPref("ui.controllerFriendly")) return $select.classList.add("bx-select"), $select; + let isControllerFriendly = forceFriendly || getPref("ui.controllerFriendly"); + if ($select.multiple && !isControllerFriendly) return $select.classList.add("bx-select"), $select; $select.removeAttribute("tabindex"); - let $wrapper = CE("div", { class: "bx-select" }), $btnPrev = createButton({ - label: "<", - style: 64 - }), $btnNext = createButton({ - label: ">", - style: 64 - }); - setNearby($wrapper, { - orientation: "horizontal", - focus: $btnNext + let $wrapper = CE("div", { + class: "bx-select", + _dataset: { + controllerFriendly: isControllerFriendly + } }); + if ($select.classList.contains("bx-full-width")) $wrapper.classList.add("bx-full-width"); let $content, self = $wrapper; - if (self.isMultiple = $select.multiple, self.visibleIndex = $select.selectedIndex, self.$select = $select, self.optionsList = Array.from($select.querySelectorAll("option")), self.$indicators = CE("div", { class: "bx-select-indicators" }), self.indicatorsList = [], self.$btnNext = $btnNext, self.$btnPrev = $btnPrev, self.isMultiple) $content = CE("button", { + self.isControllerFriendly = isControllerFriendly, self.isMultiple = $select.multiple, self.visibleIndex = $select.selectedIndex, self.$select = $select, self.optionsList = Array.from($select.querySelectorAll("option")), self.$indicators = CE("div", { class: "bx-select-indicators" }), self.indicatorsList = []; + let $btnPrev, $btnNext; + if (isControllerFriendly) { + $btnPrev = createButton({ + label: "<", + style: 64 + }), $btnNext = createButton({ + label: ">", + style: 64 + }), setNearby($wrapper, { + orientation: "horizontal", + focus: $btnNext + }), self.$btnNext = $btnNext, self.$btnPrev = $btnPrev; + let boundOnPrevNext = BxSelectElement.onPrevNext.bind(self); + $btnPrev.addEventListener("click", boundOnPrevNext), $btnNext.addEventListener("click", boundOnPrevNext); + } else $select.addEventListener("change", (e) => { + self.visibleIndex = $select.selectedIndex, BxSelectElement.resetIndicators.call(self), BxSelectElement.render.call(self); + }); + if (self.isMultiple) $content = CE("button", { class: "bx-select-value bx-focusable", tabindex: 0 }, CE("div", {}, self.$checkBox = CE("input", { type: "checkbox" }), self.$label = CE("span", {}, "")), self.$indicators), $content.addEventListener("click", (e) => { @@ -3920,8 +4029,7 @@ class BxSelectElement extends HTMLSelectElement { $option && ($option.selected = e.target.checked), BxEvent.dispatch($select, "input"); }); else $content = CE("div", {}, self.$label = CE("label", { for: $select.id + "_checkbox" }, ""), self.$indicators); - let boundOnPrevNext = BxSelectElement.onPrevNext.bind(self); - return $select.addEventListener("input", BxSelectElement.render.bind(self)), $btnPrev.addEventListener("click", boundOnPrevNext), $btnNext.addEventListener("click", boundOnPrevNext), new MutationObserver((mutationList, observer2) => { + return $select.addEventListener("input", BxSelectElement.render.bind(self)), new MutationObserver((mutationList, observer2) => { mutationList.forEach((mutation) => { if (mutation.type === "childList" || mutation.type === "attributes") self.visibleIndex = $select.selectedIndex, self.optionsList = Array.from($select.querySelectorAll("option")), BxSelectElement.resetIndicators.call(self), BxSelectElement.render.call(self); }); @@ -3929,7 +4037,7 @@ class BxSelectElement extends HTMLSelectElement { subtree: !0, childList: !0, attributes: !0 - }), self.append($select, $btnPrev, $content, $btnNext), BxSelectElement.resetIndicators.call(self), BxSelectElement.render.call(self), Object.defineProperty(self, "value", { + }), self.append($select, $btnPrev || "", $content, $btnNext || ""), BxSelectElement.resetIndicators.call(self), BxSelectElement.render.call(self), Object.defineProperty(self, "value", { get() { return $select.value; }, @@ -3978,7 +4086,6 @@ class BxSelectElement extends HTMLSelectElement { $btnNext, $btnPrev, $checkBox, - visibleIndex, optionsList, indicatorsList } = this; @@ -3987,7 +4094,7 @@ class BxSelectElement extends HTMLSelectElement { let $option = BxSelectElement.getOptionAtIndex.call(this, this.visibleIndex), content = ""; if ($option) { let $parent = $option.parentElement, hasLabel = $parent instanceof HTMLOptGroupElement || this.$select.querySelector("optgroup"); - if (content = $option.textContent || "", content && hasLabel) { + if (content = $option.dataset.label || $option.textContent || "", content && hasLabel) { let groupLabel = $parent instanceof HTMLOptGroupElement ? $parent.label : " "; $label.innerHTML = ""; let fragment = document.createDocumentFragment(); @@ -3996,12 +4103,12 @@ class BxSelectElement extends HTMLSelectElement { } else $label.textContent = content; if ($label.classList.toggle("bx-line-through", $option && $option.disabled), this.isMultiple) $checkBox.checked = $option?.selected || !1, $checkBox.classList.toggle("bx-gone", !content); let disableButtons = optionsList.length <= 1; - $btnPrev.classList.toggle("bx-inactive", disableButtons), $btnNext.classList.toggle("bx-inactive", disableButtons); + $btnPrev?.classList.toggle("bx-inactive", disableButtons), $btnNext?.classList.toggle("bx-inactive", disableButtons); for (let i = 0;i < optionsList.length; i++) { let $option2 = optionsList[i], $indicator = indicatorsList[i]; if (!$option2 || !$indicator) continue; if (clearDataSet($indicator), $option2.selected) $indicator.dataset.selected = "true"; - if ($option2.index === visibleIndex) $indicator.dataset.highlighted = "true"; + if ($option2.index === this.visibleIndex) $indicator.dataset.highlighted = "true"; } } static normalizeIndex(index) { @@ -4022,178 +4129,24 @@ class BxSelectElement extends HTMLSelectElement { else BxEvent.dispatch($select, "input"); } } -var controller_shortcuts_default = `if (window.BX_EXPOSED.disableGamepadPolling) { -this.inputConfiguration.useIntervalWorkerThreadForInput && this.intervalWorker ? this.intervalWorker.scheduleTimer(50) : this.pollGamepadssetTimeoutTimerID = setTimeout(this.pollGamepads, 50); -return; -} -const currentGamepad = \${gamepadVar}; -if (currentGamepad.buttons[17] && currentGamepad.buttons[17].pressed) { -window.dispatchEvent(new Event(BxEvent.CAPTURE_SCREENSHOT)); -} -const btnHome = currentGamepad.buttons[16]; -if (btnHome) { -if (!this.bxHomeStates) { -this.bxHomeStates = {}; -} -let intervalMs = 0; -let hijack = false; -if (btnHome.pressed) { -hijack = true; -intervalMs = 16; -this.gamepadIsIdle.set(currentGamepad.index, false); -if (this.bxHomeStates[currentGamepad.index]) { -const lastTimestamp = this.bxHomeStates[currentGamepad.index].timestamp; -if (currentGamepad.timestamp !== lastTimestamp) { -this.bxHomeStates[currentGamepad.index].timestamp = currentGamepad.timestamp; -const handled = window.BX_EXPOSED.handleControllerShortcut(currentGamepad); -if (handled) { -this.bxHomeStates[currentGamepad.index].shortcutPressed += 1; -} -} -} else { -window.BX_EXPOSED.resetControllerShortcut(currentGamepad.index); -this.bxHomeStates[currentGamepad.index] = { -shortcutPressed: 0, -timestamp: currentGamepad.timestamp, -}; -} -} else if (this.bxHomeStates[currentGamepad.index]) { -hijack = true; -const info = structuredClone(this.bxHomeStates[currentGamepad.index]); -this.bxHomeStates[currentGamepad.index] = null; -if (info.shortcutPressed === 0) { -const fakeGamepadMappings = [{ -GamepadIndex: currentGamepad.index, -A: 0, -B: 0, -X: 0, -Y: 0, -LeftShoulder: 0, -RightShoulder: 0, -LeftTrigger: 0, -RightTrigger: 0, -View: 0, -Menu: 0, -LeftThumb: 0, -RightThumb: 0, -DPadUp: 0, -DPadDown: 0, -DPadLeft: 0, -DPadRight: 0, -Nexus: 1, -LeftThumbXAxis: 0, -LeftThumbYAxis: 0, -RightThumbXAxis: 0, -RightThumbYAxis: 0, -PhysicalPhysicality: 0, -VirtualPhysicality: 0, -Dirty: true, -Virtual: false, -}]; -const isLongPress = (currentGamepad.timestamp - info.timestamp) >= 500; -intervalMs = isLongPress ? 500 : 100; -this.inputSink.onGamepadInput(performance.now() - intervalMs, fakeGamepadMappings); -} else { -intervalMs = window.BX_STREAM_SETTINGS.controllerPollingRate; -} -} -if (hijack && intervalMs) { -this.inputConfiguration.useIntervalWorkerThreadForInput && this.intervalWorker ? this.intervalWorker.scheduleTimer(intervalMs) : this.pollGamepadssetTimeoutTimerID = setTimeout(this.pollGamepads, intervalMs); -return; -} -}`; -var expose_stream_session_default = `window.BX_EXPOSED.streamSession = this; -const orgSetMicrophoneState = this.setMicrophoneState.bind(this); -this.setMicrophoneState = state => { -orgSetMicrophoneState(state); -window.BxEventBus.Stream.emit('microphone.state.changed', { state }); -}; -window.dispatchEvent(new Event(BxEvent.STREAM_SESSION_READY)); -let updateDimensionsStr = this.updateDimensions.toString(); -if (updateDimensionsStr.startsWith('function ')) { -updateDimensionsStr = updateDimensionsStr.substring(9); -} -const renderTargetVar = updateDimensionsStr.match(/if\\((\\w+)\\){/)[1]; -updateDimensionsStr = updateDimensionsStr.replaceAll(renderTargetVar + '.scroll', 'scroll'); -updateDimensionsStr = updateDimensionsStr.replace(\`if(\${renderTargetVar}){\`, \` -if (\${renderTargetVar}) { -const scrollWidth = \${renderTargetVar}.dataset.width ? parseInt(\${renderTargetVar}.dataset.width) : \${renderTargetVar}.scrollWidth; -const scrollHeight = \${renderTargetVar}.dataset.height ? parseInt(\${renderTargetVar}.dataset.height) : \${renderTargetVar}.scrollHeight; -\`); -eval(\`this.updateDimensions = function \${updateDimensionsStr}\`);`; -var local_co_op_enable_default = `this.orgOnGamepadChanged = this.onGamepadChanged; -this.orgOnGamepadInput = this.onGamepadInput; -let match; -let onGamepadChangedStr = this.onGamepadChanged.toString(); -if (onGamepadChangedStr.startsWith('function ')) { -onGamepadChangedStr = onGamepadChangedStr.substring(9); -} -onGamepadChangedStr = onGamepadChangedStr.replaceAll('0', 'arguments[1]'); -eval(\`this.patchedOnGamepadChanged = function \${onGamepadChangedStr}\`); -let onGamepadInputStr = this.onGamepadInput.toString(); -if (onGamepadInputStr.startsWith('function ')) { -onGamepadInputStr = onGamepadInputStr.substring(9); -} -match = onGamepadInputStr.match(/(\\w+\\.GamepadIndex)/); -if (match) { -const gamepadIndexVar = match[0]; -onGamepadInputStr = onGamepadInputStr.replace('this.gamepadStates.get(', \`this.gamepadStates.get(\${gamepadIndexVar},\`); -eval(\`this.patchedOnGamepadInput = function \${onGamepadInputStr}\`); -BxLogger.info('supportLocalCoOp', '✅ Successfully patched local co-op support'); -} else { -BxLogger.error('supportLocalCoOp', '❌ Unable to patch local co-op support'); -} -this.toggleLocalCoOp = enable => { -BxLogger.info('toggleLocalCoOp', enable ? 'Enabled' : 'Disabled'); -this.onGamepadChanged = enable ? this.patchedOnGamepadChanged : this.orgOnGamepadChanged; -this.onGamepadInput = enable ? this.patchedOnGamepadInput : this.orgOnGamepadInput; -const gamepads = window.navigator.getGamepads(); -for (const gamepad of gamepads) { -if (!gamepad?.connected) { -continue; -} -if (gamepad.id.includes('Better xCloud')) { -continue; -} -window.dispatchEvent(new GamepadEvent('gamepaddisconnected', { gamepad })); -window.dispatchEvent(new GamepadEvent('gamepadconnected', { gamepad })); -} -}; -window.BX_EXPOSED.toggleLocalCoOp = this.toggleLocalCoOp.bind(this);`; -var remote_play_enable_default = `connectMode: window.BX_REMOTE_PLAY_CONFIG ? "xhome-connect" : "cloud-connect", -remotePlayServerId: (window.BX_REMOTE_PLAY_CONFIG && window.BX_REMOTE_PLAY_CONFIG.serverId) || '',`; -var remote_play_keep_alive_default = `const msg = JSON.parse(e); -if (msg.reason === 'WarningForBeingIdle' && !window.location.pathname.includes('/launch/')) { -try { -this.sendKeepAlive(); -return; -} catch (ex) { console.log(ex); } -}`; -var vibration_adjust_default = `const gamepad = e.gamepad; -if (gamepad?.connected) { -const gamepadSettings = window.BX_STREAM_SETTINGS.controllers[gamepad.id]; -if (gamepadSettings) { -const intensity = gamepadSettings.vibrationIntensity; -if (intensity === 0) { -return void(e.repeat = 0); -} else if (intensity < 1) { -e.leftMotorPercent *= intensity; -e.rightMotorPercent *= intensity; -e.leftTriggerMotorPercent *= intensity; -e.rightTriggerMotorPercent *= intensity; -} -} -}`; +var controller_customization_default = "var shareButtonPressed=currentGamepad.buttons[17]?.pressed,shareButtonHandled=!1,xCloudGamepad=$xCloudGamepadVar$;if(currentGamepad.id in window.BX_STREAM_SETTINGS.controllers){let controller=window.BX_STREAM_SETTINGS.controllers[currentGamepad.id];if(controller?.customization){let{mapping,ranges}=controller.customization,pressedButtons={},releasedButtons={},isModified=!1;if(ranges.LeftTrigger){let[from,to]=ranges.LeftTrigger;xCloudGamepad.LeftTrigger=xCloudGamepad.LeftTrigger>to?1:xCloudGamepad.LeftTrigger,xCloudGamepad.LeftTrigger=xCloudGamepad.LeftTriggerto?1:xCloudGamepad.RightTrigger,xCloudGamepad.RightTrigger=xCloudGamepad.RightTriggerto?1:range;if(newRange=newRangeto?1:range;if(newRange=newRange=0.1)pressedButtons[targetX]=rangeX,pressedButtons[targetY]=rangeY}releasedButtons[sourceX]=0,releasedButtons[sourceY]=0,isModified=!0}else if(typeof mappedKey===\"string\"){let pressed=!1,value=0;if(key===\"LeftTrigger\"||key===\"RightTrigger\"){let currentRange=xCloudGamepad[key];if(mappedKey===\"LeftTrigger\"||mappedKey===\"RightTrigger\")pressed=currentRange>=0.1,value=currentRange;else pressed=!0,value=currentRange>=0.9?1:0}else if(xCloudGamepad[key])pressed=!0,value=xCloudGamepad[key];if(pressed)pressedButtons[mappedKey]=value,releasedButtons[key]=0,isModified=!0}else if(mappedKey===!1)pressedButtons[key]=0,isModified=!0}isModified&&Object.assign(xCloudGamepad,releasedButtons,pressedButtons)}}if(shareButtonPressed&&!shareButtonHandled)window.dispatchEvent(new Event(BxEvent.CAPTURE_SCREENSHOT));\n"; +var poll_gamepad_default = "var self=this;if(window.BX_EXPOSED.disableGamepadPolling){self.inputConfiguration.useIntervalWorkerThreadForInput&&self.intervalWorker?self.intervalWorker.scheduleTimer(50):self.pollGamepadssetTimeoutTimerID=window.setTimeout(self.pollGamepads,50);return}var currentGamepad=$gamepadVar$,btnHome=currentGamepad.buttons[16];if(btnHome){if(!self.bxHomeStates)self.bxHomeStates={};let intervalMs=0,hijack=!1;if(btnHome.pressed)if(hijack=!0,intervalMs=16,self.gamepadIsIdle.set(currentGamepad.index,!1),self.bxHomeStates[currentGamepad.index]){let lastTimestamp=self.bxHomeStates[currentGamepad.index].timestamp;if(currentGamepad.timestamp!==lastTimestamp){if(self.bxHomeStates[currentGamepad.index].timestamp=currentGamepad.timestamp,window.BX_EXPOSED.handleControllerShortcut(currentGamepad))self.bxHomeStates[currentGamepad.index].shortcutPressed+=1}}else window.BX_EXPOSED.resetControllerShortcut(currentGamepad.index),self.bxHomeStates[currentGamepad.index]={shortcutPressed:0,timestamp:currentGamepad.timestamp};else if(self.bxHomeStates[currentGamepad.index]){hijack=!0;let info=structuredClone(self.bxHomeStates[currentGamepad.index]);if(self.bxHomeStates[currentGamepad.index]=null,info.shortcutPressed===0){let fakeGamepadMappings=[{GamepadIndex:currentGamepad.index,A:0,B:0,X:0,Y:0,LeftShoulder:0,RightShoulder:0,LeftTrigger:0,RightTrigger:0,View:0,Menu:0,LeftThumb:0,RightThumb:0,DPadUp:0,DPadDown:0,DPadLeft:0,DPadRight:0,Nexus:1,LeftThumbXAxis:0,LeftThumbYAxis:0,RightThumbXAxis:0,RightThumbYAxis:0,PhysicalPhysicality:0,VirtualPhysicality:0,Dirty:!0,Virtual:!1}];intervalMs=currentGamepad.timestamp-info.timestamp>=500?500:100,self.inputSink.onGamepadInput(performance.now()-intervalMs,fakeGamepadMappings)}else intervalMs=window.BX_STREAM_SETTINGS.controllerPollingRate}if(hijack&&intervalMs){self.inputConfiguration.useIntervalWorkerThreadForInput&&self.intervalWorker?self.intervalWorker.scheduleTimer(intervalMs):self.pollGamepadssetTimeoutTimerID=setTimeout(self.pollGamepads,intervalMs);return}}\n"; +var expose_stream_session_default = 'var self=this;window.BX_EXPOSED.streamSession=self;var orgSetMicrophoneState=self.setMicrophoneState.bind(self);self.setMicrophoneState=(state)=>{orgSetMicrophoneState(state),window.BxEventBus.Stream.emit("microphone.state.changed",{state})};window.dispatchEvent(new Event(BxEvent.STREAM_SESSION_READY));var updateDimensionsStr=self.updateDimensions.toString();if(updateDimensionsStr.startsWith("function "))updateDimensionsStr=updateDimensionsStr.substring(9);var renderTargetVar=updateDimensionsStr.match(/if\\((\\w+)\\){/)[1];updateDimensionsStr=updateDimensionsStr.replaceAll(renderTargetVar+".scroll","scroll");updateDimensionsStr=updateDimensionsStr.replace(`if(${renderTargetVar}){`,`\nif (${renderTargetVar}) {\nconst scrollWidth = ${renderTargetVar}.dataset.width ? parseInt(${renderTargetVar}.dataset.width) : ${renderTargetVar}.scrollWidth;\nconst scrollHeight = ${renderTargetVar}.dataset.height ? parseInt(${renderTargetVar}.dataset.height) : ${renderTargetVar}.scrollHeight;\n`);eval(`this.updateDimensions = function ${updateDimensionsStr}`);\n'; +var local_co_op_enable_default = 'this.orgOnGamepadChanged=this.onGamepadChanged;this.orgOnGamepadInput=this.onGamepadInput;var match,onGamepadChangedStr=this.onGamepadChanged.toString();if(onGamepadChangedStr.startsWith("function "))onGamepadChangedStr=onGamepadChangedStr.substring(9);onGamepadChangedStr=onGamepadChangedStr.replaceAll("0","arguments[1]");eval(`this.patchedOnGamepadChanged = function ${onGamepadChangedStr}`);var onGamepadInputStr=this.onGamepadInput.toString();if(onGamepadInputStr.startsWith("function "))onGamepadInputStr=onGamepadInputStr.substring(9);match=onGamepadInputStr.match(/(\\w+\\.GamepadIndex)/);if(match){let gamepadIndexVar=match[0];onGamepadInputStr=onGamepadInputStr.replace("this.gamepadStates.get(",`this.gamepadStates.get(${gamepadIndexVar},`),eval(`this.patchedOnGamepadInput = function ${onGamepadInputStr}`),BxLogger.info("supportLocalCoOp","✅ Successfully patched local co-op support")}else BxLogger.error("supportLocalCoOp","❌ Unable to patch local co-op support");this.toggleLocalCoOp=(enable)=>{BxLogger.info("toggleLocalCoOp",enable?"Enabled":"Disabled"),this.onGamepadChanged=enable?this.patchedOnGamepadChanged:this.orgOnGamepadChanged,this.onGamepadInput=enable?this.patchedOnGamepadInput:this.orgOnGamepadInput;let gamepads=window.navigator.getGamepads();for(let gamepad of gamepads){if(!gamepad?.connected)continue;if(gamepad.id.includes("Better xCloud"))continue;window.dispatchEvent(new GamepadEvent("gamepaddisconnected",{gamepad})),window.dispatchEvent(new GamepadEvent("gamepadconnected",{gamepad}))}};window.BX_EXPOSED.toggleLocalCoOp=this.toggleLocalCoOp.bind(null);\n'; +var remote_play_keep_alive_default = `try{if(JSON.parse(e).reason==="WarningForBeingIdle"&&!window.location.pathname.includes("/launch/")){this.sendKeepAlive();return}}catch(ex){console.log(ex)}`; +var vibration_adjust_default = `if(e?.gamepad?.connected){let gamepadSettings=window.BX_STREAM_SETTINGS.controllers[e.gamepad.id];if(gamepadSettings?.customization){let intensity=gamepadSettings.customization.vibrationIntensity;if(intensity<=0){e.repeat=0;return}else if(intensity<1)e.leftMotorPercent*=intensity,e.rightMotorPercent*=intensity,e.leftTriggerMotorPercent*=intensity,e.rightTriggerMotorPercent*=intensity}}`; class PatcherUtils { - static indexOf(txt, searchString, startIndex, maxRange) { + static indexOf(txt, searchString, startIndex, maxRange = 0, after = !1) { + if (startIndex < 0) return -1; let index = txt.indexOf(searchString, startIndex); if (index < 0 || maxRange && index - startIndex > maxRange) return -1; - return index; + return after ? index + searchString.length : index; } - static lastIndexOf(txt, searchString, startIndex, maxRange) { + static lastIndexOf(txt, searchString, startIndex, maxRange = 0, after = !1) { + if (startIndex < 0) return -1; let index = txt.lastIndexOf(searchString, startIndex); if (index < 0 || maxRange && startIndex - index > maxRange) return -1; - return index; + return after ? index + searchString.length : index; } static insertAt(txt, index, insertString) { return txt.substring(0, index) + insertString + txt.substring(index); @@ -4262,7 +4215,9 @@ var LOG_TAG2 = "Patcher", PATCHES = { remotePlayConnectMode(str) { let text = 'connectMode:"cloud-connect",'; if (!str.includes(text)) return !1; - return str.replace(text, remote_play_enable_default); + let newCode = `connectMode: window.BX_REMOTE_PLAY_CONFIG ? "xhome-connect" : "cloud-connect", +remotePlayServerId: (window.BX_REMOTE_PLAY_CONFIG && window.BX_REMOTE_PLAY_CONFIG.serverId) || '',`; + return str.replace(text, newCode); }, remotePlayDisableAchievementToast(str) { let text = ".AchievementUnlock:{"; @@ -4293,14 +4248,16 @@ var LOG_TAG2 = "Patcher", PATCHES = { if (setTimeoutIndex < 0) return !1; let codeBlock = str.substring(index, setTimeoutIndex), tmp = str.substring(setTimeoutIndex, setTimeoutIndex + 150), tmpPatched = tmp.replaceAll("Math.max(0,4-", "Math.max(0,window.BX_STREAM_SETTINGS.controllerPollingRate - "); if (str = PatcherUtils.replaceWith(str, setTimeoutIndex, tmp, tmpPatched), getPref("block.tracking")) codeBlock = codeBlock.replace("this.inputPollingIntervalStats.addValue", ""), codeBlock = codeBlock.replace("this.inputPollingDurationStats.addValue", ""); - let match = codeBlock.match(/this\.gamepadTimestamps\.set\((\w+)\.index/); - if (match) { - let gamepadVar = match[1], newCode = renderString(controller_shortcuts_default, { - gamepadVar - }); - codeBlock = codeBlock.replace("this.gamepadTimestamps.set", newCode + "this.gamepadTimestamps.set"); - } - return str = str.substring(0, index) + codeBlock + str.substring(setTimeoutIndex), str; + let match = codeBlock.match(/this\.gamepadTimestamps\.set\(([A-Za-z0-9_$]+)\.index/); + if (!match) return !1; + let newCode = renderString(poll_gamepad_default, { + gamepadVar: match[1] + }); + if (codeBlock = codeBlock.replace("this.gamepadTimestamps.set", newCode + "this.gamepadTimestamps.set"), match = codeBlock.match(/let ([A-Za-z0-9_$]+)=this\.gamepadMappings\.find/), !match) return !1; + let xCloudGamepadVar = match[1], inputFeedbackManager = PatcherUtils.indexOf(codeBlock, "this.inputFeedbackManager.onGamepadConnected(", 0, 1e4), backetIndex = PatcherUtils.indexOf(codeBlock, "}", inputFeedbackManager, 100); + if (backetIndex < 0) return !1; + let customizationCode = ";"; + return customizationCode += renderString(controller_customization_default, { xCloudGamepadVar }), codeBlock = PatcherUtils.insertAt(codeBlock, backetIndex, customizationCode), str = str.substring(0, index) + codeBlock + str.substring(setTimeoutIndex), str; }, enableXcloudLogger(str) { let text = "this.telemetryProvider=e}log(e,t,r){"; @@ -5094,7 +5051,8 @@ class BaseProfileManagerDialog extends NavigationDialog { title; presetsDb; allPresets; - currentPresetId = 0; + currentPresetId = null; + activatedPresetId = null; $presets; $header; $defaultNote; @@ -5106,11 +5064,12 @@ class BaseProfileManagerDialog extends NavigationDialog { this.title = title, this.presetsDb = presetsDb; } updateButtonStates() { - let isDefaultPreset = this.currentPresetId <= 0; + let isDefaultPreset = this.currentPresetId === null || this.currentPresetId <= 0; this.$btnRename.disabled = isDefaultPreset, this.$btnDelete.disabled = isDefaultPreset, this.$defaultNote.classList.toggle("bx-gone", !isDefaultPreset); } async renderPresetsList() { - this.allPresets = await this.presetsDb.getPresets(), renderPresetsList(this.$presets, this.allPresets, this.currentPresetId, { selectedIndicator: !0 }); + if (this.allPresets = await this.presetsDb.getPresets(), this.currentPresetId === null) this.currentPresetId = this.allPresets.default[0]; + renderPresetsList(this.$presets, this.allPresets, this.activatedPresetId, { selectedIndicator: !0 }); } promptNewName(action, value = "") { let newName = ""; @@ -5121,9 +5080,12 @@ class BaseProfileManagerDialog extends NavigationDialog { return newName ? newName : !1; } async renderDialog() { - this.$presets = CE("select", { tabindex: -1 }); + this.$presets = CE("select", { + class: "bx-full-width", + tabindex: -1 + }); let $select = BxSelectElement.create(this.$presets); - $select.classList.add("bx-full-width"), $select.addEventListener("input", (e) => { + $select.addEventListener("input", (e) => { this.switchPreset(parseInt($select.value)); }); let $header = CE("div", { @@ -5172,15 +5134,20 @@ class BaseProfileManagerDialog extends NavigationDialog { })); this.$header = $header, this.$container = CE("div", { class: "bx-centered-dialog" }, CE("div", { class: "bx-dialog-title" }, CE("p", {}, this.title), createButton({ icon: BxIcon.CLOSE, - style: 64 | 1024 | 8, + style: 64 | 2048 | 8, onClick: (e) => this.hide() })), CE("div", {}, $header, this.$defaultNote = CE("div", { class: "bx-default-preset-note bx-gone" }, t("default-preset-note"))), CE("div", { class: "bx-dialog-content" }, this.$content)); } async refresh() { - await this.renderPresetsList(), this.switchPreset(this.currentPresetId); + await this.renderPresetsList(), this.$presets.value = this.currentPresetId.toString(), BxEvent.dispatch(this.$presets, "input", { manualTrigger: !0 }); } async onBeforeMount(configs = {}) { - if (configs?.id) this.currentPresetId = configs.id; + await this.renderPresetsList(); + let valid = !1; + if (typeof configs?.id === "number") { + if (configs.id in this.allPresets.data) this.currentPresetId = configs.id, this.activatedPresetId = configs.id, valid = !0; + } + if (!valid) this.currentPresetId = this.allPresets.default[0], this.activatedPresetId = null; this.refresh(); } getDialog() { @@ -5259,7 +5226,10 @@ class ControllerShortcutsManagerDialog extends BaseProfileManagerDialog { ]; constructor(title) { super(title, ControllerShortcutsTable.getInstance()); - let PREF_CONTROLLER_FRIENDLY_UI = getPref("ui.controllerFriendly"), $baseSelect = CE("select", { autocomplete: "off" }, CE("option", { value: "" }, "---")); + let $baseSelect = CE("select", { + class: "bx-full-width", + autocomplete: "off" + }, CE("option", { value: "" }, "---")); for (let groupLabel in SHORTCUT_ACTIONS) { let items = SHORTCUT_ACTIONS[groupLabel]; if (!items) continue; @@ -5275,15 +5245,6 @@ class ControllerShortcutsManagerDialog extends BaseProfileManagerDialog { let $content = CE("div", { class: "bx-controller-shortcuts-manager-container" }), onActionChanged = (e) => { - let $target = e.target, action = $target.value; - if (!PREF_CONTROLLER_FRIENDLY_UI) { - let $fakeSelect = $target.previousElementSibling, fakeText = "---"; - if (action) { - let $selectedOption = $target.options[$target.selectedIndex]; - fakeText = $selectedOption.parentElement.label + " ❯ " + $selectedOption.text; - } - $fakeSelect.firstElementChild.text = fakeText; - } if (!e.ignoreOnChange) this.updatePreset(); }, fragment = document.createDocumentFragment(); fragment.appendChild(CE("p", { class: "bx-shortcut-note" }, CE("span", { class: "bx-prompt" }, ""), ": " + t("controller-shortcuts-xbox-note"))); @@ -5293,12 +5254,10 @@ class ControllerShortcutsManagerDialog extends BaseProfileManagerDialog { _nearby: { orientation: "horizontal" } - }), $label = CE("label", { class: "bx-prompt" }, `${""}${prompt2}`), $div = CE("div", { class: "bx-shortcut-actions" }), $fakeSelect = null; - if (!PREF_CONTROLLER_FRIENDLY_UI) $fakeSelect = CE("select", { autocomplete: "off" }, CE("option", {}, "---")), $div.appendChild($fakeSelect); - let $select = BxSelectElement.create($baseSelect.cloneNode(!0)); - $select.dataset.button = button.toString(), $select.classList.add("bx-full-width"), $select.addEventListener("input", onActionChanged), this.selectActions[button] = [$select, $fakeSelect], $div.appendChild($select), setNearby($row, { + }), $label = CE("label", { class: "bx-prompt" }, `${""}${prompt2}`), $select = BxSelectElement.create($baseSelect.cloneNode(!0)); + $select.dataset.button = button.toString(), $select.addEventListener("input", onActionChanged), this.selectActions[button] = $select, setNearby($row, { focus: $select - }), $row.append($label, $div), fragment.appendChild($row); + }), $row.append($label, $select), fragment.appendChild($row); } $content.appendChild(fragment), this.$content = $content; } @@ -5311,8 +5270,8 @@ class ControllerShortcutsManagerDialog extends BaseProfileManagerDialog { this.currentPresetId = id; let isDefaultPreset = id <= 0, actions = preset.data, button; for (button in this.selectActions) { - let [$select, $fakeSelect] = this.selectActions[button]; - $select.value = actions.mapping[button] || "", $select.disabled = isDefaultPreset, $fakeSelect && ($fakeSelect.disabled = isDefaultPreset), BxEvent.dispatch($select, "input", { + let $select = this.selectActions[button]; + $select.value = actions.mapping[button] || "", $select.disabled = isDefaultPreset, BxEvent.dispatch($select, "input", { ignoreOnChange: !0, manualTrigger: !0 }); @@ -5322,12 +5281,294 @@ class ControllerShortcutsManagerDialog extends BaseProfileManagerDialog { updatePreset() { let newData = deepClone(this.BLANK_PRESET_DATA), button; for (button in this.selectActions) { - let [$select, _] = this.selectActions[button], action = $select.value; + let action = this.selectActions[button].value; if (!action) continue; newData.mapping[button] = action; } let preset = this.allPresets.data[this.currentPresetId]; - preset.data = newData, this.presetsDb.updatePreset(preset), StreamSettings.refreshControllerSettings(); + preset.data = newData, this.presetsDb.updatePreset(preset); + } + onBeforeUnmount() { + StreamSettings.refreshControllerSettings(), super.onBeforeUnmount(); + } +} +class BxDualNumberStepper extends HTMLInputElement { + controlValues; + controlMin; + controlMinDiff; + controlMax; + steps; + options; + onChange; + $text; + $rangeFrom; + $rangeTo; + $activeRange; + onRangeInput; + setValue; + getValue; + normalizeValue; + static create(key, values, options, onChange) { + options.suffix = options.suffix || "", options.disabled = !!options.disabled; + let $text, $rangeFrom, $rangeTo, self = CE("div", { + class: "bx-dual-number-stepper", + id: `bx_setting_${escapeCssSelector(key)}` + }, $text = CE("span")); + if (self.$text = $text, self.onChange = onChange, self.onRangeInput = BxDualNumberStepper.onRangeInput.bind(self), self.controlMin = options.min, self.controlMax = options.max, self.controlMinDiff = options.minDiff, self.options = options, self.steps = Math.max(options.steps || 1, 1), options.disabled) return self.disabled = !0, self; + return $rangeFrom = CE("input", { + type: "range", + min: self.controlMin, + max: self.controlMax, + step: self.steps, + tabindex: 0 + }), $rangeTo = $rangeFrom.cloneNode(), self.$rangeFrom = $rangeFrom, self.$rangeTo = $rangeTo, self.$activeRange = $rangeFrom, self.getValue = BxDualNumberStepper.getValues.bind(self), self.setValue = BxDualNumberStepper.setValues.bind(self), $rangeFrom.addEventListener("input", self.onRangeInput), $rangeTo.addEventListener("input", self.onRangeInput), self.addEventListener("input", self.onRangeInput), self.append(CE("div", {}, $rangeFrom, $rangeTo)), BxDualNumberStepper.setValues.call(self, values), self.addEventListener("contextmenu", BxDualNumberStepper.onContextMenu), setNearby(self, { + focus: $rangeFrom, + orientation: "vertical" + }), Object.defineProperty(self, "value", { + get() { + return self.controlValues; + }, + set(value) { + let from, to; + if (typeof value === "string") { + let tmp = value.split(","); + from = parseInt(tmp[0]), to = parseInt(tmp[1]); + } else if (Array.isArray(value)) [from, to] = value; + if (typeof from !== "undefined" && typeof to !== "undefined") BxDualNumberStepper.setValues.call(self, [from, to]); + } + }), self; + } + static setValues(values) { + let from, to; + if (values) [from, to] = BxDualNumberStepper.normalizeValues.call(this, values); + else from = this.controlMin, to = this.controlMax, values = [from, to]; + this.controlValues = [from, to], this.$text.textContent = BxDualNumberStepper.updateTextValue.call(this), this.$rangeFrom.value = from.toString(), this.$rangeTo.value = to.toString(); + let ratio = 100 / (this.controlMax - this.controlMin); + this.style.setProperty("--from", ratio * (from - this.controlMin) + "%"), this.style.setProperty("--to", ratio * (to - this.controlMin) + "%"); + } + static getValues() { + return this.controlValues || [this.controlMin, this.controlMax]; + } + static normalizeValues(values) { + let [from, to] = values; + if (this.$activeRange === this.$rangeFrom) to = Math.min(this.controlMax, to), from = Math.min(from, to), from = Math.min(to - this.controlMinDiff, from); + else from = Math.max(this.controlMin, from), to = Math.max(from, to), to = Math.max(this.controlMinDiff + from, to); + return to = Math.min(this.controlMax, to), from = Math.min(from, to), [from, to]; + } + static onRangeInput(e) { + this.$activeRange = e.target; + let values = BxDualNumberStepper.normalizeValues.call(this, [parseInt(this.$rangeFrom.value), parseInt(this.$rangeTo.value)]); + if (BxDualNumberStepper.setValues.call(this, values), !e.ignoreOnChange && this.onChange) this.onChange(e, values); + } + static onContextMenu(e) { + e.preventDefault(); + } + static updateTextValue() { + let values = this.controlValues, textContent = null; + if (this.options.customTextValue) textContent = this.options.customTextValue(values, this.controlMin, this.controlMax); + if (textContent === null) { + let [from, to] = values; + if (from === this.controlMin && to === this.controlMax) textContent = t("default"); + else { + let pad = to.toString().length; + textContent = `${from.toString().padStart(pad)} - ${to.toString().padEnd(pad)}${this.options.suffix}`; + } + } + return textContent; + } +} +class ControllerCustomizationsManagerDialog extends BaseProfileManagerDialog { + static instance; + static getInstance = () => ControllerCustomizationsManagerDialog.instance ?? (ControllerCustomizationsManagerDialog.instance = new ControllerCustomizationsManagerDialog(t("controller-customization"))); + $vibrationIntensity; + $leftTriggerRange; + $rightTriggerRange; + $leftStickDeadzone; + $rightStickDeadzone; + $btnDetect; + BLANK_PRESET_DATA = { + mapping: {}, + settings: { + leftTriggerRange: [0, 100], + rightTriggerRange: [0, 100], + leftStickDeadzone: [0, 100], + rightStickDeadzone: [0, 100], + vibrationIntensity: 100 + } + }; + selectsMap = {}; + selectsOrder = []; + isDetectingButton = !1; + detectIntervalId = null; + BUTTONS_ORDER = [ + 0, + 1, + 2, + 3, + 12, + 15, + 13, + 14, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 104, + 204, + 17 + ]; + constructor(title) { + super(title, ControllerCustomizationsTable.getInstance()); + this.render(); + } + render() { + let isControllerFriendly = getPref("ui.controllerFriendly"), $rows = CE("div", { class: "bx-buttons-grid" }), $baseSelect = CE("select", { class: "bx-full-width" }, CE("option", { value: "" }, "---"), CE("option", { value: "false", _dataset: { label: "🚫" } }, isControllerFriendly ? "🚫" : t("off"))), $baseButtonSelect = $baseSelect.cloneNode(!0), $baseStickSelect = $baseSelect.cloneNode(!0), onButtonChanged = (e) => { + if (!e.ignoreOnChange) this.updatePreset(); + }, boundUpdatePreset = this.updatePreset.bind(this); + for (let gamepadKey of this.BUTTONS_ORDER) { + if (gamepadKey === 17) continue; + let name = GamepadKeyName[gamepadKey][isControllerFriendly ? 1 : 0]; + (gamepadKey === 104 || gamepadKey === 204 ? $baseStickSelect : $baseButtonSelect).appendChild(CE("option", { + value: gamepadKey, + _dataset: { label: GamepadKeyName[gamepadKey][1] } + }, name)); + } + for (let gamepadKey of this.BUTTONS_ORDER) { + let [buttonName, buttonPrompt] = GamepadKeyName[gamepadKey], $clonedSelect = (gamepadKey === 104 || gamepadKey === 204 ? $baseStickSelect : $baseButtonSelect).cloneNode(!0); + $clonedSelect.querySelector(`option[value="${gamepadKey}"]`)?.remove(); + let $select = BxSelectElement.create($clonedSelect); + $select.dataset.index = gamepadKey.toString(), $select.addEventListener("input", onButtonChanged), this.selectsMap[gamepadKey] = $select, this.selectsOrder.push(gamepadKey); + let $row = CE("div", { + class: "bx-controller-key-row", + _nearby: { orientation: "horizontal" } + }, CE("label", { title: buttonName }, buttonPrompt), $select); + $rows.append($row); + } + if (getPref("ui.controllerFriendly")) for (let i = 0;i < this.selectsOrder.length; i++) { + let $select = this.selectsMap[this.selectsOrder[i]], directions = { + 1: i - 2, + 3: i + 2, + 4: i - 1, + 2: i + 1 + }; + for (let dir in directions) { + let idx = directions[dir]; + if (typeof this.selectsOrder[idx] === "undefined") continue; + let $targetSelect = this.selectsMap[this.selectsOrder[idx]]; + setNearby($select, { + [dir]: $targetSelect + }); + } + } + let params = { + min: 0, + minDiff: 1, + max: 100, + steps: 1 + }; + this.$content = CE("div", { class: "bx-controller-customizations-container" }, this.$btnDetect = createButton({ + label: t("detect-controller-button"), + classes: ["bx-btn-detect"], + style: 4096 | 64 | 128, + onClick: () => { + this.startDetectingButton(); + } + }), $rows, createSettingRow(t("vibration-intensity"), this.$vibrationIntensity = BxNumberStepper.create("controller_vibration_intensity", 50, 0, 100, { + steps: 10, + suffix: "%", + exactTicks: 20, + customTextValue: (value) => { + return value = parseInt(value), value === 0 ? t("off") : value + "%"; + } + }, boundUpdatePreset)), createSettingRow(t("left-trigger-range"), this.$leftTriggerRange = BxDualNumberStepper.create("left-trigger-range", this.BLANK_PRESET_DATA.settings.leftTriggerRange, params, boundUpdatePreset)), createSettingRow(t("right-trigger-range"), this.$rightTriggerRange = BxDualNumberStepper.create("right-trigger-range", this.BLANK_PRESET_DATA.settings.rightTriggerRange, params, boundUpdatePreset)), createSettingRow(t("left-stick-deadzone"), this.$leftStickDeadzone = BxDualNumberStepper.create("left-stick-deadzone", this.BLANK_PRESET_DATA.settings.leftStickDeadzone, params, boundUpdatePreset)), createSettingRow(t("right-stick-deadzone"), this.$rightStickDeadzone = BxDualNumberStepper.create("right-stick-deadzone", this.BLANK_PRESET_DATA.settings.rightStickDeadzone, params, boundUpdatePreset))); + } + startDetectingButton() { + this.isDetectingButton = !0; + let { $btnDetect } = this; + $btnDetect.classList.add("bx-monospaced", "bx-blink-me"), $btnDetect.disabled = !0; + let count = 4; + $btnDetect.textContent = `[${count}] ${t("press-any-button")}`, this.detectIntervalId = window.setInterval(() => { + if (count -= 1, count === 0) { + this.stopDetectingButton(), $btnDetect.focus(); + return; + } + $btnDetect.textContent = `[${count}] ${t("press-any-button")}`; + }, 1000); + } + stopDetectingButton() { + let { $btnDetect } = this; + $btnDetect.classList.remove("bx-monospaced", "bx-blink-me"), $btnDetect.textContent = t("detect-controller-button"), $btnDetect.disabled = !1, this.isDetectingButton = !1, this.detectIntervalId && window.clearInterval(this.detectIntervalId), this.detectIntervalId = null; + } + async onBeforeMount() { + this.stopDetectingButton(), super.onBeforeMount(...arguments); + } + onBeforeUnmount() { + this.stopDetectingButton(), StreamSettings.refreshControllerSettings(), super.onBeforeUnmount(); + } + handleGamepad(button) { + if (!this.isDetectingButton) return super.handleGamepad(button); + if (button in this.BUTTONS_ORDER) { + this.stopDetectingButton(); + let $select = this.selectsMap[button], $label = $select.previousElementSibling; + if ($label.addEventListener("animationend", () => { + $label.classList.remove("bx-horizontal-shaking"); + }, { once: !0 }), $label.classList.add("bx-horizontal-shaking"), getPref("ui.controllerFriendly")) + this.dialogManager.focus($select); + } + return !0; + } + switchPreset(id) { + let preset = this.allPresets.data[id]; + if (!preset) { + this.currentPresetId = 0; + return; + } + let { + $btnDetect, + $vibrationIntensity, + $leftStickDeadzone, + $rightStickDeadzone, + $leftTriggerRange, + $rightTriggerRange, + selectsMap + } = this, presetData = preset.data; + this.currentPresetId = id; + let isDefaultPreset = id <= 0; + this.updateButtonStates(), $btnDetect.classList.toggle("bx-gone", isDefaultPreset); + let buttonIndex; + for (buttonIndex in selectsMap) { + buttonIndex = buttonIndex; + let $select = selectsMap[buttonIndex]; + if (!$select) continue; + let mappedButton = presetData.mapping[buttonIndex]; + $select.value = typeof mappedButton === "undefined" ? "" : mappedButton.toString(), $select.disabled = isDefaultPreset, BxEvent.dispatch($select, "input", { + ignoreOnChange: !0, + manualTrigger: !0 + }); + } + presetData.settings = Object.assign(this.BLANK_PRESET_DATA.settings, presetData.settings), $vibrationIntensity.value = presetData.settings.vibrationIntensity.toString(), $vibrationIntensity.dataset.disabled = isDefaultPreset.toString(), $leftStickDeadzone.dataset.disabled = $rightStickDeadzone.dataset.disabled = $leftTriggerRange.dataset.disabled = $rightTriggerRange.dataset.disabled = isDefaultPreset.toString(), $leftStickDeadzone.setValue(presetData.settings.leftStickDeadzone), $rightStickDeadzone.setValue(presetData.settings.rightStickDeadzone), $leftTriggerRange.setValue(presetData.settings.leftTriggerRange), $rightTriggerRange.setValue(presetData.settings.rightTriggerRange); + } + updatePreset() { + let newData = deepClone(this.BLANK_PRESET_DATA), gamepadKey; + for (gamepadKey in this.selectsMap) { + let value = this.selectsMap[gamepadKey].value; + if (!value) continue; + let mapTo = value === "false" ? !1 : parseInt(value); + newData.mapping[gamepadKey] = mapTo; + } + Object.assign(newData.settings, { + vibrationIntensity: parseInt(this.$vibrationIntensity.value), + leftStickDeadzone: this.$leftStickDeadzone.getValue(), + rightStickDeadzone: this.$rightStickDeadzone.getValue(), + leftTriggerRange: this.$leftTriggerRange.getValue(), + rightTriggerRange: this.$rightTriggerRange.getValue() + }); + let preset = this.allPresets.data[this.currentPresetId]; + preset.data = newData, this.presetsDb.updatePreset(preset); } } class ControllerExtraSettings extends HTMLElement { @@ -5335,7 +5576,7 @@ class ControllerExtraSettings extends HTMLElement { controllerIds; $selectControllers; $selectShortcuts; - $vibrationIntensity; + $selectCustomization; updateLayout; switchController; getCurrentControllerId; @@ -5346,39 +5587,44 @@ class ControllerExtraSettings extends HTMLElement { }); $container.updateLayout = ControllerExtraSettings.updateLayout.bind($container), $container.switchController = ControllerExtraSettings.switchController.bind($container), $container.getCurrentControllerId = ControllerExtraSettings.getCurrentControllerId.bind($container), $container.saveSettings = ControllerExtraSettings.saveSettings.bind($container); let $selectControllers = BxSelectElement.create(CE("select", { + class: "bx-full-width", autocomplete: "off", _on: { input: (e) => { $container.switchController($selectControllers.value); } } - })); - $selectControllers.classList.add("bx-full-width"); - let $selectShortcuts = BxSelectElement.create(CE("select", { + })), $selectShortcuts = BxSelectElement.create(CE("select", { autocomplete: "off", - _on: { - input: $container.saveSettings - } - })), $vibrationIntensity = BxNumberStepper.create("controller_vibration_intensity", 50, 0, 100, { - steps: 10, - suffix: "%", - exactTicks: 20, - customTextValue: (value) => { - return value = parseInt(value), value === 0 ? t("off") : value + "%"; - } - }, $container.saveSettings); - return $container.append(CE("span", {}, t("no-controllers-connected")), CE("div", { class: "bx-controller-extra-wrapper" }, $selectControllers, CE("div", { class: "bx-sub-content-box" }, createSettingRow(t("controller-shortcuts-in-game"), CE("div", { + _on: { input: $container.saveSettings } + })), $selectCustomization = BxSelectElement.create(CE("select", { + autocomplete: "off", + _on: { input: $container.saveSettings } + })); + return $container.append(CE("span", {}, t("no-controllers-connected")), CE("div", { class: "bx-controller-extra-wrapper" }, $selectControllers, CE("div", { class: "bx-sub-content-box" }, createSettingRow(t("in-game-controller-shortcuts"), CE("div", { class: "bx-preset-row", - _nearby: { - orientation: "horizontal" - } + _nearby: { orientation: "horizontal" } }, $selectShortcuts, createButton({ - label: t("manage"), - style: 64, + title: t("manage"), + icon: BxIcon.MANAGE, + style: 64 | 1 | 512, onClick: () => ControllerShortcutsManagerDialog.getInstance().show({ id: parseInt($container.$selectShortcuts.value) }) - })), { multiLines: !0 }), createSettingRow(t("vibration-intensity"), $vibrationIntensity)))), $container.$selectControllers = $selectControllers, $container.$selectShortcuts = $selectShortcuts, $container.$vibrationIntensity = $vibrationIntensity, $container.updateLayout(), window.addEventListener("gamepadconnected", $container.updateLayout), window.addEventListener("gamepaddisconnected", $container.updateLayout), this.onMountedCallbacks.push(() => { + })), { multiLines: !0 }), createSettingRow(t("in-game-controller-customization"), CE("div", { + class: "bx-preset-row", + _nearby: { orientation: "horizontal" } + }, $selectCustomization, createButton({ + title: t("manage"), + icon: BxIcon.MANAGE, + style: 64 | 1 | 512, + onClick: () => ControllerCustomizationsManagerDialog.getInstance().show({ + id: $container.$selectCustomization.value ? parseInt($container.$selectCustomization.value) : null + }) + })), { + multiLines: !0, + $note: CE("div", { class: "bx-settings-dialog-note" }, "ⓘ " + t("slightly-increase-input-latency")) + })))), $container.$selectControllers = $selectControllers, $container.$selectShortcuts = $selectShortcuts, $container.$selectCustomization = $selectCustomization, $container.updateLayout(), window.addEventListener("gamepadconnected", $container.updateLayout), window.addEventListener("gamepaddisconnected", $container.updateLayout), this.onMountedCallbacks.push(() => { $container.updateLayout(); }), $container; } @@ -5393,16 +5639,18 @@ class ControllerExtraSettings extends HTMLElement { this.$selectControllers.appendChild($fragment); let allShortcutPresets = await ControllerShortcutsTable.getInstance().getPresets(); renderPresetsList(this.$selectShortcuts, allShortcutPresets, null, { addOffValue: !0 }); + let allCustomizationPresets = await ControllerCustomizationsTable.getInstance().getPresets(); + renderPresetsList(this.$selectCustomization, allCustomizationPresets, null, { addOffValue: !0 }); for (let name of this.controllerIds) { let $option = CE("option", { value: name }, name); $fragment.appendChild($option); } - BxEvent.dispatch(this.$selectControllers, "input"); + BxEvent.dispatch(this.$selectControllers, "input"), calculateSelectBoxes(this); } static async switchController(id) { if (this.currentControllerId = id, !this.getCurrentControllerId()) return; let controllerSettings = await ControllerSettingsTable.getInstance().getControllerData(this.currentControllerId); - this.$selectShortcuts.value = controllerSettings.shortcutPresetId.toString(), this.$vibrationIntensity.value = controllerSettings.vibrationIntensity.toString(); + this.$selectShortcuts.value = controllerSettings.shortcutPresetId.toString(), this.$selectCustomization.value = controllerSettings.customizationPresetId.toString(); } static getCurrentControllerId() { if (this.currentControllerId) { @@ -5419,7 +5667,7 @@ class ControllerExtraSettings extends HTMLElement { id: this.currentControllerId, data: { shortcutPresetId: parseInt(this.$selectShortcuts.value), - vibrationIntensity: parseInt(this.$vibrationIntensity.value) + customizationPresetId: parseInt(this.$selectCustomization.value) } }; await ControllerSettingsTable.getInstance().put(data), StreamSettings.refreshControllerSettings(); @@ -5525,7 +5773,7 @@ class SuggestionsSetting { _nearby: { orientation: "vertical" } - }, BxSelectElement.create($select, !0), $suggestedSettings, $btnApply, BX_FLAGS.DeviceInfo.deviceType.includes("android") && CE("a", { + }, BxSelectElement.create($select), $suggestedSettings, $btnApply, BX_FLAGS.DeviceInfo.deviceType.includes("android") && CE("a", { class: "bx-suggest-link bx-focusable", href: "https://better-xcloud.github.io/guide/android-webview-tweaks/", target: "_blank", @@ -5764,9 +6012,7 @@ class MkbMappingManagerDialog extends BaseProfileManagerDialog { }), $elm.dataset.buttonIndex = buttonIndex.toString(), $elm.dataset.keySlot = i.toString(), $elm.addEventListener("mouseup", this.onBindingKey), $fragment.appendChild($elm), this.allKeyElements.push($elm); let $keyRow = CE("div", { class: "bx-mkb-key-row", - _nearby: { - orientation: "horizontal" - } + _nearby: { orientation: "horizontal" } }, CE("label", { title: buttonName }, buttonPrompt), $fragment); $rows.appendChild($keyRow); } @@ -5818,7 +6064,10 @@ class MkbMappingManagerDialog extends BaseProfileManagerDialog { name: oldPreset.name, data: presetData }; - this.presetsDb.updatePreset(newPreset), this.allPresets.data[this.currentPresetId] = newPreset, StreamSettings.refreshMkbSettings(); + this.presetsDb.updatePreset(newPreset), this.allPresets.data[this.currentPresetId] = newPreset; + } + onBeforeUnmount() { + StreamSettings.refreshMkbSettings(), super.onBeforeUnmount(); } } class KeyboardShortcutsManagerDialog extends BaseProfileManagerDialog { @@ -5895,7 +6144,10 @@ class KeyboardShortcutsManagerDialog extends BaseProfileManagerDialog { name: oldPreset.name, data: presetData }; - this.presetsDb.updatePreset(newPreset), this.allPresets.data[this.currentPresetId] = newPreset, StreamSettings.refreshKeyboardShortcuts(); + this.presetsDb.updatePreset(newPreset), this.allPresets.data[this.currentPresetId] = newPreset; + } + onBeforeUnmount() { + StreamSettings.refreshKeyboardShortcuts(), super.onBeforeUnmount(); } } class MkbExtraSettings extends HTMLElement { @@ -5925,8 +6177,9 @@ class MkbExtraSettings extends HTMLElement { orientation: "horizontal" } }, $mappingPresets, createButton({ - label: t("manage"), - style: 64, + title: t("manage"), + icon: BxIcon.MANAGE, + style: 64 | 1 | 512, onClick: () => MkbMappingManagerDialog.getInstance().show({ id: parseInt($container.$mappingPresets.value) }) @@ -5934,14 +6187,15 @@ class MkbExtraSettings extends HTMLElement { createSettingRow(t("virtual-controller-slot"), SettingElement.fromPref("mkb.p1.slot", STORAGE.Global, () => { EmulatedMkbHandler.getInstance()?.updateGamepadSlots(); })) - ] : [], createSettingRow(t("keyboard-shortcuts-in-game"), CE("div", { + ] : [], createSettingRow(t("in-game-keyboard-shortcuts"), CE("div", { class: "bx-preset-row", _nearby: { orientation: "horizontal" } }, $shortcutsPresets, createButton({ - label: t("manage"), - style: 64, + title: t("manage"), + icon: BxIcon.MANAGE, + style: 64 | 1 | 512, onClick: () => KeyboardShortcutsManagerDialog.getInstance().show({ id: parseInt($container.$shortcutsPresets.value) }) @@ -6293,20 +6547,6 @@ class SettingsDialog extends NavigationDialog { }] }]; TAB_CONTROLLER_ITEMS = [ - STATES.browser.capabilities.deviceVibration && { - group: "device", - label: t("device"), - items: [{ - pref: "deviceVibration.mode", - multiLines: !0, - unsupported: !STATES.browser.capabilities.deviceVibration, - onChange: () => StreamSettings.refreshControllerSettings() - }, { - pref: "deviceVibration.intensity", - unsupported: !STATES.browser.capabilities.deviceVibration, - onChange: () => StreamSettings.refreshControllerSettings() - }] - }, { group: "controller", label: t("controller"), @@ -6358,6 +6598,20 @@ class SettingsDialog extends NavigationDialog { }); } }] + }, + STATES.browser.capabilities.deviceVibration && { + group: "device", + label: t("device"), + items: [{ + pref: "deviceVibration.mode", + multiLines: !0, + unsupported: !STATES.browser.capabilities.deviceVibration, + onChange: () => StreamSettings.refreshControllerSettings() + }, { + pref: "deviceVibration.intensity", + unsupported: !STATES.browser.capabilities.deviceVibration, + onChange: () => StreamSettings.refreshControllerSettings() + }] } ]; TAB_MKB_ITEMS = () => [ @@ -6498,9 +6752,8 @@ class SettingsDialog extends NavigationDialog { } let $child, children = Array.from(this.$tabContents.children); for ($child of children) - if ($child.dataset.tabGroup === $svg.dataset.group) { - if ($child.classList.remove("bx-gone"), getPref("ui.controllerFriendly")) this.dialogManager.calculateSelectBoxes($child); - } else $child.classList.add("bx-gone"); + if ($child.dataset.tabGroup === $svg.dataset.group) $child.classList.remove("bx-gone"), calculateSelectBoxes($child); + else $child.classList.add("bx-gone"); for (let $child2 of Array.from(this.$tabs.children)) $child2.classList.remove("bx-active"); $svg.classList.add("bx-active"); @@ -6534,7 +6787,6 @@ class SettingsDialog extends NavigationDialog { } }, $control = CE("select", { id: `bx_setting_${escapeCssSelector(setting.pref)}`, - title: setting.label, tabindex: 0 }); $control.name = $control.id, $control.addEventListener("input", (e) => { @@ -6633,7 +6885,7 @@ class SettingsDialog extends NavigationDialog { label = createButton({ label, url: "https://github.com/redphx/better-xcloud/releases", - style: 2048 | 16 | 64 + style: 4096 | 16 | 64 }); } if (label) { @@ -6904,7 +7156,7 @@ class TrueAchievements { label: t("true-achievements"), url: "#", icon: BxIcon.TRUE_ACHIEVEMENTS, - style: 64 | 8 | 128 | 4096, + style: 64 | 8 | 128 | 8192, onClick: this.onClick }), this.$button = createButton({ label: t("true-achievements"), @@ -7218,7 +7470,7 @@ class HeaderSection { classes: ["bx-header-remote-play-button", "bx-gone"], icon: BxIcon.REMOTE_PLAY, title: t("remote-play"), - style: 8 | 64 | 1024, + style: 8 | 64 | 2048, onClick: (e) => RemotePlayManager.getInstance()?.togglePopup() }), this.$btnSettings = createButton({ classes: ["bx-header-settings-button"], @@ -8164,7 +8416,7 @@ function getOsNameFromResolution(resolution) { return osName; } function addCss() { - let css = ':root{--bx-title-font:Bahnschrift,Arial,Helvetica,sans-serif;--bx-title-font-semibold:Bahnschrift Semibold,Arial,Helvetica,sans-serif;--bx-normal-font:"Segoe UI",Arial,Helvetica,sans-serif;--bx-monospaced-font:Consolas,"Courier New",Courier,monospace;--bx-promptfont-font:promptfont;--bx-button-height:40px;--bx-default-button-color:#2d3036;--bx-default-button-rgb:45,48,54;--bx-default-button-hover-color:#515863;--bx-default-button-hover-rgb:81,88,99;--bx-default-button-active-color:#222428;--bx-default-button-active-rgb:34,36,40;--bx-default-button-disabled-color:#8e8e8e;--bx-default-button-disabled-rgb:142,142,142;--bx-primary-button-color:#008746;--bx-primary-button-rgb:0,135,70;--bx-primary-button-hover-color:#04b358;--bx-primary-button-hover-rgb:4,179,88;--bx-primary-button-active-color:#044e2a;--bx-primary-button-active-rgb:4,78,42;--bx-primary-button-disabled-color:#448262;--bx-primary-button-disabled-rgb:68,130,98;--bx-warning-button-color:#c16e04;--bx-warning-button-rgb:193,110,4;--bx-warning-button-hover-color:#fa9005;--bx-warning-button-hover-rgb:250,144,5;--bx-warning-button-active-color:#965603;--bx-warning-button-active-rgb:150,86,3;--bx-warning-button-disabled-color:#a2816c;--bx-warning-button-disabled-rgb:162,129,108;--bx-danger-button-color:#c10404;--bx-danger-button-rgb:193,4,4;--bx-danger-button-hover-color:#e61d1d;--bx-danger-button-hover-rgb:230,29,29;--bx-danger-button-active-color:#a26c6c;--bx-danger-button-active-rgb:162,108,108;--bx-danger-button-disabled-color:#df5656;--bx-danger-button-disabled-rgb:223,86,86;--bx-fullscreen-text-z-index:9999;--bx-toast-z-index:6000;--bx-key-binding-dialog-z-index:5010;--bx-key-binding-dialog-overlay-z-index:5000;--bx-stats-bar-z-index:4010;--bx-navigation-dialog-z-index:3010;--bx-navigation-dialog-overlay-z-index:3000;--bx-mkb-pointer-lock-msg-z-index:2000;--bx-game-bar-z-index:1000;--bx-screenshot-animation-z-index:200;--bx-wait-time-box-z-index:100}@font-face{font-family:\'promptfont\';src:url("https://redphx.github.io/better-xcloud/fonts/promptfont.otf")}div[class^=HUDButton-module__hiddenContainer] ~ div:not([class^=HUDButton-module__hiddenContainer]){opacity:0;pointer-events:none !important;position:absolute;top:-9999px;left:-9999px}@media screen and (max-width:640px){header a[href="/play"]{display:none}}.bx-full-width{width:100% !important}.bx-full-height{height:100% !important}.bx-no-scroll{overflow:hidden !important}.bx-hide-scroll-bar{scrollbar-width:none}.bx-hide-scroll-bar::-webkit-scrollbar{display:none}.bx-gone{display:none !important}.bx-offscreen{position:absolute !important;top:-9999px !important;left:-9999px !important;visibility:hidden !important}.bx-hidden{visibility:hidden !important}.bx-invisible{opacity:0}.bx-unclickable{pointer-events:none}.bx-pixel{width:1px !important;height:1px !important}.bx-no-margin{margin:0 !important}.bx-no-padding{padding:0 !important}.bx-prompt{font-family:var(--bx-promptfont-font) !important}.bx-line-through{text-decoration:line-through !important}.bx-normal-case{text-transform:none !important}.bx-normal-link{text-transform:none !important;text-align:left !important;font-weight:400 !important;font-family:var(--bx-normal-font) !important}select[multiple],select[multiple]:focus{overflow:auto;border:none}select[multiple] option,select[multiple]:focus option{padding:4px 6px}select[multiple] option:checked,select[multiple]:focus option:checked{background:#1a7bc0 linear-gradient(0deg,#1a7bc0 0%,#1a7bc0 100%)}select[multiple] option:checked::before,select[multiple]:focus option:checked::before{content:\'☑️\';font-size:12px;display:inline-block;margin-right:6px;height:100%;line-height:100%;vertical-align:middle}#headerArea,#uhfSkipToMain,.uhf-footer{display:none}div[class*=NotFocusedDialog]{position:absolute !important;top:-9999px !important;left:-9999px !important;width:0 !important;height:0 !important}#game-stream video:not([src]){visibility:hidden}div[class*=SupportedInputsBadge]:not(:has(:nth-child(2))),div[class*=SupportedInputsBadge] svg:first-of-type{display:none}.bx-game-tile-wait-time{position:absolute;top:0;left:0;z-index:1;background:rgba(0,0,0,0.5);display:flex;border-radius:4px 0 4px 0;align-items:center;padding:4px 8px}.bx-game-tile-wait-time svg{width:14px;height:16px;margin-right:2px}.bx-game-tile-wait-time span{display:inline-block;height:16px;line-height:16px;font-size:12px;font-weight:bold;margin-left:2px}.bx-game-tile-wait-time[data-duration=short]{background-color:rgba(0,133,133,0.75)}.bx-game-tile-wait-time[data-duration=medium]{background-color:rgba(213,133,0,0.75)}.bx-game-tile-wait-time[data-duration=long]{background-color:rgba(150,0,0,0.75)}.bx-fullscreen-text{position:fixed;top:0;bottom:0;left:0;right:0;background:rgba(0,0,0,0.8);z-index:var(--bx-fullscreen-text-z-index);line-height:100vh;color:#fff;text-align:center;font-weight:400;font-family:var(--bx-normal-font);font-size:1.3rem;user-select:none;-webkit-user-select:none}#root section[class*=DeviceCodePage-module__page]{margin-left:20px !important;margin-right:20px !important;margin-top:20px !important;max-width:800px !important}#root div[class*=DeviceCodePage-module__back]{display:none}.bx-blink-me{animation:bx-blinker 1s linear infinite}@-moz-keyframes bx-blinker{100%{opacity:0}}@-webkit-keyframes bx-blinker{100%{opacity:0}}@-o-keyframes bx-blinker{100%{opacity:0}}@keyframes bx-blinker{100%{opacity:0}}.bx-button{--button-rgb:var(--bx-default-button-rgb);--button-hover-rgb:var(--bx-default-button-hover-rgb);--button-active-rgb:var(--bx-default-button-active-rgb);--button-disabled-rgb:var(--bx-default-button-disabled-rgb);background-color:rgb(var(--button-rgb));user-select:none;-webkit-user-select:none;color:#fff;font-family:var(--bx-title-font-semibold);font-size:14px;border:none;font-weight:400;height:var(--bx-button-height);border-radius:4px;padding:0 8px;text-transform:uppercase;cursor:pointer;overflow:hidden}.bx-button:not([disabled]):active{background-color:rgb(var(--button-active-rgb))}.bx-button:focus{outline:none !important}.bx-button:not([disabled]):not(:active):hover,.bx-button:not([disabled]):not(:active).bx-focusable:focus{background-color:rgb(var(--button-hover-rgb))}.bx-button:disabled{cursor:default;background-color:rgb(var(--button-disabled-rgb))}.bx-button.bx-ghost{background-color:transparent}.bx-button.bx-ghost:not([disabled]):not(:active):hover,.bx-button.bx-ghost:not([disabled]):not(:active).bx-focusable:focus{background-color:rgb(var(--button-hover-rgb))}.bx-button.bx-primary{--button-rgb:var(--bx-primary-button-rgb)}.bx-button.bx-primary:not([disabled]):active{--button-active-rgb:var(--bx-primary-button-active-rgb)}.bx-button.bx-primary:not([disabled]):not(:active):hover,.bx-button.bx-primary:not([disabled]):not(:active).bx-focusable:focus{--button-hover-rgb:var(--bx-primary-button-hover-rgb)}.bx-button.bx-primary:disabled{--button-disabled-rgb:var(--bx-primary-button-disabled-rgb)}.bx-button.bx-warning{--button-rgb:var(--bx-warning-button-rgb)}.bx-button.bx-warning:not([disabled]):active{--button-active-rgb:var(--bx-warning-button-active-rgb)}.bx-button.bx-warning:not([disabled]):not(:active):hover,.bx-button.bx-warning:not([disabled]):not(:active).bx-focusable:focus{--button-hover-rgb:var(--bx-warning-button-hover-rgb)}.bx-button.bx-warning:disabled{--button-disabled-rgb:var(--bx-warning-button-disabled-rgb)}.bx-button.bx-danger{--button-rgb:var(--bx-danger-button-rgb)}.bx-button.bx-danger:not([disabled]):active{--button-active-rgb:var(--bx-danger-button-active-rgb)}.bx-button.bx-danger:not([disabled]):not(:active):hover,.bx-button.bx-danger:not([disabled]):not(:active).bx-focusable:focus{--button-hover-rgb:var(--bx-danger-button-hover-rgb)}.bx-button.bx-danger:disabled{--button-disabled-rgb:var(--bx-danger-button-disabled-rgb)}.bx-button.bx-frosted{--button-alpha:.2;background-color:rgba(var(--button-rgb), var(--button-alpha));backdrop-filter:blur(4px) brightness(1.5)}.bx-button.bx-frosted:not([disabled]):not(:active):hover,.bx-button.bx-frosted:not([disabled]):not(:active).bx-focusable:focus{background-color:rgba(var(--button-hover-rgb), var(--button-alpha))}.bx-button.bx-drop-shadow{box-shadow:0 0 4px rgba(0,0,0,0.502)}.bx-button.bx-tall{height:calc(var(--bx-button-height) * 1.5) !important}.bx-button.bx-circular{border-radius:var(--bx-button-height);width:var(--bx-button-height);height:var(--bx-button-height)}.bx-button svg{display:inline-block;width:16px;height:var(--bx-button-height)}.bx-button span{display:inline-block;line-height:var(--bx-button-height);vertical-align:middle;color:#fff;overflow:hidden;white-space:nowrap}.bx-button span:not(:only-child){margin-left:10px}.bx-button.bx-button-multi-lines{height:auto;text-align:left;padding:10px 0}.bx-button.bx-button-multi-lines span{line-height:unset;display:block}.bx-button.bx-button-multi-lines span:last-of-type{text-transform:none;font-weight:normal;font-family:"Segoe Sans Variable Text";font-size:12px;margin-top:4px}.bx-focusable{position:relative;overflow:visible}.bx-focusable::after{border:2px solid transparent;border-radius:10px}.bx-focusable:focus::after{content:\'\';border-color:#fff;position:absolute;top:-6px;left:-6px;right:-6px;bottom:-6px}html[data-active-input=touch] .bx-focusable:focus::after,html[data-active-input=mouse] .bx-focusable:focus::after{border-color:transparent !important}.bx-focusable.bx-circular::after{border-radius:var(--bx-button-height)}a.bx-button{display:inline-block}a.bx-button.bx-full-width{text-align:center}button.bx-inactive{pointer-events:none;opacity:.2;background:transparent !important}.bx-header-remote-play-button{height:auto;margin-right:8px !important}.bx-header-remote-play-button svg{width:24px;height:24px}.bx-header-settings-button{line-height:30px;font-size:14px;text-transform:uppercase;position:relative}.bx-header-settings-button[data-update-available]::before{content:\'🌟\' !important;line-height:var(--bx-button-height);display:inline-block;margin-left:4px}.bx-key-binding-dialog-overlay{position:fixed;inset:0;z-index:var(--bx-key-binding-dialog-overlay-z-index);background:#000;opacity:50%}.bx-key-binding-dialog{display:flex;flex-flow:column;max-height:90vh;position:fixed;top:50%;left:50%;margin-right:-50%;transform:translate(-50%,-50%);min-width:420px;padding:16px;border-radius:8px;z-index:var(--bx-key-binding-dialog-z-index);background:#1a1b1e;color:#fff;font-weight:400;font-size:16px;font-family:var(--bx-normal-font);box-shadow:0 0 6px #000;user-select:none;-webkit-user-select:none}.bx-key-binding-dialog *:focus{outline:none !important}.bx-key-binding-dialog h2{margin-bottom:12px;color:#fff;display:block;font-family:var(--bx-title-font);font-size:32px;font-weight:400;line-height:var(--bx-button-height)}.bx-key-binding-dialog > div{overflow:auto;padding:2px 0}.bx-key-binding-dialog > button{padding:8px 32px;margin:10px auto 0;border:none;border-radius:4px;display:block;background-color:#2d3036;text-align:center;color:#fff;text-transform:uppercase;font-family:var(--bx-title-font);font-weight:400;line-height:18px;font-size:14px}@media (hover:hover){.bx-key-binding-dialog > button:hover{background-color:#515863}}.bx-key-binding-dialog > button:focus{background-color:#515863}.bx-key-binding-dialog ul{margin-bottom:1rem}.bx-key-binding-dialog ul li{display:none}.bx-key-binding-dialog ul[data-flags*="[1]"] > li[data-flag="1"],.bx-key-binding-dialog ul[data-flags*="[2]"] > li[data-flag="2"],.bx-key-binding-dialog ul[data-flags*="[4]"] > li[data-flag="4"],.bx-key-binding-dialog ul[data-flags*="[8]"] > li[data-flag="8"]{display:list-item}@media screen and (max-width:450px){.bx-key-binding-dialog{min-width:100%}}.bx-navigation-dialog{position:absolute;z-index:var(--bx-navigation-dialog-z-index);font-family:var(--bx-title-font)}.bx-navigation-dialog *:focus{outline:none !important}.bx-navigation-dialog select:disabled{-webkit-appearance:none;text-align-last:right;text-align:right;color:#fff;background:#131416;border:none;border-radius:4px;padding:0 5px}.bx-navigation-dialog-overlay{position:fixed;background:rgba(11,11,11,0.89);top:0;left:0;right:0;bottom:0;z-index:var(--bx-navigation-dialog-overlay-z-index)}.bx-navigation-dialog-overlay[data-is-playing="true"]{background:transparent}.bx-centered-dialog{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);color:#fff;background:#1a1b1e;border-radius:10px;width:450px;max-width:calc(100vw - 20px);margin:0 0 0 auto;padding:16px;max-height:95vh;flex-direction:column;overflow:hidden;display:flex;flex-direction:column}.bx-centered-dialog .bx-dialog-title{display:flex;flex-direction:row;align-items:center;margin-bottom:10px}.bx-centered-dialog .bx-dialog-title p{padding:0;margin:0;flex:1;font-size:1.2rem;font-weight:bold}.bx-centered-dialog .bx-dialog-title button{flex-shrink:0}.bx-centered-dialog .bx-dialog-content{flex:1;overflow:auto;overflow-x:hidden}.bx-centered-dialog .bx-dialog-preset-tools{display:flex;margin-bottom:12px;gap:6px}.bx-centered-dialog .bx-dialog-preset-tools select{flex:1}.bx-centered-dialog .bx-default-preset-note{font-size:12px;font-style:italic;text-align:center;margin-bottom:10px}.bx-centered-dialog input,.bx-settings-dialog input{accent-color:var(--bx-primary-button-color)}.bx-centered-dialog input:focus,.bx-settings-dialog input:focus{accent-color:var(--bx-danger-button-color)}.bx-centered-dialog select:disabled,.bx-settings-dialog select:disabled{-webkit-appearance:none;background:transparent;text-align-last:right;border:none;color:#fff}.bx-centered-dialog select option:disabled,.bx-settings-dialog select option:disabled{display:none}.bx-centered-dialog input[type=checkbox]:focus,.bx-settings-dialog input[type=checkbox]:focus,.bx-centered-dialog select:focus,.bx-settings-dialog select:focus{filter:drop-shadow(1px 0 0 #fff) drop-shadow(-1px 0 0 #fff) drop-shadow(0 1px 0 #fff) drop-shadow(0 -1px 0 #fff)}.bx-centered-dialog a,.bx-settings-dialog a{color:#1c9d1c;text-decoration:none}.bx-centered-dialog a:hover,.bx-settings-dialog a:hover,.bx-centered-dialog a:focus,.bx-settings-dialog a:focus{color:#5dc21e}.bx-centered-dialog label,.bx-settings-dialog label{margin:0}.bx-controller-shortcuts-manager-container .bx-shortcut-note{margin-top:10px;font-size:14px;text-align:center}.bx-controller-shortcuts-manager-container .bx-shortcut-row{display:flex;gap:10px;margin-bottom:10px;align-items:center}.bx-controller-shortcuts-manager-container .bx-shortcut-row label.bx-prompt{flex-shrink:0;font-size:32px;margin:0}.bx-controller-shortcuts-manager-container .bx-shortcut-row label.bx-prompt::first-letter{letter-spacing:6px}.bx-controller-shortcuts-manager-container .bx-shortcut-row .bx-shortcut-actions{flex:1;position:relative}.bx-controller-shortcuts-manager-container .bx-shortcut-row .bx-shortcut-actions select{width:100%;height:100%;min-height:38px;display:block}.bx-controller-shortcuts-manager-container .bx-shortcut-row .bx-shortcut-actions select:first-of-type{position:absolute;top:0;left:0}.bx-controller-shortcuts-manager-container .bx-shortcut-row .bx-shortcut-actions select:last-of-type{opacity:0;z-index:calc(var(--bx-settings-z-index) + 1)}.bx-controller-shortcuts-manager-container select:disabled{text-align:left;text-align-last:left}.bx-keyboard-shortcuts-manager-container{display:flex;flex-direction:column;gap:16px}.bx-keyboard-shortcuts-manager-container fieldset{background:#2a2a2a;border:1px solid #2a2a2a;border-radius:4px;padding:4px}.bx-keyboard-shortcuts-manager-container legend{width:auto;padding:4px 8px;margin:0 4px 4px;background:#004f87;box-shadow:0 2px 0 #071e3d;border-radius:4px;font-size:14px;font-weight:bold;text-transform:uppercase}.bx-keyboard-shortcuts-manager-container .bx-settings-row{background:none;padding:10px}.bx-settings-dialog{display:flex;position:fixed;top:0;right:0;bottom:0;opacity:.98;user-select:none;-webkit-user-select:none}.bx-settings-dialog .bx-focusable::after{border-radius:4px}.bx-settings-dialog .bx-focusable:focus::after{top:0;left:0;right:0;bottom:0}.bx-settings-dialog .bx-settings-reload-note{font-size:.8rem;display:block;padding:8px;font-style:italic;font-weight:normal;height:var(--bx-button-height)}.bx-settings-tabs-container{position:fixed;width:48px;max-height:100vh;display:flex;flex-direction:column}.bx-settings-tabs-container > div:last-of-type{display:flex;flex-direction:column;align-items:end}.bx-settings-tabs-container > div:last-of-type button{flex-shrink:0;border-top-right-radius:0;border-bottom-right-radius:0;margin-top:8px;height:unset;padding:8px 10px}.bx-settings-tabs-container > div:last-of-type button svg{width:16px;height:16px}.bx-settings-tabs{display:flex;flex-direction:column;border-radius:0 0 0 8px;box-shadow:0 0 6px #000;overflow:overlay;flex:1}.bx-settings-tabs svg{width:24px;height:24px;padding:10px;flex-shrink:0;box-sizing:content-box;background:#131313;cursor:pointer;border-left:4px solid #1e1e1e}.bx-settings-tabs svg.bx-active{background:#222;border-color:#008746}.bx-settings-tabs svg:not(.bx-active):hover{background:#2f2f2f;border-color:#484848}.bx-settings-tabs svg:focus{border-color:#fff}.bx-settings-tabs svg[data-group=global][data-need-refresh=true]{background:var(--bx-danger-button-color) !important}.bx-settings-tabs svg[data-group=global][data-need-refresh=true]:hover{background:var(--bx-danger-button-hover-color) !important}.bx-settings-tab-contents{flex-direction:column;padding:10px;margin-left:48px;width:450px;max-width:calc(100vw - tabsWidth);background:#1a1b1e;color:#fff;font-weight:400;font-size:16px;font-family:var(--bx-title-font);text-align:center;box-shadow:0 0 6px #000;overflow:overlay;z-index:1}.bx-settings-tab-contents > div[data-tab-group=mkb]{display:flex;flex-direction:column;height:100%;overflow:hidden}.bx-settings-tab-contents .bx-top-buttons{display:flex;flex-direction:column;gap:8px;margin-bottom:8px}.bx-settings-tab-contents .bx-top-buttons .bx-button{display:block}.bx-settings-tab-contents h2{margin:16px 0 8px 0;display:flex;align-items:center}.bx-settings-tab-contents h2:first-of-type{margin-top:0}.bx-settings-tab-contents h2 span{display:inline-block;font-size:20px;font-weight:bold;text-align:left;flex:1;text-overflow:ellipsis;overflow:hidden;white-space:nowrap;min-height:var(--bx-button-height);align-content:center}@media (max-width:500px){.bx-settings-tab-contents{width:calc(100vw - 48px)}}.bx-settings-row{display:flex;gap:10px;padding:16px 10px;margin:0;background:#2a2a2a;border-bottom:1px solid #343434}.bx-settings-row:hover,.bx-settings-row:focus-within{background-color:#242424}.bx-settings-row:not(:has(> input[type=checkbox])){flex-wrap:wrap}.bx-settings-row > span.bx-settings-label{font-size:14px;display:block;text-align:left;align-self:center;margin-bottom:0 !important;flex:1}.bx-settings-row > span.bx-settings-label + *{margin:0 0 0 auto}.bx-settings-row[data-multi-lines="true"]{flex-direction:column}.bx-settings-row[data-multi-lines="true"] > span.bx-settings-label{align-self:start}.bx-settings-row[data-multi-lines="true"] > span.bx-settings-label + *{margin:unset}.bx-settings-dialog-note{display:block;color:#afafb0;font-size:12px;font-weight:lighter;font-style:italic}.bx-settings-dialog-note:not(:has(a)){margin-top:4px}.bx-settings-dialog-note a{display:inline-block;padding:4px}.bx-settings-custom-user-agent{display:block;width:100%;padding:6px}.bx-donation-link{display:block;text-align:center;text-decoration:none;height:20px;line-height:20px;font-size:14px;margin-top:10px;margin-bottom:10px}.bx-debug-info button{margin-top:10px}.bx-debug-info pre{margin-top:10px;cursor:copy;color:#fff;padding:8px;border:1px solid #2d2d2d;background:#212121;white-space:break-spaces;text-align:left}.bx-debug-info pre:hover{background:#272727}.bx-settings-app-version{margin-top:10px;text-align:center;color:#747474;font-size:12px}.bx-note-unsupported{display:block;font-size:12px;font-style:italic;font-weight:normal;color:#828282}.bx-settings-tab-contents > div *:not(.bx-settings-row):has(+ .bx-settings-row) + .bx-settings-row:has(+ .bx-settings-row){border-top-left-radius:6px;border-top-right-radius:6px}.bx-settings-tab-contents > div .bx-settings-row:not(:has(+ .bx-settings-row)){border:none;border-bottom-left-radius:6px;border-bottom-right-radius:6px}.bx-settings-tab-contents > div *:not(.bx-settings-row):has(+ .bx-settings-row) + .bx-settings-row:not(:has(+ .bx-settings-row)){border:none;border-radius:6px}.bx-suggest-toggler{text-align:left;display:flex;border-radius:4px;overflow:hidden;background:#003861;height:45px;align-items:center}.bx-suggest-toggler label{flex:1;align-content:center;padding:0 10px;background:#004f87;height:100%}.bx-suggest-toggler span{display:inline-block;align-self:center;padding:10px;width:45px;text-align:center}.bx-suggest-toggler:hover,.bx-suggest-toggler:focus{cursor:pointer;background:#005da1}.bx-suggest-toggler:hover label,.bx-suggest-toggler:focus label{background:#006fbe}.bx-suggest-toggler[bx-open] span{transform:rotate(90deg)}.bx-suggest-toggler[bx-open]+ .bx-suggest-box{display:block}.bx-suggest-box{display:none}.bx-suggest-wrapper{display:flex;flex-direction:column;gap:10px;margin:10px}.bx-suggest-note{font-size:11px;color:#8c8c8c;font-style:italic;font-weight:100}.bx-suggest-link{font-size:14px;display:inline-block;margin-top:4px;padding:4px}.bx-suggest-row{display:flex;flex-direction:row;gap:10px}.bx-suggest-row label{flex:1;overflow:overlay;border-radius:4px}.bx-suggest-row label .bx-suggest-label{background:#323232;padding:4px 10px;font-size:12px;text-align:left}.bx-suggest-row label .bx-suggest-value{padding:6px;font-size:14px}.bx-suggest-row label .bx-suggest-value.bx-suggest-change{background-color:var(--bx-warning-color)}.bx-suggest-row.bx-suggest-ok input{visibility:hidden}.bx-suggest-row.bx-suggest-ok .bx-suggest-label{background-color:#008114}.bx-suggest-row.bx-suggest-ok .bx-suggest-value{background-color:#13a72a}.bx-suggest-row.bx-suggest-change .bx-suggest-label{background-color:#a65e08}.bx-suggest-row.bx-suggest-change .bx-suggest-value{background-color:#d57f18}.bx-suggest-row.bx-suggest-change:hover label{cursor:pointer}.bx-suggest-row.bx-suggest-change:hover .bx-suggest-label{background-color:#995707}.bx-suggest-row.bx-suggest-change:hover .bx-suggest-value{background-color:#bd7115}.bx-suggest-row.bx-suggest-change input:not(:checked) + label{opacity:.5}.bx-suggest-row.bx-suggest-change input:not(:checked) + label .bx-suggest-label{background-color:#2a2a2a}.bx-suggest-row.bx-suggest-change input:not(:checked) + label .bx-suggest-value{background-color:#393939}.bx-suggest-row.bx-suggest-change:hover input:not(:checked) + label{opacity:1}.bx-suggest-row.bx-suggest-change:hover input:not(:checked) + label .bx-suggest-label{background-color:#202020}.bx-suggest-row.bx-suggest-change:hover input:not(:checked) + label .bx-suggest-value{background-color:#303030}.bx-sub-content-box{background:#161616;padding:10px;box-shadow:0 0 12px #0f0f0f inset;border-radius:10px}.bx-settings-row .bx-sub-content-box{background:#202020;padding:12px;box-shadow:0 0 4px #000 inset;border-radius:6px}.bx-controller-extra-settings[data-has-gamepad=true] > :first-child{display:none}.bx-controller-extra-settings[data-has-gamepad=true] > :last-child{display:block}.bx-controller-extra-settings[data-has-gamepad=false] > :first-child{display:block}.bx-controller-extra-settings[data-has-gamepad=false] > :last-child{display:none}.bx-controller-extra-settings .bx-controller-extra-wrapper{flex:1;min-width:1px}.bx-controller-extra-settings .bx-sub-content-box{flex:1;text-align:left;display:flex;flex-direction:column;margin-top:10px}.bx-controller-extra-settings .bx-sub-content-box > label{font-size:14px}.bx-preset-row{display:flex;gap:8px}.bx-preset-row .bx-select{flex:1}.bx-toast{user-select:none;-webkit-user-select:none;position:fixed;left:50%;top:24px;transform:translate(-50%,0);background:#000;border-radius:16px;color:#fff;z-index:var(--bx-toast-z-index);font-family:var(--bx-normal-font);border:2px solid #fff;display:flex;align-items:center;opacity:0;overflow:clip;transition:opacity .2s ease-in}.bx-toast.bx-show{opacity:.85}.bx-toast.bx-hide{opacity:0;pointer-events:none}.bx-toast-msg{font-size:14px;display:inline-block;padding:12px 16px;white-space:pre}.bx-toast-status{font-weight:bold;font-size:14px;text-transform:uppercase;display:inline-block;background:#515863;padding:12px 16px;color:#fff;white-space:pre}.bx-wait-time-box{position:fixed;top:0;right:0;background-color:rgba(0,0,0,0.8);color:#fff;z-index:var(--bx-wait-time-box-z-index);padding:12px;border-radius:0 0 0 8px}.bx-wait-time-box label{display:block;text-transform:uppercase;text-align:right;font-size:12px;font-weight:bold;margin:0}.bx-wait-time-box span{display:block;font-family:var(--bx-monospaced-font);text-align:right;font-size:16px;margin-bottom:10px}.bx-wait-time-box span:last-of-type{margin-bottom:0}.bx-remote-play-container{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);color:#fff;background:#1a1b1e;border-radius:10px;width:420px;max-width:calc(100vw - 20px);margin:0 0 0 auto;padding:16px}.bx-remote-play-container > .bx-button{display:table;margin:0 0 0 auto}.bx-remote-play-settings{margin-bottom:12px;padding-bottom:12px;border-bottom:1px solid #2d2d2d}.bx-remote-play-settings > div{display:flex}.bx-remote-play-settings label{flex:1}.bx-remote-play-settings label p{margin:4px 0 0;padding:0;color:#888;font-size:12px}.bx-remote-play-resolution{display:block}.bx-remote-play-resolution input[type="radio"]{accent-color:var(--bx-primary-button-color);margin-right:6px}.bx-remote-play-resolution input[type="radio"]:focus{accent-color:var(--bx-primary-button-hover-color)}.bx-remote-play-device-wrapper{display:flex;margin-bottom:12px}.bx-remote-play-device-wrapper:last-child{margin-bottom:2px}.bx-remote-play-device-info{flex:1;padding:4px 0}.bx-remote-play-device-name{font-size:20px;font-weight:bold;display:inline-block;vertical-align:middle}.bx-remote-play-console-type{font-size:12px;background:#004c87;color:#fff;display:inline-block;border-radius:14px;padding:2px 10px;margin-left:8px;vertical-align:middle}.bx-remote-play-power-state{color:#888;font-size:12px}.bx-remote-play-connect-button{min-height:100%;margin:4px 0}.bx-remote-play-buttons{display:flex;justify-content:space-between}select.bx-select{min-height:30px}div.bx-select{display:flex;align-items:center;flex:0 1 auto;gap:8px}div.bx-select select{position:absolute !important;top:-9999px !important;left:-9999px !important;visibility:hidden !important}div.bx-select select:disabled ~ button{display:none}div.bx-select select:disabled ~ div{background:#131416;color:#fff;pointer-events:none}div.bx-select select:disabled ~ div .bx-select-indicators{visibility:hidden}div.bx-select > div,div.bx-select button.bx-select-value{min-width:120px;text-align:left;line-height:24px;vertical-align:middle;background:#fff;color:#000;border-radius:4px;padding:2px 8px;display:flex;flex:1;flex-direction:column}div.bx-select > div{min-height:24px;box-sizing:content-box}div.bx-select > div input{display:inline-block;margin-right:8px}div.bx-select > div label{margin-bottom:0;font-size:14px;width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;min-height:15px}div.bx-select > div label span{display:block;font-size:10px;font-weight:bold;text-align:left;line-height:initial;white-space:pre;min-height:15px;align-content:center}div.bx-select button.bx-select-value{border:none;cursor:pointer;min-height:30px;font-size:.9rem;align-items:center}div.bx-select button.bx-select-value > div{display:flex;width:100%}div.bx-select button.bx-select-value span{flex:1;text-align:left;display:inline-block}div.bx-select button.bx-select-value input{margin:0 4px;accent-color:var(--bx-primary-button-color);pointer-events:none}div.bx-select button.bx-select-value:hover input,div.bx-select button.bx-select-value:focus input{accent-color:var(--bx-danger-button-color)}div.bx-select button.bx-select-value:hover::after,div.bx-select button.bx-select-value:focus::after{border-color:#4d4d4d !important}div.bx-select button.bx-button{border:none;height:24px;width:24px;padding:0;line-height:24px;color:#fff;border-radius:4px;font-weight:bold;font-size:12px;font-family:var(--bx-monospaced-font);flex-shrink:0}div.bx-select button.bx-button span{line-height:unset}.bx-select-indicators{display:flex;height:4px;gap:2px;margin-bottom:2px}.bx-select-indicators span{content:\' \';display:inline-block;flex:1;background:#cfcfcf;border-radius:4px}.bx-select-indicators span[data-highlighted]{background:#9c9c9c}.bx-select-indicators span[data-selected]{background:#aacfe7}.bx-select-indicators span[data-highlighted][data-selected]{background:#5fa3d0}.bx-guide-home-achievements-progress{display:flex;gap:10px;flex-direction:row}.bx-guide-home-achievements-progress .bx-button{margin-bottom:0 !important}body[data-bx-media-type=tv] .bx-guide-home-achievements-progress{flex-direction:column}body:not([data-bx-media-type=tv]) .bx-guide-home-achievements-progress{flex-direction:row}body:not([data-bx-media-type=tv]) .bx-guide-home-achievements-progress > button:first-of-type{flex:1}body:not([data-bx-media-type=tv]) .bx-guide-home-achievements-progress > button:last-of-type{width:40px}body:not([data-bx-media-type=tv]) .bx-guide-home-achievements-progress > button:last-of-type span{display:none}.bx-guide-home-buttons > div{display:flex;flex-direction:row;gap:12px}body[data-bx-media-type=tv] .bx-guide-home-buttons > div{flex-direction:column}body[data-bx-media-type=tv] .bx-guide-home-buttons > div button{margin-bottom:0 !important}body:not([data-bx-media-type=tv]) .bx-guide-home-buttons > div button span{display:none}.bx-guide-home-buttons[data-is-playing="true"] button[data-state=\'normal\']{display:none}.bx-guide-home-buttons[data-is-playing="false"] button[data-state=\'playing\']{display:none}div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module]{overflow:visible}.bx-stream-menu-button-on{fill:#000 !important;background-color:#2d2d2d !important;color:#000 !important}.bx-stream-refresh-button{top:calc(env(safe-area-inset-top, 0px) + 10px + 50px) !important}body[data-media-type=default] .bx-stream-refresh-button{left:calc(env(safe-area-inset-left, 0px) + 11px) !important}body[data-media-type=tv] .bx-stream-refresh-button{top:calc(var(--gds-focus-borderSize) + 80px) !important}.bx-stream-home-button{top:calc(env(safe-area-inset-top, 0px) + 10px + 50px * 2) !important}body[data-media-type=default] .bx-stream-home-button{left:calc(env(safe-area-inset-left, 0px) + 12px) !important}body[data-media-type=tv] .bx-stream-home-button{top:calc(var(--gds-focus-borderSize) + 80px * 2) !important}div[data-testid=media-container][data-position=center]{display:flex}div[data-testid=media-container][data-position=top] video,div[data-testid=media-container][data-position=top] canvas{top:0}div[data-testid=media-container][data-position=bottom] video,div[data-testid=media-container][data-position=bottom] canvas{bottom:0}#game-stream video{margin:auto;align-self:center;background:#000;position:absolute;left:0;right:0}#game-stream canvas{align-self:center;margin:auto;position:absolute;left:0;right:0}#game-stream.bx-taking-screenshot:before{animation:bx-anim-taking-screenshot .5s ease;content:\' \';position:absolute;width:100%;height:100%;z-index:var(--bx-screenshot-animation-z-index)}#gamepass-dialog-root div[class^=Guide-module__guide] .bx-button{overflow:visible;margin-bottom:12px}@-moz-keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}@-webkit-keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}@-o-keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}@keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}.bx-number-stepper{text-align:center}.bx-number-stepper > div{display:flex;align-items:center}.bx-number-stepper > div span{flex:1;display:inline-block;min-width:40px;font-family:var(--bx-monospaced-font);font-size:13px;margin:0 4px}.bx-number-stepper > div button{flex-shrink:0;border:none;width:24px;height:24px;margin:0;line-height:24px;background-color:var(--bx-default-button-color);color:#fff;border-radius:4px;font-weight:bold;font-size:14px;font-family:var(--bx-monospaced-font)}@media (hover:hover){.bx-number-stepper > div button:hover{background-color:var(--bx-default-button-hover-color)}}.bx-number-stepper > div button:active{background-color:var(--bx-default-button-hover-color)}.bx-number-stepper > div button:disabled + span{font-family:var(--bx-title-font)}.bx-number-stepper input[type="range"]{display:block;margin:8px 0 2px auto;min-width:180px;width:100%;color:#959595 !important}.bx-number-stepper input[type=range]:disabled,.bx-number-stepper button:disabled{display:none}.bx-number-stepper[data-disabled=true] input[type=range],.bx-number-stepper[disabled=true] input[type=range],.bx-number-stepper[data-disabled=true] button,.bx-number-stepper[disabled=true] button{display:none}#bx-game-bar{z-index:var(--bx-game-bar-z-index);position:fixed;bottom:0;width:40px;height:90px;overflow:visible;cursor:pointer}#bx-game-bar > svg{display:none;pointer-events:none;position:absolute;height:28px;margin-top:16px}@media (hover:hover){#bx-game-bar:hover > svg{display:block}}#bx-game-bar .bx-game-bar-container{opacity:0;position:absolute;display:flex;overflow:hidden;background:rgba(26,27,30,0.91);box-shadow:0 0 6px #1c1c1c;transition:opacity .1s ease-in}#bx-game-bar .bx-game-bar-container.bx-show{opacity:.9}#bx-game-bar .bx-game-bar-container.bx-show + svg{display:none !important}#bx-game-bar .bx-game-bar-container.bx-hide{opacity:0;pointer-events:none}#bx-game-bar .bx-game-bar-container button{width:60px;height:60px;border-radius:0}#bx-game-bar .bx-game-bar-container button svg{width:28px;height:28px;transition:transform .08s ease 0s}#bx-game-bar .bx-game-bar-container button:hover{border-radius:0}#bx-game-bar .bx-game-bar-container button:active svg{transform:scale(.75)}#bx-game-bar .bx-game-bar-container button.bx-activated{background-color:#fff}#bx-game-bar .bx-game-bar-container button.bx-activated svg{filter:invert(1)}#bx-game-bar .bx-game-bar-container div[data-activated] button{display:none}#bx-game-bar .bx-game-bar-container div[data-activated=\'false\'] button:first-of-type{display:block}#bx-game-bar .bx-game-bar-container div[data-activated=\'true\'] button:last-of-type{display:block}#bx-game-bar[data-position="bottom-left"]{left:0;direction:ltr}#bx-game-bar[data-position="bottom-left"] .bx-game-bar-container{border-radius:0 10px 10px 0}#bx-game-bar[data-position="bottom-right"]{right:0;direction:rtl}#bx-game-bar[data-position="bottom-right"] .bx-game-bar-container{direction:ltr;border-radius:10px 0 0 10px}.bx-badges{margin-left:0;user-select:none;-webkit-user-select:none}.bx-badge{border:none;display:inline-block;line-height:24px;color:#fff;font-family:var(--bx-title-font-semibold);font-size:14px;font-weight:400;margin:0 8px 8px 0;box-shadow:0 0 6px #000;border-radius:4px}.bx-badge-name{background-color:#2d3036;border-radius:4px 0 0 4px}.bx-badge-name svg{width:16px;height:16px}.bx-badge-value{background-color:#808080;border-radius:0 4px 4px 0}.bx-badge-name,.bx-badge-value{display:inline-block;padding:0 8px;line-height:30px;vertical-align:bottom}.bx-badge-battery[data-charging=true] span:first-of-type::after{content:\' ⚡️\'}div[class^=StreamMenu-module__container] .bx-badges{position:absolute;max-width:500px}#gamepass-dialog-root .bx-badges{position:fixed;top:60px;left:460px;max-width:500px}@media (min-width:568px) and (max-height:480px){#gamepass-dialog-root .bx-badges{position:unset;top:unset;left:unset;margin:8px 0}}.bx-stats-bar{display:flex;flex-direction:row;gap:8px;user-select:none;-webkit-user-select:none;position:fixed;top:0;background-color:#000;color:#fff;font-family:var(--bx-monospaced-font);font-size:.9rem;padding-left:8px;z-index:var(--bx-stats-bar-z-index);text-wrap:nowrap}.bx-stats-bar[data-stats*="[time]"] > .bx-stat-time,.bx-stats-bar[data-stats*="[play]"] > .bx-stat-play,.bx-stats-bar[data-stats*="[batt]"] > .bx-stat-batt,.bx-stats-bar[data-stats*="[fps]"] > .bx-stat-fps,.bx-stats-bar[data-stats*="[ping]"] > .bx-stat-ping,.bx-stats-bar[data-stats*="[jit]"] > .bx-stat-jit,.bx-stats-bar[data-stats*="[btr]"] > .bx-stat-btr,.bx-stats-bar[data-stats*="[dt]"] > .bx-stat-dt,.bx-stats-bar[data-stats*="[pl]"] > .bx-stat-pl,.bx-stats-bar[data-stats*="[fl]"] > .bx-stat-fl,.bx-stats-bar[data-stats*="[dl]"] > .bx-stat-dl,.bx-stats-bar[data-stats*="[ul]"] > .bx-stat-ul{display:inline-flex;align-items:baseline}.bx-stats-bar[data-stats$="[time]"] > .bx-stat-time,.bx-stats-bar[data-stats$="[play]"] > .bx-stat-play,.bx-stats-bar[data-stats$="[batt]"] > .bx-stat-batt,.bx-stats-bar[data-stats$="[fps]"] > .bx-stat-fps,.bx-stats-bar[data-stats$="[ping]"] > .bx-stat-ping,.bx-stats-bar[data-stats$="[jit]"] > .bx-stat-jit,.bx-stats-bar[data-stats$="[btr]"] > .bx-stat-btr,.bx-stats-bar[data-stats$="[dt]"] > .bx-stat-dt,.bx-stats-bar[data-stats$="[pl]"] > .bx-stat-pl,.bx-stats-bar[data-stats$="[fl]"] > .bx-stat-fl,.bx-stats-bar[data-stats$="[dl]"] > .bx-stat-dl,.bx-stats-bar[data-stats$="[ul]"] > .bx-stat-ul{border-right:none}.bx-stats-bar::before{display:none;content:\'👀\';vertical-align:middle;margin-right:8px}.bx-stats-bar[data-display=glancing]::before{display:inline-block}.bx-stats-bar[data-position=top-left]{left:0;border-radius:0 0 4px 0}.bx-stats-bar[data-position=top-right]{right:0;border-radius:0 0 0 4px}.bx-stats-bar[data-position=top-center]{transform:translate(-50%,0);left:50%;border-radius:0 0 4px 4px}.bx-stats-bar[data-shadow=true]{background:none;filter:drop-shadow(1px 0 0 rgba(0,0,0,0.941)) drop-shadow(-1px 0 0 rgba(0,0,0,0.941)) drop-shadow(0 1px 0 rgba(0,0,0,0.941)) drop-shadow(0 -1px 0 rgba(0,0,0,0.941))}.bx-stats-bar > div{display:none;border-right:1px solid #fff;padding-right:8px}.bx-stats-bar label{margin:0 8px 0 0;font-family:var(--bx-title-font);font-size:70%;font-weight:bold;vertical-align:middle;cursor:help}.bx-stats-bar span{display:inline-block;text-align:right;vertical-align:middle;white-space:pre}.bx-stats-bar span[data-grade=good]{color:#6bffff}.bx-stats-bar span[data-grade=ok]{color:#fff16b}.bx-stats-bar span[data-grade=bad]{color:#ff5f5f}.bx-mkb-settings{display:flex;flex-direction:column;flex:1;padding-bottom:10px;overflow:hidden}.bx-mkb-pointer-lock-msg{user-select:none;-webkit-user-select:none;position:fixed;left:50%;bottom:40px;transform:translateX(-50%);margin:auto;background:#151515;z-index:var(--bx-mkb-pointer-lock-msg-z-index);color:#fff;font-weight:400;font-family:"Segoe UI",Arial,Helvetica,sans-serif;font-size:1.3rem;padding:12px;border-radius:8px;align-items:center;box-shadow:0 0 6px #000;min-width:300px;opacity:.9;display:flex;flex-direction:column;gap:10px}.bx-mkb-pointer-lock-msg:hover{opacity:1}.bx-mkb-pointer-lock-msg > p{margin:0;width:100%;font-size:22px;margin-bottom:4px;font-weight:bold;text-align:left}.bx-mkb-pointer-lock-msg > div{width:100%;display:flex;flex-direction:row;gap:10px}.bx-mkb-pointer-lock-msg > div button:first-of-type{flex-shrink:1}.bx-mkb-pointer-lock-msg > div button:last-of-type{flex-grow:1}.bx-mkb-key-row{display:flex;margin-bottom:10px;align-items:center;gap:20px}.bx-mkb-key-row label{margin-bottom:0;font-family:var(--bx-promptfont-font);font-size:32px;text-align:center}.bx-mkb-settings.bx-editing .bx-mkb-key-row button{background:#393939;border-radius:4px;border:none}.bx-mkb-settings.bx-editing .bx-mkb-key-row button:hover{background:#333;cursor:pointer}.bx-mkb-action-buttons > div{text-align:right;display:none}.bx-mkb-action-buttons button{margin-left:8px}.bx-mkb-settings:not(.bx-editing) .bx-mkb-action-buttons > div:first-child{display:block}.bx-mkb-settings.bx-editing .bx-mkb-action-buttons > div:last-child{display:block}.bx-mkb-note{display:block;margin:0 0 10px;font-size:12px;text-align:center}button.bx-binding-button{flex:1;min-height:38px;border:none;border-radius:4px;font-size:14px;color:#fff;display:flex;align-items:center;align-self:center;padding:0 6px}button.bx-binding-button:disabled{background:#131416;padding:0 8px}button.bx-binding-button:not(:disabled){border:2px solid transparent;border-top:none;border-bottom:4px solid #252525;background:#3b3b3b;cursor:pointer}button.bx-binding-button:not(:disabled):hover,button.bx-binding-button:not(:disabled).bx-focusable:focus{background:#20b217;border-bottom-color:#186c13}button.bx-binding-button:not(:disabled):active{background:#16900f;border-bottom:3px solid #0c4e08;border-left-width:2px;border-right-width:2px}button.bx-binding-button:not(:disabled).bx-focusable:focus::after{top:-6px;left:-8px;right:-8px;bottom:-10px}.bx-settings-row .bx-binding-button-wrapper button.bx-binding-button{min-width:60px}.bx-product-details-buttons{display:flex;gap:10px;flex-direction:row}.bx-product-details-buttons button{max-width:max-content;margin:10px 0 0 0;display:flex}@media (min-width:568px) and (max-height:480px){.bx-product-details-buttons{flex-direction:column}.bx-product-details-buttons button{margin:8px 0 0 10px}}', PREF_HIDE_SECTIONS = getPref("ui.hideSections"), selectorToHide = []; + let css = ':root{--bx-title-font:Bahnschrift,Arial,Helvetica,sans-serif;--bx-title-font-semibold:Bahnschrift Semibold,Arial,Helvetica,sans-serif;--bx-normal-font:"Segoe UI",Arial,Helvetica,sans-serif;--bx-monospaced-font:Consolas,"Courier New",Courier,monospace;--bx-promptfont-font:promptfont;--bx-button-height:40px;--bx-default-button-color:#2d3036;--bx-default-button-rgb:45,48,54;--bx-default-button-hover-color:#515863;--bx-default-button-hover-rgb:81,88,99;--bx-default-button-active-color:#222428;--bx-default-button-active-rgb:34,36,40;--bx-default-button-disabled-color:#8e8e8e;--bx-default-button-disabled-rgb:142,142,142;--bx-primary-button-color:#008746;--bx-primary-button-rgb:0,135,70;--bx-primary-button-hover-color:#04b358;--bx-primary-button-hover-rgb:4,179,88;--bx-primary-button-active-color:#044e2a;--bx-primary-button-active-rgb:4,78,42;--bx-primary-button-disabled-color:#448262;--bx-primary-button-disabled-rgb:68,130,98;--bx-warning-button-color:#c16e04;--bx-warning-button-rgb:193,110,4;--bx-warning-button-hover-color:#fa9005;--bx-warning-button-hover-rgb:250,144,5;--bx-warning-button-active-color:#965603;--bx-warning-button-active-rgb:150,86,3;--bx-warning-button-disabled-color:#a2816c;--bx-warning-button-disabled-rgb:162,129,108;--bx-danger-button-color:#c10404;--bx-danger-button-rgb:193,4,4;--bx-danger-button-hover-color:#e61d1d;--bx-danger-button-hover-rgb:230,29,29;--bx-danger-button-active-color:#a26c6c;--bx-danger-button-active-rgb:162,108,108;--bx-danger-button-disabled-color:#df5656;--bx-danger-button-disabled-rgb:223,86,86;--bx-fullscreen-text-z-index:9999;--bx-toast-z-index:6000;--bx-key-binding-dialog-z-index:5010;--bx-key-binding-dialog-overlay-z-index:5000;--bx-stats-bar-z-index:4010;--bx-navigation-dialog-z-index:3010;--bx-navigation-dialog-overlay-z-index:3000;--bx-mkb-pointer-lock-msg-z-index:2000;--bx-game-bar-z-index:1000;--bx-screenshot-animation-z-index:200;--bx-wait-time-box-z-index:100}@font-face{font-family:\'promptfont\';src:url("https://redphx.github.io/better-xcloud/fonts/promptfont.otf");unicode-range:U+2196-E011}div[class^=HUDButton-module__hiddenContainer] ~ div:not([class^=HUDButton-module__hiddenContainer]){opacity:0;pointer-events:none !important;position:absolute;top:-9999px;left:-9999px}@media screen and (max-width:640px){header a[href="/play"]{display:none}}.bx-full-width{width:100% !important}.bx-full-height{height:100% !important}.bx-auto-height{height:auto !important}.bx-no-scroll{overflow:hidden !important}.bx-hide-scroll-bar{scrollbar-width:none}.bx-hide-scroll-bar::-webkit-scrollbar{display:none}.bx-gone{display:none !important}.bx-offscreen{position:absolute !important;top:-9999px !important;left:-9999px !important;visibility:hidden !important}.bx-hidden{visibility:hidden !important}.bx-invisible{opacity:0}.bx-unclickable{pointer-events:none}.bx-pixel{width:1px !important;height:1px !important}.bx-no-margin{margin:0 !important}.bx-no-padding{padding:0 !important}.bx-prompt{font-family:var(--bx-promptfont-font) !important}.bx-monospaced{font-family:var(--bx-monospaced-font) !important}.bx-line-through{text-decoration:line-through !important}.bx-normal-case{text-transform:none !important}.bx-normal-link{text-transform:none !important;text-align:left !important;font-weight:400 !important;font-family:var(--bx-normal-font) !important}select[multiple],select[multiple]:focus{overflow:auto;border:none}select[multiple] option,select[multiple]:focus option{padding:4px 6px}select[multiple] option:checked,select[multiple]:focus option:checked{background:#1a7bc0 linear-gradient(0deg,#1a7bc0 0%,#1a7bc0 100%)}select[multiple] option:checked::before,select[multiple]:focus option:checked::before{content:\'☑️\';font-size:12px;display:inline-block;margin-right:6px;height:100%;line-height:100%;vertical-align:middle}#headerArea,#uhfSkipToMain,.uhf-footer{display:none}div[class*=NotFocusedDialog]{position:absolute !important;top:-9999px !important;left:-9999px !important;width:0 !important;height:0 !important}#game-stream video:not([src]){visibility:hidden}div[class*=SupportedInputsBadge]:not(:has(:nth-child(2))),div[class*=SupportedInputsBadge] svg:first-of-type{display:none}.bx-game-tile-wait-time{position:absolute;top:0;left:0;z-index:1;background:rgba(0,0,0,0.5);display:flex;border-radius:4px 0 4px 0;align-items:center;padding:4px 8px}.bx-game-tile-wait-time svg{width:14px;height:16px;margin-right:2px}.bx-game-tile-wait-time span{display:inline-block;height:16px;line-height:16px;font-size:12px;font-weight:bold;margin-left:2px}.bx-game-tile-wait-time[data-duration=short]{background-color:rgba(0,133,133,0.75)}.bx-game-tile-wait-time[data-duration=medium]{background-color:rgba(213,133,0,0.75)}.bx-game-tile-wait-time[data-duration=long]{background-color:rgba(150,0,0,0.75)}.bx-fullscreen-text{position:fixed;top:0;bottom:0;left:0;right:0;background:rgba(0,0,0,0.8);z-index:var(--bx-fullscreen-text-z-index);line-height:100vh;color:#fff;text-align:center;font-weight:400;font-family:var(--bx-normal-font);font-size:1.3rem;user-select:none;-webkit-user-select:none}#root section[class*=DeviceCodePage-module__page]{margin-left:20px !important;margin-right:20px !important;margin-top:20px !important;max-width:800px !important}#root div[class*=DeviceCodePage-module__back]{display:none}.bx-blink-me{animation:bx-blinker 1s linear infinite}.bx-horizontal-shaking{animation:bx-horizontal-shaking .4s ease-in-out 2}@-moz-keyframes bx-blinker{100%{opacity:0}}@-webkit-keyframes bx-blinker{100%{opacity:0}}@-o-keyframes bx-blinker{100%{opacity:0}}@keyframes bx-blinker{100%{opacity:0}}@-moz-keyframes bx-horizontal-shaking{0%{transform:translateX(0)}25%{transform:translateX(5px)}50%{transform:translateX(-5px)}75%{transform:translateX(5px)}100%{transform:translateX(0)}}@-webkit-keyframes bx-horizontal-shaking{0%{transform:translateX(0)}25%{transform:translateX(5px)}50%{transform:translateX(-5px)}75%{transform:translateX(5px)}100%{transform:translateX(0)}}@-o-keyframes bx-horizontal-shaking{0%{transform:translateX(0)}25%{transform:translateX(5px)}50%{transform:translateX(-5px)}75%{transform:translateX(5px)}100%{transform:translateX(0)}}@keyframes bx-horizontal-shaking{0%{transform:translateX(0)}25%{transform:translateX(5px)}50%{transform:translateX(-5px)}75%{transform:translateX(5px)}100%{transform:translateX(0)}}.bx-button{--button-rgb:var(--bx-default-button-rgb);--button-hover-rgb:var(--bx-default-button-hover-rgb);--button-active-rgb:var(--bx-default-button-active-rgb);--button-disabled-rgb:var(--bx-default-button-disabled-rgb);background-color:rgb(var(--button-rgb));user-select:none;-webkit-user-select:none;color:#fff;font-family:var(--bx-title-font-semibold);font-size:14px;border:none;font-weight:400;height:var(--bx-button-height);border-radius:4px;padding:0 8px;text-transform:uppercase;cursor:pointer;overflow:hidden}.bx-button:not([disabled]):active{background-color:rgb(var(--button-active-rgb))}.bx-button:focus{outline:none !important}.bx-button:not([disabled]):not(:active):hover,.bx-button:not([disabled]):not(:active).bx-focusable:focus{background-color:rgb(var(--button-hover-rgb))}.bx-button:disabled{cursor:default;background-color:rgb(var(--button-disabled-rgb))}.bx-button.bx-ghost{background-color:transparent}.bx-button.bx-ghost:not([disabled]):not(:active):hover,.bx-button.bx-ghost:not([disabled]):not(:active).bx-focusable:focus{background-color:rgb(var(--button-hover-rgb))}.bx-button.bx-primary{--button-rgb:var(--bx-primary-button-rgb)}.bx-button.bx-primary:not([disabled]):active{--button-active-rgb:var(--bx-primary-button-active-rgb)}.bx-button.bx-primary:not([disabled]):not(:active):hover,.bx-button.bx-primary:not([disabled]):not(:active).bx-focusable:focus{--button-hover-rgb:var(--bx-primary-button-hover-rgb)}.bx-button.bx-primary:disabled{--button-disabled-rgb:var(--bx-primary-button-disabled-rgb)}.bx-button.bx-warning{--button-rgb:var(--bx-warning-button-rgb)}.bx-button.bx-warning:not([disabled]):active{--button-active-rgb:var(--bx-warning-button-active-rgb)}.bx-button.bx-warning:not([disabled]):not(:active):hover,.bx-button.bx-warning:not([disabled]):not(:active).bx-focusable:focus{--button-hover-rgb:var(--bx-warning-button-hover-rgb)}.bx-button.bx-warning:disabled{--button-disabled-rgb:var(--bx-warning-button-disabled-rgb)}.bx-button.bx-danger{--button-rgb:var(--bx-danger-button-rgb)}.bx-button.bx-danger:not([disabled]):active{--button-active-rgb:var(--bx-danger-button-active-rgb)}.bx-button.bx-danger:not([disabled]):not(:active):hover,.bx-button.bx-danger:not([disabled]):not(:active).bx-focusable:focus{--button-hover-rgb:var(--bx-danger-button-hover-rgb)}.bx-button.bx-danger:disabled{--button-disabled-rgb:var(--bx-danger-button-disabled-rgb)}.bx-button.bx-frosted{--button-alpha:.2;background-color:rgba(var(--button-rgb), var(--button-alpha));backdrop-filter:blur(4px) brightness(1.5)}.bx-button.bx-frosted:not([disabled]):not(:active):hover,.bx-button.bx-frosted:not([disabled]):not(:active).bx-focusable:focus{background-color:rgba(var(--button-hover-rgb), var(--button-alpha))}.bx-button.bx-drop-shadow{box-shadow:0 0 4px rgba(0,0,0,0.502)}.bx-button.bx-tall{height:calc(var(--bx-button-height) * 1.5) !important}.bx-button.bx-circular{border-radius:var(--bx-button-height);width:var(--bx-button-height);height:var(--bx-button-height)}.bx-button svg{display:inline-block;width:16px;height:var(--bx-button-height)}.bx-button span{display:inline-block;line-height:var(--bx-button-height);vertical-align:middle;color:#fff;overflow:hidden;white-space:nowrap}.bx-button span:not(:only-child){margin-left:10px}.bx-button.bx-button-multi-lines{height:auto;text-align:left;padding:10px 0}.bx-button.bx-button-multi-lines span{line-height:unset;display:block}.bx-button.bx-button-multi-lines span:last-of-type{text-transform:none;font-weight:normal;font-family:"Segoe Sans Variable Text";font-size:12px;margin-top:4px}.bx-focusable{position:relative;overflow:visible}.bx-focusable::after{border:2px solid transparent;border-radius:10px}.bx-focusable:focus::after{content:\'\';border-color:#fff;position:absolute;top:-6px;left:-6px;right:-6px;bottom:-6px}html[data-active-input=touch] .bx-focusable:focus::after,html[data-active-input=mouse] .bx-focusable:focus::after{border-color:transparent !important}.bx-focusable.bx-circular::after{border-radius:var(--bx-button-height)}a.bx-button{display:inline-block}a.bx-button.bx-full-width{text-align:center}button.bx-inactive{pointer-events:none;opacity:.2;background:transparent !important}.bx-header-remote-play-button{height:auto;margin-right:8px !important}.bx-header-remote-play-button svg{width:24px;height:24px}.bx-header-settings-button{line-height:30px;font-size:14px;text-transform:uppercase;position:relative}.bx-header-settings-button[data-update-available]::before{content:\'🌟\' !important;line-height:var(--bx-button-height);display:inline-block;margin-left:4px}.bx-key-binding-dialog-overlay{position:fixed;inset:0;z-index:var(--bx-key-binding-dialog-overlay-z-index);background:#000;opacity:50%}.bx-key-binding-dialog{display:flex;flex-flow:column;max-height:90vh;position:fixed;top:50%;left:50%;margin-right:-50%;transform:translate(-50%,-50%);min-width:420px;padding:16px;border-radius:8px;z-index:var(--bx-key-binding-dialog-z-index);background:#1a1b1e;color:#fff;font-weight:400;font-size:16px;font-family:var(--bx-normal-font);box-shadow:0 0 6px #000;user-select:none;-webkit-user-select:none}.bx-key-binding-dialog *:focus{outline:none !important}.bx-key-binding-dialog h2{margin-bottom:12px;color:#fff;display:block;font-family:var(--bx-title-font);font-size:32px;font-weight:400;line-height:var(--bx-button-height)}.bx-key-binding-dialog > div{overflow:auto;padding:2px 0}.bx-key-binding-dialog > button{padding:8px 32px;margin:10px auto 0;border:none;border-radius:4px;display:block;background-color:#2d3036;text-align:center;color:#fff;text-transform:uppercase;font-family:var(--bx-title-font);font-weight:400;line-height:18px;font-size:14px}@media (hover:hover){.bx-key-binding-dialog > button:hover{background-color:#515863}}.bx-key-binding-dialog > button:focus{background-color:#515863}.bx-key-binding-dialog ul{margin-bottom:1rem}.bx-key-binding-dialog ul li{display:none}.bx-key-binding-dialog ul[data-flags*="[1]"] > li[data-flag="1"],.bx-key-binding-dialog ul[data-flags*="[2]"] > li[data-flag="2"],.bx-key-binding-dialog ul[data-flags*="[4]"] > li[data-flag="4"],.bx-key-binding-dialog ul[data-flags*="[8]"] > li[data-flag="8"]{display:list-item}@media screen and (max-width:450px){.bx-key-binding-dialog{min-width:100%}}.bx-navigation-dialog{position:absolute;z-index:var(--bx-navigation-dialog-z-index);font-family:var(--bx-title-font)}.bx-navigation-dialog *:focus{outline:none !important}.bx-navigation-dialog select:disabled{-webkit-appearance:none;text-align-last:right;text-align:right;color:#fff;background:#131416;border:none;border-radius:4px;padding:0 5px}.bx-navigation-dialog .bx-focusable::after{border-radius:4px}.bx-navigation-dialog .bx-focusable:focus::after{top:0;left:0;right:0;bottom:0}.bx-navigation-dialog-overlay{position:fixed;background:rgba(11,11,11,0.89);top:0;left:0;right:0;bottom:0;z-index:var(--bx-navigation-dialog-overlay-z-index)}.bx-navigation-dialog-overlay[data-is-playing="true"]{background:transparent}.bx-centered-dialog{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);color:#fff;background:#1a1b1e;border-radius:10px;min-width:min(calc(100vw - 20px), 500px);max-width:calc(100vw - 20px);margin:0 0 0 auto;padding:16px;max-height:95vh;flex-direction:column;overflow:hidden;display:flex;flex-direction:column}.bx-centered-dialog .bx-dialog-title{display:flex;flex-direction:row;align-items:center;margin-bottom:10px}.bx-centered-dialog .bx-dialog-title p{padding:0;margin:0;flex:1;font-size:1.2rem;font-weight:bold}.bx-centered-dialog .bx-dialog-title button{flex-shrink:0}.bx-centered-dialog .bx-dialog-content{flex:1;padding:6px;overflow:auto;overflow-x:hidden}.bx-centered-dialog .bx-dialog-preset-tools{display:flex;margin-bottom:12px;gap:6px}.bx-centered-dialog .bx-dialog-preset-tools button{align-self:center;min-height:50px}.bx-centered-dialog .bx-default-preset-note{font-size:12px;font-style:italic;text-align:center;margin-bottom:10px}.bx-centered-dialog input,.bx-settings-dialog input{accent-color:var(--bx-primary-button-color)}.bx-centered-dialog input:focus,.bx-settings-dialog input:focus{accent-color:var(--bx-danger-button-color)}.bx-centered-dialog select:disabled,.bx-settings-dialog select:disabled{-webkit-appearance:none;background:transparent;text-align-last:right;border:none;color:#fff}.bx-centered-dialog select option:disabled,.bx-settings-dialog select option:disabled{display:none}.bx-centered-dialog input[type=checkbox]:focus,.bx-settings-dialog input[type=checkbox]:focus,.bx-centered-dialog select:focus,.bx-settings-dialog select:focus{filter:drop-shadow(1px 0 0 #fff) drop-shadow(-1px 0 0 #fff) drop-shadow(0 1px 0 #fff) drop-shadow(0 -1px 0 #fff)}.bx-centered-dialog a,.bx-settings-dialog a{color:#1c9d1c;text-decoration:none}.bx-centered-dialog a:hover,.bx-settings-dialog a:hover,.bx-centered-dialog a:focus,.bx-settings-dialog a:focus{color:#5dc21e}.bx-centered-dialog label,.bx-settings-dialog label{margin:0}.bx-controller-shortcuts-manager-container .bx-shortcut-note{margin-top:10px;font-size:14px;text-align:center}.bx-controller-shortcuts-manager-container .bx-shortcut-row{display:flex;gap:10px;margin-bottom:10px;align-items:center}.bx-controller-shortcuts-manager-container .bx-shortcut-row label.bx-prompt{flex-shrink:0;font-size:32px;margin:0}.bx-controller-shortcuts-manager-container .bx-shortcut-row label.bx-prompt::first-letter{letter-spacing:6px}.bx-controller-shortcuts-manager-container select:disabled{text-align:left;text-align-last:left}.bx-keyboard-shortcuts-manager-container{display:flex;flex-direction:column;gap:16px}.bx-keyboard-shortcuts-manager-container fieldset{background:#2a2a2a;border:1px solid #2a2a2a;border-radius:4px;padding:4px}.bx-keyboard-shortcuts-manager-container legend{width:auto;padding:4px 8px;margin:0 4px 4px;background:#004f87;box-shadow:0 2px 0 #071e3d;border-radius:4px;font-size:14px;font-weight:bold;text-transform:uppercase}.bx-keyboard-shortcuts-manager-container .bx-settings-row{background:none;padding:10px}.bx-settings-dialog{display:flex;position:fixed;top:0;right:0;bottom:0;opacity:.98;user-select:none;-webkit-user-select:none}.bx-settings-dialog .bx-settings-reload-note{font-size:.8rem;display:block;padding:8px;font-style:italic;font-weight:normal;height:var(--bx-button-height)}.bx-settings-tabs-container{position:fixed;width:48px;max-height:100vh;display:flex;flex-direction:column}.bx-settings-tabs-container > div:last-of-type{display:flex;flex-direction:column;align-items:end}.bx-settings-tabs-container > div:last-of-type button{flex-shrink:0;border-top-right-radius:0;border-bottom-right-radius:0;margin-top:8px;height:unset;padding:8px 10px}.bx-settings-tabs-container > div:last-of-type button svg{width:16px;height:16px}.bx-settings-tabs{display:flex;flex-direction:column;border-radius:0 0 0 8px;box-shadow:0 0 6px #000;overflow:overlay;flex:1}.bx-settings-tabs svg{width:24px;height:24px;padding:10px;flex-shrink:0;box-sizing:content-box;background:#131313;cursor:pointer;border-left:4px solid #1e1e1e}.bx-settings-tabs svg.bx-active{background:#222;border-color:#008746}.bx-settings-tabs svg:not(.bx-active):hover{background:#2f2f2f;border-color:#484848}.bx-settings-tabs svg:focus{border-color:#fff}.bx-settings-tabs svg[data-group=global][data-need-refresh=true]{background:var(--bx-danger-button-color) !important}.bx-settings-tabs svg[data-group=global][data-need-refresh=true]:hover{background:var(--bx-danger-button-hover-color) !important}.bx-settings-tab-contents{flex-direction:column;padding:10px;margin-left:48px;width:450px;max-width:calc(100vw - tabsWidth);background:#1a1b1e;color:#fff;font-weight:400;font-size:16px;font-family:var(--bx-title-font);text-align:center;box-shadow:0 0 6px #000;overflow:overlay;z-index:1}.bx-settings-tab-contents > div[data-tab-group=mkb]{display:flex;flex-direction:column;height:100%;overflow:hidden}.bx-settings-tab-contents .bx-top-buttons{display:flex;flex-direction:column;gap:8px;margin-bottom:8px}.bx-settings-tab-contents .bx-top-buttons .bx-button{display:block}.bx-settings-tab-contents h2{margin:16px 0 8px 0;display:flex;align-items:center}.bx-settings-tab-contents h2:first-of-type{margin-top:0}.bx-settings-tab-contents h2 span{display:inline-block;font-size:20px;font-weight:bold;text-align:left;flex:1;text-overflow:ellipsis;overflow:hidden;white-space:nowrap;min-height:var(--bx-button-height);align-content:center}@media (max-width:500px){.bx-settings-tab-contents{width:calc(100vw - 48px)}}.bx-settings-row{display:flex;gap:10px;padding:16px 10px;margin:0;background:#2a2a2a;border-bottom:1px solid #343434}.bx-settings-row:hover,.bx-settings-row:focus-within{background-color:#242424}.bx-settings-row:not(:has(> input[type=checkbox])){flex-wrap:wrap}.bx-settings-row > span.bx-settings-label{font-size:14px;display:block;text-align:left;align-self:center;margin-bottom:0 !important;flex:1}.bx-settings-row > span.bx-settings-label + *{margin:0 0 0 auto}.bx-settings-row[data-multi-lines="true"]{flex-direction:column}.bx-settings-row[data-multi-lines="true"] > span.bx-settings-label{align-self:start}.bx-settings-row[data-multi-lines="true"] > span.bx-settings-label + *{margin:unset}.bx-settings-dialog-note{display:block;color:#afafb0;font-size:12px;font-weight:lighter;font-style:italic}.bx-settings-dialog-note:not(:has(a)){margin-top:4px}.bx-settings-dialog-note a{display:inline-block;padding:4px}.bx-settings-custom-user-agent{display:block;width:100%;padding:6px}.bx-donation-link{display:block;text-align:center;text-decoration:none;height:20px;line-height:20px;font-size:14px;margin-top:10px;margin-bottom:10px}.bx-debug-info button{margin-top:10px}.bx-debug-info pre{margin-top:10px;cursor:copy;color:#fff;padding:8px;border:1px solid #2d2d2d;background:#212121;white-space:break-spaces;text-align:left}.bx-debug-info pre:hover{background:#272727}.bx-settings-app-version{margin-top:10px;text-align:center;color:#747474;font-size:12px}.bx-note-unsupported{display:block;font-size:12px;font-style:italic;font-weight:normal;color:#828282}.bx-settings-tab-contents > div *:not(.bx-settings-row):has(+ .bx-settings-row) + .bx-settings-row:has(+ .bx-settings-row){border-top-left-radius:6px;border-top-right-radius:6px}.bx-settings-tab-contents > div .bx-settings-row:not(:has(+ .bx-settings-row)){border:none;border-bottom-left-radius:6px;border-bottom-right-radius:6px}.bx-settings-tab-contents > div *:not(.bx-settings-row):has(+ .bx-settings-row) + .bx-settings-row:not(:has(+ .bx-settings-row)){border:none;border-radius:6px}.bx-suggest-toggler{text-align:left;display:flex;border-radius:4px;overflow:hidden;background:#003861;height:45px;align-items:center}.bx-suggest-toggler label{flex:1;align-content:center;padding:0 10px;background:#004f87;height:100%}.bx-suggest-toggler span{display:inline-block;align-self:center;padding:10px;width:45px;text-align:center}.bx-suggest-toggler:hover,.bx-suggest-toggler:focus{cursor:pointer;background:#005da1}.bx-suggest-toggler:hover label,.bx-suggest-toggler:focus label{background:#006fbe}.bx-suggest-toggler[bx-open] span{transform:rotate(90deg)}.bx-suggest-toggler[bx-open]+ .bx-suggest-box{display:block}.bx-suggest-box{display:none}.bx-suggest-wrapper{display:flex;flex-direction:column;gap:10px;margin:10px}.bx-suggest-note{font-size:11px;color:#8c8c8c;font-style:italic;font-weight:100}.bx-suggest-link{font-size:14px;display:inline-block;margin-top:4px;padding:4px}.bx-suggest-row{display:flex;flex-direction:row;gap:10px}.bx-suggest-row label{flex:1;overflow:overlay;border-radius:4px}.bx-suggest-row label .bx-suggest-label{background:#323232;padding:4px 10px;font-size:12px;text-align:left}.bx-suggest-row label .bx-suggest-value{padding:6px;font-size:14px}.bx-suggest-row label .bx-suggest-value.bx-suggest-change{background-color:var(--bx-warning-color)}.bx-suggest-row.bx-suggest-ok input{visibility:hidden}.bx-suggest-row.bx-suggest-ok .bx-suggest-label{background-color:#008114}.bx-suggest-row.bx-suggest-ok .bx-suggest-value{background-color:#13a72a}.bx-suggest-row.bx-suggest-change .bx-suggest-label{background-color:#a65e08}.bx-suggest-row.bx-suggest-change .bx-suggest-value{background-color:#d57f18}.bx-suggest-row.bx-suggest-change:hover label{cursor:pointer}.bx-suggest-row.bx-suggest-change:hover .bx-suggest-label{background-color:#995707}.bx-suggest-row.bx-suggest-change:hover .bx-suggest-value{background-color:#bd7115}.bx-suggest-row.bx-suggest-change input:not(:checked) + label{opacity:.5}.bx-suggest-row.bx-suggest-change input:not(:checked) + label .bx-suggest-label{background-color:#2a2a2a}.bx-suggest-row.bx-suggest-change input:not(:checked) + label .bx-suggest-value{background-color:#393939}.bx-suggest-row.bx-suggest-change:hover input:not(:checked) + label{opacity:1}.bx-suggest-row.bx-suggest-change:hover input:not(:checked) + label .bx-suggest-label{background-color:#202020}.bx-suggest-row.bx-suggest-change:hover input:not(:checked) + label .bx-suggest-value{background-color:#303030}.bx-sub-content-box{background:#161616;padding:10px;box-shadow:0 0 12px #0f0f0f inset;border-radius:10px}.bx-settings-row .bx-sub-content-box{background:#202020;padding:12px;box-shadow:0 0 4px #000 inset;border-radius:6px}.bx-controller-extra-settings[data-has-gamepad=true] > :first-child{display:none}.bx-controller-extra-settings[data-has-gamepad=true] > :last-child{display:block}.bx-controller-extra-settings[data-has-gamepad=false] > :first-child{display:block}.bx-controller-extra-settings[data-has-gamepad=false] > :last-child{display:none}.bx-controller-extra-settings .bx-controller-extra-wrapper{flex:1;min-width:1px}.bx-controller-extra-settings .bx-sub-content-box{flex:1;text-align:left;display:flex;flex-direction:column;margin-top:10px}.bx-controller-extra-settings .bx-sub-content-box > label{font-size:14px}.bx-preset-row{display:flex;gap:8px}.bx-preset-row .bx-select{flex:1}.bx-toast{user-select:none;-webkit-user-select:none;position:fixed;left:50%;top:24px;transform:translate(-50%,0);background:#000;border-radius:16px;color:#fff;z-index:var(--bx-toast-z-index);font-family:var(--bx-normal-font);border:2px solid #fff;display:flex;align-items:center;opacity:0;overflow:clip;transition:opacity .2s ease-in}.bx-toast.bx-show{opacity:.85}.bx-toast.bx-hide{opacity:0;pointer-events:none}.bx-toast-msg{font-size:14px;display:inline-block;padding:12px 16px;white-space:pre}.bx-toast-status{font-weight:bold;font-size:14px;text-transform:uppercase;display:inline-block;background:#515863;padding:12px 16px;color:#fff;white-space:pre}.bx-wait-time-box{position:fixed;top:0;right:0;background-color:rgba(0,0,0,0.8);color:#fff;z-index:var(--bx-wait-time-box-z-index);padding:12px;border-radius:0 0 0 8px}.bx-wait-time-box label{display:block;text-transform:uppercase;text-align:right;font-size:12px;font-weight:bold;margin:0}.bx-wait-time-box span{display:block;font-family:var(--bx-monospaced-font);text-align:right;font-size:16px;margin-bottom:10px}.bx-wait-time-box span:last-of-type{margin-bottom:0}.bx-remote-play-container{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);color:#fff;background:#1a1b1e;border-radius:10px;width:420px;max-width:calc(100vw - 20px);margin:0 0 0 auto;padding:16px}.bx-remote-play-container > .bx-button{display:table;margin:0 0 0 auto}.bx-remote-play-settings{margin-bottom:12px;padding-bottom:12px;border-bottom:1px solid #2d2d2d}.bx-remote-play-settings > div{display:flex}.bx-remote-play-settings label{flex:1}.bx-remote-play-settings label p{margin:4px 0 0;padding:0;color:#888;font-size:12px}.bx-remote-play-resolution{display:block}.bx-remote-play-resolution input[type="radio"]{accent-color:var(--bx-primary-button-color);margin-right:6px}.bx-remote-play-resolution input[type="radio"]:focus{accent-color:var(--bx-primary-button-hover-color)}.bx-remote-play-device-wrapper{display:flex;margin-bottom:12px}.bx-remote-play-device-wrapper:last-child{margin-bottom:2px}.bx-remote-play-device-info{flex:1;padding:4px 0}.bx-remote-play-device-name{font-size:20px;font-weight:bold;display:inline-block;vertical-align:middle}.bx-remote-play-console-type{font-size:12px;background:#004c87;color:#fff;display:inline-block;border-radius:14px;padding:2px 10px;margin-left:8px;vertical-align:middle}.bx-remote-play-power-state{color:#888;font-size:12px}.bx-remote-play-connect-button{min-height:100%;margin:4px 0}.bx-remote-play-buttons{display:flex;justify-content:space-between}select.bx-select{min-height:30px}div.bx-select{display:flex;align-items:stretch;flex:0 1 auto;gap:8px}div.bx-select select:disabled ~ button{display:none}div.bx-select select:disabled ~ div{background:#131416;color:#fff;pointer-events:none}div.bx-select select:disabled ~ div .bx-select-indicators{visibility:hidden}div.bx-select > div,div.bx-select button.bx-select-value{min-width:120px;text-align:left;line-height:24px;vertical-align:middle;background:#fff;color:#000;border-radius:4px;padding:2px 8px;display:flex;flex:1;flex-direction:column}div.bx-select > div{min-height:24px}div.bx-select > div input{display:inline-block;margin-right:8px}div.bx-select > div label{margin-bottom:0;font-size:14px;width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;min-height:15px}div.bx-select > div label span{display:block;font-size:10px;font-weight:bold;text-align:left;line-height:20px;white-space:pre;min-height:15px;align-content:center}div.bx-select button.bx-select-value{border:none;cursor:pointer;min-height:30px;font-size:.9rem;align-items:center}div.bx-select button.bx-select-value > div{display:flex;width:100%}div.bx-select button.bx-select-value span{flex:1;text-align:left;display:inline-block}div.bx-select button.bx-select-value input{margin:0 4px;accent-color:var(--bx-primary-button-color);pointer-events:none}div.bx-select button.bx-select-value:hover input,div.bx-select button.bx-select-value:focus input{accent-color:var(--bx-danger-button-color)}div.bx-select button.bx-select-value:hover::after,div.bx-select button.bx-select-value:focus::after{border-color:#4d4d4d !important}div.bx-select button.bx-button{border:none;width:24px;height:auto;padding:0;color:#fff;border-radius:4px;font-weight:bold;font-size:12px;font-family:var(--bx-monospaced-font);flex-shrink:0}div.bx-select button.bx-button span{line-height:unset}div.bx-select[data-controller-friendly=true] > div{box-sizing:content-box}div.bx-select[data-controller-friendly=true] select{position:absolute !important;top:-9999px !important;left:-9999px !important;visibility:hidden !important}div.bx-select[data-controller-friendly=false]{position:relative}div.bx-select[data-controller-friendly=false] > div{box-sizing:border-box}div.bx-select[data-controller-friendly=false] > div label{margin-right:24px}div.bx-select[data-controller-friendly=false] select:disabled{display:none}div.bx-select[data-controller-friendly=false] select:not(:disabled){cursor:pointer;position:absolute;top:0;right:0;bottom:0;display:block;opacity:0;z-index:calc(var(--bx-settings-z-index) + 1)}div.bx-select[data-controller-friendly=false] select:not(:disabled):hover + div{background:#f0f0f0}div.bx-select[data-controller-friendly=false] select:not(:disabled) + div label::after{content:\'▾\';font-size:14px;position:absolute;right:8px;pointer-events:none}.bx-select-indicators{display:flex;height:4px;gap:2px;margin-bottom:2px}.bx-select-indicators span{content:\' \';display:inline-block;flex:1;background:#cfcfcf;border-radius:4px;min-width:1px}.bx-select-indicators span[data-highlighted]{background:#9c9c9c;min-width:6px}.bx-select-indicators span[data-selected]{background:#aacfe7}.bx-select-indicators span[data-highlighted][data-selected]{background:#5fa3d0}.bx-guide-home-achievements-progress{display:flex;gap:10px;flex-direction:row}.bx-guide-home-achievements-progress .bx-button{margin-bottom:0 !important}body[data-bx-media-type=tv] .bx-guide-home-achievements-progress{flex-direction:column}body:not([data-bx-media-type=tv]) .bx-guide-home-achievements-progress{flex-direction:row}body:not([data-bx-media-type=tv]) .bx-guide-home-achievements-progress > button:first-of-type{flex:1}body:not([data-bx-media-type=tv]) .bx-guide-home-achievements-progress > button:last-of-type{width:40px}body:not([data-bx-media-type=tv]) .bx-guide-home-achievements-progress > button:last-of-type span{display:none}.bx-guide-home-buttons > div{display:flex;flex-direction:row;gap:12px}body[data-bx-media-type=tv] .bx-guide-home-buttons > div{flex-direction:column}body[data-bx-media-type=tv] .bx-guide-home-buttons > div button{margin-bottom:0 !important}body:not([data-bx-media-type=tv]) .bx-guide-home-buttons > div button span{display:none}.bx-guide-home-buttons[data-is-playing="true"] button[data-state=\'normal\']{display:none}.bx-guide-home-buttons[data-is-playing="false"] button[data-state=\'playing\']{display:none}div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module]{overflow:visible}.bx-stream-menu-button-on{fill:#000 !important;background-color:#2d2d2d !important;color:#000 !important}.bx-stream-refresh-button{top:calc(env(safe-area-inset-top, 0px) + 10px + 50px) !important}body[data-media-type=default] .bx-stream-refresh-button{left:calc(env(safe-area-inset-left, 0px) + 11px) !important}body[data-media-type=tv] .bx-stream-refresh-button{top:calc(var(--gds-focus-borderSize) + 80px) !important}.bx-stream-home-button{top:calc(env(safe-area-inset-top, 0px) + 10px + 50px * 2) !important}body[data-media-type=default] .bx-stream-home-button{left:calc(env(safe-area-inset-left, 0px) + 12px) !important}body[data-media-type=tv] .bx-stream-home-button{top:calc(var(--gds-focus-borderSize) + 80px * 2) !important}div[data-testid=media-container][data-position=center]{display:flex}div[data-testid=media-container][data-position=top] video,div[data-testid=media-container][data-position=top] canvas{top:0}div[data-testid=media-container][data-position=bottom] video,div[data-testid=media-container][data-position=bottom] canvas{bottom:0}#game-stream video{margin:auto;align-self:center;background:#000;position:absolute;left:0;right:0}#game-stream canvas{align-self:center;margin:auto;position:absolute;left:0;right:0}#game-stream.bx-taking-screenshot:before{animation:bx-anim-taking-screenshot .5s ease;content:\' \';position:absolute;width:100%;height:100%;z-index:var(--bx-screenshot-animation-z-index)}#gamepass-dialog-root div[class^=Guide-module__guide] .bx-button{overflow:visible;margin-bottom:12px}@-moz-keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}@-webkit-keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}@-o-keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}@keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}.bx-number-stepper{text-align:center}.bx-number-stepper > div{display:flex;align-items:center}.bx-number-stepper > div span{flex:1;display:inline-block;min-width:40px;font-family:var(--bx-monospaced-font);white-space:pre;font-size:13px;margin:0 4px}.bx-number-stepper > div button{flex-shrink:0;border:none;width:24px;height:24px;margin:0;line-height:24px;background-color:var(--bx-default-button-color);color:#fff;border-radius:4px;font-weight:bold;font-size:14px;font-family:var(--bx-monospaced-font)}@media (hover:hover){.bx-number-stepper > div button:hover{background-color:var(--bx-default-button-hover-color)}}.bx-number-stepper > div button:active{background-color:var(--bx-default-button-hover-color)}.bx-number-stepper > div button:disabled + span{font-family:var(--bx-title-font)}.bx-number-stepper input[type=range]{display:block;margin:8px 0 2px auto;min-width:180px;width:100%;color:#959595 !important}.bx-number-stepper input[type=range]:disabled,.bx-number-stepper button:disabled{display:none}.bx-number-stepper[data-disabled=true] input[type=range],.bx-number-stepper[disabled=true] input[type=range],.bx-number-stepper[data-disabled=true] button,.bx-number-stepper[disabled=true] button{display:none}.bx-dual-number-stepper > span{display:block;font-family:var(--bx-monospaced-font);font-size:13px;white-space:pre;margin:0 4px;text-align:center}.bx-dual-number-stepper > div input[type=range]{display:block;width:100%;min-width:180px;background:transparent;color:#959595 !important;appearance:none;padding:8px 0}.bx-dual-number-stepper > div input[type=range]::-webkit-slider-runnable-track{background:linear-gradient(90deg,#fff var(--from),var(--bx-primary-button-color) var(--from) var(--to),#fff var(--to) 100%);height:8px;border-radius:2px}.bx-dual-number-stepper > div input[type=range]::-moz-range-track{background:linear-gradient(90deg,#fff var(--from),var(--bx-primary-button-color) var(--from) var(--to),#fff var(--to) 100%);height:8px;border-radius:2px}.bx-dual-number-stepper > div input[type=range]::-webkit-slider-thumb{margin-top:-4px;appearance:none;width:4px;height:16px;background:#00b85f;border:none;border-radius:2px}.bx-dual-number-stepper > div input[type=range]::-moz-range-thumb{margin-top:-4px;appearance:none;width:4px;height:16px;background:#00b85f;border:none;border-radius:2px}.bx-dual-number-stepper > div input[type=range]:hover::-webkit-slider-runnable-track,.bx-dual-number-stepper > div input[type=range].bx-dual-number-stepper > div input[type=range]:active::-webkit-slider-runnable-track,.bx-dual-number-stepper > div input[type=range]:focus::-webkit-slider-runnable-track{background:linear-gradient(90deg,#fff var(--from),#006635 var(--from) var(--to),#fff var(--to) 100%)}.bx-dual-number-stepper > div input[type=range]:hover::-moz-range-track,.bx-dual-number-stepper > div input[type=range].bx-dual-number-stepper > div input[type=range]:active::-moz-range-track,.bx-dual-number-stepper > div input[type=range]:focus::-moz-range-track{background:linear-gradient(90deg,#fff var(--from),#006635 var(--from) var(--to),#fff var(--to) 100%)}.bx-dual-number-stepper > div input[type=range]:hover::-webkit-slider-thumb,.bx-dual-number-stepper > div input[type=range].bx-dual-number-stepper > div input[type=range]:active::-webkit-slider-thumb,.bx-dual-number-stepper > div input[type=range]:focus::-webkit-slider-thumb{background:#fb3232}.bx-dual-number-stepper > div input[type=range]:hover::-moz-range-thumb,.bx-dual-number-stepper > div input[type=range].bx-dual-number-stepper > div input[type=range]:active::-moz-range-thumb,.bx-dual-number-stepper > div input[type=range]:focus::-moz-range-thumb{background:#fb3232}.bx-dual-number-stepper[data-disabled=true] input[type=range],.bx-dual-number-stepper[disabled=true] input[type=range]{display:none}#bx-game-bar{z-index:var(--bx-game-bar-z-index);position:fixed;bottom:0;width:40px;height:90px;overflow:visible;cursor:pointer}#bx-game-bar > svg{display:none;pointer-events:none;position:absolute;height:28px;margin-top:16px}@media (hover:hover){#bx-game-bar:hover > svg{display:block}}#bx-game-bar .bx-game-bar-container{opacity:0;position:absolute;display:flex;overflow:hidden;background:rgba(26,27,30,0.91);box-shadow:0 0 6px #1c1c1c;transition:opacity .1s ease-in}#bx-game-bar .bx-game-bar-container.bx-show{opacity:.9}#bx-game-bar .bx-game-bar-container.bx-show + svg{display:none !important}#bx-game-bar .bx-game-bar-container.bx-hide{opacity:0;pointer-events:none}#bx-game-bar .bx-game-bar-container button{width:60px;height:60px;border-radius:0}#bx-game-bar .bx-game-bar-container button svg{width:28px;height:28px;transition:transform .08s ease 0s}#bx-game-bar .bx-game-bar-container button:hover{border-radius:0}#bx-game-bar .bx-game-bar-container button:active svg{transform:scale(.75)}#bx-game-bar .bx-game-bar-container button.bx-activated{background-color:#fff}#bx-game-bar .bx-game-bar-container button.bx-activated svg{filter:invert(1)}#bx-game-bar .bx-game-bar-container div[data-activated] button{display:none}#bx-game-bar .bx-game-bar-container div[data-activated=\'false\'] button:first-of-type{display:block}#bx-game-bar .bx-game-bar-container div[data-activated=\'true\'] button:last-of-type{display:block}#bx-game-bar[data-position="bottom-left"]{left:0;direction:ltr}#bx-game-bar[data-position="bottom-left"] .bx-game-bar-container{border-radius:0 10px 10px 0}#bx-game-bar[data-position="bottom-right"]{right:0;direction:rtl}#bx-game-bar[data-position="bottom-right"] .bx-game-bar-container{direction:ltr;border-radius:10px 0 0 10px}.bx-badges{margin-left:0;user-select:none;-webkit-user-select:none}.bx-badge{border:none;display:inline-block;line-height:24px;color:#fff;font-family:var(--bx-title-font-semibold);font-size:14px;font-weight:400;margin:0 8px 8px 0;box-shadow:0 0 6px #000;border-radius:4px}.bx-badge-name{background-color:#2d3036;border-radius:4px 0 0 4px}.bx-badge-name svg{width:16px;height:16px}.bx-badge-value{background-color:#808080;border-radius:0 4px 4px 0}.bx-badge-name,.bx-badge-value{display:inline-block;padding:0 8px;line-height:30px;vertical-align:bottom}.bx-badge-battery[data-charging=true] span:first-of-type::after{content:\' ⚡️\'}div[class^=StreamMenu-module__container] .bx-badges{position:absolute;max-width:500px}#gamepass-dialog-root .bx-badges{position:fixed;top:60px;left:460px;max-width:500px}@media (min-width:568px) and (max-height:480px){#gamepass-dialog-root .bx-badges{position:unset;top:unset;left:unset;margin:8px 0}}.bx-stats-bar{display:flex;flex-direction:row;gap:8px;user-select:none;-webkit-user-select:none;position:fixed;top:0;background-color:#000;color:#fff;font-family:var(--bx-monospaced-font);font-size:.9rem;padding-left:8px;z-index:var(--bx-stats-bar-z-index);text-wrap:nowrap}.bx-stats-bar[data-stats*="[time]"] > .bx-stat-time,.bx-stats-bar[data-stats*="[play]"] > .bx-stat-play,.bx-stats-bar[data-stats*="[batt]"] > .bx-stat-batt,.bx-stats-bar[data-stats*="[fps]"] > .bx-stat-fps,.bx-stats-bar[data-stats*="[ping]"] > .bx-stat-ping,.bx-stats-bar[data-stats*="[jit]"] > .bx-stat-jit,.bx-stats-bar[data-stats*="[btr]"] > .bx-stat-btr,.bx-stats-bar[data-stats*="[dt]"] > .bx-stat-dt,.bx-stats-bar[data-stats*="[pl]"] > .bx-stat-pl,.bx-stats-bar[data-stats*="[fl]"] > .bx-stat-fl,.bx-stats-bar[data-stats*="[dl]"] > .bx-stat-dl,.bx-stats-bar[data-stats*="[ul]"] > .bx-stat-ul{display:inline-flex;align-items:baseline}.bx-stats-bar[data-stats$="[time]"] > .bx-stat-time,.bx-stats-bar[data-stats$="[play]"] > .bx-stat-play,.bx-stats-bar[data-stats$="[batt]"] > .bx-stat-batt,.bx-stats-bar[data-stats$="[fps]"] > .bx-stat-fps,.bx-stats-bar[data-stats$="[ping]"] > .bx-stat-ping,.bx-stats-bar[data-stats$="[jit]"] > .bx-stat-jit,.bx-stats-bar[data-stats$="[btr]"] > .bx-stat-btr,.bx-stats-bar[data-stats$="[dt]"] > .bx-stat-dt,.bx-stats-bar[data-stats$="[pl]"] > .bx-stat-pl,.bx-stats-bar[data-stats$="[fl]"] > .bx-stat-fl,.bx-stats-bar[data-stats$="[dl]"] > .bx-stat-dl,.bx-stats-bar[data-stats$="[ul]"] > .bx-stat-ul{border-right:none}.bx-stats-bar::before{display:none;content:\'👀\';vertical-align:middle;margin-right:8px}.bx-stats-bar[data-display=glancing]::before{display:inline-block}.bx-stats-bar[data-position=top-left]{left:0;border-radius:0 0 4px 0}.bx-stats-bar[data-position=top-right]{right:0;border-radius:0 0 0 4px}.bx-stats-bar[data-position=top-center]{transform:translate(-50%,0);left:50%;border-radius:0 0 4px 4px}.bx-stats-bar[data-shadow=true]{background:none;filter:drop-shadow(1px 0 0 rgba(0,0,0,0.941)) drop-shadow(-1px 0 0 rgba(0,0,0,0.941)) drop-shadow(0 1px 0 rgba(0,0,0,0.941)) drop-shadow(0 -1px 0 rgba(0,0,0,0.941))}.bx-stats-bar > div{display:none;border-right:1px solid #fff;padding-right:8px}.bx-stats-bar label{margin:0 8px 0 0;font-family:var(--bx-title-font);font-size:70%;font-weight:bold;vertical-align:middle;cursor:help}.bx-stats-bar span{display:inline-block;text-align:right;vertical-align:middle;white-space:pre}.bx-stats-bar span[data-grade=good]{color:#6bffff}.bx-stats-bar span[data-grade=ok]{color:#fff16b}.bx-stats-bar span[data-grade=bad]{color:#ff5f5f}.bx-mkb-settings{display:flex;flex-direction:column;flex:1;padding-bottom:10px;overflow:hidden}.bx-mkb-pointer-lock-msg{user-select:none;-webkit-user-select:none;position:fixed;left:50%;bottom:40px;transform:translateX(-50%);margin:auto;background:#151515;z-index:var(--bx-mkb-pointer-lock-msg-z-index);color:#fff;font-weight:400;font-family:"Segoe UI",Arial,Helvetica,sans-serif;font-size:1.3rem;padding:12px;border-radius:8px;align-items:center;box-shadow:0 0 6px #000;min-width:300px;opacity:.9;display:flex;flex-direction:column;gap:10px}.bx-mkb-pointer-lock-msg:hover{opacity:1}.bx-mkb-pointer-lock-msg > p{margin:0;width:100%;font-size:22px;margin-bottom:4px;font-weight:bold;text-align:left}.bx-mkb-pointer-lock-msg > div{width:100%;display:flex;flex-direction:row;gap:10px}.bx-mkb-pointer-lock-msg > div button:first-of-type{flex-shrink:1}.bx-mkb-pointer-lock-msg > div button:last-of-type{flex-grow:1}.bx-mkb-key-row{display:flex;margin-bottom:10px;align-items:center;gap:20px}.bx-mkb-key-row label{margin-bottom:0;font-family:var(--bx-promptfont-font);font-size:32px;text-align:center}.bx-mkb-settings.bx-editing .bx-mkb-key-row button{background:#393939;border-radius:4px;border:none}.bx-mkb-settings.bx-editing .bx-mkb-key-row button:hover{background:#333;cursor:pointer}.bx-mkb-action-buttons > div{text-align:right;display:none}.bx-mkb-action-buttons button{margin-left:8px}.bx-mkb-settings:not(.bx-editing) .bx-mkb-action-buttons > div:first-child{display:block}.bx-mkb-settings.bx-editing .bx-mkb-action-buttons > div:last-child{display:block}.bx-mkb-note{display:block;margin:0 0 10px;font-size:12px;text-align:center}button.bx-binding-button{flex:1;min-height:38px;border:none;border-radius:4px;font-size:14px;color:#fff;display:flex;align-items:center;align-self:center;padding:0 6px}button.bx-binding-button:disabled{background:#131416;padding:0 8px}button.bx-binding-button:not(:disabled){border:2px solid transparent;border-top:none;border-bottom:4px solid #252525;background:#3b3b3b;cursor:pointer}button.bx-binding-button:not(:disabled):hover,button.bx-binding-button:not(:disabled).bx-focusable:focus{background:#20b217;border-bottom-color:#186c13}button.bx-binding-button:not(:disabled):active{background:#16900f;border-bottom:3px solid #0c4e08;border-left-width:2px;border-right-width:2px}button.bx-binding-button:not(:disabled).bx-focusable:focus::after{top:-6px;left:-8px;right:-8px;bottom:-10px}.bx-settings-row .bx-binding-button-wrapper button.bx-binding-button{min-width:60px}.bx-controller-customizations-container .bx-btn-detect{display:block;margin-bottom:20px}.bx-controller-customizations-container .bx-btn-detect.bx-monospaced{background:none;font-weight:bold;font-size:12px}.bx-controller-customizations-container .bx-buttons-grid{display:grid;grid-template-columns:auto auto;column-gap:20px;row-gap:10px;margin-bottom:20px}.bx-controller-key-row{display:flex;align-items:stretch}.bx-controller-key-row > label{margin-bottom:0;font-family:var(--bx-promptfont-font);font-size:32px;text-align:center;min-width:50px;flex-shrink:0;display:flex;align-self:center}.bx-controller-key-row > label::after{content:\'❯\';margin:0 12px;font-size:16px;align-self:center}.bx-controller-key-row .bx-select{width:100% !important}.bx-controller-key-row .bx-select > div{min-width:50px}.bx-controller-key-row .bx-select label{font-family:var(--bx-promptfont-font),var(--bx-normal-font);font-size:32px;text-align:center;margin-bottom:6px;height:40px;line-height:40px}.bx-controller-key-row:hover > label{color:#ffe64b}.bx-controller-key-row:hover > label::after{color:#fff}.bx-product-details-buttons{display:flex;gap:10px;flex-direction:row}.bx-product-details-buttons button{max-width:max-content;margin:10px 0 0 0;display:flex}@media (min-width:568px) and (max-height:480px){.bx-product-details-buttons{flex-direction:column}.bx-product-details-buttons button{margin:8px 0 0 10px}}', PREF_HIDE_SECTIONS = getPref("ui.hideSections"), selectorToHide = []; if (PREF_HIDE_SECTIONS.includes("news")) selectorToHide.push("#BodyContent > div[class*=CarouselRow-module]"); if (getPref("block.features").includes("byog")) selectorToHide.push("#BodyContent > div[class*=ByogRow-module__container___]"); if (PREF_HIDE_SECTIONS.includes("all-games")) selectorToHide.push("#BodyContent div[class*=AllGamesRow-module__gridContainer]"), selectorToHide.push("#BodyContent div[class*=AllGamesRow-module__rowHeader]"); @@ -9171,7 +9423,7 @@ class RootDialogObserver { static $btnShortcut = AppInterface && createButton({ icon: BxIcon.CREATE_SHORTCUT, label: t("create-shortcut"), - style: 64 | 8 | 128 | 2048 | 4096, + style: 64 | 8 | 128 | 4096 | 8192, onClick: (e) => { window.BX_EXPOSED.dialogRoutes?.closeAll(); let $btn = e.target.closest("button"); @@ -9181,7 +9433,7 @@ class RootDialogObserver { static $btnWallpaper = AppInterface && createButton({ icon: BxIcon.DOWNLOAD, label: t("wallpaper"), - style: 64 | 8 | 128 | 2048 | 4096, + style: 64 | 8 | 128 | 4096 | 8192, onClick: (e) => { window.BX_EXPOSED.dialogRoutes?.closeAll(); let $btn = e.target.closest("button"), details = parseDetailsPath($btn.dataset.path); diff --git a/package.json b/package.json index f082668..14c4fcc 100755 --- a/package.json +++ b/package.json @@ -11,10 +11,10 @@ }, "devDependencies": { "@types/bun": "^1.1.14", - "@types/node": "^22.10.1", + "@types/node": "^22.10.2", "@types/stylus": "^0.48.43", - "eslint": "^9.16.0", - "eslint-plugin-compat": "^6.0.1", + "eslint": "^9.17.0", + "eslint-plugin-compat": "^6.0.2", "stylus": "^0.64.0" }, "peerDependencies": { diff --git a/src/assets/css/controller.styl b/src/assets/css/controller.styl new file mode 100644 index 0000000..e12089d --- /dev/null +++ b/src/assets/css/controller.styl @@ -0,0 +1,71 @@ +.bx-controller-customizations-container { + .bx-btn-detect { + display: block; + margin-bottom: 20px; + + &.bx-monospaced { + background: none; + font-weight: bold; + font-size: 12px; + } + } + + .bx-buttons-grid { + display: grid; + grid-template-columns: auto auto; + column-gap: 20px; + row-gap: 10px; + margin-bottom: 20px; + } + +} + +.bx-controller-key-row { + display: flex; + align-items: stretch; + + > label { + margin-bottom: 0; + font-family: var(--bx-promptfont-font); + font-size: 32px; + text-align: center; + min-width: 50px; + flex-shrink: 0; + display: flex; + align-self: center; + + &::after { + content: '❯'; + margin: 0 12px; + font-size: 16px; + align-self: center; + } + } + + .bx-select { + width: 100% !important; + + > div { + min-width: 50px; + } + + label { + font-family: var(--bx-promptfont-font), var(--bx-normal-font); + font-size: 32px; + text-align: center; + margin-bottom: 6px; + height: 40px; + line-height: 40px; + } + } + + &:hover { + > label { + color: #ffe64b; + + &::after { + color: #fff; + } + } + } +} diff --git a/src/assets/css/navigation-dialog.styl b/src/assets/css/navigation-dialog.styl index fbcb88c..70af076 100755 --- a/src/assets/css/navigation-dialog.styl +++ b/src/assets/css/navigation-dialog.styl @@ -17,6 +17,21 @@ border-radius: 4px; padding: 0 5px; } + + .bx-focusable { + &::after { + border-radius: 4px; + } + + &:focus::after { + offset = 0; + + top: offset; + left: offset; + right: offset; + bottom: offset; + } + } } .bx-navigation-dialog-overlay { @@ -42,7 +57,7 @@ color: white; background: #1a1b1e; border-radius: 10px; - width: 450px; + min-width: @css{ min(calc(100vw - 20px), 500px) }; max-width: calc(100vw - 20px); margin: 0 0 0 auto; padding: 16px; @@ -74,11 +89,9 @@ .bx-dialog-content { flex: 1; + padding: 6px; overflow: auto; overflow-x: hidden; - - > div { - } } .bx-dialog-preset-tools { @@ -86,8 +99,9 @@ margin-bottom: 12px; gap: 6px; - select { - flex: 1; + button { + align-self: center; + min-height: 50px; } } @@ -164,29 +178,6 @@ letter-spacing: 6px; } } - - .bx-shortcut-actions { - flex: 1; - position: relative; - - select { - width: 100%; - height: 100%; - min-height: 38px; - display: block; - - &:first-of-type { - position: absolute; - top: 0; - left: 0; - } - - &:last-of-type { - opacity: 0; - z-index: calc(var(--bx-settings-z-index) + 1); - } - } - } } select:disabled { diff --git a/src/assets/css/number-stepper.styl b/src/assets/css/number-stepper.styl index b017633..3e21957 100755 --- a/src/assets/css/number-stepper.styl +++ b/src/assets/css/number-stepper.styl @@ -10,6 +10,7 @@ display: inline-block; min-width: 40px; font-family: var(--bx-monospaced-font); + white-space: pre; font-size: 13px; margin: 0 4px; } @@ -44,7 +45,7 @@ } } - input[type="range"] { + input[type=range] { display: block; margin: 8px 0 2px auto; min-width: 180px; @@ -62,3 +63,91 @@ } } } + + +.bx-dual-number-stepper { + > span { + display: block; + font-family: var(--bx-monospaced-font); + font-size: 13px; + white-space: pre; + margin: 0 4px; + text-align: center; + } + + > div { + input[type=range] { + display: block; + width: 100%; + min-width: 180px; + background: transparent; + color: #959595 !important; + appearance: none; + padding: 8px 0; + + range-track() { + background: linear-gradient(90deg, #fff var(--from), var(--bx-primary-button-color) var(--from) var(--to), #fff var(--to) 100%); + height: 8px; + border-radius: 2px; + } + + range-track-hover() { + background: linear-gradient(90deg, #fff var(--from), #006635 var(--from) var(--to), #fff var(--to) 100%); + } + + thumb() { + margin-top: -4px; + appearance: none; + width: 4px; + height: 16px; + background: #00b85f; + border: none; + border-radius: 2px; + } + + thumb-hover() { + background: #fb3232; + } + + &::-webkit-slider-runnable-track { + range-track() + } + + &::-moz-range-track { + range-track() + } + + &::-webkit-slider-thumb { + thumb(); + } + + &::-moz-range-thumb { + thumb(); + } + + &:hover, &&:active, &:focus { + &::-webkit-slider-runnable-track { + range-track-hover(); + } + + &::-moz-range-track { + range-track-hover(); + } + + &::-webkit-slider-thumb { + thumb-hover(); + } + + &::-moz-range-thumb { + thumb-hover(); + } + } + } + } + + &[data-disabled=true], &[disabled=true] { + input[type=range] { + display: none; + } + } +} diff --git a/src/assets/css/root.styl b/src/assets/css/root.styl index 957e48d..da56668 100755 --- a/src/assets/css/root.styl +++ b/src/assets/css/root.styl @@ -47,6 +47,7 @@ button_color(name, normal, hover, active, disabled) @font-face { font-family: 'promptfont'; src: url('https://redphx.github.io/better-xcloud/fonts/promptfont.otf'); + unicode-range: U+2196-E011; } /* Fix Stream menu buttons not hiding */ @@ -73,6 +74,10 @@ div[class^=HUDButton-module__hiddenContainer] ~ div:not([class^=HUDButton-module height: 100% !important; } +.bx-auto-height { + height: auto !important; +} + .bx-no-scroll { overflow: hidden !important; } @@ -125,6 +130,10 @@ div[class^=HUDButton-module__hiddenContainer] ~ div:not([class^=HUDButton-module font-family: var(--bx-promptfont-font) !important; } +.bx-monospaced { + font-family: var(--bx-monospaced-font) !important; +} + .bx-line-through { text-decoration: line-through !important; } @@ -270,3 +279,15 @@ div[class*=SupportedInputsBadge] { opacity: 0; } } + +.bx-horizontal-shaking { + animation: bx-horizontal-shaking .4s ease-in-out 2; +} + +@keyframes bx-horizontal-shaking { + 0% { transform: translateX(0) } + 25% { transform: translateX(5px) } + 50% { transform: translateX(-5px) } + 75% { transform: translateX(5px) } + 100% { transform: translateX(0) } +} diff --git a/src/assets/css/settings-dialog.styl b/src/assets/css/settings-dialog.styl index 925ebd5..092676b 100755 --- a/src/assets/css/settings-dialog.styl +++ b/src/assets/css/settings-dialog.styl @@ -8,21 +8,6 @@ user-select: none; -webkit-user-select: none; - .bx-focusable { - &::after { - border-radius: 4px; - } - - &:focus::after { - offset = 0; - - top: offset; - left: offset; - right: offset; - bottom: offset; - } - } - .bx-settings-reload-note { font-size: 0.8rem; display: block; diff --git a/src/assets/css/styles.styl b/src/assets/css/styles.styl index b8c1a15..62c9b80 100755 --- a/src/assets/css/styles.styl +++ b/src/assets/css/styles.styl @@ -16,4 +16,5 @@ @import 'game-bar.styl'; @import 'stream-stats.styl'; @import 'mkb.styl'; +@import 'controller.styl'; @import 'misc.styl'; diff --git a/src/assets/css/web-components.styl b/src/assets/css/web-components.styl index 7eb2a8f..6989178 100755 --- a/src/assets/css/web-components.styl +++ b/src/assets/css/web-components.styl @@ -4,17 +4,11 @@ select.bx-select { div.bx-select { display: flex; - align-items: center; + align-items: stretch; flex: 0 1 auto; gap: 8px; select { - // Render offscreen instead of "display: none" so we could get its size - position: absolute !important; - top: -9999px !important; - left: -9999px !important; - visibility: hidden !important; - &:disabled { & ~ button { display: none; @@ -48,7 +42,6 @@ div.bx-select { > div { min-height: 24px; - box-sizing: content-box; input { display: inline-block; @@ -69,7 +62,7 @@ div.bx-select { font-size: 10px; font-weight: bold; text-align: left; - line-height: initial; + line-height: 20px; white-space: pre; min-height: 15px; align-content: center; @@ -115,10 +108,9 @@ div.bx-select { button.bx-button { border: none; - height: 24px; width: 24px; + height: auto; padding: 0; - line-height: 24px; color: #fff; border-radius: 4px; font-weight: bold; @@ -130,6 +122,68 @@ div.bx-select { line-height: unset; } } + + &[data-controller-friendly=true] { + > div { + box-sizing: content-box; + } + + select { + // Render offscreen instead of "display: none" so we could get its size + position: absolute !important; + top: -9999px !important; + left: -9999px !important; + visibility: hidden !important; + } + } + + &[data-controller-friendly=false] { + position: relative; + + > div { + box-sizing: border-box; + + label { + margin-right: 24px; + } + } + + select { + &:disabled { + display: none; + } + + &:not(:disabled) { + cursor: pointer; + position: absolute; + top: 0; + right: 0; + bottom: 0; + display: block; + + opacity: 0; + z-index: calc(var(--bx-settings-z-index) + 1); + + &:hover { + + div { + background: #f0f0f0; + } + } + + + div { + label { + &::after { + content: '▾'; + font-size: 14px; + position: absolute; + right: 8px; + pointer-events: none; + } + } + } + } + } + } } .bx-select-indicators { @@ -144,9 +198,11 @@ div.bx-select { flex: 1; background: #cfcfcf; border-radius: 4px; + min-width: 1px; &[data-highlighted] { background: #9c9c9c; + min-width: 6px; } &[data-selected] { diff --git a/src/assets/svg/pencil-simple-line.svg b/src/assets/svg/pencil-simple-line.svg new file mode 100644 index 0000000..c1bd3b2 --- /dev/null +++ b/src/assets/svg/pencil-simple-line.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/enums/gamepad.ts b/src/enums/gamepad.ts index 1f19674..70994e4 100755 --- a/src/enums/gamepad.ts +++ b/src/enums/gamepad.ts @@ -24,11 +24,13 @@ export enum GamepadKey { LS_DOWN = 101, LS_LEFT = 102, LS_RIGHT = 103, + LS = 104, RS_UP = 200, RS_DOWN = 201, RS_LEFT = 202, RS_RIGHT = 203, + RS = 204, }; export const GamepadKeyName: Record = { @@ -56,12 +58,16 @@ export const GamepadKeyName: Record = { [GamepadKey.LS_DOWN]: ['Left Stick Down', PrompFont.LS_DOWN], [GamepadKey.LS_LEFT]: ['Left Stick Left', PrompFont.LS_LEFT], [GamepadKey.LS_RIGHT]: ['Left Stick Right', PrompFont.LS_RIGHT], + [GamepadKey.LS]: ['Left Stick', PrompFont.LS], [GamepadKey.R3]: ['R3', PrompFont.R3], [GamepadKey.RS_UP]: ['Right Stick Up', PrompFont.RS_UP], [GamepadKey.RS_DOWN]: ['Right Stick Down', PrompFont.RS_DOWN], [GamepadKey.RS_LEFT]: ['Right Stick Left', PrompFont.RS_LEFT], [GamepadKey.RS_RIGHT]: ['Right Stick Right', PrompFont.RS_RIGHT], + [GamepadKey.RS]: ['Right Stick', PrompFont.RS], + + [GamepadKey.SHARE]: ['Screenshot', PrompFont.SHARE], }; diff --git a/src/enums/pref-keys.ts b/src/enums/pref-keys.ts index 7f8d29e..bc84df1 100755 --- a/src/enums/pref-keys.ts +++ b/src/enums/pref-keys.ts @@ -1,3 +1,5 @@ +import type { BlockFeature, CodecProfile, DeviceVibrationMode, GameBarPosition, LoadingScreenRocket, NativeMkbMode, StreamPlayerType, StreamResolution, StreamStat, StreamStatPosition, StreamVideoProcessing, TouchControllerMode, TouchControllerStyleCustom, TouchControllerStyleStandard, UiLayout, UiSection, VideoPosition, VideoPowerPreference, VideoRatio } from "./pref-values" + export const enum StorageKey { GLOBAL = 'BetterXcloud', @@ -12,6 +14,7 @@ export const enum StorageKey { LIST_FORCE_NATIVE_MKB = 'BetterXcloud.GhPages.ForceNativeMkb', } + export const enum PrefKey { VERSION_LAST_CHECK = 'version.lastCheck', VERSION_LATEST = 'version.latest', @@ -112,3 +115,78 @@ export const enum PrefKey { GAME_FORTNITE_FORCE_CONSOLE = 'game.fortnite.forceConsole', } + + +export type PrefTypeMap = { + [PrefKey.AUDIO_MIC_ON_PLAYING]: boolean, + [PrefKey.AUDIO_VOLUME_CONTROL_ENABLED]: boolean, + [PrefKey.AUDIO_VOLUME]: number, + [PrefKey.BLOCK_FEATURES]: BlockFeature[], + [PrefKey.BLOCK_TRACKING]: boolean, + [PrefKey.CONTROLLER_POLLING_RATE]: number, + [PrefKey.DEVICE_VIBRATION_INTENSITY]: number, + [PrefKey.DEVICE_VIBRATION_MODE]: DeviceVibrationMode, + [PrefKey.GAME_BAR_POSITION]: GameBarPosition, + [PrefKey.GAME_FORTNITE_FORCE_CONSOLE]: boolean, + [PrefKey.KEYBOARD_SHORTCUTS_IN_GAME_PRESET_ID]: number, + [PrefKey.LOADING_SCREEN_GAME_ART]: boolean, + [PrefKey.LOADING_SCREEN_ROCKET]: LoadingScreenRocket, + [PrefKey.LOADING_SCREEN_SHOW_WAIT_TIME]: boolean, + [PrefKey.LOCAL_CO_OP_ENABLED]: boolean, + [PrefKey.MKB_ENABLED]: boolean, + [PrefKey.MKB_HIDE_IDLE_CURSOR]: boolean, + [PrefKey.MKB_P1_MAPPING_PRESET_ID]: number, + [PrefKey.MKB_P1_SLOT]: number, + [PrefKey.NATIVE_MKB_FORCED_GAMES]: string[], + [PrefKey.NATIVE_MKB_MODE]: NativeMkbMode, + [PrefKey.NATIVE_MKB_SCROLL_HORIZONTAL_SENSITIVITY]: number, + [PrefKey.NATIVE_MKB_SCROLL_VERTICAL_SENSITIVITY]: number, + [PrefKey.REMOTE_PLAY_ENABLED]: boolean, + [PrefKey.REMOTE_PLAY_STREAM_RESOLUTION]: StreamResolution, + [PrefKey.SCREENSHOT_APPLY_FILTERS]: boolean, + [PrefKey.SERVER_BYPASS_RESTRICTION]: string, + [PrefKey.SERVER_PREFER_IPV6]: boolean, + [PrefKey.SERVER_REGION]: string, + [PrefKey.STATS_CONDITIONAL_FORMATTING]: boolean, + [PrefKey.STATS_ITEMS]: StreamStat[], + [PrefKey.STATS_OPACITY_ALL]: number, + [PrefKey.STATS_OPACITY_BACKGROUND]: number, + [PrefKey.STATS_POSITION]: StreamStatPosition, + [PrefKey.STATS_QUICK_GLANCE_ENABLED]: boolean, + [PrefKey.STATS_SHOW_WHEN_PLAYING]: boolean, + [PrefKey.STATS_TEXT_SIZE]: string, + [PrefKey.STREAM_CODEC_PROFILE]: CodecProfile, + [PrefKey.STREAM_COMBINE_SOURCES]: boolean, + [PrefKey.STREAM_MAX_VIDEO_BITRATE]: number, + [PrefKey.STREAM_PREFERRED_LOCALE]: StreamPreferredLocale, + [PrefKey.STREAM_RESOLUTION]: StreamResolution, + [PrefKey.TOUCH_CONTROLLER_AUTO_OFF]: boolean, + [PrefKey.TOUCH_CONTROLLER_DEFAULT_OPACITY]: number, + [PrefKey.TOUCH_CONTROLLER_MODE]: TouchControllerMode, + [PrefKey.TOUCH_CONTROLLER_STYLE_CUSTOM]: TouchControllerStyleCustom, + [PrefKey.TOUCH_CONTROLLER_STYLE_STANDARD]: TouchControllerStyleStandard, + [PrefKey.UI_CONTROLLER_FRIENDLY]: boolean, + [PrefKey.UI_CONTROLLER_SHOW_STATUS]: boolean, + [PrefKey.UI_DISABLE_FEEDBACK_DIALOG]: boolean, + [PrefKey.UI_GAME_CARD_SHOW_WAIT_TIME]: boolean, + [PrefKey.UI_HIDE_SECTIONS]: UiSection[], + [PrefKey.UI_HIDE_SYSTEM_MENU_ICON]: boolean, + [PrefKey.UI_LAYOUT]: UiLayout, + [PrefKey.UI_REDUCE_ANIMATIONS]: boolean, + [PrefKey.UI_SCROLLBAR_HIDE]: boolean, + [PrefKey.UI_SIMPLIFY_STREAM_MENU]: boolean, + [PrefKey.UI_SKIP_SPLASH_VIDEO]: boolean, + [PrefKey.VERSION_CURRENT]: string, + [PrefKey.VERSION_LAST_CHECK]: number, + [PrefKey.VERSION_LATEST]: string, + [PrefKey.VIDEO_BRIGHTNESS]: number, + [PrefKey.VIDEO_CONTRAST]: number, + [PrefKey.VIDEO_MAX_FPS]: number, + [PrefKey.VIDEO_PLAYER_TYPE]: StreamPlayerType, + [PrefKey.VIDEO_POSITION]: VideoPosition, + [PrefKey.VIDEO_POWER_PREFERENCE]: VideoPowerPreference, + [PrefKey.VIDEO_PROCESSING]: StreamVideoProcessing, + [PrefKey.VIDEO_RATIO]: VideoRatio, + [PrefKey.VIDEO_SATURATION]: number, + [PrefKey.VIDEO_SHARPNESS]: number, +} diff --git a/src/enums/pref-values.ts b/src/enums/pref-values.ts index 812b78f..bcb3a2d 100755 --- a/src/enums/pref-values.ts +++ b/src/enums/pref-values.ts @@ -84,6 +84,12 @@ export const enum StreamStat { CLOCK = 'time', }; +export const enum StreamStatPosition { + TOP_LEFT = 'top-left', + TOP_CENTER = 'top-center', + TOP_RIGHT = 'top-right', +} + export const enum VideoRatio { '16:9' = '16:9', '18:9' = '18:9', @@ -101,6 +107,12 @@ export const enum VideoPosition { BOTTOM_HALF = 'bottom-half', } +export const enum VideoPowerPreference { + DEFAULT = 'default', + LOW_POWER = 'low-power', + HIGH_PERFORMANCE = 'high-performance', +} + export const enum StreamPlayerType { VIDEO = 'default', WEBGL2 = 'webgl2', diff --git a/src/enums/prompt-font.ts b/src/enums/prompt-font.ts index a64f45b..fc3a367 100755 --- a/src/enums/prompt-font.ts +++ b/src/enums/prompt-font.ts @@ -18,15 +18,19 @@ export enum PrompFont { LEFT = '≺', RIGHT = '≼', + LS = '⇱', L3 = '↺', LS_UP = '↾', LS_DOWN = '⇂', LS_LEFT = '↼', LS_RIGHT = '⇀', + RS = '⇲', R3 = '↻', RS_UP = '↿', RS_DOWN = '⇃', RS_LEFT = '↽', RS_RIGHT = '⇁', + + SHARE = '📸', } diff --git a/src/index.ts b/src/index.ts index 8a4bf70..757b696 100755 --- a/src/index.ts +++ b/src/index.ts @@ -171,7 +171,7 @@ document.addEventListener('readystatechange', e => { } // Hide "Play with Friends" skeleton section - if (getPref(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.FRIENDS) || getPref(PrefKey.BLOCK_FEATURES).includes(BlockFeature.FRIENDS)) { + if (getPref(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.FRIENDS) || getPref(PrefKey.BLOCK_FEATURES).includes(BlockFeature.FRIENDS)) { const $parent = document.querySelector('div[class*=PlayWithFriendsSkeleton]')?.closest('div[class*=HomePage-module]'); $parent && ($parent.style.display = 'none'); } @@ -357,8 +357,8 @@ 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); + if (getPref(PrefKey.NATIVE_MKB_MODE) !== NativeMkbMode.OFF) { + const customList = getPref(PrefKey.NATIVE_MKB_FORCED_GAMES); BX_FLAGS.ForceNativeMkbTitles.push(...customList); } @@ -405,12 +405,12 @@ function main() { RemotePlayManager.detect(); } - if (getPref(PrefKey.TOUCH_CONTROLLER_MODE) === TouchControllerMode.ALL) { + if (getPref(PrefKey.TOUCH_CONTROLLER_MODE) === TouchControllerMode.ALL) { TouchController.setup(); } // Start PointerProviderServer - if (AppInterface && (getPref(PrefKey.MKB_ENABLED) || getPref(PrefKey.NATIVE_MKB_MODE) === NativeMkbMode.ON)) { + if (AppInterface && (getPref(PrefKey.MKB_ENABLED) || getPref(PrefKey.NATIVE_MKB_MODE) === NativeMkbMode.ON)) { STATES.pointerServerPort = AppInterface.startPointerServer() || 9269; BxLogger.info('startPointerServer', 'Port', STATES.pointerServerPort.toString()); } diff --git a/src/modules/game-bar/game-bar.ts b/src/modules/game-bar/game-bar.ts index 870dd6e..e8c339f 100755 --- a/src/modules/game-bar/game-bar.ts +++ b/src/modules/game-bar/game-bar.ts @@ -20,7 +20,7 @@ export class GameBar { private static instance: GameBar | null | undefined; public static getInstance(): typeof GameBar['instance'] { if (typeof GameBar.instance === 'undefined') { - if (getPref(PrefKey.GAME_BAR_POSITION) !== GameBarPosition.OFF) { + if (getPref(PrefKey.GAME_BAR_POSITION) !== GameBarPosition.OFF) { GameBar.instance = new GameBar(); } else { GameBar.instance = null; @@ -46,7 +46,7 @@ export class GameBar { let $container; - const position = getPref(PrefKey.GAME_BAR_POSITION); + const position = getPref(PrefKey.GAME_BAR_POSITION); const $gameBar = CE('div', { id: 'bx-game-bar', class: 'bx-gone', 'data-position': position }, $container = CE('div', { class: 'bx-game-bar-container bx-offscreen' }), @@ -55,7 +55,7 @@ export class GameBar { this.actions = [ new ScreenshotAction(), - ...(STATES.userAgent.capabilities.touch && (getPref(PrefKey.TOUCH_CONTROLLER_MODE) !== TouchControllerMode.OFF) ? [new TouchControlAction()] : []), + ...(STATES.userAgent.capabilities.touch && (getPref(PrefKey.TOUCH_CONTROLLER_MODE) !== TouchControllerMode.OFF) ? [new TouchControlAction()] : []), new SpeakerAction(), new RendererAction(), new MicrophoneAction(), diff --git a/src/modules/loading-screen.ts b/src/modules/loading-screen.ts index 4ba5548..ed836bb 100755 --- a/src/modules/loading-screen.ts +++ b/src/modules/loading-screen.ts @@ -37,7 +37,7 @@ export class LoadingScreen { LoadingScreen.setBackground(titleInfo.product.heroImageUrl || titleInfo.product.titledHeroImageUrl || titleInfo.product.tileImageUrl); - if (getPref(PrefKey.LOADING_SCREEN_ROCKET) === LoadingScreenRocket.HIDE) { + if (getPref(PrefKey.LOADING_SCREEN_ROCKET) === LoadingScreenRocket.HIDE) { LoadingScreen.hideRocket(); } } @@ -89,7 +89,7 @@ export class LoadingScreen { static setupWaitTime(waitTime: number) { // Hide rocket when queing - if (getPref(PrefKey.LOADING_SCREEN_ROCKET) === LoadingScreenRocket.HIDE_QUEUE) { + if (getPref(PrefKey.LOADING_SCREEN_ROCKET) === LoadingScreenRocket.HIDE_QUEUE) { LoadingScreen.hideRocket(); } diff --git a/src/modules/mkb/mkb-handler.ts b/src/modules/mkb/mkb-handler.ts index 0a75528..05ae42f 100755 --- a/src/modules/mkb/mkb-handler.ts +++ b/src/modules/mkb/mkb-handler.ts @@ -580,7 +580,7 @@ export class EmulatedMkbHandler extends MkbHandler { updateGamepadSlots() { // Set gamepad slot - this.VIRTUAL_GAMEPAD.index = getPref(PrefKey.MKB_P1_SLOT) - 1; + this.VIRTUAL_GAMEPAD.index = getPref(PrefKey.MKB_P1_SLOT) - 1; } start() { diff --git a/src/modules/mkb/mkb-popup.ts b/src/modules/mkb/mkb-popup.ts index f0bb0f6..a845473 100755 --- a/src/modules/mkb/mkb-popup.ts +++ b/src/modules/mkb/mkb-popup.ts @@ -7,6 +7,7 @@ import { NativeMkbHandler } from "./native-mkb-handler"; import { StreamSettings } from "@/utils/stream-settings"; import { KeyHelper } from "./key-helper"; import { BxEventBus } from "@/utils/bx-event-bus"; +import { BxIcon } from "@/utils/bx-icon"; type MkbPopupType = 'virtual' | 'native'; @@ -90,6 +91,7 @@ export class MkbPopup { createButton({ label: t('manage'), + icon: BxIcon.MANAGE, style: ButtonStyle.FOCUSABLE, onClick: () => { const dialog = SettingsDialog.getInstance(); diff --git a/src/modules/mkb/native-mkb-handler.ts b/src/modules/mkb/native-mkb-handler.ts index 4c2e920..eaec396 100755 --- a/src/modules/mkb/native-mkb-handler.ts +++ b/src/modules/mkb/native-mkb-handler.ts @@ -43,7 +43,7 @@ export class NativeMkbHandler extends MkbHandler { private readonly LOG_TAG = 'NativeMkbHandler'; static isAllowed = () => { - return STATES.browser.capabilities.emulatedNativeMkb && getPref(PrefKey.NATIVE_MKB_MODE) === NativeMkbMode.ON; + return STATES.browser.capabilities.emulatedNativeMkb && getPref(PrefKey.NATIVE_MKB_MODE) === NativeMkbMode.ON; } private pointerClient: PointerClient | undefined; diff --git a/src/modules/patcher/patcher-utils.ts b/src/modules/patcher/patcher-utils.ts index 362f7e3..ab94621 100644 --- a/src/modules/patcher/patcher-utils.ts +++ b/src/modules/patcher/patcher-utils.ts @@ -1,22 +1,30 @@ import type { PatchArray, PatchName, PatchPage } from "./patcher"; export class PatcherUtils { - static indexOf(txt: string, searchString: string, startIndex: number, maxRange: number): number { + static indexOf(txt: string, searchString: string, startIndex: number, maxRange=0, after=false): number { + if (startIndex < 0) { + return -1; + } + const index = txt.indexOf(searchString, startIndex); if (index < 0 || (maxRange && index - startIndex > maxRange)) { return -1; } - return index; + return after ? index + searchString.length : index; } - static lastIndexOf(txt: string, searchString: string, startIndex: number, maxRange: number): number { + static lastIndexOf(txt: string, searchString: string, startIndex: number, maxRange=0, after=false): number { + if (startIndex < 0) { + return -1; + } + const index = txt.lastIndexOf(searchString, startIndex); if (index < 0 || (maxRange && startIndex - index > maxRange)) { return -1; } - return index; + return after ? index + searchString.length : index; } static insertAt(txt: string, index: number, insertString: string): string { diff --git a/src/modules/patcher/patcher.ts b/src/modules/patcher/patcher.ts index 09d25f9..8e4ac04 100755 --- a/src/modules/patcher/patcher.ts +++ b/src/modules/patcher/patcher.ts @@ -4,10 +4,10 @@ import { BxLogger } from "@utils/bx-logger"; import { blockSomeNotifications, hashCode, renderString } from "@utils/utils"; import { BxEvent } from "@/utils/bx-event"; -import codeControllerShortcuts from "./patches/controller-shortcuts.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 codeExposeStreamSession from "./patches/expose-stream-session.js" with { type: "text" }; import codeLocalCoOpEnable from "./patches/local-co-op-enable.js" with { type: "text" }; -import codeRemotePlayEnable from "./patches/remote-play-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" }; import { PrefKey, StorageKey } from "@/enums/pref-keys.js"; @@ -88,7 +88,7 @@ const PATCHES = { return false; } - const layout = getPref(PrefKey.UI_LAYOUT) === UiLayout.TV ? UiLayout.TV : UiLayout.DEFAULT; + const layout = getPref(PrefKey.UI_LAYOUT) === UiLayout.TV ? UiLayout.TV : UiLayout.DEFAULT; return str.replace(text, `?"${layout}":"${layout}"`); }, @@ -120,7 +120,9 @@ const PATCHES = { return false; } - return str.replace(text, codeRemotePlayEnable); + const newCode = `connectMode: window.BX_REMOTE_PLAY_CONFIG ? "xhome-connect" : "cloud-connect", +remotePlayServerId: (window.BX_REMOTE_PLAY_CONFIG && window.BX_REMOTE_PLAY_CONFIG.serverId) || '',`; + return str.replace(text, newCode); }, // Remote Play: Disable achievement toast @@ -191,17 +193,34 @@ const PATCHES = { codeBlock = codeBlock.replace('this.inputPollingDurationStats.addValue', ''); } - // Map the Share button on Xbox Series controller with the capturing screenshot feature - const match = codeBlock.match(/this\.gamepadTimestamps\.set\((\w+)\.index/); - if (match) { - const gamepadVar = match[1]; - const newCode = renderString(codeControllerShortcuts, { - gamepadVar, - }); - - codeBlock = codeBlock.replace('this.gamepadTimestamps.set', newCode + 'this.gamepadTimestamps.set'); + // Controller shortcuts + let match = codeBlock.match(/this\.gamepadTimestamps\.set\(([A-Za-z0-9_$]+)\.index/); + if (!match) { + return false; } + let newCode = renderString(codePollGamepad, { + gamepadVar: match[1], + }); + codeBlock = codeBlock.replace('this.gamepadTimestamps.set', newCode + 'this.gamepadTimestamps.set'); + + // Controller customization + match = codeBlock.match(/let ([A-Za-z0-9_$]+)=this\.gamepadMappings\.find/); + if (!match) { + return false; + } + + const xCloudGamepadVar = match[1]; + const inputFeedbackManager = PatcherUtils.indexOf(codeBlock, 'this.inputFeedbackManager.onGamepadConnected(', 0, 10000); + const backetIndex = PatcherUtils.indexOf(codeBlock, '}', inputFeedbackManager, 100); + if (backetIndex < 0) { + return false; + } + + let customizationCode = ';'; // End previous code line + customizationCode += renderString(codeControllerCustomization, { xCloudGamepadVar }); + codeBlock = PatcherUtils.insertAt(codeBlock, backetIndex, customizationCode); + str = str.substring(0, index) + codeBlock + str.substring(setTimeoutIndex); return str; }, @@ -357,7 +376,7 @@ if (window.BX_EXPOSED.stopTakRendering) { } let autoOffCode = ''; - if (getPref(PrefKey.TOUCH_CONTROLLER_MODE) === TouchControllerMode.OFF) { + if (getPref(PrefKey.TOUCH_CONTROLLER_MODE) === TouchControllerMode.OFF) { autoOffCode = 'return;'; } else if (getPref(PrefKey.TOUCH_CONTROLLER_AUTO_OFF)) { autoOffCode = ` @@ -414,7 +433,7 @@ e.guideUI = null; `; // Remove the TAK Edit button when the touch controller is disabled - if (getPref(PrefKey.TOUCH_CONTROLLER_MODE) === TouchControllerMode.OFF) { + if (getPref(PrefKey.TOUCH_CONTROLLER_MODE) === TouchControllerMode.OFF) { newCode += 'e.canShowTakHUD = false;'; } @@ -534,7 +553,7 @@ BxLogger.info('patchRemotePlayMkb', ${configsVar}); return false; } - const opacity = (getPref(PrefKey.TOUCH_CONTROLLER_DEFAULT_OPACITY) / 100).toFixed(1); + const opacity = (getPref(PrefKey.TOUCH_CONTROLLER_DEFAULT_OPACITY) / 100).toFixed(1); const newCode = `opacityMultiplier: ${opacity}`; str = str.replace(text, newCode); return str; @@ -771,7 +790,7 @@ true` + text; return false; } - const PREF_HIDE_SECTIONS = getPref(PrefKey.UI_HIDE_SECTIONS); + const PREF_HIDE_SECTIONS = getPref(PrefKey.UI_HIDE_SECTIONS); const siglIds: GamePassCloudGallery[] = []; const sections: PartialRecord = { @@ -962,7 +981,7 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) { // Find index after { index = str.indexOf('{', index) + 1; - const blockFeatures = getPref(PrefKey.BLOCK_FEATURES); + const blockFeatures = getPref(PrefKey.BLOCK_FEATURES); const filters = []; if (blockFeatures.includes(BlockFeature.NOTIFICATIONS_INVITES)) { filters.push('GameInvite', 'PartyInvite'); @@ -987,7 +1006,7 @@ ${subsVar} = subs; }; let PATCH_ORDERS = PatcherUtils.filterPatches([ - ...(AppInterface && getPref(PrefKey.NATIVE_MKB_MODE) === NativeMkbMode.ON ? [ + ...(AppInterface && getPref(PrefKey.NATIVE_MKB_MODE) === NativeMkbMode.ON ? [ 'enableNativeMkb', 'exposeInputSink', 'disableAbsoluteMouse', @@ -1019,7 +1038,7 @@ let PATCH_ORDERS = PatcherUtils.filterPatches([ 'overrideStorageGetSettings', getPref(PrefKey.UI_GAME_CARD_SHOW_WAIT_TIME) && 'patchSetCurrentFocus', - getPref(PrefKey.UI_LAYOUT) !== UiLayout.DEFAULT && 'websiteLayout', + getPref(PrefKey.UI_LAYOUT) !== UiLayout.DEFAULT && 'websiteLayout', getPref(PrefKey.GAME_FORTNITE_FORCE_CONSOLE) && 'forceFortniteConsole', ...(STATES.userAgent.capabilities.touch ? [ @@ -1055,7 +1074,7 @@ let PATCH_ORDERS = PatcherUtils.filterPatches([ ] : []), ]); -const hideSections = getPref(PrefKey.UI_HIDE_SECTIONS); +const hideSections = getPref(PrefKey.UI_HIDE_SECTIONS); let HOME_PAGE_PATCH_ORDERS = PatcherUtils.filterPatches([ hideSections.includes(UiSection.NEWS) && 'ignoreNewsSection', hideSections.includes(UiSection.FRIENDS) && 'ignorePlayWithFriendsSection', @@ -1086,11 +1105,11 @@ let STREAM_PAGE_PATCH_ORDERS = PatcherUtils.filterPatches([ getPref(PrefKey.UI_DISABLE_FEEDBACK_DIALOG) && 'skipFeedbackDialog', ...(STATES.userAgent.capabilities.touch ? [ - getPref(PrefKey.TOUCH_CONTROLLER_MODE) === TouchControllerMode.ALL && 'patchShowSensorControls', - getPref(PrefKey.TOUCH_CONTROLLER_MODE) === TouchControllerMode.ALL && 'exposeTouchLayoutManager', - (getPref(PrefKey.TOUCH_CONTROLLER_MODE) === TouchControllerMode.OFF || getPref(PrefKey.TOUCH_CONTROLLER_AUTO_OFF)) && 'disableTakRenderer', - getPref(PrefKey.TOUCH_CONTROLLER_DEFAULT_OPACITY) !== 100 && 'patchTouchControlDefaultOpacity', - (getPref(PrefKey.TOUCH_CONTROLLER_MODE) !== TouchControllerMode.OFF && (getPref(PrefKey.MKB_ENABLED) || getPref(PrefKey.NATIVE_MKB_MODE) === NativeMkbMode.ON)) && 'patchBabylonRendererClass', + getPref(PrefKey.TOUCH_CONTROLLER_MODE) === TouchControllerMode.ALL && 'patchShowSensorControls', + getPref(PrefKey.TOUCH_CONTROLLER_MODE) === TouchControllerMode.ALL && 'exposeTouchLayoutManager', + (getPref(PrefKey.TOUCH_CONTROLLER_MODE) === TouchControllerMode.OFF || getPref(PrefKey.TOUCH_CONTROLLER_AUTO_OFF)) && 'disableTakRenderer', + getPref(PrefKey.TOUCH_CONTROLLER_DEFAULT_OPACITY) !== 100 && 'patchTouchControlDefaultOpacity', + (getPref(PrefKey.TOUCH_CONTROLLER_MODE) !== TouchControllerMode.OFF && (getPref(PrefKey.MKB_ENABLED) || getPref(PrefKey.NATIVE_MKB_MODE) === NativeMkbMode.ON)) && 'patchBabylonRendererClass', ] : []), BX_FLAGS.EnableXcloudLogging && 'enableConsoleLogging', @@ -1105,7 +1124,7 @@ let STREAM_PAGE_PATCH_ORDERS = PatcherUtils.filterPatches([ ] : []), // Native MKB - ...(AppInterface && getPref(PrefKey.NATIVE_MKB_MODE) === NativeMkbMode.ON ? [ + ...(AppInterface && getPref(PrefKey.NATIVE_MKB_MODE) === NativeMkbMode.ON ? [ 'patchMouseAndKeyboardEnabled', 'disableNativeRequestPointerLock', ] : []), diff --git a/src/modules/patcher/patches/controller-shortcuts.js b/src/modules/patcher/patches/controller-shortcuts.js deleted file mode 100755 index d3a1376..0000000 --- a/src/modules/patcher/patches/controller-shortcuts.js +++ /dev/null @@ -1,99 +0,0 @@ -if (window.BX_EXPOSED.disableGamepadPolling) { - this.inputConfiguration.useIntervalWorkerThreadForInput && this.intervalWorker ? this.intervalWorker.scheduleTimer(50) : this.pollGamepadssetTimeoutTimerID = setTimeout(this.pollGamepads, 50); - return; -} - -const currentGamepad = ${gamepadVar}; - -// Share button on XS controller -if (currentGamepad.buttons[17] && currentGamepad.buttons[17].pressed) { - window.dispatchEvent(new Event(BxEvent.CAPTURE_SCREENSHOT)); -} - -const btnHome = currentGamepad.buttons[16]; -if (btnHome) { - if (!this.bxHomeStates) { - this.bxHomeStates = {}; - } - - let intervalMs = 0; - let hijack = false; - - if (btnHome.pressed) { - hijack = true; - intervalMs = 16; - this.gamepadIsIdle.set(currentGamepad.index, false); - - if (this.bxHomeStates[currentGamepad.index]) { - const lastTimestamp = this.bxHomeStates[currentGamepad.index].timestamp; - - if (currentGamepad.timestamp !== lastTimestamp) { - this.bxHomeStates[currentGamepad.index].timestamp = currentGamepad.timestamp; - - const handled = window.BX_EXPOSED.handleControllerShortcut(currentGamepad); - if (handled) { - this.bxHomeStates[currentGamepad.index].shortcutPressed += 1; - } - } - } else { - // First time pressing > save current timestamp - window.BX_EXPOSED.resetControllerShortcut(currentGamepad.index); - this.bxHomeStates[currentGamepad.index] = { - shortcutPressed: 0, - timestamp: currentGamepad.timestamp, - }; - } - } else if (this.bxHomeStates[currentGamepad.index]) { - hijack = true; - const info = structuredClone(this.bxHomeStates[currentGamepad.index]); - - // Home button released - this.bxHomeStates[currentGamepad.index] = null; - - if (info.shortcutPressed === 0) { - const fakeGamepadMappings = [{ - GamepadIndex: currentGamepad.index, - A: 0, - B: 0, - X: 0, - Y: 0, - LeftShoulder: 0, - RightShoulder: 0, - LeftTrigger: 0, - RightTrigger: 0, - View: 0, - Menu: 0, - LeftThumb: 0, - RightThumb: 0, - DPadUp: 0, - DPadDown: 0, - DPadLeft: 0, - DPadRight: 0, - Nexus: 1, - LeftThumbXAxis: 0, - LeftThumbYAxis: 0, - RightThumbXAxis: 0, - RightThumbYAxis: 0, - PhysicalPhysicality: 0, - VirtualPhysicality: 0, - Dirty: true, - Virtual: false, - }]; - - const isLongPress = (currentGamepad.timestamp - info.timestamp) >= 500; - intervalMs = isLongPress ? 500 : 100; - - this.inputSink.onGamepadInput(performance.now() - intervalMs, fakeGamepadMappings); - } else { - intervalMs = window.BX_STREAM_SETTINGS.controllerPollingRate; - } - } - - if (hijack && intervalMs) { - // Listen to next button press - this.inputConfiguration.useIntervalWorkerThreadForInput && this.intervalWorker ? this.intervalWorker.scheduleTimer(intervalMs) : this.pollGamepadssetTimeoutTimerID = setTimeout(this.pollGamepads, intervalMs); - - // Hijack this button - return; - } -} diff --git a/src/modules/patcher/patches/remote-play-enable.js b/src/modules/patcher/patches/remote-play-enable.js deleted file mode 100755 index 56f88dc..0000000 --- a/src/modules/patcher/patches/remote-play-enable.js +++ /dev/null @@ -1,2 +0,0 @@ -connectMode: window.BX_REMOTE_PLAY_CONFIG ? "xhome-connect" : "cloud-connect", -remotePlayServerId: (window.BX_REMOTE_PLAY_CONFIG && window.BX_REMOTE_PLAY_CONFIG.serverId) || '', diff --git a/src/modules/patcher/patches/remote-play-keep-alive.js b/src/modules/patcher/patches/remote-play-keep-alive.js deleted file mode 100755 index 5ccafac..0000000 --- a/src/modules/patcher/patches/remote-play-keep-alive.js +++ /dev/null @@ -1,7 +0,0 @@ -const msg = JSON.parse(e); -if (msg.reason === 'WarningForBeingIdle' && !window.location.pathname.includes('/launch/')) { - try { - this.sendKeepAlive(); - return; - } catch (ex) { console.log(ex); } -} diff --git a/src/modules/patcher/patches/src/controller-customization.ts b/src/modules/patcher/patches/src/controller-customization.ts new file mode 100644 index 0000000..a536507 --- /dev/null +++ b/src/modules/patcher/patches/src/controller-customization.ts @@ -0,0 +1,167 @@ +import { BxEvent as BxEventNamespace } from "@/utils/bx-event"; + +// "currentGamepad" variable in poll-gamepad.js +declare const currentGamepad: Gamepad; +declare const $xCloudGamepadVar$: XcloudGamepad; +declare const BxEvent: typeof BxEventNamespace; + +// Share button on XS controller +const shareButtonPressed = currentGamepad.buttons[17]?.pressed; +let shareButtonHandled = false; + +const xCloudGamepad: XcloudGamepad = $xCloudGamepadVar$; +if (currentGamepad.id in window.BX_STREAM_SETTINGS.controllers) { + const controller = window.BX_STREAM_SETTINGS.controllers[currentGamepad.id]; + if (controller?.customization) { + const MIN_RANGE = 0.1; + + const { mapping, ranges } = controller.customization; + const pressedButtons: Partial> = {}; + const releasedButtons: Partial> = {}; + let isModified = false; + + // Limit left trigger range + if (ranges.LeftTrigger) { + const [from, to] = ranges.LeftTrigger; + xCloudGamepad.LeftTrigger = xCloudGamepad.LeftTrigger > to ? 1 : xCloudGamepad.LeftTrigger; + xCloudGamepad.LeftTrigger = xCloudGamepad.LeftTrigger < from ? 0 : xCloudGamepad.LeftTrigger; + } + + // Limit right trigger range + if (ranges.RightTrigger) { + const [from, to] = ranges.RightTrigger; + xCloudGamepad.RightTrigger = xCloudGamepad.RightTrigger > to ? 1 : xCloudGamepad.RightTrigger; + xCloudGamepad.RightTrigger = xCloudGamepad.RightTrigger < from ? 0 : xCloudGamepad.RightTrigger; + } + + // Limit left stick deadzone + if (ranges.LeftThumb) { + const [from, to] = ranges.LeftThumb; + + const xAxis = xCloudGamepad.LeftThumbXAxis; + const yAxis = xCloudGamepad.LeftThumbYAxis; + + const range = Math.abs(Math.sqrt(xAxis * xAxis + yAxis * yAxis)); + let newRange = range > to ? 1 : range; + newRange = newRange < from ? 0 : newRange; + + if (newRange !== range) { + xCloudGamepad.LeftThumbXAxis = xAxis * (newRange / range); + xCloudGamepad.LeftThumbYAxis = yAxis * (newRange / range); + } + } + + // Limit right stick deadzone + if (ranges.RightThumb) { + const [from, to] = ranges.RightThumb; + + const xAxis = xCloudGamepad.RightThumbXAxis; + const yAxis = xCloudGamepad.RightThumbYAxis; + + const range = Math.abs(Math.sqrt(xAxis * xAxis + yAxis * yAxis)); + let newRange = range > to ? 1 : range; + newRange = newRange < from ? 0 : newRange; + + if (newRange !== range) { + xCloudGamepad.RightThumbXAxis = xAxis * (newRange / range); + xCloudGamepad.RightThumbYAxis = yAxis * (newRange / range); + } + } + + // Handle the Share button + if (shareButtonPressed && 'Share' in mapping) { + const targetButton = mapping['Share']; + if (typeof targetButton === 'string') { + pressedButtons[targetButton] = 1; + } + + // Don't send capturing request + shareButtonHandled = true; + delete mapping['Share']; + } + + // Handle other buttons + let key: keyof typeof mapping; + for (key in mapping) { + const mappedKey = mapping[key]; + + if (key === 'LeftStickAxes' || key === 'RightStickAxes') { + let sourceX: keyof XcloudGamepad; + let sourceY: keyof XcloudGamepad; + let targetX: keyof XcloudGamepad; + let targetY: keyof XcloudGamepad; + + if (key === 'LeftStickAxes') { + sourceX = 'LeftThumbXAxis'; + sourceY = 'LeftThumbYAxis'; + targetX = 'RightThumbXAxis'; + targetY = 'RightThumbYAxis'; + } else { + sourceX = 'RightThumbXAxis'; + sourceY = 'RightThumbYAxis'; + targetX = 'LeftThumbXAxis'; + targetY = 'LeftThumbYAxis'; + } + + if (typeof mappedKey === 'string') { + // Calculate moved range + const rangeX = xCloudGamepad[sourceX]; + const rangeY = xCloudGamepad[sourceY]; + const movedRange = Math.abs(Math.sqrt(rangeX * rangeX + rangeY * rangeY)); + const moved = movedRange >= MIN_RANGE; + + // Swap sticks + if (moved) { + pressedButtons[targetX] = rangeX; + pressedButtons[targetY] = rangeY; + } + } + + // Unbind original stick + releasedButtons[sourceX] = 0; + releasedButtons[sourceY] = 0; + + isModified = true; + } else if (typeof mappedKey === 'string') { + let pressed = false; + let value = 0; + + if (key === 'LeftTrigger' || key === 'RightTrigger') { + // Only set pressed state when pressing pass max range + const currentRange = xCloudGamepad[key]; + if (mappedKey === 'LeftTrigger' || mappedKey === 'RightTrigger') { + pressed = currentRange >= MIN_RANGE; + value = currentRange; + } else { + pressed = true; + value = currentRange >= 0.9 ? 1 : 0; + } + } else if (xCloudGamepad[key]) { + pressed = true; + value = xCloudGamepad[key] as number; + } + + if (pressed) { + // Only copy button value when it's being pressed + pressedButtons[mappedKey] = value; + // Unbind original button + releasedButtons[key] = 0; + + isModified = true; + } + } else if (mappedKey === false) { + // Disable key + pressedButtons[key] = 0; + + isModified = true; + } + } + + isModified && Object.assign(xCloudGamepad, releasedButtons, pressedButtons); + } +} + +// Capture screenshot when the Share button is pressed +if (shareButtonPressed && !shareButtonHandled) { + window.dispatchEvent(new Event(BxEvent.CAPTURE_SCREENSHOT)); +} diff --git a/src/modules/patcher/patches/expose-stream-session.js b/src/modules/patcher/patches/src/expose-stream-session.ts old mode 100755 new mode 100644 similarity index 66% rename from src/modules/patcher/patches/expose-stream-session.js rename to src/modules/patcher/patches/src/expose-stream-session.ts index ea595eb..2c4bd86 --- a/src/modules/patcher/patches/expose-stream-session.js +++ b/src/modules/patcher/patches/src/expose-stream-session.ts @@ -1,7 +1,15 @@ -window.BX_EXPOSED.streamSession = this; +import type { MicrophoneState } from "@/modules/shortcuts/microphone-shortcut"; +import { BxEvent as BxEventNamespace } from "@/utils/bx-event"; -const orgSetMicrophoneState = this.setMicrophoneState.bind(this); -this.setMicrophoneState = state => { +declare const $this$: any; +declare const BxEvent: typeof BxEventNamespace; + +const self = $this$; +window.BX_EXPOSED.streamSession = self; + +// Patch setMicrophoneState() +const orgSetMicrophoneState = self.setMicrophoneState.bind(self); +self.setMicrophoneState = (state: MicrophoneState) => { orgSetMicrophoneState(state); window.BxEventBus.Stream.emit('microphone.state.changed', { state }); }; @@ -9,7 +17,7 @@ this.setMicrophoneState = state => { window.dispatchEvent(new Event(BxEvent.STREAM_SESSION_READY)); // Patch updateDimensions() to make native touch work correctly with WebGL2 -let updateDimensionsStr = this.updateDimensions.toString(); +let updateDimensionsStr = self.updateDimensions.toString(); if (updateDimensionsStr.startsWith('function ')) { updateDimensionsStr = updateDimensionsStr.substring(9); @@ -19,7 +27,6 @@ if (updateDimensionsStr.startsWith('function ')) { const renderTargetVar = updateDimensionsStr.match(/if\((\w+)\){/)[1]; updateDimensionsStr = updateDimensionsStr.replaceAll(renderTargetVar + '.scroll', 'scroll'); - updateDimensionsStr = updateDimensionsStr.replace(`if(${renderTargetVar}){`, ` if (${renderTargetVar}) { const scrollWidth = ${renderTargetVar}.dataset.width ? parseInt(${renderTargetVar}.dataset.width) : ${renderTargetVar}.scrollWidth; diff --git a/src/modules/patcher/patches/local-co-op-enable.js b/src/modules/patcher/patches/src/local-co-op-enable.ts old mode 100755 new mode 100644 similarity index 58% rename from src/modules/patcher/patches/local-co-op-enable.js rename to src/modules/patcher/patches/src/local-co-op-enable.ts index f3ed93c..106c072 --- a/src/modules/patcher/patches/local-co-op-enable.js +++ b/src/modules/patcher/patches/src/local-co-op-enable.ts @@ -1,9 +1,14 @@ +import { BxLogger as OrgBxLogger } from "@/utils/bx-logger"; + +declare const BxLogger: typeof OrgBxLogger; +declare const $this$: any; + // Save the original onGamepadChanged() and onGamepadInput() -this.orgOnGamepadChanged = this.onGamepadChanged; -this.orgOnGamepadInput = this.onGamepadInput; +$this$.orgOnGamepadChanged = $this$.onGamepadChanged; +$this$.orgOnGamepadInput = $this$.onGamepadInput; let match; -let onGamepadChangedStr = this.onGamepadChanged.toString(); +let onGamepadChangedStr = $this$.onGamepadChanged.toString(); // Fix problem with Safari if (onGamepadChangedStr.startsWith('function ')) { @@ -11,9 +16,9 @@ if (onGamepadChangedStr.startsWith('function ')) { } onGamepadChangedStr = onGamepadChangedStr.replaceAll('0', 'arguments[1]'); -eval(`this.patchedOnGamepadChanged = function ${onGamepadChangedStr}`); +eval(`$this$.patchedOnGamepadChanged = function ${onGamepadChangedStr}`); -let onGamepadInputStr = this.onGamepadInput.toString(); +let onGamepadInputStr = $this$.onGamepadInput.toString(); // Fix problem with Safari if (onGamepadInputStr.startsWith('function ')) { onGamepadInputStr = onGamepadInputStr.substring(9); @@ -22,19 +27,19 @@ if (onGamepadInputStr.startsWith('function ')) { match = onGamepadInputStr.match(/(\w+\.GamepadIndex)/); if (match) { const gamepadIndexVar = match[0]; - onGamepadInputStr = onGamepadInputStr.replace('this.gamepadStates.get(', `this.gamepadStates.get(${gamepadIndexVar},`); - eval(`this.patchedOnGamepadInput = function ${onGamepadInputStr}`); + onGamepadInputStr = onGamepadInputStr.replace('$this$.gamepadStates.get(', `$this$.gamepadStates.get(${gamepadIndexVar},`); + eval(`$this$.patchedOnGamepadInput = function ${onGamepadInputStr}`); BxLogger.info('supportLocalCoOp', '✅ Successfully patched local co-op support'); } else { BxLogger.error('supportLocalCoOp', '❌ Unable to patch local co-op support'); } // Add method to switch between patched and original methods -this.toggleLocalCoOp = enable => { +$this$.toggleLocalCoOp = (enable: boolean) => { BxLogger.info('toggleLocalCoOp', enable ? 'Enabled' : 'Disabled'); - this.onGamepadChanged = enable ? this.patchedOnGamepadChanged : this.orgOnGamepadChanged; - this.onGamepadInput = enable ? this.patchedOnGamepadInput : this.orgOnGamepadInput; + $this$.onGamepadChanged = enable ? $this$.patchedOnGamepadChanged : $this$.orgOnGamepadChanged; + $this$.onGamepadInput = enable ? $this$.patchedOnGamepadInput : $this$.orgOnGamepadInput; // Reconnect all gamepads const gamepads = window.navigator.getGamepads(); @@ -54,4 +59,4 @@ this.toggleLocalCoOp = enable => { }; // Expose this method -window.BX_EXPOSED.toggleLocalCoOp = this.toggleLocalCoOp.bind(this); +window.BX_EXPOSED.toggleLocalCoOp = $this$.toggleLocalCoOp.bind(this); diff --git a/src/modules/patcher/patches/src/poll-gamepad.ts b/src/modules/patcher/patches/src/poll-gamepad.ts new file mode 100644 index 0000000..53cca1c --- /dev/null +++ b/src/modules/patcher/patches/src/poll-gamepad.ts @@ -0,0 +1,98 @@ +type GamepadManager = { + pollGamepadssetTimeoutTimerID: number; + intervalWorker: any; + pollGamepads(pollGamepads: any, arg1: number): any; + gamepadIsIdle: any; + inputSink: any; + inputConfiguration: any; + + bxHomeStates: any; +}; + +declare const $gamepadVar$: Gamepad; +declare const $this$: GamepadManager; + +const self = $this$; +if (window.BX_EXPOSED.disableGamepadPolling) { + self.inputConfiguration.useIntervalWorkerThreadForInput && self.intervalWorker ? self.intervalWorker.scheduleTimer(50) : self.pollGamepadssetTimeoutTimerID = window.setTimeout(self.pollGamepads, 50); + // @ts-ignore + return; +} + +const currentGamepad = $gamepadVar$; + +const btnHome = currentGamepad.buttons[16]; +// Controller shortcuts +if (btnHome) { + if (!self.bxHomeStates) { + self.bxHomeStates = {}; + } + + let intervalMs = 0; + let hijack = false; + + if (btnHome.pressed) { + hijack = true; + intervalMs = 16; + self.gamepadIsIdle.set(currentGamepad.index, false); + + if (self.bxHomeStates[currentGamepad.index]) { + const lastTimestamp = self.bxHomeStates[currentGamepad.index].timestamp; + + if (currentGamepad.timestamp !== lastTimestamp) { + self.bxHomeStates[currentGamepad.index].timestamp = currentGamepad.timestamp; + + const handled = window.BX_EXPOSED.handleControllerShortcut(currentGamepad); + if (handled) { + self.bxHomeStates[currentGamepad.index].shortcutPressed += 1; + } + } + } else { + // First time pressing > save current timestamp + window.BX_EXPOSED.resetControllerShortcut(currentGamepad.index); + self.bxHomeStates[currentGamepad.index] = { + shortcutPressed: 0, + timestamp: currentGamepad.timestamp, + }; + } + } else if (self.bxHomeStates[currentGamepad.index]) { + hijack = true; + const info = structuredClone(self.bxHomeStates[currentGamepad.index]); + + // Home button released + self.bxHomeStates[currentGamepad.index] = null; + + if (info.shortcutPressed === 0) { + const fakeGamepadMappings: XcloudGamepad[] = [{ + GamepadIndex: currentGamepad.index, + A: 0, B: 0, X: 0, Y: 0, + LeftShoulder: 0, RightShoulder: 0, + LeftTrigger: 0, RightTrigger: 0, + View: 0, Menu: 0, + LeftThumb: 0, RightThumb: 0, + DPadUp: 0, DPadDown: 0, DPadLeft: 0, DPadRight: 0, + Nexus: 1, + LeftThumbXAxis: 0, LeftThumbYAxis: 0, + RightThumbXAxis: 0, RightThumbYAxis: 0, + PhysicalPhysicality: 0, VirtualPhysicality: 0, + Dirty: true, Virtual: false, + }]; + + const isLongPress = (currentGamepad.timestamp - info.timestamp) >= 500; + intervalMs = isLongPress ? 500 : 100; + + self.inputSink.onGamepadInput(performance.now() - intervalMs, fakeGamepadMappings); + } else { + intervalMs = window.BX_STREAM_SETTINGS.controllerPollingRate; + } + } + + if (hijack && intervalMs) { + // Listen to next button press + self.inputConfiguration.useIntervalWorkerThreadForInput && self.intervalWorker ? self.intervalWorker.scheduleTimer(intervalMs) : self.pollGamepadssetTimeoutTimerID = setTimeout(self.pollGamepads, intervalMs); + + // Hijack this button + // @ts-ignore + return; + } +} diff --git a/src/modules/patcher/patches/src/remote-play-keep-alive.ts b/src/modules/patcher/patches/src/remote-play-keep-alive.ts new file mode 100644 index 0000000..54f797d --- /dev/null +++ b/src/modules/patcher/patches/src/remote-play-keep-alive.ts @@ -0,0 +1,11 @@ +declare const $this$: any; +declare const e: string; + +try { + const msg = JSON.parse(e); + if (msg.reason === 'WarningForBeingIdle' && !window.location.pathname.includes('/launch/')) { + $this$.sendKeepAlive(); + // @ts-ignore + return; + } +} catch (ex) { console.log(ex); } diff --git a/src/modules/patcher/patches/src/vibration-adjust.ts b/src/modules/patcher/patches/src/vibration-adjust.ts new file mode 100644 index 0000000..dabe03b --- /dev/null +++ b/src/modules/patcher/patches/src/vibration-adjust.ts @@ -0,0 +1,26 @@ +declare const e: { + gamepad: Gamepad; + repeat: number; + leftMotorPercent: number; + rightMotorPercent: number; + leftTriggerMotorPercent: number; + rightTriggerMotorPercent: number; +}; + +if (e?.gamepad?.connected) { + const gamepadSettings = window.BX_STREAM_SETTINGS.controllers[e.gamepad.id]; + if (gamepadSettings?.customization) { + const intensity = gamepadSettings.customization.vibrationIntensity; + + if (intensity <= 0) { + e.repeat = 0; + // @ts-ignore + return; + } else if (intensity < 1) { + e.leftMotorPercent *= intensity; + e.rightMotorPercent *= intensity; + e.leftTriggerMotorPercent *= intensity; + e.rightTriggerMotorPercent *= intensity; + } + } +} diff --git a/src/modules/patcher/patches/vibration-adjust.js b/src/modules/patcher/patches/vibration-adjust.js deleted file mode 100755 index f75356d..0000000 --- a/src/modules/patcher/patches/vibration-adjust.js +++ /dev/null @@ -1,16 +0,0 @@ -const gamepad = e.gamepad; -if (gamepad?.connected) { - const gamepadSettings = window.BX_STREAM_SETTINGS.controllers[gamepad.id]; - if (gamepadSettings) { - const intensity = gamepadSettings.vibrationIntensity; - - if (intensity === 0) { - return void(e.repeat = 0); - } else if (intensity < 1) { - e.leftMotorPercent *= intensity; - e.rightMotorPercent *= intensity; - e.leftTriggerMotorPercent *= intensity; - e.rightTriggerMotorPercent *= intensity; - } - } -} diff --git a/src/modules/shortcuts/sound-shortcut.ts b/src/modules/shortcuts/sound-shortcut.ts index dc0e3ef..6010d7e 100755 --- a/src/modules/shortcuts/sound-shortcut.ts +++ b/src/modules/shortcuts/sound-shortcut.ts @@ -17,7 +17,7 @@ export class SoundShortcut { return 0; } - const currentValue = getPref(PrefKey.AUDIO_VOLUME); + const currentValue = getPref(PrefKey.AUDIO_VOLUME); let nearestValue: number; if (amount > 0) { // Increase @@ -49,7 +49,7 @@ export class SoundShortcut { static muteUnmute() { if (getPref(PrefKey.AUDIO_VOLUME_CONTROL_ENABLED) && STATES.currentStream.audioGainNode) { const gainValue = STATES.currentStream.audioGainNode.gain.value; - const settingValue = getPref(PrefKey.AUDIO_VOLUME); + const settingValue = getPref(PrefKey.AUDIO_VOLUME); let targetValue: number; if (settingValue === 0) { // settingValue is 0 => set to 100 diff --git a/src/modules/stream-player.ts b/src/modules/stream-player.ts index d323a24..5bb50ba 100755 --- a/src/modules/stream-player.ts +++ b/src/modules/stream-player.ts @@ -7,7 +7,7 @@ import { STATES } from "@/utils/global"; import { PrefKey } from "@/enums/pref-keys"; import { getPref } from "@/utils/settings-storages/global-settings-storage"; import { BX_FLAGS } from "@/utils/bx-flags"; -import { StreamPlayerType, StreamVideoProcessing, VideoPosition, VideoRatio } from "@/enums/pref-values"; +import { StreamPlayerType, StreamVideoProcessing, VideoPosition } from "@/enums/pref-values"; export type StreamPlayerOptions = Partial<{ processing: string, @@ -44,7 +44,7 @@ export class StreamPlayer { const $fragment = document.createDocumentFragment(); - this.$videoCss = CE('style', { id: 'bx-video-css' }); + this.$videoCss = CE('style', { id: 'bx-video-css' }); $fragment.appendChild(this.$videoCss); // Setup SVG filters @@ -60,7 +60,7 @@ export class StreamPlayer { id: 'bx-filter-usm-matrix', order: '3', xmlns: 'http://www.w3.org/2000/svg', - })), + }) as unknown as SVGFEConvolveMatrixElement), ), ); $fragment.appendChild($svg); @@ -98,7 +98,7 @@ export class StreamPlayer { } private resizePlayer() { - const PREF_RATIO = getPref(PrefKey.VIDEO_RATIO); + const PREF_RATIO = getPref(PrefKey.VIDEO_RATIO); const $video = this.$video; const isNativeTouchGame = STATES.currentStream.titleInfo?.details.hasNativeTouchSupport; @@ -142,7 +142,7 @@ export class StreamPlayer { // Set position const $parent = $video.parentElement!; - const position = getPref(PrefKey.VIDEO_POSITION); + const position = getPref(PrefKey.VIDEO_POSITION); $parent.style.removeProperty('padding-top'); $parent.dataset.position = position; diff --git a/src/modules/stream/stream-settings-utils.ts b/src/modules/stream/stream-settings-utils.ts index 7c4bea3..51fa98a 100755 --- a/src/modules/stream/stream-settings-utils.ts +++ b/src/modules/stream/stream-settings-utils.ts @@ -7,7 +7,7 @@ import { StreamVideoProcessing, StreamPlayerType } from "@/enums/pref-values"; import { escapeCssSelector } from "@/utils/html"; export function onChangeVideoPlayerType() { - const playerType = getPref(PrefKey.VIDEO_PLAYER_TYPE); + const playerType = getPref(PrefKey.VIDEO_PLAYER_TYPE); const $videoProcessing = document.getElementById(`bx_setting_${escapeCssSelector(PrefKey.VIDEO_PROCESSING)}`) as HTMLSelectElement; const $videoSharpness = document.getElementById(`bx_setting_${escapeCssSelector(PrefKey.VIDEO_SHARPNESS)}`) as HTMLElement; const $videoPowerPreference = document.getElementById(`bx_setting_${escapeCssSelector(PrefKey.VIDEO_POWER_PREFERENCE)}`) as HTMLElement; diff --git a/src/modules/stream/stream-stats.ts b/src/modules/stream/stream-stats.ts index 4417dc6..d1e7fc0 100755 --- a/src/modules/stream/stream-stats.ts +++ b/src/modules/stream/stream-stats.ts @@ -192,8 +192,8 @@ export class StreamStats { } refreshStyles() { - const PREF_ITEMS = getPref(PrefKey.STATS_ITEMS); - const PREF_OPACITY_BG = getPref(PrefKey.STATS_OPACITY_BACKGROUND); + const PREF_ITEMS = getPref(PrefKey.STATS_ITEMS); + const PREF_OPACITY_BG = getPref(PrefKey.STATS_OPACITY_BACKGROUND); const $container = this.$container; $container.dataset.stats = '[' + PREF_ITEMS.join('][') + ']'; diff --git a/src/modules/touch-controller.ts b/src/modules/touch-controller.ts index a2bd247..43a6519 100755 --- a/src/modules/touch-controller.ts +++ b/src/modules/touch-controller.ts @@ -289,8 +289,8 @@ export class TouchController { TouchController.#$style = $style; - const PREF_STYLE_STANDARD = getPref(PrefKey.TOUCH_CONTROLLER_STYLE_STANDARD); - const PREF_STYLE_CUSTOM = getPref(PrefKey.TOUCH_CONTROLLER_STYLE_CUSTOM); + const PREF_STYLE_STANDARD = getPref(PrefKey.TOUCH_CONTROLLER_STYLE_STANDARD); + const PREF_STYLE_CUSTOM = getPref(PrefKey.TOUCH_CONTROLLER_STYLE_CUSTOM); BxEventBus.Stream.on('dataChannelCreated', payload => { const { dataChannel } = payload; diff --git a/src/modules/ui/dialog/navigation-dialog.ts b/src/modules/ui/dialog/navigation-dialog.ts index dbb6918..d39b376 100755 --- a/src/modules/ui/dialog/navigation-dialog.ts +++ b/src/modules/ui/dialog/navigation-dialog.ts @@ -1,12 +1,10 @@ import { GamepadKey } from "@/enums/gamepad"; -import { PrefKey } from "@/enums/pref-keys"; import { VIRTUAL_GAMEPAD_ID } from "@/modules/mkb/mkb-handler"; import { BxEvent } from "@/utils/bx-event"; import { BxEventBus } from "@/utils/bx-event-bus"; import { BxLogger } from "@/utils/bx-logger"; -import { CE, isElementVisible } from "@/utils/html"; +import { calculateSelectBoxes, CE, isElementVisible } from "@/utils/html"; import { setNearby } from "@/utils/navigation-utils"; -import { getPref } from "@/utils/settings-storages/global-settings-storage"; export enum NavigationDirection { UP = 1, @@ -21,10 +19,10 @@ export type NavigationNearbyElements = Partial<{ focus: NavigationElement | (() => boolean), loop: ((direction: NavigationDirection) => boolean), - [NavigationDirection.UP]: NavigationElement | (() => void) | 'previous' | 'next', - [NavigationDirection.DOWN]: NavigationElement | (() => void) | 'previous' | 'next', - [NavigationDirection.LEFT]: NavigationElement | (() => void) | 'previous' | 'next', - [NavigationDirection.RIGHT]: NavigationElement | (() => void) | 'previous' | 'next', + [NavigationDirection.UP]: NavigationElement, + [NavigationDirection.DOWN]: NavigationElement, + [NavigationDirection.LEFT]: NavigationElement, + [NavigationDirection.RIGHT]: NavigationElement, }>; export interface NavigationElement extends HTMLElement { @@ -107,16 +105,18 @@ export class NavigationDialogManager { private static readonly GAMEPAD_POLLING_INTERVAL = 50; private static readonly GAMEPAD_KEYS = [ - GamepadKey.UP, - GamepadKey.DOWN, - GamepadKey.LEFT, - GamepadKey.RIGHT, - GamepadKey.A, - GamepadKey.B, - GamepadKey.LB, - GamepadKey.RB, - GamepadKey.LT, - GamepadKey.RT, + GamepadKey.A, GamepadKey.B, + GamepadKey.X, GamepadKey.Y, + + GamepadKey.UP, GamepadKey.RIGHT, + GamepadKey.DOWN, GamepadKey.LEFT, + + GamepadKey.LB, GamepadKey.RB, + GamepadKey.LT, GamepadKey.RT, + + GamepadKey.L3, GamepadKey.R3, + + GamepadKey.SELECT, GamepadKey.START, ]; private static readonly GAMEPAD_DIRECTION_MAP = { @@ -172,61 +172,21 @@ export class NavigationDialogManager { window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, e => this.hide()); // Calculate minimum width of controller-friendly elements - this.calculateSelectBoxes($dialog); - }); - observer.observe(this.$container, { childList: true }); - } - } - - calculateSelectBoxes($root: HTMLElement) { - const selects = Array.from($root.querySelectorAll('.bx-select:not([data-calculated]) select')); - - for (const $select of selects) { - const $parent = $select.parentElement! as HTMLElement; - - // Don't apply to select.bx-full-width elements - if ($parent.classList.contains('bx-full-width')) { - $parent.dataset.calculated = 'true'; + const observer = new MutationObserver(mutationList => { + if (mutationList.length === 0 || mutationList[0].addedNodes.length === 0) { return; } - const rect = $select.getBoundingClientRect(); - - let $label: HTMLElement; - let width = Math.ceil(rect.width); - if (!width) { + // Get dialog + const $dialog = mutationList[0].addedNodes[0]; + if (!$dialog || !($dialog instanceof HTMLElement)) { return; } - if (($select as HTMLSelectElement).multiple) { - $label = $parent.querySelector('.bx-select-value')!; - width += 20; // Add checkbox's width - } else { - $label = $parent.querySelector('div')!; - } - - // Reduce width if it has - if ($select.querySelector('optgroup')) { - width -= 15; - } - - // Set min-width - $label.style.minWidth = width + 'px'; - $parent.dataset.calculated = 'true'; - }; + // Find un-calculated if not using controller-friendly UI - if (!forceFriendly && !getPref(PrefKey.UI_CONTROLLER_FRIENDLY)) { + const isControllerFriendly = forceFriendly || getPref(PrefKey.UI_CONTROLLER_FRIENDLY); + + // Return normal + if ($select.multiple && !isControllerFriendly) { $select.classList.add('bx-select'); // @ts-ignore return $select; @@ -29,25 +32,22 @@ export class BxSelectElement extends HTMLSelectElement { // Remove "tabindex" attribute from