From 9199351af1bfb54bee7d1ca64909fd790df94d76 Mon Sep 17 00:00:00 2001 From: redphx <96280+redphx@users.noreply.github.com> Date: Thu, 5 Dec 2024 17:10:39 +0700 Subject: [PATCH] 6.0 --- .github/FUNDING.yml | 0 .github/ISSUE_TEMPLATE/01-bug-report.yml | 0 .github/ISSUE_TEMPLATE/02-feature-request.yml | 0 .github/ISSUE_TEMPLATE/config.yml | 0 .gitignore | 0 .vscode/settings.json | 0 .vscode/tasks.json | 0 LICENSE | 0 README.md | 0 build.ts | 0 dist/better-xcloud.lite.user.js | 5294 +++++++++-------- dist/better-xcloud.user.js | 818 +-- eslint.config.mjs | 0 package.json | 2 +- scripts/custom-flags.user.js | 0 src/assets/css/button.styl | 38 + src/assets/css/game-bar.styl | 0 src/assets/css/guide-menu.styl | 0 src/assets/css/header.styl | 0 .../{dialog.styl => key-binding-dialog.styl} | 50 +- src/assets/css/loading-screen.styl | 0 src/assets/css/misc.styl | 0 src/assets/css/mkb.styl | 174 +- src/assets/css/navigation-dialog.styl | 195 + src/assets/css/number-stepper.styl | 70 +- src/assets/css/remote-play.styl | 0 src/assets/css/root.styl | 36 +- src/assets/css/settings-dialog.styl | 193 +- src/assets/css/stream-stats.styl | 0 src/assets/css/stream.styl | 0 src/assets/css/styles.styl | 2 +- src/assets/css/toast.styl | 0 src/assets/css/web-components.styl | 66 +- src/assets/header_meta.txt | 0 src/assets/header_script.lite.txt | 0 src/assets/header_script.txt | 0 src/assets/svg/battery-full.svg | 0 src/assets/svg/better-xcloud.svg | 0 src/assets/svg/camera.svg | 0 src/assets/svg/caret-left.svg | 0 src/assets/svg/caret-right.svg | 0 src/assets/svg/clock.svg | 0 src/assets/svg/close.svg | 0 src/assets/svg/cloud.svg | 0 src/assets/svg/command.svg | 0 src/assets/svg/controller.svg | 0 src/assets/svg/copy.svg | 0 src/assets/svg/create-shortcut.svg | 0 src/assets/svg/cursor-text.svg | 0 src/assets/svg/display.svg | 0 src/assets/svg/download.svg | 0 src/assets/svg/eye-slash.svg | 0 src/assets/svg/eye.svg | 0 src/assets/svg/home.svg | 0 src/assets/svg/microphone-slash.svg | 0 src/assets/svg/microphone.svg | 0 src/assets/svg/mouse-settings.svg | 0 src/assets/svg/mouse.svg | 0 src/assets/svg/native-mkb.svg | 0 src/assets/svg/new.svg | 0 src/assets/svg/power.svg | 0 src/assets/svg/question.svg | 0 src/assets/svg/refresh.svg | 0 src/assets/svg/remote-play.svg | 0 src/assets/svg/speaker-high.svg | 0 src/assets/svg/speaker-slash.svg | 0 src/assets/svg/stream-settings.svg | 0 src/assets/svg/stream-stats.svg | 0 src/assets/svg/touch-control-disable.svg | 0 src/assets/svg/touch-control-enable.svg | 0 src/assets/svg/trash.svg | 0 src/assets/svg/true-achievements.svg | 0 src/assets/svg/upload.svg | 0 src/assets/svg/virtual-controller.svg | 0 src/build-config.ts | 0 src/enums/bypass-servers.ts | 24 +- src/enums/game-pass-gallery.ts | 0 src/enums/gamepad.ts | 71 + src/enums/mkb.ts | 227 +- src/enums/pref-keys.ts | 164 +- src/enums/pref-values.ts | 104 + src/enums/prompt-font.ts | 0 src/enums/shortcut-actions.ts | 25 + src/enums/stream-player.ts | 9 - src/enums/ui-sections.ts | 8 - src/enums/user-agent.ts | 0 src/index.ts | 88 +- src/macros/build.ts | 0 src/modules/controller-shortcut.ts | 396 +- src/modules/device-vibration-manager.ts | 145 + src/modules/dialog.ts | 102 - .../{action-base.ts => base-action.ts} | 0 src/modules/game-bar/game-bar.ts | 53 +- ...ion-microphone.ts => microphone-action.ts} | 10 +- ...{action-renderer.ts => renderer-action.ts} | 19 +- ...ion-screenshot.ts => screenshot-action.ts} | 6 +- .../{action-speaker.ts => speaker-action.ts} | 10 +- ...uch-control.ts => touch-control-action.ts} | 8 +- ...vements.ts => true-achievements-action.ts} | 6 +- src/modules/loading-screen.ts | 23 +- src/modules/mkb/base-mkb-handler.ts | 6 +- src/modules/mkb/key-helper.ts | 113 +- src/modules/mkb/keyboard-shortcut-handler.ts | 40 + src/modules/mkb/mkb-handler.ts | 551 +- src/modules/mkb/mkb-popup.ts | 110 + src/modules/mkb/mkb-preset.ts | 135 - src/modules/mkb/mkb-remapper.ts | 541 -- src/modules/mkb/mouse-cursor-hider.ts | 54 +- src/modules/mkb/native-mkb-handler.ts | 232 +- src/modules/mkb/pointer-client.ts | 0 src/modules/patcher.ts | 131 +- src/modules/patches/controller-shortcuts.js | 2 +- src/modules/patches/expose-stream-session.js | 0 src/modules/patches/local-co-op-enable.js | 40 +- src/modules/patches/remote-play-enable.js | 0 src/modules/patches/remote-play-keep-alive.js | 0 .../set-currently-focused-interactable.js | 0 src/modules/patches/vibration-adjust.js | 27 +- src/modules/player/shaders/clarity_boost.fs | 0 src/modules/player/shaders/clarity_boost.vert | 0 src/modules/player/webgl2-player.ts | 1 + src/modules/remote-play-manager.ts | 36 +- ...t-microphone.ts => microphone-shortcut.ts} | 0 ...rtcut-renderer.ts => renderer-shortcut.ts} | 9 +- src/modules/shortcuts/shortcut-actions.ts | 59 + .../{shortcut-sound.ts => sound-shortcut.ts} | 10 +- ...cut-stream-ui.ts => stream-ui-shortcut.ts} | 0 src/modules/stream-player.ts | 4 +- src/modules/stream/stream-badges.ts | 7 +- src/modules/stream/stream-settings-utils.ts | 13 +- src/modules/stream/stream-stats.ts | 13 +- src/modules/stream/stream-ui.ts | 4 +- src/modules/touch-controller.ts | 32 +- src/modules/ui/dialog/navigation-dialog.ts | 85 +- .../base-profile-manager-dialog.ts | 205 + .../controller-shortcuts-manager-dialog.ts | 194 + .../keyboard-shortcuts-manager-dialog.ts | 151 + .../mkb-mapping-manager-dialog.ts | 254 + src/modules/ui/dialog/remote-play-dialog.ts | 22 +- src/modules/ui/dialog/settings-dialog.ts | 842 +-- .../ui/dialog/settings/controller-extra.ts | 201 + src/modules/ui/dialog/settings/mkb-extra.ts | 131 + src/modules/ui/dialog/settings/suggestions.ts | 337 ++ src/modules/ui/fullscreen-text.ts | 0 src/modules/ui/game-tile.ts | 38 +- src/modules/ui/guide-menu.ts | 20 +- src/modules/ui/header.ts | 12 +- src/modules/ui/product-details.ts | 2 - src/modules/ui/ui.ts | 0 src/modules/vibration-manager.ts | 152 - src/types/db.d.ts | 12 + src/types/global.d.ts | 28 + src/types/index.d.ts | 19 +- src/types/mkb.d.ts | 27 - src/types/network.ts | 0 src/types/preferences.d.ts | 0 src/types/prefs.d.ts | 17 + src/types/presets.d.ts | 73 + src/types/setting-definition.d.ts | 77 +- src/types/stream-stats.d.ts | 0 src/types/titles-info.d.ts | 0 src/utils/bx-event.ts | 7 + src/utils/bx-exposed.ts | 19 +- src/utils/bx-flags.ts | 0 src/utils/bx-icon.ts | 4 - src/utils/bx-logger.ts | 0 src/utils/css.ts | 15 +- src/utils/feature-gates.ts | 23 +- src/utils/gamepad.ts | 24 +- src/utils/gh-pages.ts | 84 + src/utils/global.ts | 16 +- src/utils/history.ts | 0 src/utils/html.ts | 207 +- src/utils/local-db/base-presets-table.ts | 84 + src/utils/local-db/base-table.ts | 70 + .../local-db/controller-settings-table.ts | 38 + .../local-db/controller-shortcuts-table.ts | 61 + .../local-db/keyboard-shortcuts-table.ts | 45 + src/utils/local-db/local-db.ts | 113 +- .../local-db/mkb-mapping-presets-table.ts | 126 + src/utils/local-db/mkb-presets-db.ts | 102 - src/utils/monkey-patches.ts | 23 +- src/utils/navigation-utils.ts | 0 src/utils/network.ts | 10 +- src/utils/region.ts | 16 +- src/utils/root-dialog-observer.ts | 2 - src/utils/screenshot-manager.ts | 2 +- src/utils/sdp.ts | 0 src/utils/setting-element.ts | 277 +- .../base-settings-storage.ts | 59 +- .../global-settings-storage.ts | 363 +- src/utils/shortcut-handler.ts | 74 + src/utils/stream-settings.ts | 215 + src/utils/stream-stats-collector.ts | 18 +- src/utils/toast.ts | 4 +- src/utils/translation.ts | 103 +- src/utils/true-achievements.ts | 6 +- src/utils/user-agent.ts | 3 +- src/utils/utils.ts | 35 +- src/utils/xbox-api.ts | 0 src/utils/xcloud-api.ts | 4 +- src/utils/xcloud-interceptor.ts | 84 +- src/utils/xhome-interceptor.ts | 22 +- src/web-components/bx-key-binding-button.ts | 270 + src/web-components/bx-number-stepper.ts | 297 + src/web-components/bx-select.ts | 355 +- tsconfig.json | 1 + 207 files changed, 9833 insertions(+), 6953 deletions(-) mode change 100644 => 100755 .github/FUNDING.yml mode change 100644 => 100755 .github/ISSUE_TEMPLATE/01-bug-report.yml mode change 100644 => 100755 .github/ISSUE_TEMPLATE/02-feature-request.yml mode change 100644 => 100755 .github/ISSUE_TEMPLATE/config.yml mode change 100644 => 100755 .gitignore mode change 100644 => 100755 .vscode/settings.json mode change 100644 => 100755 .vscode/tasks.json mode change 100644 => 100755 LICENSE mode change 100644 => 100755 README.md mode change 100644 => 100755 build.ts mode change 100644 => 100755 dist/better-xcloud.lite.user.js mode change 100644 => 100755 dist/better-xcloud.user.js mode change 100644 => 100755 eslint.config.mjs mode change 100644 => 100755 package.json mode change 100644 => 100755 scripts/custom-flags.user.js mode change 100644 => 100755 src/assets/css/button.styl mode change 100644 => 100755 src/assets/css/game-bar.styl mode change 100644 => 100755 src/assets/css/guide-menu.styl mode change 100644 => 100755 src/assets/css/header.styl rename src/assets/css/{dialog.styl => key-binding-dialog.styl} (66%) mode change 100644 => 100755 mode change 100644 => 100755 src/assets/css/loading-screen.styl mode change 100644 => 100755 src/assets/css/misc.styl mode change 100644 => 100755 src/assets/css/mkb.styl mode change 100644 => 100755 src/assets/css/navigation-dialog.styl mode change 100644 => 100755 src/assets/css/number-stepper.styl mode change 100644 => 100755 src/assets/css/remote-play.styl mode change 100644 => 100755 src/assets/css/root.styl mode change 100644 => 100755 src/assets/css/settings-dialog.styl mode change 100644 => 100755 src/assets/css/stream-stats.styl mode change 100644 => 100755 src/assets/css/stream.styl mode change 100644 => 100755 src/assets/css/styles.styl mode change 100644 => 100755 src/assets/css/toast.styl mode change 100644 => 100755 src/assets/css/web-components.styl mode change 100644 => 100755 src/assets/header_meta.txt mode change 100644 => 100755 src/assets/header_script.lite.txt mode change 100644 => 100755 src/assets/header_script.txt mode change 100644 => 100755 src/assets/svg/battery-full.svg mode change 100644 => 100755 src/assets/svg/better-xcloud.svg mode change 100644 => 100755 src/assets/svg/camera.svg mode change 100644 => 100755 src/assets/svg/caret-left.svg mode change 100644 => 100755 src/assets/svg/caret-right.svg mode change 100644 => 100755 src/assets/svg/clock.svg mode change 100644 => 100755 src/assets/svg/close.svg mode change 100644 => 100755 src/assets/svg/cloud.svg mode change 100644 => 100755 src/assets/svg/command.svg mode change 100644 => 100755 src/assets/svg/controller.svg mode change 100644 => 100755 src/assets/svg/copy.svg mode change 100644 => 100755 src/assets/svg/create-shortcut.svg mode change 100644 => 100755 src/assets/svg/cursor-text.svg mode change 100644 => 100755 src/assets/svg/display.svg mode change 100644 => 100755 src/assets/svg/download.svg mode change 100644 => 100755 src/assets/svg/eye-slash.svg mode change 100644 => 100755 src/assets/svg/eye.svg mode change 100644 => 100755 src/assets/svg/home.svg mode change 100644 => 100755 src/assets/svg/microphone-slash.svg mode change 100644 => 100755 src/assets/svg/microphone.svg mode change 100644 => 100755 src/assets/svg/mouse-settings.svg mode change 100644 => 100755 src/assets/svg/mouse.svg mode change 100644 => 100755 src/assets/svg/native-mkb.svg mode change 100644 => 100755 src/assets/svg/new.svg mode change 100644 => 100755 src/assets/svg/power.svg mode change 100644 => 100755 src/assets/svg/question.svg mode change 100644 => 100755 src/assets/svg/refresh.svg mode change 100644 => 100755 src/assets/svg/remote-play.svg mode change 100644 => 100755 src/assets/svg/speaker-high.svg mode change 100644 => 100755 src/assets/svg/speaker-slash.svg mode change 100644 => 100755 src/assets/svg/stream-settings.svg mode change 100644 => 100755 src/assets/svg/stream-stats.svg mode change 100644 => 100755 src/assets/svg/touch-control-disable.svg mode change 100644 => 100755 src/assets/svg/touch-control-enable.svg mode change 100644 => 100755 src/assets/svg/trash.svg mode change 100644 => 100755 src/assets/svg/true-achievements.svg mode change 100644 => 100755 src/assets/svg/upload.svg mode change 100644 => 100755 src/assets/svg/virtual-controller.svg mode change 100644 => 100755 src/build-config.ts mode change 100644 => 100755 src/enums/bypass-servers.ts mode change 100644 => 100755 src/enums/game-pass-gallery.ts create mode 100755 src/enums/gamepad.ts mode change 100644 => 100755 src/enums/mkb.ts mode change 100644 => 100755 src/enums/pref-keys.ts create mode 100755 src/enums/pref-values.ts mode change 100644 => 100755 src/enums/prompt-font.ts create mode 100755 src/enums/shortcut-actions.ts delete mode 100644 src/enums/stream-player.ts delete mode 100644 src/enums/ui-sections.ts mode change 100644 => 100755 src/enums/user-agent.ts mode change 100644 => 100755 src/index.ts mode change 100644 => 100755 src/macros/build.ts mode change 100644 => 100755 src/modules/controller-shortcut.ts create mode 100755 src/modules/device-vibration-manager.ts delete mode 100644 src/modules/dialog.ts rename src/modules/game-bar/{action-base.ts => base-action.ts} (100%) mode change 100644 => 100755 mode change 100644 => 100755 src/modules/game-bar/game-bar.ts rename src/modules/game-bar/{action-microphone.ts => microphone-action.ts} (87%) mode change 100644 => 100755 rename src/modules/game-bar/{action-renderer.ts => renderer-action.ts} (58%) mode change 100644 => 100755 rename src/modules/game-bar/{action-screenshot.ts => screenshot-action.ts} (82%) mode change 100644 => 100755 rename src/modules/game-bar/{action-speaker.ts => speaker-action.ts} (81%) mode change 100644 => 100755 rename src/modules/game-bar/{action-touch-control.ts => touch-control-action.ts} (86%) mode change 100644 => 100755 rename src/modules/game-bar/{action-true-achievements.ts => true-achievements-action.ts} (81%) mode change 100644 => 100755 mode change 100644 => 100755 src/modules/loading-screen.ts mode change 100644 => 100755 src/modules/mkb/base-mkb-handler.ts mode change 100644 => 100755 src/modules/mkb/key-helper.ts create mode 100755 src/modules/mkb/keyboard-shortcut-handler.ts mode change 100644 => 100755 src/modules/mkb/mkb-handler.ts create mode 100755 src/modules/mkb/mkb-popup.ts delete mode 100644 src/modules/mkb/mkb-preset.ts delete mode 100644 src/modules/mkb/mkb-remapper.ts mode change 100644 => 100755 src/modules/mkb/mouse-cursor-hider.ts mode change 100644 => 100755 src/modules/mkb/native-mkb-handler.ts mode change 100644 => 100755 src/modules/mkb/pointer-client.ts mode change 100644 => 100755 src/modules/patcher.ts mode change 100644 => 100755 src/modules/patches/controller-shortcuts.js mode change 100644 => 100755 src/modules/patches/expose-stream-session.js mode change 100644 => 100755 src/modules/patches/local-co-op-enable.js mode change 100644 => 100755 src/modules/patches/remote-play-enable.js mode change 100644 => 100755 src/modules/patches/remote-play-keep-alive.js mode change 100644 => 100755 src/modules/patches/set-currently-focused-interactable.js mode change 100644 => 100755 src/modules/patches/vibration-adjust.js mode change 100644 => 100755 src/modules/player/shaders/clarity_boost.fs mode change 100644 => 100755 src/modules/player/shaders/clarity_boost.vert mode change 100644 => 100755 src/modules/player/webgl2-player.ts mode change 100644 => 100755 src/modules/remote-play-manager.ts rename src/modules/shortcuts/{shortcut-microphone.ts => microphone-shortcut.ts} (100%) mode change 100644 => 100755 rename src/modules/shortcuts/{shortcut-renderer.ts => renderer-shortcut.ts} (69%) mode change 100644 => 100755 create mode 100755 src/modules/shortcuts/shortcut-actions.ts rename src/modules/shortcuts/{shortcut-sound.ts => sound-shortcut.ts} (90%) mode change 100644 => 100755 rename src/modules/shortcuts/{shortcut-stream-ui.ts => stream-ui-shortcut.ts} (100%) mode change 100644 => 100755 mode change 100644 => 100755 src/modules/stream-player.ts mode change 100644 => 100755 src/modules/stream/stream-badges.ts mode change 100644 => 100755 src/modules/stream/stream-settings-utils.ts mode change 100644 => 100755 src/modules/stream/stream-stats.ts mode change 100644 => 100755 src/modules/stream/stream-ui.ts mode change 100644 => 100755 src/modules/touch-controller.ts mode change 100644 => 100755 src/modules/ui/dialog/navigation-dialog.ts create mode 100755 src/modules/ui/dialog/profile-manger/base-profile-manager-dialog.ts create mode 100755 src/modules/ui/dialog/profile-manger/controller-shortcuts-manager-dialog.ts create mode 100755 src/modules/ui/dialog/profile-manger/keyboard-shortcuts-manager-dialog.ts create mode 100755 src/modules/ui/dialog/profile-manger/mkb-mapping-manager-dialog.ts mode change 100644 => 100755 src/modules/ui/dialog/remote-play-dialog.ts mode change 100644 => 100755 src/modules/ui/dialog/settings-dialog.ts create mode 100755 src/modules/ui/dialog/settings/controller-extra.ts create mode 100755 src/modules/ui/dialog/settings/mkb-extra.ts create mode 100755 src/modules/ui/dialog/settings/suggestions.ts mode change 100644 => 100755 src/modules/ui/fullscreen-text.ts mode change 100644 => 100755 src/modules/ui/game-tile.ts mode change 100644 => 100755 src/modules/ui/guide-menu.ts mode change 100644 => 100755 src/modules/ui/header.ts mode change 100644 => 100755 src/modules/ui/product-details.ts mode change 100644 => 100755 src/modules/ui/ui.ts delete mode 100644 src/modules/vibration-manager.ts create mode 100755 src/types/db.d.ts create mode 100755 src/types/global.d.ts mode change 100644 => 100755 src/types/index.d.ts delete mode 100644 src/types/mkb.d.ts mode change 100644 => 100755 src/types/network.ts mode change 100644 => 100755 src/types/preferences.d.ts create mode 100755 src/types/prefs.d.ts create mode 100755 src/types/presets.d.ts mode change 100644 => 100755 src/types/setting-definition.d.ts mode change 100644 => 100755 src/types/stream-stats.d.ts mode change 100644 => 100755 src/types/titles-info.d.ts mode change 100644 => 100755 src/utils/bx-event.ts mode change 100644 => 100755 src/utils/bx-exposed.ts mode change 100644 => 100755 src/utils/bx-flags.ts mode change 100644 => 100755 src/utils/bx-icon.ts mode change 100644 => 100755 src/utils/bx-logger.ts mode change 100644 => 100755 src/utils/css.ts mode change 100644 => 100755 src/utils/feature-gates.ts mode change 100644 => 100755 src/utils/gamepad.ts create mode 100755 src/utils/gh-pages.ts mode change 100644 => 100755 src/utils/global.ts mode change 100644 => 100755 src/utils/history.ts mode change 100644 => 100755 src/utils/html.ts create mode 100755 src/utils/local-db/base-presets-table.ts create mode 100755 src/utils/local-db/base-table.ts create mode 100755 src/utils/local-db/controller-settings-table.ts create mode 100755 src/utils/local-db/controller-shortcuts-table.ts create mode 100755 src/utils/local-db/keyboard-shortcuts-table.ts mode change 100644 => 100755 src/utils/local-db/local-db.ts create mode 100755 src/utils/local-db/mkb-mapping-presets-table.ts delete mode 100644 src/utils/local-db/mkb-presets-db.ts mode change 100644 => 100755 src/utils/monkey-patches.ts mode change 100644 => 100755 src/utils/navigation-utils.ts mode change 100644 => 100755 src/utils/network.ts mode change 100644 => 100755 src/utils/region.ts mode change 100644 => 100755 src/utils/root-dialog-observer.ts mode change 100644 => 100755 src/utils/screenshot-manager.ts mode change 100644 => 100755 src/utils/sdp.ts mode change 100644 => 100755 src/utils/setting-element.ts mode change 100644 => 100755 src/utils/settings-storages/base-settings-storage.ts mode change 100644 => 100755 src/utils/settings-storages/global-settings-storage.ts create mode 100755 src/utils/shortcut-handler.ts create mode 100755 src/utils/stream-settings.ts mode change 100644 => 100755 src/utils/stream-stats-collector.ts mode change 100644 => 100755 src/utils/toast.ts mode change 100644 => 100755 src/utils/translation.ts mode change 100644 => 100755 src/utils/true-achievements.ts mode change 100644 => 100755 src/utils/user-agent.ts mode change 100644 => 100755 src/utils/utils.ts mode change 100644 => 100755 src/utils/xbox-api.ts mode change 100644 => 100755 src/utils/xcloud-api.ts mode change 100644 => 100755 src/utils/xcloud-interceptor.ts mode change 100644 => 100755 src/utils/xhome-interceptor.ts create mode 100755 src/web-components/bx-key-binding-button.ts create mode 100755 src/web-components/bx-number-stepper.ts mode change 100644 => 100755 src/web-components/bx-select.ts mode change 100644 => 100755 tsconfig.json diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml old mode 100644 new mode 100755 diff --git a/.github/ISSUE_TEMPLATE/01-bug-report.yml b/.github/ISSUE_TEMPLATE/01-bug-report.yml old mode 100644 new mode 100755 diff --git a/.github/ISSUE_TEMPLATE/02-feature-request.yml b/.github/ISSUE_TEMPLATE/02-feature-request.yml old mode 100644 new mode 100755 diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml old mode 100644 new mode 100755 diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 diff --git a/.vscode/settings.json b/.vscode/settings.json old mode 100644 new mode 100755 diff --git a/.vscode/tasks.json b/.vscode/tasks.json old mode 100644 new mode 100755 diff --git a/LICENSE b/LICENSE old mode 100644 new mode 100755 diff --git a/README.md b/README.md old mode 100644 new mode 100755 diff --git a/build.ts b/build.ts old mode 100644 new mode 100755 diff --git a/dist/better-xcloud.lite.user.js b/dist/better-xcloud.lite.user.js old mode 100644 new mode 100755 index e1543fe..0641c97 --- 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 5.9.8-beta +// @version 6.0.0-beta-3 // @description Improve Xbox Cloud Gaming (xCloud) experience // @author redphx // @license MIT @@ -44,7 +44,7 @@ if (!!window.chrome || window.navigator.userAgent.includes("Chrome")) { if (match) CHROMIUM_VERSION = match[1]; } class UserAgent { - static STORAGE_KEY = "better_xcloud_user_agent"; + static STORAGE_KEY = "BetterXcloud.UserAgent"; static #config; static #isMobile = null; static #isSafari = null; @@ -105,9 +105,9 @@ class UserAgent { }); } } -var SCRIPT_VERSION = "5.9.8-beta", SCRIPT_VARIANT = "lite", AppInterface = window.AppInterface; +var SCRIPT_VERSION = "6.0.0-beta-3", 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, supportMkb = AppInterface || !userAgent.match(/(android|iphone|ipad)/), STATES = { +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, serverRegions: {}, selectedRegion: {}, @@ -117,14 +117,17 @@ var userAgent = window.navigator.userAgent.toLowerCase(), isTv = userAgent.inclu browser: { capabilities: { touch: browserHasTouchSupport, - batteryApi: "getBattery" in window.navigator + batteryApi: "getBattery" in window.navigator, + deviceVibration: !!window.navigator.vibrate, + mkb: AppInterface || !UserAgent.getDefault().toLowerCase().match(/(android|iphone|ipad)/), + emulatedNativeMkb: !!AppInterface } }, userAgent: { isTv, capabilities: { touch: userAgentHasTouchSupport, - mkb: supportMkb + mkb: AppInterface || !userAgent.match(/(android|iphone|ipad)/) } }, currentStream: {}, @@ -132,13 +135,13 @@ var userAgent = window.navigator.userAgent.toLowerCase(), isTv = userAgent.inclu pointerServerPort: 9269 }, STORAGE = {}; function deepClone(obj) { - if ("structuredClone" in window) return structuredClone(obj); if (!obj) return {}; + if ("structuredClone" in window) return structuredClone(obj); return JSON.parse(JSON.stringify(obj)); } var BxEvent; ((BxEvent) => { - BxEvent.JUMP_BACK_IN_READY = "bx-jump-back-in-ready", BxEvent.POPSTATE = "bx-popstate", BxEvent.TITLE_INFO_READY = "bx-title-info-ready", BxEvent.SETTINGS_CHANGED = "bx-settings-changed", BxEvent.STREAM_LOADING = "bx-stream-loading", BxEvent.STREAM_STARTING = "bx-stream-starting", BxEvent.STREAM_STARTED = "bx-stream-started", BxEvent.STREAM_PLAYING = "bx-stream-playing", BxEvent.STREAM_STOPPED = "bx-stream-stopped", BxEvent.STREAM_ERROR_PAGE = "bx-stream-error-page", BxEvent.STREAM_WEBRTC_CONNECTED = "bx-stream-webrtc-connected", BxEvent.STREAM_WEBRTC_DISCONNECTED = "bx-stream-webrtc-disconnected", BxEvent.STREAM_SESSION_READY = "bx-stream-session-ready", BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED = "bx-custom-touch-layouts-loaded", BxEvent.TOUCH_LAYOUT_MANAGER_READY = "bx-touch-layout-manager-ready", BxEvent.REMOTE_PLAY_READY = "bx-remote-play-ready", BxEvent.REMOTE_PLAY_FAILED = "bx-remote-play-failed", BxEvent.XCLOUD_SERVERS_READY = "bx-servers-ready", BxEvent.XCLOUD_SERVERS_UNAVAILABLE = "bx-servers-unavailable", BxEvent.DATA_CHANNEL_CREATED = "bx-data-channel-created", BxEvent.GAME_BAR_ACTION_ACTIVATED = "bx-game-bar-action-activated", BxEvent.MICROPHONE_STATE_CHANGED = "bx-microphone-state-changed", BxEvent.SPEAKER_STATE_CHANGED = "bx-speaker-state-changed", BxEvent.CAPTURE_SCREENSHOT = "bx-capture-screenshot", BxEvent.POINTER_LOCK_REQUESTED = "bx-pointer-lock-requested", BxEvent.POINTER_LOCK_EXITED = "bx-pointer-lock-exited", BxEvent.NAVIGATION_FOCUS_CHANGED = "bx-nav-focus-changed", BxEvent.XCLOUD_DIALOG_SHOWN = "bx-xcloud-dialog-shown", BxEvent.XCLOUD_DIALOG_DISMISSED = "bx-xcloud-dialog-dismissed", BxEvent.XCLOUD_GUIDE_MENU_SHOWN = "bx-xcloud-guide-menu-shown", BxEvent.XCLOUD_POLLING_MODE_CHANGED = "bx-xcloud-polling-mode-changed", BxEvent.XCLOUD_RENDERING_COMPONENT = "bx-xcloud-rendering-component", BxEvent.XCLOUD_ROUTER_HISTORY_READY = "bx-xcloud-router-history-ready"; + BxEvent.JUMP_BACK_IN_READY = "bx-jump-back-in-ready", BxEvent.POPSTATE = "bx-popstate", BxEvent.TITLE_INFO_READY = "bx-title-info-ready", BxEvent.SETTINGS_CHANGED = "bx-settings-changed", BxEvent.STREAM_LOADING = "bx-stream-loading", BxEvent.STREAM_STARTING = "bx-stream-starting", BxEvent.STREAM_STARTED = "bx-stream-started", BxEvent.STREAM_PLAYING = "bx-stream-playing", BxEvent.STREAM_STOPPED = "bx-stream-stopped", BxEvent.STREAM_ERROR_PAGE = "bx-stream-error-page", BxEvent.STREAM_WEBRTC_CONNECTED = "bx-stream-webrtc-connected", BxEvent.STREAM_WEBRTC_DISCONNECTED = "bx-stream-webrtc-disconnected", BxEvent.MKB_UPDATED = "bx-mkb-updated", BxEvent.KEYBOARD_SHORTCUTS_UPDATED = "bx-keyboard-shortcuts-updated", BxEvent.STREAM_SESSION_READY = "bx-stream-session-ready", BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED = "bx-custom-touch-layouts-loaded", BxEvent.TOUCH_LAYOUT_MANAGER_READY = "bx-touch-layout-manager-ready", BxEvent.REMOTE_PLAY_READY = "bx-remote-play-ready", BxEvent.REMOTE_PLAY_FAILED = "bx-remote-play-failed", BxEvent.XCLOUD_SERVERS_READY = "bx-servers-ready", BxEvent.XCLOUD_SERVERS_UNAVAILABLE = "bx-servers-unavailable", BxEvent.DATA_CHANNEL_CREATED = "bx-data-channel-created", BxEvent.DEVICE_VIBRATION_CHANGED = "bx-device-vibration-changed", BxEvent.GAME_BAR_ACTION_ACTIVATED = "bx-game-bar-action-activated", BxEvent.MICROPHONE_STATE_CHANGED = "bx-microphone-state-changed", BxEvent.SPEAKER_STATE_CHANGED = "bx-speaker-state-changed", BxEvent.VIDEO_VISIBILITY_CHANGED = "bx-video-visibility-changed", BxEvent.CAPTURE_SCREENSHOT = "bx-capture-screenshot", BxEvent.POINTER_LOCK_REQUESTED = "bx-pointer-lock-requested", BxEvent.POINTER_LOCK_EXITED = "bx-pointer-lock-exited", BxEvent.NAVIGATION_FOCUS_CHANGED = "bx-nav-focus-changed", BxEvent.GH_PAGES_FORCE_NATIVE_MKB_UPDATED = "bx-gh-pages-force-native-mkb-updated", BxEvent.XCLOUD_DIALOG_SHOWN = "bx-xcloud-dialog-shown", BxEvent.XCLOUD_DIALOG_DISMISSED = "bx-xcloud-dialog-dismissed", BxEvent.XCLOUD_GUIDE_MENU_SHOWN = "bx-xcloud-guide-menu-shown", BxEvent.XCLOUD_POLLING_MODE_CHANGED = "bx-xcloud-polling-mode-changed", BxEvent.XCLOUD_RENDERING_COMPONENT = "bx-xcloud-rendering-component", BxEvent.XCLOUD_ROUTER_HISTORY_READY = "bx-xcloud-router-history-ready"; function dispatch(target, eventName, data) { if (!target) return; if (!eventName) { @@ -153,88 +156,41 @@ var BxEvent; BxEvent.dispatch = dispatch; })(BxEvent ||= {}); window.BxEvent = BxEvent; -class NavigationUtils { - static setNearby($elm, nearby) { - $elm.nearby = $elm.nearby || {}; - let key; - for (key in nearby) - $elm.nearby[key] = nearby[key]; +class GhPagesUtils { + static fetchLatestCommit() { + NATIVE_FETCH("https://api.github.com/repos/redphx/better-xcloud/branches/gh-pages", { + method: "GET", + headers: { + Accept: "application/vnd.github.v3+json" + } + }).then((response) => response.json()).then((data) => { + let latestCommitHash = data.commit.sha; + window.localStorage.setItem("BetterXcloud.GhPages.CommitHash", latestCommitHash); + }).catch((error) => { + BxLogger.error("GhPagesUtils", "Error fetching the latest commit:", error); + }); } -} -var setNearby = NavigationUtils.setNearby; -var ButtonStyleClass = { - 1: "bx-primary", - 2: "bx-danger", - 4: "bx-ghost", - 8: "bx-frosted", - 16: "bx-drop-shadow", - 32: "bx-focusable", - 64: "bx-full-width", - 128: "bx-full-height", - 256: "bx-tall", - 512: "bx-circular", - 1024: "bx-normal-case", - 2048: "bx-normal-link" -}; -function createElement(elmName, props = {}, ..._) { - let $elm, hasNs = "xmlns" in props; - if (hasNs) $elm = document.createElementNS(props.xmlns, elmName), delete props.xmlns; - else $elm = document.createElement(elmName); - if (props._nearby) setNearby($elm, props._nearby), delete props._nearby; - for (let key in props) { - if ($elm.hasOwnProperty(key)) continue; - if (hasNs) $elm.setAttributeNS(null, key, props[key]); - else if (key === "on") for (let eventName in props[key]) - $elm.addEventListener(eventName, props[key][eventName]); - else $elm.setAttribute(key, props[key]); + static getUrl(path) { + if (path[0] === "/") alert('`path` must not starts with "/"'); + let prefix = "https://raw.githubusercontent.com/redphx/better-xcloud", latestCommitHash = window.localStorage.getItem("BetterXcloud.GhPages.CommitHash"); + if (latestCommitHash) return `${prefix}/${latestCommitHash}/${path}`; + else return `${prefix}/refs/heads/gh-pages/${path}`; } - for (let i = 2, size = arguments.length;i < size; i++) { - let arg = arguments[i]; - if (arg instanceof Node) $elm.appendChild(arg); - else if (arg !== null && arg !== !1 && typeof arg !== "undefined") $elm.appendChild(document.createTextNode(arg)); + static getNativeMkbCustomList(update = !1) { + let key = "BetterXcloud.GhPages.ForceNativeMkb"; + update && NATIVE_FETCH(GhPagesUtils.getUrl("native-mkb/ids.json")).then((response) => response.json()).then((json) => { + if (json.$schemaVersion === 1) window.localStorage.setItem(key, JSON.stringify(json)), BxEvent.dispatch(window, BxEvent.GH_PAGES_FORCE_NATIVE_MKB_UPDATED); + }); + let info = JSON.parse(window.localStorage.getItem(key) || "{}"); + if (info.$schemaVersion !== 1) return window.localStorage.removeItem(key), {}; + return info.data; } - return $elm; -} -var CE = createElement, domParser = new DOMParser; -function createSvgIcon(icon) { - return domParser.parseFromString(icon.toString(), "image/svg+xml").documentElement; -} -var ButtonStyleIndices = Object.keys(ButtonStyleClass).map((i) => parseInt(i)); -function createButton(options) { - let $btn; - if (options.url) $btn = CE("a", { class: "bx-button" }), $btn.href = options.url, $btn.target = "_blank"; - else $btn = CE("button", { class: "bx-button", type: "button" }); - let style = options.style || 0; - if (style) { - let index; - for (index of ButtonStyleIndices) - style & index && $btn.classList.add(ButtonStyleClass[index]); + static getTouchControlCustomList() { + let key = "BetterXcloud.GhPages.CustomTouchLayouts"; + return NATIVE_FETCH(GhPagesUtils.getUrl("touch-layouts/ids.json")).then((response) => response.json()).then((json) => { + if (Array.isArray(json)) window.localStorage.setItem(key, JSON.stringify(json)); + }), JSON.parse(window.localStorage.getItem(key) || "[]"); } - options.classes && $btn.classList.add(...options.classes), options.icon && $btn.appendChild(createSvgIcon(options.icon)), options.label && $btn.appendChild(CE("span", {}, options.label)), options.title && $btn.setAttribute("title", options.title), options.disabled && ($btn.disabled = !0), options.onClick && $btn.addEventListener("click", options.onClick), $btn.tabIndex = typeof options.tabIndex === "number" ? options.tabIndex : 0; - for (let key in options.attributes) - if (!$btn.hasOwnProperty(key)) $btn.setAttribute(key, options.attributes[key]); - return $btn; -} -function isElementVisible($elm) { - let rect = $elm.getBoundingClientRect(); - return (rect.x >= 0 || rect.y >= 0) && !!rect.width && !!rect.height; -} -var CTN = document.createTextNode.bind(document); -window.BX_CE = createElement; -function removeChildElements($parent) { - while ($parent.firstElementChild) - $parent.firstElementChild.remove(); -} -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)); - return (size / Math.pow(1024, i)).toFixed(2) + " " + FILE_SIZE_UNITS[i]; -} -function secondsToHm(seconds) { - let h = Math.floor(seconds / 3600), m = Math.floor(seconds % 3600 / 60) + 1; - if (m === 60) h += 1, m = 0; - let output = []; - return h > 0 && output.push(`${h}h`), m > 0 && output.push(`${m}m`), output.join(" "); } var SUPPORTED_LANGUAGES = { "en-US": "English (US)", @@ -289,6 +245,9 @@ var SUPPORTED_LANGUAGES = { "clarity-boost": "Clarity boost", "clarity-boost-warning": "These settings don't work when the Clarity Boost mode is ON", clear: "Clear", + "clear-data": "Clear data", + "clear-data-confirm": "Do you want to clear all Better xCloud settings and data?", + "clear-data-success": "Data cleared! Refresh the page to apply the changes.", clock: "Clock", close: "Close", "close-app": "Close app", @@ -309,6 +268,7 @@ var SUPPORTED_LANGUAGES = { "controller-friendly-ui": "Controller-friendly UI", "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", @@ -323,6 +283,7 @@ var SUPPORTED_LANGUAGES = { "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-home-context-menu": "Disable context menu in Home page", "disable-post-stream-feedback-dialog": "Disable post-stream feedback dialog", "disable-social-features": "Disable social features", @@ -344,6 +305,7 @@ var SUPPORTED_LANGUAGES = { experimental: "Experimental", export: "Export", fast: "Fast", + "force-native-mkb-games": "Force native Mouse & Keyboard for these games", "fortnite-allow-stw-mode": 'Allows playing "Save the World" mode on mobile', "fortnite-force-console-version": "Fortnite: force console version", "game-bar": "Game Bar", @@ -369,7 +331,9 @@ var SUPPORTED_LANGUAGES = { "install-android": "Better xCloud app for Android", japan: "Japan", jitter: "Jitter", + "keyboard-key": "Keyboard key", "keyboard-shortcuts": "Keyboard shortcuts", + "keyboard-shortcuts-in-game": "In-game keyboard shortcuts", korea: "Korea", language: "Language", large: "Large", @@ -379,6 +343,7 @@ var SUPPORTED_LANGUAGES = { "loading-screen": "Loading screen", "local-co-op": "Local co-op", "lowest-quality": "Lowest quality", + manage: "Manage", "map-mouse-to": "Map mouse to", "max-fps": "Max FPS", "may-not-work-properly": "May not work properly!", @@ -386,17 +351,18 @@ var SUPPORTED_LANGUAGES = { microphone: "Microphone", "mkb-adjust-ingame-settings": "You may also need to adjust the in-game sensitivity & deadzone settings", "mkb-click-to-activate": "Click to activate", - "mkb-disclaimer": "Using this feature when playing online could be viewed as cheating", + "mkb-disclaimer": "This could be viewed as cheating when playing online", + "modifiers-note": "To use more than one key, include Ctrl, Alt or Shift in your shortcut. Command key is not allowed.", "mouse-and-keyboard": "Mouse & Keyboard", + "mouse-click": "Mouse click", "mouse-wheel": "Mouse wheel", - "msfs2020-force-native-mkb": "MSFS2020: force native M&KB support", muted: "Muted", name: "Name", "native-mkb": "Native Mouse & Keyboard", new: "New", "new-version-available": [ e => `Version ${e.version} available`, - , + e => `Versió ${e.version} disponible`, , e => `Version ${e.version} verfügbar`, e => `Versi ${e.version} tersedia`, @@ -416,8 +382,10 @@ var SUPPORTED_LANGUAGES = { e => `已可更新為 ${e.version} 版` ], "no-consoles-found": "No consoles found", + "no-controllers-connected": "No controllers connected", normal: "Normal", off: "Off", + official: "Official", on: "On", "only-supports-some-games": "Only supports some games", opacity: "Opacity", @@ -496,6 +464,7 @@ 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", @@ -525,6 +494,7 @@ var SUPPORTED_LANGUAGES = { small: "Small", "smart-tv": "Smart TV", sound: "Sound", + standard: "Standard", standby: "Standby", "stat-bitrate": "Bitrate", "stat-decode-time": "Decode time", @@ -589,6 +559,8 @@ var SUPPORTED_LANGUAGES = { unknown: "Unknown", unlimited: "Unlimited", unmuted: "Unmuted", + unofficial: "Unofficial", + "unofficial-game-list": "Unofficial game list", "unsharp-masking": "Unsharp masking", upload: "Upload", uploaded: "Uploaded", @@ -601,6 +573,7 @@ var SUPPORTED_LANGUAGES = { "vibration-status": "Vibration", video: "Video", "virtual-controller": "Virtual controller", + "virtual-controller-slot": "Virtual controller slot", "visual-quality": "Visual quality", "visual-quality-high": "High", "visual-quality-low": "Low", @@ -608,59 +581,60 @@ var SUPPORTED_LANGUAGES = { volume: "Volume", "wait-time-countdown": "Countdown", "wait-time-estimated": "Estimated finish time", + "waiting-for-input": "Waiting for input...", wallpaper: "Wallpaper", webgl2: "WebGL2" }; class Translations { - static #EN_US = "en-US"; - static #KEY_LOCALE = "better_xcloud_locale"; - static #KEY_TRANSLATIONS = "better_xcloud_translations"; - static #enUsIndex = -1; - static #selectedLocaleIndex = -1; - static #selectedLocale = "en-US"; - static #supportedLocales = Object.keys(SUPPORTED_LANGUAGES); - static #foreignTranslations = {}; + static EN_US = "en-US"; + static KEY_LOCALE = "BetterXcloud.Locale"; + static KEY_TRANSLATIONS = "BetterXcloud.Locale.Translations"; + static selectedLocaleIndex = -1; + static selectedLocale = "en-US"; + static supportedLocales = Object.keys(SUPPORTED_LANGUAGES); + static foreignTranslations = {}; + static enUsIndex = Translations.supportedLocales.indexOf(Translations.EN_US); static async init() { - Translations.#enUsIndex = Translations.#supportedLocales.indexOf(Translations.#EN_US), Translations.refreshLocale(), await Translations.#loadTranslations(); + Translations.refreshLocale(), await Translations.loadTranslations(); } static refreshLocale(newLocale) { let locale; - if (newLocale) localStorage.setItem(Translations.#KEY_LOCALE, newLocale), locale = newLocale; - else locale = localStorage.getItem(Translations.#KEY_LOCALE); - let supportedLocales = Translations.#supportedLocales; + if (newLocale) localStorage.setItem(Translations.KEY_LOCALE, newLocale), locale = newLocale; + else locale = localStorage.getItem(Translations.KEY_LOCALE); + let supportedLocales = Translations.supportedLocales; if (!locale) { - if (locale = window.navigator.language || Translations.#EN_US, supportedLocales.indexOf(locale) === -1) locale = Translations.#EN_US; - localStorage.setItem(Translations.#KEY_LOCALE, locale); + if (locale = window.navigator.language || Translations.EN_US, supportedLocales.indexOf(locale) === -1) locale = Translations.EN_US; + localStorage.setItem(Translations.KEY_LOCALE, locale); } - Translations.#selectedLocale = locale, Translations.#selectedLocaleIndex = supportedLocales.indexOf(locale); + Translations.selectedLocale = locale, Translations.selectedLocaleIndex = supportedLocales.indexOf(locale); } static get(key, values) { let text = null; - if (Translations.#foreignTranslations && Translations.#selectedLocale !== Translations.#EN_US) text = Translations.#foreignTranslations[key]; + if (Translations.foreignTranslations && Translations.selectedLocale !== Translations.EN_US) text = Translations.foreignTranslations[key]; if (!text) text = Texts[key] || alert(`Missing translation key: ${key}`); let translation; - if (Array.isArray(text)) return translation = text[Translations.#selectedLocaleIndex] || text[Translations.#enUsIndex], translation(values); + if (Array.isArray(text)) return translation = text[Translations.selectedLocaleIndex] || text[Translations.enUsIndex], translation(values); return translation = text, translation; } - static async#loadTranslations() { - if (Translations.#selectedLocale === Translations.#EN_US) return; + static async loadTranslations() { + if (Translations.selectedLocale === Translations.EN_US) return; try { - Translations.#foreignTranslations = JSON.parse(window.localStorage.getItem(Translations.#KEY_TRANSLATIONS)); + Translations.foreignTranslations = JSON.parse(window.localStorage.getItem(Translations.KEY_TRANSLATIONS)); } catch (e) {} - if (!Translations.#foreignTranslations) await this.downloadTranslations(Translations.#selectedLocale); + if (!Translations.foreignTranslations) await this.downloadTranslations(Translations.selectedLocale); } static async updateTranslations(async = !1) { - if (Translations.#selectedLocale === Translations.#EN_US) { - localStorage.removeItem(Translations.#KEY_TRANSLATIONS); + if (Translations.selectedLocale === Translations.EN_US) { + localStorage.removeItem(Translations.KEY_TRANSLATIONS); return; } - if (async) Translations.downloadTranslationsAsync(Translations.#selectedLocale); - else await Translations.downloadTranslations(Translations.#selectedLocale); + if (async) Translations.downloadTranslationsAsync(Translations.selectedLocale); + else await Translations.downloadTranslations(Translations.selectedLocale); } static async downloadTranslations(locale) { try { - let translations = await (await NATIVE_FETCH(`https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/translations/${locale}.json`)).json(); - if (localStorage.getItem(Translations.#KEY_LOCALE) === locale) window.localStorage.setItem(Translations.#KEY_TRANSLATIONS, JSON.stringify(translations)), Translations.#foreignTranslations = translations; + let translations = await (await NATIVE_FETCH(GhPagesUtils.getUrl(`translations/${locale}.json`))).json(); + if (localStorage.getItem(Translations.KEY_LOCALE) === locale) window.localStorage.setItem(Translations.KEY_TRANSLATIONS, JSON.stringify(translations)), Translations.foreignTranslations = translations; return !0; } catch (e) { debugger; @@ -668,16 +642,185 @@ class Translations { return !1; } static downloadTranslationsAsync(locale) { - NATIVE_FETCH(`https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/translations/${locale}.json`).then((resp) => resp.json()).then((translations) => { - window.localStorage.setItem(Translations.#KEY_TRANSLATIONS, JSON.stringify(translations)), Translations.#foreignTranslations = translations; + NATIVE_FETCH(GhPagesUtils.getUrl(`translations/${locale}.json`)).then((resp) => resp.json()).then((translations) => { + window.localStorage.setItem(Translations.KEY_TRANSLATIONS, JSON.stringify(translations)), Translations.foreignTranslations = translations; }); } static switchLocale(locale) { - localStorage.setItem(Translations.#KEY_LOCALE, locale); + localStorage.setItem(Translations.KEY_LOCALE, locale); } } var t = Translations.get; Translations.init(); +class NavigationUtils { + static setNearby($elm, nearby) { + $elm.nearby = $elm.nearby || {}; + let key; + for (key in nearby) + $elm.nearby[key] = nearby[key]; + } +} +var setNearby = NavigationUtils.setNearby; +var ButtonStyleClass = { + 1: "bx-primary", + 2: "bx-warning", + 4: "bx-danger", + 8: "bx-ghost", + 16: "bx-frosted", + 32: "bx-drop-shadow", + 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" +}; +function createElement(elmName, props = {}, ..._) { + let $elm, hasNs = "xmlns" in props; + if (hasNs) $elm = document.createElementNS(props.xmlns, elmName), delete props.xmlns; + else $elm = document.createElement(elmName); + if (props._nearby) setNearby($elm, props._nearby), delete props._nearby; + if (props._on) { + for (let name in props._on) + $elm.addEventListener(name, props._on[name]); + delete props._on; + } + if (props._dataset) { + for (let name in props._dataset) + $elm.dataset[name] = props._dataset[name]; + delete props._dataset; + } + for (let key in props) { + if ($elm.hasOwnProperty(key)) continue; + let value = props[key]; + if (hasNs) $elm.setAttributeNS(null, key, value); + else $elm.setAttribute(key, value); + } + for (let i = 2, size = arguments.length;i < size; i++) { + let arg = arguments[i]; + if (arg !== null && arg !== !1 && typeof arg !== "undefined") $elm.append(arg); + } + return $elm; +} +var domParser = new DOMParser; +function createSvgIcon(icon) { + return domParser.parseFromString(icon.toString(), "image/svg+xml").documentElement; +} +var ButtonStyleIndices = Object.keys(ButtonStyleClass).map((i) => parseInt(i)); +function createButton(options) { + let $btn; + if (options.url) $btn = CE("a", { + class: "bx-button", + href: options.url, + target: "_blank" + }); + else $btn = CE("button", { + class: "bx-button", + type: "button" + }), options.disabled && ($btn.disabled = !0); + let style = options.style || 0; + if (style) { + let index; + for (index of ButtonStyleIndices) + style & index && $btn.classList.add(ButtonStyleClass[index]); + } + if (options.classes && $btn.classList.add(...options.classes), options.icon && $btn.appendChild(createSvgIcon(options.icon)), options.label && $btn.appendChild(CE("span", {}, options.label)), options.title && $btn.setAttribute("title", options.title), options.onClick && $btn.addEventListener("click", options.onClick), $btn.tabIndex = typeof options.tabIndex === "number" ? options.tabIndex : 0, options.secondaryText) $btn.classList.add("bx-button-multi-lines"), $btn.appendChild(CE("span", {}, options.secondaryText)); + for (let key in options.attributes) + if (!$btn.hasOwnProperty(key)) $btn.setAttribute(key, options.attributes[key]); + return $btn; +} +function createSettingRow(label, $control, options = {}) { + let $label, $row = CE("label", { class: "bx-settings-row" }, $label = CE("span", { class: "bx-settings-label" }, label, options.$note), $control), $link = $label.querySelector("a"); + if ($link) $link.classList.add("bx-focusable"), setNearby($label, { + focus: $link + }); + if (setNearby($row, { + orientation: options.multiLines ? "vertical" : "horizontal" + }), options.multiLines) + $row.dataset.multiLines = "true"; + if ($control instanceof HTMLElement && $control.id) $row.htmlFor = $control.id; + return $row; +} +function isElementVisible($elm) { + let rect = $elm.getBoundingClientRect(); + return (rect.x >= 0 || rect.y >= 0) && !!rect.width && !!rect.height; +} +function removeChildElements($parent) { + if ($parent instanceof HTMLDivElement && $parent.classList.contains("bx-select")) $parent = $parent.querySelector("select"); + while ($parent.firstElementChild) + $parent.firstElementChild.remove(); +} +function clearDataSet($elm) { + Object.keys($elm.dataset).forEach((key) => { + delete $elm.dataset[key]; + }); +} +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)); + return (size / Math.pow(1024, i)).toFixed(2) + " " + FILE_SIZE_UNITS[i]; +} +function secondsToHm(seconds) { + let h = Math.floor(seconds / 3600), m = Math.floor(seconds % 3600 / 60) + 1; + if (m === 60) h += 1, m = 0; + let output = []; + return h > 0 && output.push(`${h}h`), m > 0 && output.push(`${m}m`), output.join(" "); +} +function escapeCssSelector(name) { + return name.replaceAll(".", "-"); +} +var CE = createElement; +window.BX_CE = createElement; +class Toast { + static instance; + static getInstance = () => Toast.instance ?? (Toast.instance = new Toast); + LOG_TAG = "Toast"; + $wrapper; + $msg; + $status; + stack = []; + isShowing = !1; + timeoutId; + DURATION = 3000; + constructor() { + BxLogger.info(this.LOG_TAG, "constructor()"), this.$wrapper = CE("div", { class: "bx-toast bx-offscreen" }, this.$msg = CE("span", { class: "bx-toast-msg" }), this.$status = CE("span", { class: "bx-toast-status" })), this.$wrapper.addEventListener("transitionend", (e) => { + let classList = this.$wrapper.classList; + if (classList.contains("bx-hide")) classList.remove("bx-offscreen", "bx-hide"), classList.add("bx-offscreen"), this.showNext(); + }), document.documentElement.appendChild(this.$wrapper); + } + show(msg, status, options = {}) { + options = options || {}; + let args = Array.from(arguments); + if (options.instant) this.stack = [args], this.showNext(); + else this.stack.push(args), !this.isShowing && this.showNext(); + } + showNext() { + if (!this.stack.length) { + this.isShowing = !1; + return; + } + this.isShowing = !0, this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = window.setTimeout(this.hide, this.DURATION); + let [msg, status, options] = this.stack.shift(); + if (options && options.html) this.$msg.innerHTML = msg; + else this.$msg.textContent = msg; + if (status) this.$status.classList.remove("bx-gone"), this.$status.textContent = status; + else this.$status.classList.add("bx-gone"); + let classList = this.$wrapper.classList; + classList.remove("bx-offscreen", "bx-hide"), classList.add("bx-show"); + } + hide = () => { + this.timeoutId = null; + let classList = this.$wrapper.classList; + classList.remove("bx-show"), classList.add("bx-hide"); + }; + static show(msg, status, options = {}) { + Toast.getInstance().show(msg, status, options); + } + static showNext() { + Toast.getInstance().showNext(); + } +} var BypassServers = { br: t("brazil"), jp: t("japan"), @@ -691,175 +834,6 @@ var BypassServers = { pl: "45.134.212.66", us: "143.244.47.65" }; -class SettingElement { - static #renderOptions(key, setting, currentValue, onChange) { - let $control = CE("select", { - tabindex: 0 - }), $parent; - if (setting.optionsGroup) $parent = CE("optgroup", { - label: setting.optionsGroup - }), $control.appendChild($parent); - else $parent = $control; - for (let value in setting.options) { - let label = setting.options[value], $option = CE("option", { value }, label); - $parent.appendChild($option); - } - return $control.value = currentValue, onChange && $control.addEventListener("input", (e) => { - let target = e.target, value = setting.type && setting.type === "number" ? parseInt(target.value) : target.value; - !e.ignoreOnChange && onChange(e, value); - }), $control.setValue = (value) => { - $control.value = value; - }, $control; - } - static #renderMultipleOptions(key, setting, currentValue, onChange, params = {}) { - let $control = CE("select", { - multiple: !0, - tabindex: 0 - }); - if (params && params.size) $control.setAttribute("size", params.size.toString()); - for (let value in setting.multipleOptions) { - let label = setting.multipleOptions[value], $option = CE("option", { value }, label); - $option.selected = currentValue.indexOf(value) > -1, $option.addEventListener("mousedown", function(e) { - e.preventDefault(); - let target = e.target; - target.selected = !target.selected; - let $parent = target.parentElement; - $parent.focus(), BxEvent.dispatch($parent, "input"); - }), $control.appendChild($option); - } - return $control.addEventListener("mousedown", function(e) { - let self = this, orgScrollTop = self.scrollTop; - window.setTimeout(() => self.scrollTop = orgScrollTop, 0); - }), $control.addEventListener("mousemove", (e) => e.preventDefault()), onChange && $control.addEventListener("input", (e) => { - let target = e.target, values = Array.from(target.selectedOptions).map((i) => i.value); - !e.ignoreOnChange && onChange(e, values); - }), $control; - } - static #renderNumber(key, setting, currentValue, onChange) { - let $control = CE("input", { - tabindex: 0, - type: "number", - min: setting.min, - max: setting.max - }); - return $control.value = currentValue, onChange && $control.addEventListener("input", (e) => { - let target = e.target, value = Math.max(setting.min, Math.min(setting.max, parseInt(target.value))); - target.value = value.toString(), !e.ignoreOnChange && onChange(e, value); - }), $control; - } - static #renderCheckbox(key, setting, currentValue, onChange) { - let $control = CE("input", { type: "checkbox", tabindex: 0 }); - return $control.checked = currentValue, onChange && $control.addEventListener("input", (e) => { - !e.ignoreOnChange && onChange(e, e.target.checked); - }), $control.setValue = (value) => { - $control.checked = !!value; - }, $control; - } - static #renderNumberStepper(key, setting, value, onChange, options = {}) { - options = options || {}, options.suffix = options.suffix || "", options.disabled = !!options.disabled, options.hideSlider = !!options.hideSlider; - let $text, $btnDec, $btnInc, $range = null, controlValue = value, MIN = options.reverse ? -setting.max : setting.min, MAX = options.reverse ? -setting.min : setting.max, STEPS = Math.max(setting.steps || 1, 1), intervalId, isHolding = !1, clearIntervalId = () => { - intervalId && clearInterval(intervalId), intervalId = null; - }, renderTextValue = (value2) => { - value2 = parseInt(value2); - let textContent = null; - if (options.customTextValue) textContent = options.customTextValue(value2); - if (textContent === null) textContent = value2.toString() + options.suffix; - return textContent; - }, updateButtonsVisibility = () => { - if ($btnDec.classList.toggle("bx-inactive", controlValue === MIN), $btnInc.classList.toggle("bx-inactive", controlValue === MAX), controlValue === MIN || controlValue === MAX) clearIntervalId(); - }, $wrapper = CE("div", { class: "bx-number-stepper", id: `bx_setting_${key}` }, $btnDec = CE("button", { - "data-type": "dec", - type: "button", - class: options.hideSlider ? "bx-focusable" : "", - tabindex: options.hideSlider ? 0 : -1 - }, "-"), $text = CE("span", {}, renderTextValue(value)), $btnInc = CE("button", { - "data-type": "inc", - type: "button", - class: options.hideSlider ? "bx-focusable" : "", - tabindex: options.hideSlider ? 0 : -1 - }, "+")); - if (options.disabled) return $btnInc.disabled = !0, $btnInc.classList.add("bx-inactive"), $btnDec.disabled = !0, $btnDec.classList.add("bx-inactive"), $wrapper.disabled = !0, $wrapper; - if ($range = CE("input", { - id: `bx_inp_setting_${key}`, - type: "range", - min: MIN, - max: MAX, - value: options.reverse ? -value : value, - step: STEPS, - tabindex: 0 - }), options.hideSlider && $range.classList.add("bx-gone"), $range.addEventListener("input", (e) => { - if (value = parseInt(e.target.value), options.reverse) value *= -1; - if (controlValue === value) return; - controlValue = options.reverse ? -value : value, updateButtonsVisibility(), $text.textContent = renderTextValue(value), !e.ignoreOnChange && onChange && onChange(e, value); - }), $wrapper.addEventListener("input", (e) => { - BxEvent.dispatch($range, "input"); - }), $wrapper.appendChild($range), options.ticks || options.exactTicks) { - let markersId = `markers-${key}`, $markers = CE("datalist", { id: markersId }); - if ($range.setAttribute("list", markersId), options.exactTicks) { - let start = Math.max(Math.floor(setting.min / options.exactTicks), 1) * options.exactTicks; - if (start === setting.min) start += options.exactTicks; - for (let i = start;i < setting.max; i += options.exactTicks) - $markers.appendChild(CE("option", { - value: options.reverse ? -i : i - })); - } else for (let i = MIN + options.ticks;i < MAX; i += options.ticks) - $markers.appendChild(CE("option", { value: i })); - $wrapper.appendChild($markers); - } - updateButtonsVisibility(); - let buttonPressed = (e, $btn) => { - let value2 = parseInt(controlValue); - if ($btn.dataset.type === "dec") value2 = Math.max(MIN, value2 - STEPS); - else value2 = Math.min(MAX, value2 + STEPS); - controlValue = value2, updateButtonsVisibility(), $text.textContent = renderTextValue(value2), $range && ($range.value = value2.toString()), onChange && onChange(e, value2); - }, onClick = (e) => { - if (e.preventDefault(), isHolding) return; - let $btn = e.target.closest("button"); - $btn && buttonPressed(e, $btn), clearIntervalId(), isHolding = !1; - }, onPointerDown = (e) => { - clearIntervalId(); - let $btn = e.target.closest("button"); - if (!$btn) return; - isHolding = !0, e.preventDefault(), intervalId = window.setInterval((e2) => { - buttonPressed(e2, $btn); - }, 200), window.addEventListener("pointerup", onPointerUp, { once: !0 }), window.addEventListener("pointercancel", onPointerUp, { once: !0 }); - }, onPointerUp = (e) => { - clearIntervalId(), isHolding = !1; - }, onContextMenu = (e) => e.preventDefault(); - return $wrapper.setValue = (value2) => { - $text.textContent = renderTextValue(value2), $range.value = options.reverse ? -value2 : value2; - }, $wrapper.addEventListener("click", onClick), $wrapper.addEventListener("pointerdown", onPointerDown), $wrapper.addEventListener("contextmenu", onContextMenu), setNearby($wrapper, { - focus: options.hideSlider ? $btnInc : $range - }), $wrapper; - } - static #METHOD_MAP = { - options: SettingElement.#renderOptions, - "multiple-options": SettingElement.#renderMultipleOptions, - number: SettingElement.#renderNumber, - "number-stepper": SettingElement.#renderNumberStepper, - checkbox: SettingElement.#renderCheckbox - }; - static render(type, key, setting, currentValue, onChange, options) { - let method = SettingElement.#METHOD_MAP[type], $control = method(...Array.from(arguments).slice(1)); - if (type !== "number-stepper") $control.id = `bx_setting_${key}`; - if (type === "options" || type === "multiple-options") $control.name = $control.id; - return $control; - } - static fromPref(key, storage, onChange, overrideParams = {}) { - let definition = storage.getDefinition(key), currentValue = storage.getSetting(key), type; - if ("type" in definition) type = definition.type; - else if ("options" in definition) type = "options"; - else if ("multipleOptions" in definition) type = "multiple-options"; - else if (typeof definition.default === "number") type = "number"; - else type = "checkbox"; - let params = {}; - if ("params" in definition) params = Object.assign(overrideParams, definition.params || {}); - if (params.disabled) currentValue = definition.default; - return SettingElement.render(type, key, definition, currentValue, (e, value) => { - storage.setSetting(key, value), onChange && onChange(e, value); - }, params); - } -} class BaseSettingsStore { storage; storageKey; @@ -878,6 +852,8 @@ class BaseSettingsStore { get settings() { if (this._settings) return this._settings; let settings = JSON.parse(this.storage.getItem(this.storageKey) || "{}"); + for (let key in settings) + settings[key] = this.validateValue("get", key, settings[key]); return this._settings = settings, settings; } getDefinition(key) { @@ -888,18 +864,15 @@ class BaseSettingsStore { return this.definitions[key]; } getSetting(key, checkUnsupported = !0) { - if (typeof key === "undefined") { - debugger; - return; - } let definition = this.definitions[key]; if (definition.requiredVariants && !definition.requiredVariants.includes(SCRIPT_VARIANT)) return definition.default; - if (checkUnsupported && definition.unsupported) return definition.default; - if (!(key in this.settings)) this.settings[key] = this.validateValue(key, null); + if (checkUnsupported && definition.unsupported) if ("unsupportedValue" in definition) return definition.unsupportedValue; + else return definition.default; + if (!(key in this.settings)) this.settings[key] = this.validateValue("get", key, null); return this.settings[key]; } setSetting(key, value, emitEvent = !1) { - return value = this.validateValue(key, value), this.settings[key] = value, this.saveSettings(), emitEvent && BxEvent.dispatch(window, BxEvent.SETTINGS_CHANGED, { + return value = this.validateValue("set", key, value), this.settings[key] = this.validateValue("get", key, value), this.saveSettings(), emitEvent && BxEvent.dispatch(window, BxEvent.SETTINGS_CHANGED, { storageKey: this.storageKey, settingKey: key, settingValue: value @@ -908,14 +881,16 @@ class BaseSettingsStore { saveSettings() { this.storage.setItem(this.storageKey, JSON.stringify(this.settings)); } - validateValue(key, value) { + validateValue(action, key, value) { let def = this.definitions[key]; if (!def) return value; if (typeof value === "undefined" || value === null) value = def.default; + if (def.transformValue && action === "get") value = def.transformValue.get.call(def, value); if ("min" in def) value = Math.max(def.min, value); if ("max" in def) value = Math.min(def.max, value); - if ("options" in def && !(value in def.options)) value = def.default; - else if ("multipleOptions" in def) { + if ("options" in def) { + if (!(value in def.options)) value = def.default; + } else if ("multipleOptions" in def) { if (value.length) { let validOptions = Object.keys(def.multipleOptions); value.forEach((item, idx) => { @@ -924,6 +899,7 @@ class BaseSettingsStore { } if (!value.length) value = def.default; } + if (def.transformValue && action === "set") value = def.transformValue.set.call(def, value); return value; } getLabel(key) { @@ -931,10 +907,11 @@ class BaseSettingsStore { } getValueText(key, value) { let definition = this.definitions[key]; - if (definition.type === "number-stepper") { + if ("min" in definition) { let params = definition.params; if (params.customTextValue) { - let text = params.customTextValue(value); + if (definition.transformValue) value = definition.transformValue.get.call(definition, value); + let text = params.customTextValue(value, definition.min, definition.max); if (text) return text; } return value.toString(); @@ -945,6 +922,1044 @@ class BaseSettingsStore { return value.toString(); } } +class LocalDb { + static instance; + static getInstance = () => LocalDb.instance ?? (LocalDb.instance = new LocalDb); + static DB_NAME = "BetterXcloud"; + static DB_VERSION = 3; + static TABLE_VIRTUAL_CONTROLLERS = "virtual_controllers"; + static TABLE_CONTROLLER_SHORTCUTS = "controller_shortcuts"; + static TABLE_CONTROLLER_SETTINGS = "controller_settings"; + static TABLE_KEYBOARD_SHORTCUTS = "keyboard_shortcuts"; + db; + open() { + return new Promise((resolve, reject) => { + if (this.db) { + resolve(this.db); + return; + } + let request = window.indexedDB.open(LocalDb.DB_NAME, LocalDb.DB_VERSION); + request.onupgradeneeded = (e) => { + let db = e.target.result; + if (db.objectStoreNames.contains("undefined")) db.deleteObjectStore("undefined"); + if (!db.objectStoreNames.contains(LocalDb.TABLE_VIRTUAL_CONTROLLERS)) db.createObjectStore(LocalDb.TABLE_VIRTUAL_CONTROLLERS, { + keyPath: "id", + autoIncrement: !0 + }); + if (!db.objectStoreNames.contains(LocalDb.TABLE_CONTROLLER_SHORTCUTS)) db.createObjectStore(LocalDb.TABLE_CONTROLLER_SHORTCUTS, { + keyPath: "id", + autoIncrement: !0 + }); + if (!db.objectStoreNames.contains(LocalDb.TABLE_CONTROLLER_SETTINGS)) db.createObjectStore(LocalDb.TABLE_CONTROLLER_SETTINGS, { + keyPath: "id" + }); + if (!db.objectStoreNames.contains(LocalDb.TABLE_KEYBOARD_SHORTCUTS)) db.createObjectStore(LocalDb.TABLE_KEYBOARD_SHORTCUTS, { + keyPath: "id", + autoIncrement: !0 + }); + }, request.onerror = (e) => { + console.log(e), alert(e.target.error.message), reject && reject(); + }, request.onsuccess = (e) => { + this.db = e.target.result, resolve(this.db); + }; + }); + } +} +class BaseLocalTable { + tableName; + constructor(tableName) { + this.tableName = tableName; + } + async prepareTable(type = "readonly") { + return (await LocalDb.getInstance().open()).transaction(this.tableName, type).objectStore(this.tableName); + } + call(method) { + return new Promise((resolve) => { + let request = method.call(null, ...Array.from(arguments).slice(1)); + request.onsuccess = (e) => { + resolve(e.target.result); + }; + }); + } + async count() { + let table = await this.prepareTable(); + return this.call(table.count.bind(table)); + } + async add(data) { + let table = await this.prepareTable("readwrite"); + return this.call(table.add.bind(table), ...arguments); + } + async put(data) { + let table = await this.prepareTable("readwrite"); + return this.call(table.put.bind(table), ...arguments); + } + async delete(id) { + let table = await this.prepareTable("readwrite"); + return this.call(table.delete.bind(table), ...arguments); + } + async get(id) { + let table = await this.prepareTable(); + return this.call(table.get.bind(table), ...arguments); + } + async getAll() { + let table = await this.prepareTable(), all = await this.call(table.getAll.bind(table), ...arguments), results = {}; + return all.forEach((item) => { + results[item.id] = item; + }), results; + } +} +class BasePresetsTable extends BaseLocalTable { + async newPreset(name, data) { + let newRecord = { name, data }; + return await this.add(newRecord); + } + async updatePreset(preset) { + return await this.put(preset); + } + async deletePreset(id) { + return this.delete(id); + } + async getPreset(id) { + if (id === 0) return null; + if (id < 0) return this.DEFAULT_PRESETS[id]; + let preset = await this.get(id); + if (!preset) preset = this.DEFAULT_PRESETS[this.DEFAULT_PRESET_ID]; + return preset; + } + async getPresets() { + let all = deepClone(this.DEFAULT_PRESETS), presets = { + default: Object.keys(this.DEFAULT_PRESETS).map((key) => parseInt(key)), + custom: [], + data: {} + }; + if (await this.count() > 0) { + let items = await this.getAll(), id; + for (id in items) { + let item = items[id]; + presets.custom.push(item.id), all[item.id] = item; + } + } + return presets.data = all, presets; + } + async getPresetsData() { + let presetsData = {}; + for (let id in this.DEFAULT_PRESETS) { + let preset = this.DEFAULT_PRESETS[id]; + presetsData[id] = deepClone(preset.data); + } + if (await this.count() > 0) { + let items = await this.getAll(), id; + for (id in items) { + let item = items[id]; + presetsData[item.id] = item.data; + } + } + return presetsData; + } +} +class MkbMappingPresetsTable extends BasePresetsTable { + static instance; + static getInstance = () => MkbMappingPresetsTable.instance ?? (MkbMappingPresetsTable.instance = new MkbMappingPresetsTable); + LOG_TAG = "MkbMappingPresetsTable"; + TABLE_PRESETS = LocalDb.TABLE_VIRTUAL_CONTROLLERS; + DEFAULT_PRESETS = { + [-1]: { + id: -1, + name: t("standard"), + data: { + mapping: { + 16: ["Backquote"], + 12: ["ArrowUp", "Digit1"], + 13: ["ArrowDown", "Digit2"], + 14: ["ArrowLeft", "Digit3"], + 15: ["ArrowRight", "Digit4"], + 100: ["KeyW"], + 101: ["KeyS"], + 102: ["KeyA"], + 103: ["KeyD"], + 200: ["KeyU"], + 201: ["KeyJ"], + 202: ["KeyH"], + 203: ["KeyK"], + 0: ["Space", "KeyE"], + 2: ["KeyR"], + 1: ["KeyC", "Backspace"], + 3: ["KeyE"], + 9: ["Enter"], + 8: ["Tab"], + 4: ["KeyQ"], + 5: ["KeyF"], + 7: ["Mouse0"], + 6: ["Mouse2"], + 10: ["KeyX"], + 11: ["KeyZ"] + }, + mouse: { + mapTo: 2, + sensitivityX: 100, + sensitivityY: 100, + deadzoneCounterweight: 20 + } + } + }, + [-2]: { + id: -2, + name: "Shooter", + data: { + mapping: { + 16: ["Backquote"], + 12: ["ArrowUp"], + 13: ["ArrowDown"], + 14: ["ArrowLeft"], + 15: ["ArrowRight"], + 100: ["KeyW"], + 101: ["KeyS"], + 102: ["KeyA"], + 103: ["KeyD"], + 200: ["KeyI"], + 201: ["KeyK"], + 202: ["KeyJ"], + 203: ["KeyL"], + 0: ["Space", "KeyE"], + 2: ["KeyR"], + 1: ["ControlLeft", "Backspace"], + 3: ["KeyV"], + 9: ["Enter"], + 8: ["Tab"], + 4: ["KeyC", "KeyG"], + 5: ["KeyQ"], + 7: ["Mouse0"], + 6: ["Mouse2"], + 10: ["ShiftLeft"], + 11: ["KeyF"] + }, + mouse: { + mapTo: 2, + sensitivityX: 100, + sensitivityY: 100, + deadzoneCounterweight: 20 + } + } + } + }; + DEFAULT_PRESET_ID = -1; + constructor() { + super(LocalDb.TABLE_VIRTUAL_CONTROLLERS); + BxLogger.info(this.LOG_TAG, "constructor()"); + } +} +class KeyboardShortcutsTable extends BasePresetsTable { + static instance; + static getInstance = () => KeyboardShortcutsTable.instance ?? (KeyboardShortcutsTable.instance = new KeyboardShortcutsTable); + LOG_TAG = "KeyboardShortcutsTable"; + TABLE_PRESETS = LocalDb.TABLE_KEYBOARD_SHORTCUTS; + DEFAULT_PRESETS = { + [-1]: { + id: -1, + name: t("standard"), + data: { + mapping: { + "mkb.toggle": { + code: "F8" + }, + "stream.screenshot.capture": { + code: "Slash" + } + } + } + } + }; + DEFAULT_PRESET_ID = -1; + constructor() { + super(LocalDb.TABLE_KEYBOARD_SHORTCUTS); + BxLogger.info(this.LOG_TAG, "constructor()"); + } +} +function getSupportedCodecProfiles() { + let options = { + default: t("default") + }; + if (!("getCapabilities" in RTCRtpReceiver)) return options; + let hasLowCodec = !1, hasNormalCodec = !1, hasHighCodec = !1, codecs = RTCRtpReceiver.getCapabilities("video").codecs; + for (let codec of codecs) { + if (codec.mimeType.toLowerCase() !== "video/h264" || !codec.sdpFmtpLine) continue; + let fmtp = codec.sdpFmtpLine.toLowerCase(); + if (fmtp.includes("profile-level-id=4d")) hasHighCodec = !0; + else if (fmtp.includes("profile-level-id=42e")) hasNormalCodec = !0; + else if (fmtp.includes("profile-level-id=420")) hasLowCodec = !0; + } + if (hasLowCodec) if (!hasNormalCodec && !hasHighCodec) options["default"] = `${t("visual-quality-low")} (${t("default")})`; + else options["low"] = t("visual-quality-low"); + if (hasNormalCodec) if (!hasLowCodec && !hasHighCodec) options["default"] = `${t("visual-quality-normal")} (${t("default")})`; + else options["normal"] = t("visual-quality-normal"); + if (hasHighCodec) if (!hasLowCodec && !hasNormalCodec) options["default"] = `${t("visual-quality-high")} (${t("default")})`; + else options["high"] = t("visual-quality-high"); + return options; +} +class GlobalSettingsStorage extends BaseSettingsStore { + static DEFINITIONS = { + "version.lastCheck": { + default: 0 + }, + "version.latest": { + default: "" + }, + "version.current": { + default: "" + }, + "bx.locale": { + label: t("language"), + default: localStorage.getItem("BetterXcloud.Locale") || "en-US", + options: SUPPORTED_LANGUAGES + }, + "server.region": { + label: t("region"), + note: CE("a", { target: "_blank", href: "https://umap.openstreetmap.fr/en/map/xbox-cloud-gaming-servers_1135022" }, t("server-locations")), + default: "default" + }, + "server.bypassRestriction": { + label: t("bypass-region-restriction"), + note: "⚠️ " + t("use-this-at-your-own-risk"), + default: "off", + optionsGroup: t("region"), + options: Object.assign({ + off: t("off") + }, BypassServers) + }, + "stream.locale": { + label: t("preferred-game-language"), + default: "default", + options: { + default: t("default"), + "ar-SA": "العربية", + "bg-BG": "Български", + "cs-CZ": "čeština", + "da-DK": "dansk", + "de-DE": "Deutsch", + "el-GR": "Ελληνικά", + "en-GB": "English (UK)", + "en-US": "English (US)", + "es-ES": "español (España)", + "es-MX": "español (Latinoamérica)", + "fi-FI": "suomi", + "fr-FR": "français", + "he-IL": "עברית", + "hu-HU": "magyar", + "it-IT": "italiano", + "ja-JP": "日本語", + "ko-KR": "한국어", + "nb-NO": "norsk bokmål", + "nl-NL": "Nederlands", + "pl-PL": "polski", + "pt-BR": "português (Brasil)", + "pt-PT": "português (Portugal)", + "ro-RO": "Română", + "ru-RU": "русский", + "sk-SK": "slovenčina", + "sv-SE": "svenska", + "th-TH": "ไทย", + "tr-TR": "Türkçe", + "zh-CN": "中文(简体)", + "zh-TW": "中文 (繁體)" + } + }, + "stream.video.resolution": { + label: t("target-resolution"), + default: "auto", + options: { + auto: t("default"), + "720p": "720p", + "1080p": "1080p", + "1080p-hq": "1080p (HQ)" + }, + suggest: { + lowest: "720p", + highest: "1080p-hq" + } + }, + "stream.video.codecProfile": { + label: t("visual-quality"), + default: "default", + options: getSupportedCodecProfiles(), + ready: (setting) => { + let options = setting.options, keys = Object.keys(options); + if (keys.length <= 1) setting.unsupported = !0, setting.unsupportedNote = "⚠️ " + t("browser-unsupported-feature"); + setting.suggest = { + lowest: keys.length === 1 ? keys[0] : keys[1], + highest: keys[keys.length - 1] + }; + } + }, + "server.ipv6.prefer": { + label: t("prefer-ipv6-server"), + default: !1 + }, + "screenshot.applyFilters": { + requiredVariants: "full", + label: t("screenshot-apply-filters"), + default: !1 + }, + "ui.splashVideo.skip": { + label: t("skip-splash-video"), + default: !1 + }, + "ui.systemMenu.hideHandle": { + label: t("hide-system-menu-icon"), + default: !1 + }, + "stream.video.combineAudio": { + requiredVariants: "full", + label: t("combine-audio-video-streams"), + default: !1, + experimental: !0, + note: t("combine-audio-video-streams-summary") + }, + "touchController.mode": { + requiredVariants: "full", + label: t("tc-availability"), + default: "all", + options: { + default: t("default"), + off: t("off"), + all: t("tc-all-games") + }, + unsupported: !STATES.userAgent.capabilities.touch, + unsupportedValue: "default" + }, + "touchController.autoOff": { + requiredVariants: "full", + label: t("tc-auto-off"), + default: !1, + unsupported: !STATES.userAgent.capabilities.touch + }, + "touchController.opacity.default": { + requiredVariants: "full", + label: t("tc-default-opacity"), + default: 100, + min: 10, + max: 100, + params: { + steps: 10, + suffix: "%", + ticks: 10, + hideSlider: !0 + }, + unsupported: !STATES.userAgent.capabilities.touch + }, + "touchController.style.standard": { + requiredVariants: "full", + label: t("tc-standard-layout-style"), + default: "default", + options: { + default: t("default"), + white: t("tc-all-white"), + muted: t("tc-muted-colors") + }, + unsupported: !STATES.userAgent.capabilities.touch + }, + "touchController.style.custom": { + requiredVariants: "full", + label: t("tc-custom-layout-style"), + default: "default", + options: { + default: t("default"), + muted: t("tc-muted-colors") + }, + unsupported: !STATES.userAgent.capabilities.touch + }, + "ui.streamMenu.simplify": { + label: t("simplify-stream-menu"), + default: !1 + }, + "mkb.cursor.hideIdle": { + requiredVariants: "full", + label: t("hide-idle-cursor"), + default: !1 + }, + "ui.feedbackDialog.disabled": { + requiredVariants: "full", + label: t("disable-post-stream-feedback-dialog"), + default: !1 + }, + "stream.video.maxBitrate": { + requiredVariants: "full", + label: t("bitrate-video-maximum"), + note: "⚠️ " + t("unexpected-behavior"), + default: 0, + min: 102400, + max: 15360000, + transformValue: { + get(value) { + return value === 0 ? this.max : value; + }, + set(value) { + return value === this.max ? 0 : value; + } + }, + params: { + steps: 102400, + exactTicks: 5120000, + customTextValue: (value, min, max) => { + if (value = parseInt(value), value === max) return t("unlimited"); + else return (value / 1024000).toFixed(1) + " Mb/s"; + } + }, + suggest: { + highest: 0 + } + }, + "gameBar.position": { + requiredVariants: "full", + label: t("position"), + default: "bottom-left", + options: { + off: t("off"), + "bottom-left": t("bottom-left"), + "bottom-right": t("bottom-right") + } + }, + "localCoOp.enabled": { + requiredVariants: "full", + label: t("enable-local-co-op-support"), + default: !1, + note: () => CE("a", { + href: "https://github.com/redphx/better-xcloud/discussions/275", + target: "_blank" + }, t("enable-local-co-op-support-note")) + }, + "ui.controllerStatus.show": { + label: t("show-controller-connection-status"), + default: !0 + }, + "deviceVibration.mode": { + requiredVariants: "full", + label: t("device-vibration"), + default: "off", + options: { + off: t("off"), + on: t("on"), + auto: t("device-vibration-not-using-gamepad") + } + }, + "deviceVibration.intensity": { + requiredVariants: "full", + label: t("vibration-intensity"), + default: 50, + min: 10, + max: 100, + params: { + steps: 10, + suffix: "%", + exactTicks: 20 + } + }, + "controller.pollingRate": { + requiredVariants: "full", + label: t("polling-rate"), + default: 4, + min: 4, + max: 60, + params: { + steps: 4, + exactTicks: 20, + reverse: !0, + customTextValue(value) { + value = parseInt(value); + let text = +(1000 / value).toFixed(2) + " Hz"; + if (value === 4) text = `${text} (${t("default")})`; + return text; + } + } + }, + "mkb.enabled": { + requiredVariants: "full", + label: t("enable-mkb"), + default: !1, + unsupported: !STATES.userAgent.capabilities.mkb || !STATES.browser.capabilities.mkb, + ready: (setting) => { + let note, url; + if (setting.unsupported) note = t("browser-unsupported-feature"), url = "https://github.com/redphx/better-xcloud/issues/206#issuecomment-1920475657"; + else note = t("mkb-disclaimer"), url = "https://better-xcloud.github.io/mouse-and-keyboard/#disclaimer"; + setting.unsupportedNote = () => CE("a", { + href: url, + target: "_blank" + }, "⚠️ " + note); + } + }, + "nativeMkb.mode": { + requiredVariants: "full", + label: t("native-mkb"), + default: "default", + options: { + default: t("default"), + off: t("off"), + on: t("on") + }, + ready: (setting) => { + if (STATES.browser.capabilities.emulatedNativeMkb) ; + else if (UserAgent.isMobile()) setting.unsupported = !0, setting.unsupportedValue = "off", delete setting.options["default"], delete setting.options["on"]; + else delete setting.options["on"]; + } + }, + "nativeMkb.forcedGames": { + label: t("force-native-mkb-games"), + default: [], + unsupported: !AppInterface && UserAgent.isMobile(), + ready: (setting) => { + if (!setting.unsupported) setting.multipleOptions = GhPagesUtils.getNativeMkbCustomList(!0), window.addEventListener(BxEvent.GH_PAGES_FORCE_NATIVE_MKB_UPDATED, (e) => { + setting.multipleOptions = GhPagesUtils.getNativeMkbCustomList(); + }); + } + }, + "nativeMkb.scroll.sensitivityX": { + requiredVariants: "full", + label: t("horizontal-scroll-sensitivity"), + default: 0, + min: 0, + max: 1e4, + params: { + steps: 10, + exactTicks: 2000, + customTextValue: (value) => { + if (!value) return t("default"); + return (value / 100).toFixed(1) + "x"; + } + } + }, + "nativeMkb.scroll.sensitivityY": { + requiredVariants: "full", + label: t("vertical-scroll-sensitivity"), + default: 0, + min: 0, + max: 1e4, + params: { + steps: 10, + exactTicks: 2000, + customTextValue: (value) => { + if (!value) return t("default"); + return (value / 100).toFixed(1) + "x"; + } + } + }, + "mkb.p1.preset.mappingId": { + requiredVariants: "full", + default: -1 + }, + "mkb.p1.slot": { + requiredVariants: "full", + default: 1, + min: 1, + max: 4, + params: { + hideSlider: !0 + } + }, + "mkb.p2.preset.mappingId": { + requiredVariants: "full", + default: 0 + }, + "mkb.p2.slot": { + requiredVariants: "full", + default: 0, + min: 0, + max: 4, + params: { + hideSlider: !0, + customTextValue(value) { + return value = parseInt(value), value === 0 ? t("off") : value.toString(); + } + } + }, + "keyboardShortcuts.preset.inGameId": { + requiredVariants: "full", + default: -1 + }, + "ui.reduceAnimations": { + label: t("reduce-animations"), + default: !1 + }, + "loadingScreen.gameArt.show": { + requiredVariants: "full", + label: t("show-game-art"), + default: !0 + }, + "loadingScreen.waitTime.show": { + label: t("show-wait-time"), + default: !0 + }, + "loadingScreen.rocket": { + label: t("rocket-animation"), + default: "show", + options: { + show: t("rocket-always-show"), + "hide-queue": t("rocket-hide-queue"), + hide: t("rocket-always-hide") + } + }, + "ui.controllerFriendly": { + label: t("controller-friendly-ui"), + default: BX_FLAGS.DeviceInfo.deviceType !== "unknown" + }, + "ui.layout": { + requiredVariants: "full", + label: t("layout"), + default: "default", + options: { + default: t("default"), + normal: t("normal"), + tv: t("smart-tv") + } + }, + "ui.hideScrollbar": { + label: t("hide-scrollbar"), + default: !1 + }, + "ui.hideSections": { + requiredVariants: "full", + label: t("hide-sections"), + default: [], + multipleOptions: { + news: t("section-news"), + friends: t("section-play-with-friends"), + "native-mkb": t("section-native-mkb"), + touch: t("section-touch"), + "most-popular": t("section-most-popular"), + "all-games": t("section-all-games") + }, + params: { + size: 0 + } + }, + "feature.byog.disabled": { + label: t("disable-byog-feature"), + default: !1 + }, + "ui.gameCard.waitTime.show": { + requiredVariants: "full", + label: t("show-wait-time-in-game-card"), + default: !0 + }, + "block.social": { + label: t("disable-social-features"), + default: !1 + }, + "block.tracking": { + label: t("disable-xcloud-analytics"), + default: !1 + }, + "userAgent.profile": { + label: t("user-agent-profile"), + note: "⚠️ " + t("unexpected-behavior"), + default: BX_FLAGS.DeviceInfo.deviceType === "android-tv" || BX_FLAGS.DeviceInfo.deviceType === "webos" ? "vr-oculus" : "default", + options: { + default: t("default"), + "windows-edge": "Edge + Windows", + "macos-safari": "Safari + macOS", + "vr-oculus": "Android TV", + "smarttv-generic": "Smart TV", + "smarttv-tizen": "Samsung Smart TV", + custom: t("custom") + } + }, + "video.player.type": { + label: t("renderer"), + default: "default", + options: { + default: t("default"), + webgl2: t("webgl2") + }, + suggest: { + lowest: "default", + highest: "webgl2" + } + }, + "video.processing": { + label: t("clarity-boost"), + default: "usm", + options: { + usm: t("unsharp-masking"), + cas: t("amd-fidelity-cas") + }, + suggest: { + lowest: "usm", + highest: "cas" + } + }, + "video.player.powerPreference": { + label: t("renderer-configuration"), + default: "default", + options: { + default: t("default"), + "low-power": t("battery-saving"), + "high-performance": t("high-performance") + }, + suggest: { + highest: "low-power" + } + }, + "video.maxFps": { + label: t("max-fps"), + default: 60, + min: 10, + max: 60, + params: { + steps: 10, + exactTicks: 10, + customTextValue: (value) => { + return value = parseInt(value), value === 60 ? t("unlimited") : value + "fps"; + } + } + }, + "video.processing.sharpness": { + label: t("sharpness"), + default: 0, + min: 0, + max: 10, + params: { + exactTicks: 2, + customTextValue: (value) => { + return value = parseInt(value), value === 0 ? t("off") : value.toString(); + } + }, + suggest: { + lowest: 0, + highest: 2 + } + }, + "video.ratio": { + label: t("aspect-ratio"), + note: t("aspect-ratio-note"), + default: "16:9", + options: { + "16:9": "16:9", + "18:9": "18:9", + "21:9": "21:9", + "16:10": "16:10", + "4:3": "4:3", + fill: t("stretch") + } + }, + "video.saturation": { + label: t("saturation"), + default: 100, + min: 50, + max: 150, + params: { + suffix: "%", + ticks: 25 + } + }, + "video.contrast": { + label: t("contrast"), + default: 100, + min: 50, + max: 150, + params: { + suffix: "%", + ticks: 25 + } + }, + "video.brightness": { + label: t("brightness"), + default: 100, + min: 50, + max: 150, + params: { + suffix: "%", + ticks: 25 + } + }, + "audio.mic.onPlaying": { + label: t("enable-mic-on-startup"), + default: !1 + }, + "audio.volume.booster.enabled": { + requiredVariants: "full", + label: t("enable-volume-control"), + default: !1 + }, + "audio.volume": { + label: t("volume"), + default: 100, + min: 0, + max: 600, + params: { + steps: 10, + suffix: "%", + ticks: 100 + } + }, + "stats.items": { + label: t("stats"), + default: ["ping", "fps", "btr", "dt", "pl", "fl"], + multipleOptions: { + time: t("clock"), + play: t("playtime"), + batt: t("battery"), + ping: t("stat-ping"), + jit: t("jitter"), + fps: t("stat-fps"), + btr: t("stat-bitrate"), + dt: t("stat-decode-time"), + pl: t("stat-packets-lost"), + fl: t("stat-frames-lost"), + dl: t("downloaded"), + ul: t("uploaded") + }, + params: { + size: 0 + }, + ready: (setting) => { + let multipleOptions = setting.multipleOptions; + if (!STATES.browser.capabilities.batteryApi) delete multipleOptions["batt"]; + for (let key in multipleOptions) + multipleOptions[key] = key.toUpperCase() + ": " + multipleOptions[key]; + } + }, + "stats.showWhenPlaying": { + label: t("show-stats-on-startup"), + default: !1 + }, + "stats.quickGlance.enabled": { + label: "👀 " + t("enable-quick-glance-mode"), + default: !0 + }, + "stats.position": { + label: t("position"), + default: "top-right", + options: { + "top-left": t("top-left"), + "top-center": t("top-center"), + "top-right": t("top-right") + } + }, + "stats.textSize": { + label: t("text-size"), + default: "0.9rem", + options: { + "0.9rem": t("small"), + "1.0rem": t("normal"), + "1.1rem": t("large") + } + }, + "stats.transparent": { + label: t("transparent-background"), + default: !1 + }, + "stats.opacity": { + label: t("opacity"), + default: 80, + min: 50, + max: 100, + params: { + steps: 10, + suffix: "%", + ticks: 10 + } + }, + "stats.colors": { + label: t("conditional-formatting"), + default: !1 + }, + "xhome.enabled": { + requiredVariants: "full", + label: t("enable-remote-play-feature"), + default: !1 + }, + "xhome.video.resolution": { + requiredVariants: "full", + default: "1080p", + options: { + "720p": "720p", + "1080p": "1080p", + "1080p-hq": "1080p (HQ)" + } + }, + "game.fortnite.forceConsole": { + requiredVariants: "full", + label: "🎮 " + t("fortnite-force-console-version"), + default: !1, + note: t("fortnite-allow-stw-mode") + } + }; + constructor() { + super("BetterXcloud", GlobalSettingsStorage.DEFINITIONS); + } +} +var globalSettings = new GlobalSettingsStorage, getPrefDefinition = globalSettings.getDefinition.bind(globalSettings), getPref = globalSettings.getSetting.bind(globalSettings), setPref = globalSettings.setSetting.bind(globalSettings); +STORAGE.Global = globalSettings; +function ceilToNearest(value, interval) { + return Math.ceil(value / interval) * interval; +} +function floorToNearest(value, interval) { + return Math.floor(value / interval) * interval; +} +async function copyToClipboard(text, showToast = !0) { + try { + return await navigator.clipboard.writeText(text), showToast && Toast.show("Copied to clipboard", "", { instant: !0 }), !0; + } catch (err) { + console.error("Failed to copy: ", err), showToast && Toast.show("Failed to copy", "", { instant: !0 }); + } + return !1; +} +function productTitleToSlug(title) { + return title.replace(/[;,/?:@&=+_`~$%#^*()!^™\xae\xa9]/g, "").replace(/\|/g, "-").replace(/ {2,}/g, " ").trim().substr(0, 50).replace(/ /g, "-").toLowerCase(); +} +function parseDetailsPath(path) { + let matches = /\/games\/(?[^\/]+)\/(?\w+)/.exec(path); + if (!matches?.groups) return; + let titleSlug = matches.groups.titleSlug.replaceAll("|", "-"), productId = matches.groups.productId; + return { titleSlug, productId }; +} +function clearAllData() { + for (let i = 0;i < localStorage.length; i++) { + let key = localStorage.key(i); + if (!key) continue; + if (key.startsWith("BetterXcloud") || key.startsWith("better_xcloud")) localStorage.removeItem(key); + } + try { + indexedDB.deleteDatabase(LocalDb.DB_NAME); + } catch (e) {} + alert(t("clear-data-success")); +} +class SoundShortcut { + static adjustGainNodeVolume(amount) { + if (!getPref("audio.volume.booster.enabled")) return 0; + let currentValue = getPref("audio.volume"), nearestValue; + if (amount > 0) nearestValue = ceilToNearest(currentValue, amount); + else nearestValue = floorToNearest(currentValue, -1 * amount); + let newValue; + if (currentValue !== nearestValue) newValue = nearestValue; + else newValue = currentValue + amount; + return newValue = setPref("audio.volume", newValue, !0), SoundShortcut.setGainNodeVolume(newValue), Toast.show(`${t("stream")} ❯ ${t("volume")}`, newValue + "%", { instant: !0 }), newValue; + } + static setGainNodeVolume(value) { + STATES.currentStream.audioGainNode && (STATES.currentStream.audioGainNode.gain.value = value / 100); + } + static muteUnmute() { + if (getPref("audio.volume.booster.enabled") && STATES.currentStream.audioGainNode) { + let gainValue = STATES.currentStream.audioGainNode.gain.value, settingValue = getPref("audio.volume"), targetValue; + if (settingValue === 0) targetValue = 100, setPref("audio.volume", targetValue, !0); + else if (gainValue === 0) targetValue = settingValue; + else targetValue = 0; + let status; + if (targetValue === 0) status = t("muted"); + else status = targetValue + "%"; + SoundShortcut.setGainNodeVolume(targetValue), Toast.show(`${t("stream")} ❯ ${t("volume")}`, status, { instant: !0 }), BxEvent.dispatch(window, BxEvent.SPEAKER_STATE_CHANGED, { + speakerState: targetValue === 0 ? 1 : 0 + }); + return; + } + let $media = document.querySelector("div[data-testid=media-container] audio") ?? document.querySelector("div[data-testid=media-container] video"); + if ($media) { + $media.muted = !$media.muted; + let status = $media.muted ? t("muted") : t("unmuted"); + Toast.show(`${t("stream")} ❯ ${t("volume")}`, status, { instant: !0 }), BxEvent.dispatch(window, BxEvent.SPEAKER_STATE_CHANGED, { + speakerState: $media.muted ? 1 : 0 + }); + } + } +} class StreamStatsCollector { static instance; static getInstance = () => StreamStatsCollector.instance ?? (StreamStatsCollector.instance = new StreamStatsCollector); @@ -971,7 +1986,7 @@ class StreamStatsCollector { fps: { current: 0, toString() { - let maxFps = getPref("video_max_fps"); + let maxFps = getPref("video.maxFps"); return maxFps < 60 ? `${maxFps}/${this.current}` : this.current.toString(); } }, @@ -1056,29 +2071,29 @@ class StreamStatsCollector { if (!stats) return; stats.forEach((stat) => { if (stat.type === "inbound-rtp" && stat.kind === "video") { - let fps = this.currentStats.fps; + let fps = this.currentStats["fps"]; fps.current = stat.framesPerSecond || 0; - let pl = this.currentStats.pl; + let pl = this.currentStats["pl"]; pl.dropped = Math.max(0, stat.packetsLost), pl.received = stat.packetsReceived; - let fl = this.currentStats.fl; + let fl = this.currentStats["fl"]; if (fl.dropped = stat.framesDropped, fl.received = stat.framesReceived, !this.lastVideoStat) { this.lastVideoStat = stat; return; } - let lastStat = this.lastVideoStat, jit = this.currentStats.jit, bufferDelayDiff = stat.jitterBufferDelay - lastStat.jitterBufferDelay, emittedCountDiff = stat.jitterBufferEmittedCount - lastStat.jitterBufferEmittedCount; + let lastStat = this.lastVideoStat, jit = this.currentStats["jit"], bufferDelayDiff = stat.jitterBufferDelay - lastStat.jitterBufferDelay, emittedCountDiff = stat.jitterBufferEmittedCount - lastStat.jitterBufferEmittedCount; if (emittedCountDiff > 0) jit.current = bufferDelayDiff / emittedCountDiff * 1000; - let btr = this.currentStats.btr, timeDiff = stat.timestamp - lastStat.timestamp; + let btr = this.currentStats["btr"], timeDiff = stat.timestamp - lastStat.timestamp; btr.current = 8 * (stat.bytesReceived - lastStat.bytesReceived) / timeDiff / 1000; - let dt = this.currentStats.dt; + let dt = this.currentStats["dt"]; dt.total = stat.totalDecodeTime - lastStat.totalDecodeTime; let framesDecodedDiff = stat.framesDecoded - lastStat.framesDecoded; dt.current = dt.total / framesDecodedDiff * 1000, this.lastVideoStat = stat; } else if (stat.type === "candidate-pair" && stat.packetsReceived > 0 && stat.state === "succeeded") { - let ping = this.currentStats.ping; + let ping = this.currentStats["ping"]; ping.current = stat.currentRoundTripTime ? stat.currentRoundTripTime * 1000 : -1; - let dl = this.currentStats.dl; + let dl = this.currentStats["dl"]; dl.total = stat.bytesReceived; - let ul = this.currentStats.ul; + let ul = this.currentStats["ul"]; ul.total = stat.bytesSent; } }); @@ -1087,20 +2102,20 @@ class StreamStatsCollector { let bm = await navigator.getBattery(); isCharging = bm.charging, batteryLevel = Math.round(bm.level * 100); } catch (e) {} - let battery = this.currentStats.batt; + let battery = this.currentStats["batt"]; battery.current = batteryLevel, battery.isCharging = isCharging; - let playTime = this.currentStats.play, now = +new Date; + let playTime = this.currentStats["play"], now = +new Date; playTime.seconds = Math.ceil((now - playTime.startTime) / 1000); } getStat(kind) { return this.currentStats[kind]; } reset() { - let playTime = this.currentStats.play; + let playTime = this.currentStats["play"]; playTime.seconds = 0, playTime.startTime = +new Date; try { STATES.browser.capabilities.batteryApi && navigator.getBattery().then((bm) => { - this.currentStats.batt.start = Math.round(bm.level * 100); + this.currentStats["batt"].start = Math.round(bm.level * 100); }); } catch (e) {} } @@ -1110,727 +2125,6 @@ class StreamStatsCollector { }); } } -function getSupportedCodecProfiles() { - let options = { - default: t("default") - }; - if (!("getCapabilities" in RTCRtpReceiver)) return options; - let hasLowCodec = !1, hasNormalCodec = !1, hasHighCodec = !1, codecs = RTCRtpReceiver.getCapabilities("video").codecs; - for (let codec of codecs) { - if (codec.mimeType.toLowerCase() !== "video/h264" || !codec.sdpFmtpLine) continue; - let fmtp = codec.sdpFmtpLine.toLowerCase(); - if (fmtp.includes("profile-level-id=4d")) hasHighCodec = !0; - else if (fmtp.includes("profile-level-id=42e")) hasNormalCodec = !0; - else if (fmtp.includes("profile-level-id=420")) hasLowCodec = !0; - } - if (hasLowCodec) if (!hasNormalCodec && !hasHighCodec) options.default = `${t("visual-quality-low")} (${t("default")})`; - else options.low = t("visual-quality-low"); - if (hasNormalCodec) if (!hasLowCodec && !hasHighCodec) options.default = `${t("visual-quality-normal")} (${t("default")})`; - else options.normal = t("visual-quality-normal"); - if (hasHighCodec) if (!hasLowCodec && !hasNormalCodec) options.default = `${t("visual-quality-high")} (${t("default")})`; - else options.high = t("visual-quality-high"); - return options; -} -class GlobalSettingsStorage extends BaseSettingsStore { - static DEFINITIONS = { - version_last_check: { - default: 0 - }, - version_latest: { - default: "" - }, - version_current: { - default: "" - }, - bx_locale: { - label: t("language"), - default: localStorage.getItem("better_xcloud_locale") || "en-US", - options: SUPPORTED_LANGUAGES - }, - server_region: { - label: t("region"), - note: CE("a", { target: "_blank", href: "https://umap.openstreetmap.fr/en/map/xbox-cloud-gaming-servers_1135022" }, t("server-locations")), - default: "default" - }, - server_bypass_restriction: { - label: t("bypass-region-restriction"), - note: "⚠️ " + t("use-this-at-your-own-risk"), - default: "off", - optionsGroup: t("region"), - options: Object.assign({ - off: t("off") - }, BypassServers) - }, - stream_preferred_locale: { - label: t("preferred-game-language"), - default: "default", - options: { - default: t("default"), - "ar-SA": "العربية", - "bg-BG": "Български", - "cs-CZ": "čeština", - "da-DK": "dansk", - "de-DE": "Deutsch", - "el-GR": "Ελληνικά", - "en-GB": "English (UK)", - "en-US": "English (US)", - "es-ES": "español (España)", - "es-MX": "español (Latinoamérica)", - "fi-FI": "suomi", - "fr-FR": "français", - "he-IL": "עברית", - "hu-HU": "magyar", - "it-IT": "italiano", - "ja-JP": "日本語", - "ko-KR": "한국어", - "nb-NO": "norsk bokmål", - "nl-NL": "Nederlands", - "pl-PL": "polski", - "pt-BR": "português (Brasil)", - "pt-PT": "português (Portugal)", - "ro-RO": "Română", - "ru-RU": "русский", - "sk-SK": "slovenčina", - "sv-SE": "svenska", - "th-TH": "ไทย", - "tr-TR": "Türkçe", - "zh-CN": "中文(简体)", - "zh-TW": "中文 (繁體)" - } - }, - stream_target_resolution: { - label: t("target-resolution"), - default: "auto", - options: { - auto: t("default"), - "720p": "720p", - "1080p": "1080p" - }, - suggest: { - lowest: "720p", - highest: "1080p" - } - }, - stream_codec_profile: { - label: t("visual-quality"), - default: "default", - options: getSupportedCodecProfiles(), - ready: (setting) => { - let options = setting.options, keys = Object.keys(options); - if (keys.length <= 1) setting.unsupported = !0, setting.unsupportedNote = "⚠️ " + t("browser-unsupported-feature"); - setting.suggest = { - lowest: keys.length === 1 ? keys[0] : keys[1], - highest: keys[keys.length - 1] - }; - } - }, - prefer_ipv6_server: { - label: t("prefer-ipv6-server"), - default: !1 - }, - screenshot_apply_filters: { - requiredVariants: "full", - label: t("screenshot-apply-filters"), - default: !1 - }, - skip_splash_video: { - label: t("skip-splash-video"), - default: !1 - }, - hide_dots_icon: { - label: t("hide-system-menu-icon"), - default: !1 - }, - stream_combine_sources: { - requiredVariants: "full", - label: t("combine-audio-video-streams"), - default: !1, - experimental: !0, - note: t("combine-audio-video-streams-summary") - }, - stream_touch_controller: { - requiredVariants: "full", - label: t("tc-availability"), - default: "all", - options: { - default: t("default"), - all: t("tc-all-games"), - off: t("off") - }, - unsupported: !STATES.userAgent.capabilities.touch, - ready: (setting) => { - if (setting.unsupported) setting.default = "default"; - } - }, - stream_touch_controller_auto_off: { - requiredVariants: "full", - label: t("tc-auto-off"), - default: !1, - unsupported: !STATES.userAgent.capabilities.touch - }, - stream_touch_controller_default_opacity: { - requiredVariants: "full", - type: "number-stepper", - label: t("tc-default-opacity"), - default: 100, - min: 10, - max: 100, - steps: 10, - params: { - suffix: "%", - ticks: 10, - hideSlider: !0 - }, - unsupported: !STATES.userAgent.capabilities.touch - }, - stream_touch_controller_style_standard: { - requiredVariants: "full", - label: t("tc-standard-layout-style"), - default: "default", - options: { - default: t("default"), - white: t("tc-all-white"), - muted: t("tc-muted-colors") - }, - unsupported: !STATES.userAgent.capabilities.touch - }, - stream_touch_controller_style_custom: { - requiredVariants: "full", - label: t("tc-custom-layout-style"), - default: "default", - options: { - default: t("default"), - muted: t("tc-muted-colors") - }, - unsupported: !STATES.userAgent.capabilities.touch - }, - stream_simplify_menu: { - label: t("simplify-stream-menu"), - default: !1 - }, - mkb_hide_idle_cursor: { - requiredVariants: "full", - label: t("hide-idle-cursor"), - default: !1 - }, - stream_disable_feedback_dialog: { - requiredVariants: "full", - label: t("disable-post-stream-feedback-dialog"), - default: !1 - }, - bitrate_video_max: { - requiredVariants: "full", - type: "number-stepper", - label: t("bitrate-video-maximum"), - note: "⚠️ " + t("unexpected-behavior"), - default: 0, - min: 0, - max: 14336000, - steps: 102400, - params: { - exactTicks: 5120000, - customTextValue: (value) => { - if (value = parseInt(value), value === 0) return t("unlimited"); - else return (value / 1024000).toFixed(1) + " Mb/s"; - } - }, - suggest: { - highest: 0 - } - }, - game_bar_position: { - requiredVariants: "full", - label: t("position"), - default: "bottom-left", - options: { - "bottom-left": t("bottom-left"), - "bottom-right": t("bottom-right"), - off: t("off") - } - }, - local_co_op_enabled: { - requiredVariants: "full", - label: t("enable-local-co-op-support"), - default: !1, - note: () => CE("a", { - href: "https://github.com/redphx/better-xcloud/discussions/275", - target: "_blank" - }, t("enable-local-co-op-support-note")) - }, - controller_show_connection_status: { - label: t("show-controller-connection-status"), - default: !0 - }, - controller_enable_vibration: { - requiredVariants: "full", - label: t("controller-vibration"), - default: !0 - }, - controller_device_vibration: { - requiredVariants: "full", - label: t("device-vibration"), - default: "off", - options: { - on: t("on"), - auto: t("device-vibration-not-using-gamepad"), - off: t("off") - } - }, - controller_vibration_intensity: { - requiredVariants: "full", - label: t("vibration-intensity"), - type: "number-stepper", - default: 100, - min: 0, - max: 100, - steps: 10, - params: { - suffix: "%", - ticks: 10 - } - }, - controller_polling_rate: { - requiredVariants: "full", - label: t("polling-rate"), - type: "number-stepper", - default: 4, - min: 4, - max: 60, - steps: 4, - params: { - exactTicks: 20, - reverse: !0, - customTextValue(value) { - value = parseInt(value); - let text = +(1000 / value).toFixed(2) + " Hz"; - if (value === 4) text = `${text} (${t("default")})`; - return text; - } - } - }, - mkb_enabled: { - requiredVariants: "full", - label: t("enable-mkb"), - default: !1, - unsupported: !STATES.userAgent.capabilities.mkb, - ready: (setting) => { - let note, url; - if (setting.unsupported) note = t("browser-unsupported-feature"), url = "https://github.com/redphx/better-xcloud/issues/206#issuecomment-1920475657"; - else note = t("mkb-disclaimer"), url = "https://better-xcloud.github.io/mouse-and-keyboard/#disclaimer"; - setting.unsupportedNote = () => CE("a", { - href: url, - target: "_blank" - }, "⚠️ " + note); - } - }, - native_mkb_enabled: { - requiredVariants: "full", - label: t("native-mkb"), - default: "default", - options: { - default: t("default"), - on: t("on"), - off: t("off") - }, - ready: (setting) => { - if (AppInterface) ; - else if (UserAgent.isMobile()) setting.unsupported = !0, setting.default = "off", delete setting.options.default, delete setting.options.on; - else delete setting.options.on; - } - }, - native_mkb_scroll_x_sensitivity: { - requiredVariants: "full", - label: t("horizontal-scroll-sensitivity"), - type: "number-stepper", - default: 0, - min: 0, - max: 1e4, - steps: 10, - params: { - exactTicks: 2000, - customTextValue: (value) => { - if (!value) return t("default"); - return (value / 100).toFixed(1) + "x"; - } - } - }, - native_mkb_scroll_y_sensitivity: { - requiredVariants: "full", - label: t("vertical-scroll-sensitivity"), - type: "number-stepper", - default: 0, - min: 0, - max: 1e4, - steps: 10, - params: { - exactTicks: 2000, - customTextValue: (value) => { - if (!value) return t("default"); - return (value / 100).toFixed(1) + "x"; - } - } - }, - mkb_default_preset_id: { - requiredVariants: "full", - default: 0 - }, - mkb_absolute_mouse: { - requiredVariants: "full", - default: !1 - }, - reduce_animations: { - label: t("reduce-animations"), - default: !1 - }, - ui_loading_screen_game_art: { - requiredVariants: "full", - label: t("show-game-art"), - default: !0 - }, - ui_loading_screen_wait_time: { - label: t("show-wait-time"), - default: !0 - }, - ui_loading_screen_rocket: { - label: t("rocket-animation"), - default: "show", - options: { - show: t("rocket-always-show"), - "hide-queue": t("rocket-hide-queue"), - hide: t("rocket-always-hide") - } - }, - ui_controller_friendly: { - label: t("controller-friendly-ui"), - default: BX_FLAGS.DeviceInfo.deviceType !== "unknown" - }, - ui_layout: { - requiredVariants: "full", - label: t("layout"), - default: "default", - options: { - default: t("default"), - normal: t("normal"), - tv: t("smart-tv") - } - }, - ui_scrollbar_hide: { - label: t("hide-scrollbar"), - default: !1 - }, - ui_hide_sections: { - requiredVariants: "full", - label: t("hide-sections"), - default: [], - multipleOptions: { - news: t("section-news"), - friends: t("section-play-with-friends"), - "native-mkb": t("section-native-mkb"), - touch: t("section-touch"), - "most-popular": t("section-most-popular"), - "all-games": t("section-all-games") - }, - params: { - size: 6 - } - }, - ui_game_card_show_wait_time: { - requiredVariants: "full", - label: t("show-wait-time-in-game-card"), - default: !1 - }, - block_social_features: { - label: t("disable-social-features"), - default: !1 - }, - block_tracking: { - label: t("disable-xcloud-analytics"), - default: !1 - }, - user_agent_profile: { - label: t("user-agent-profile"), - note: "⚠️ " + t("unexpected-behavior"), - default: BX_FLAGS.DeviceInfo.deviceType === "android-tv" || BX_FLAGS.DeviceInfo.deviceType === "webos" ? "vr-oculus" : "default", - options: { - default: t("default"), - "windows-edge": "Edge + Windows", - "macos-safari": "Safari + macOS", - "vr-oculus": "Android TV", - "smarttv-generic": "Smart TV", - "smarttv-tizen": "Samsung Smart TV", - custom: t("custom") - } - }, - video_player_type: { - label: t("renderer"), - default: "default", - options: { - default: t("default"), - webgl2: t("webgl2") - }, - suggest: { - lowest: "default", - highest: "webgl2" - } - }, - video_processing: { - label: t("clarity-boost"), - default: "usm", - options: { - usm: t("unsharp-masking"), - cas: t("amd-fidelity-cas") - }, - suggest: { - lowest: "usm", - highest: "cas" - } - }, - video_power_preference: { - label: t("renderer-configuration"), - default: "default", - options: { - default: t("default"), - "low-power": t("battery-saving"), - "high-performance": t("high-performance") - }, - suggest: { - highest: "low-power" - } - }, - video_max_fps: { - label: t("max-fps"), - type: "number-stepper", - default: 60, - min: 10, - max: 60, - steps: 10, - params: { - exactTicks: 10, - customTextValue: (value) => { - return value = parseInt(value), value === 60 ? t("unlimited") : value + "fps"; - } - } - }, - video_sharpness: { - label: t("sharpness"), - type: "number-stepper", - default: 0, - min: 0, - max: 10, - params: { - exactTicks: 2, - customTextValue: (value) => { - return value = parseInt(value), value === 0 ? t("off") : value.toString(); - } - }, - suggest: { - lowest: 0, - highest: 2 - } - }, - video_ratio: { - label: t("aspect-ratio"), - note: t("aspect-ratio-note"), - default: "16:9", - options: { - "16:9": "16:9", - "18:9": "18:9", - "21:9": "21:9", - "16:10": "16:10", - "4:3": "4:3", - fill: t("stretch") - } - }, - video_saturation: { - label: t("saturation"), - type: "number-stepper", - default: 100, - min: 50, - max: 150, - params: { - suffix: "%", - ticks: 25 - } - }, - video_contrast: { - label: t("contrast"), - type: "number-stepper", - default: 100, - min: 50, - max: 150, - params: { - suffix: "%", - ticks: 25 - } - }, - video_brightness: { - label: t("brightness"), - type: "number-stepper", - default: 100, - min: 50, - max: 150, - params: { - suffix: "%", - ticks: 25 - } - }, - audio_mic_on_playing: { - label: t("enable-mic-on-startup"), - default: !1 - }, - audio_enable_volume_control: { - requiredVariants: "full", - label: t("enable-volume-control"), - default: !1 - }, - audio_volume: { - label: t("volume"), - type: "number-stepper", - default: 100, - min: 0, - max: 600, - steps: 10, - params: { - suffix: "%", - ticks: 100 - } - }, - stats_items: { - label: t("stats"), - default: ["ping", "fps", "btr", "dt", "pl", "fl"], - multipleOptions: { - time: `TIME: ${t("clock")}`, - play: `PLAY: ${t("playtime")}`, - batt: `BATT: ${t("battery")}`, - ping: `PING: ${t("stat-ping")}`, - jit: `JIT: ${t("jitter")}`, - fps: `FPS: ${t("stat-fps")}`, - btr: `BTR: ${t("stat-bitrate")}`, - dt: `DT: ${t("stat-decode-time")}`, - pl: `PL: ${t("stat-packets-lost")}`, - fl: `FL: ${t("stat-frames-lost")}`, - dl: `DL: ${t("downloaded")}`, - ul: `UL: ${t("uploaded")}` - }, - params: { - size: 6 - }, - ready: (setting) => { - let multipleOptions = setting.multipleOptions; - if (!STATES.browser.capabilities.batteryApi) delete multipleOptions["batt"]; - } - }, - stats_show_when_playing: { - label: t("show-stats-on-startup"), - default: !1 - }, - stats_quick_glance: { - label: "👀 " + t("enable-quick-glance-mode"), - default: !0 - }, - stats_position: { - label: t("position"), - default: "top-right", - options: { - "top-left": t("top-left"), - "top-center": t("top-center"), - "top-right": t("top-right") - } - }, - stats_text_size: { - label: t("text-size"), - default: "0.9rem", - options: { - "0.9rem": t("small"), - "1.0rem": t("normal"), - "1.1rem": t("large") - } - }, - stats_transparent: { - label: t("transparent-background"), - default: !1 - }, - stats_opacity: { - label: t("opacity"), - type: "number-stepper", - default: 80, - min: 50, - max: 100, - steps: 10, - params: { - suffix: "%", - ticks: 10 - } - }, - stats_conditional_formatting: { - label: t("conditional-formatting"), - default: !1 - }, - xhome_enabled: { - requiredVariants: "full", - label: t("enable-remote-play-feature"), - default: !1 - }, - xhome_resolution: { - requiredVariants: "full", - default: "1080p", - options: { - "1080p": "1080p", - "720p": "720p" - } - }, - game_fortnite_force_console: { - requiredVariants: "full", - label: "🎮 " + t("fortnite-force-console-version"), - default: !1, - note: t("fortnite-allow-stw-mode") - }, - game_msfs2020_force_native_mkb: { - requiredVariants: "full", - label: "✈️ " + t("msfs2020-force-native-mkb"), - default: !1, - note: t("may-not-work-properly") - } - }; - constructor() { - super("better_xcloud", GlobalSettingsStorage.DEFINITIONS); - } -} -var globalSettings = new GlobalSettingsStorage, getPrefDefinition = globalSettings.getDefinition.bind(globalSettings), getPref = globalSettings.getSetting.bind(globalSettings), setPref = globalSettings.setSetting.bind(globalSettings); -STORAGE.Global = globalSettings; -var GamepadKeyName = { - 0: ["A", "⇓"], - 1: ["B", "⇒"], - 2: ["X", "⇐"], - 3: ["Y", "⇑"], - 4: ["LB", "↘"], - 5: ["RB", "↙"], - 6: ["LT", "↖"], - 7: ["RT", "↗"], - 8: ["Select", "⇺"], - 9: ["Start", "⇻"], - 16: ["Home", ""], - 12: ["D-Pad Up", "≻"], - 13: ["D-Pad Down", "≽"], - 14: ["D-Pad Left", "≺"], - 15: ["D-Pad Right", "≼"], - 10: ["L3", "↺"], - 100: ["Left Stick Up", "↾"], - 101: ["Left Stick Down", "⇂"], - 102: ["Left Stick Left", "↼"], - 103: ["Left Stick Right", "⇀"], - 11: ["R3", "↻"], - 200: ["Right Stick Up", "↿"], - 201: ["Right Stick Down", "⇃"], - 202: ["Right Stick Left", "↽"], - 203: ["Right Stick Right", "⇁"] -}; -var MouseMapTo; -((MouseMapTo2) => { - MouseMapTo2[MouseMapTo2.OFF = 0] = "OFF"; - MouseMapTo2[MouseMapTo2.LS = 1] = "LS"; - MouseMapTo2[MouseMapTo2.RS = 2] = "RS"; -})(MouseMapTo ||= {}); class StreamStats { static instance; static getInstance = () => StreamStats.instance ?? (StreamStats.instance = new StreamStats); @@ -1894,7 +2188,7 @@ class StreamStats { } async start(glancing = !1) { if (!this.isHidden() || glancing && this.isGlancing()) return; - this.intervalId && clearInterval(this.intervalId), await this.update(!0), this.$container.classList.remove("bx-gone"), this.$container.dataset.display = glancing ? "glancing" : "fixed", this.intervalId = window.setInterval(this.update.bind(this), this.REFRESH_INTERVAL); + this.intervalId && clearInterval(this.intervalId), await this.update(!0), this.$container.classList.remove("bx-gone"), this.$container.dataset.display = glancing ? "glancing" : "fixed", this.intervalId = window.setInterval(this.update, this.REFRESH_INTERVAL); } async stop(glancing = !1) { if (glancing && !this.isGlancing()) return; @@ -1929,12 +2223,12 @@ class StreamStats { quickGlanceStop() { this.quickGlanceObserver && this.quickGlanceObserver.disconnect(), this.quickGlanceObserver = null; } - async update(forceUpdate = !1) { + update = async (forceUpdate = !1) => { if (!forceUpdate && this.isHidden() || !STATES.currentStream.peerConnection) { this.destroy(); return; } - let PREF_STATS_CONDITIONAL_FORMATTING = getPref("stats_conditional_formatting"), grade = "", statsCollector = StreamStatsCollector.getInstance(); + let PREF_STATS_CONDITIONAL_FORMATTING = getPref("stats.colors"), grade = "", statsCollector = StreamStatsCollector.getInstance(); await statsCollector.collect(); let statKey; for (statKey in this.stats) { @@ -1943,13 +2237,13 @@ class StreamStats { if ($element.textContent = value.toString(), PREF_STATS_CONDITIONAL_FORMATTING && "grades" in value) grade = statsCollector.calculateGrade(value.current, value.grades); if ($element.dataset.grade !== grade) $element.dataset.grade = grade; } - } + }; refreshStyles() { - let PREF_ITEMS = getPref("stats_items"), $container = this.$container; - $container.dataset.stats = "[" + PREF_ITEMS.join("][") + "]", $container.dataset.position = getPref("stats_position"), $container.dataset.transparent = getPref("stats_transparent"), $container.style.opacity = getPref("stats_opacity") + "%", $container.style.fontSize = getPref("stats_text_size"); + let PREF_ITEMS = getPref("stats.items"), $container = this.$container; + $container.dataset.stats = "[" + PREF_ITEMS.join("][") + "]", $container.dataset.position = getPref("stats.position"), $container.dataset.transparent = getPref("stats.transparent"), $container.style.opacity = getPref("stats.opacity") + "%", $container.style.fontSize = getPref("stats.textSize"); } hideSettingsUi() { - if (this.isGlancing() && !getPref("stats_quick_glance")) this.stop(); + if (this.isGlancing() && !getPref("stats.quickGlance.enabled")) this.stop(); } async render() { this.$container = CE("div", { class: "bx-stats-bar bx-gone" }); @@ -1965,7 +2259,7 @@ class StreamStats { } static setupEvents() { window.addEventListener(BxEvent.STREAM_PLAYING, (e) => { - let PREF_STATS_QUICK_GLANCE = getPref("stats_quick_glance"), PREF_STATS_SHOW_WHEN_PLAYING = getPref("stats_show_when_playing"), streamStats = StreamStats.getInstance(); + let PREF_STATS_QUICK_GLANCE = getPref("stats.quickGlance.enabled"), PREF_STATS_SHOW_WHEN_PLAYING = getPref("stats.showWhenPlaying"), streamStats = StreamStats.getInstance(); if (PREF_STATS_SHOW_WHEN_PLAYING) streamStats.start(); else if (PREF_STATS_QUICK_GLANCE) streamStats.quickGlanceSetup(), !PREF_STATS_SHOW_WHEN_PLAYING && streamStats.start(!0); }); @@ -1974,201 +2268,12 @@ class StreamStats { StreamStats.getInstance().refreshStyles(); } } -class Toast { - static instance; - static getInstance = () => Toast.instance ?? (Toast.instance = new Toast); - LOG_TAG = "Toast"; - $wrapper; - $msg; - $status; - stack = []; - isShowing = !1; - timeoutId; - DURATION = 3000; - constructor() { - BxLogger.info(this.LOG_TAG, "constructor()"), this.$wrapper = CE("div", { class: "bx-toast bx-offscreen" }, this.$msg = CE("span", { class: "bx-toast-msg" }), this.$status = CE("span", { class: "bx-toast-status" })), this.$wrapper.addEventListener("transitionend", (e) => { - let classList = this.$wrapper.classList; - if (classList.contains("bx-hide")) classList.remove("bx-offscreen", "bx-hide"), classList.add("bx-offscreen"), this.showNext(); - }), document.documentElement.appendChild(this.$wrapper); - } - show(msg, status, options = {}) { - options = options || {}; - let args = Array.from(arguments); - if (options.instant) this.stack = [args], this.showNext(); - else this.stack.push(args), !this.isShowing && this.showNext(); - } - showNext() { - if (!this.stack.length) { - this.isShowing = !1; - return; - } - this.isShowing = !0, this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = window.setTimeout(this.hide.bind(this), this.DURATION); - let [msg, status, options] = this.stack.shift(); - if (options && options.html) this.$msg.innerHTML = msg; - else this.$msg.textContent = msg; - if (status) this.$status.classList.remove("bx-gone"), this.$status.textContent = status; - else this.$status.classList.add("bx-gone"); - let classList = this.$wrapper.classList; - classList.remove("bx-offscreen", "bx-hide"), classList.add("bx-show"); - } - hide() { - this.timeoutId = null; - let classList = this.$wrapper.classList; - classList.remove("bx-show"), classList.add("bx-hide"); - } - static show(msg, status, options = {}) { - Toast.getInstance().show(msg, status, options); - } - static showNext() { - Toast.getInstance().showNext(); - } -} -function ceilToNearest(value, interval) { - return Math.ceil(value / interval) * interval; -} -function floorToNearest(value, interval) { - return Math.floor(value / interval) * interval; -} -async function copyToClipboard(text, showToast = !0) { - try { - return await navigator.clipboard.writeText(text), showToast && Toast.show("Copied to clipboard", "", { instant: !0 }), !0; - } catch (err) { - console.error("Failed to copy: ", err), showToast && Toast.show("Failed to copy", "", { instant: !0 }); - } - return !1; -} -function productTitleToSlug(title) { - return title.replace(/[;,/?:@&=+_`~$%#^*()!^™\xae\xa9]/g, "").replace(/\|/g, "-").replace(/ {2,}/g, " ").trim().substr(0, 50).replace(/ /g, "-").toLowerCase(); -} -function parseDetailsPath(path) { - let matches = /\/games\/(?[^\/]+)\/(?\w+)/.exec(path); - if (!matches?.groups) return; - let titleSlug = matches.groups.titleSlug.replaceAll("|", "-"), productId = matches.groups.productId; - return { titleSlug, productId }; -} -class SoundShortcut { - static adjustGainNodeVolume(amount) { - if (!getPref("audio_enable_volume_control")) return 0; - let currentValue = getPref("audio_volume"), nearestValue; - if (amount > 0) nearestValue = ceilToNearest(currentValue, amount); - else nearestValue = floorToNearest(currentValue, -1 * amount); - let newValue; - if (currentValue !== nearestValue) newValue = nearestValue; - else newValue = currentValue + amount; - return newValue = setPref("audio_volume", newValue, !0), SoundShortcut.setGainNodeVolume(newValue), Toast.show(`${t("stream")} ❯ ${t("volume")}`, newValue + "%", { instant: !0 }), newValue; - } - static setGainNodeVolume(value) { - STATES.currentStream.audioGainNode && (STATES.currentStream.audioGainNode.gain.value = value / 100); - } - static muteUnmute() { - if (getPref("audio_enable_volume_control") && STATES.currentStream.audioGainNode) { - let gainValue = STATES.currentStream.audioGainNode.gain.value, settingValue = getPref("audio_volume"), targetValue; - if (settingValue === 0) targetValue = 100, setPref("audio_volume", targetValue, !0); - else if (gainValue === 0) targetValue = settingValue; - else targetValue = 0; - let status; - if (targetValue === 0) status = t("muted"); - else status = targetValue + "%"; - SoundShortcut.setGainNodeVolume(targetValue), Toast.show(`${t("stream")} ❯ ${t("volume")}`, status, { instant: !0 }), BxEvent.dispatch(window, BxEvent.SPEAKER_STATE_CHANGED, { - speakerState: targetValue === 0 ? 1 : 0 - }); - return; - } - let $media = document.querySelector("div[data-testid=media-container] audio") ?? document.querySelector("div[data-testid=media-container] video"); - if ($media) { - $media.muted = !$media.muted; - let status = $media.muted ? t("muted") : t("unmuted"); - Toast.show(`${t("stream")} ❯ ${t("volume")}`, status, { instant: !0 }), BxEvent.dispatch(window, BxEvent.SPEAKER_STATE_CHANGED, { - speakerState: $media.muted ? 1 : 0 - }); - } - } -} -class BxSelectElement { - static wrap($select) { - $select.removeAttribute("tabindex"); - let $btnPrev = createButton({ - label: "<", - style: 32 - }), $btnNext = createButton({ - label: ">", - style: 32 - }), isMultiple = $select.multiple, $checkBox, $label, visibleIndex = $select.selectedIndex, $content; - if (isMultiple) $content = CE("button", { - class: "bx-select-value bx-focusable", - tabindex: 0 - }, $checkBox = CE("input", { type: "checkbox" }), $label = CE("span", {}, "")), $content.addEventListener("click", (e) => { - $checkBox.click(); - }), $checkBox.addEventListener("input", (e) => { - let $option = getOptionAtIndex(visibleIndex); - $option && ($option.selected = e.target.checked), BxEvent.dispatch($select, "input"); - }); - else $content = CE("div", {}, $label = CE("label", { for: $select.id + "_checkbox" }, "")); - let getOptionAtIndex = (index) => { - return Array.from($select.querySelectorAll("option"))[index]; - }, render = (e) => { - if (e && e.manualTrigger) visibleIndex = $select.selectedIndex; - visibleIndex = normalizeIndex(visibleIndex); - let $option = getOptionAtIndex(visibleIndex), content = ""; - if ($option) if (content = $option.textContent || "", content && $option.parentElement.tagName === "OPTGROUP") { - $label.innerHTML = ""; - let fragment = document.createDocumentFragment(); - fragment.appendChild(CE("span", {}, $option.parentElement.label)), fragment.appendChild(document.createTextNode(content)), $label.appendChild(fragment); - } else $label.textContent = content; - else $label.textContent = content; - if ($label.classList.toggle("bx-line-through", $option && $option.disabled), isMultiple) $checkBox.checked = $option?.selected || !1, $checkBox.classList.toggle("bx-gone", !content); - let disablePrev = visibleIndex <= 0, disableNext = visibleIndex === $select.querySelectorAll("option").length - 1; - $btnPrev.classList.toggle("bx-inactive", disablePrev), $btnNext.classList.toggle("bx-inactive", disableNext), disablePrev && !disableNext && document.activeElement === $btnPrev && $btnNext.focus(), disableNext && !disablePrev && document.activeElement === $btnNext && $btnPrev.focus(); - }, normalizeIndex = (index) => { - return Math.min(Math.max(index, 0), $select.querySelectorAll("option").length - 1); - }, onPrevNext = (e) => { - if (!e.target) return; - let goNext = e.target.closest("button") === $btnNext, currentIndex = visibleIndex, newIndex = goNext ? currentIndex + 1 : currentIndex - 1; - if (newIndex = normalizeIndex(newIndex), visibleIndex = newIndex, !isMultiple && newIndex !== currentIndex) $select.selectedIndex = newIndex; - if (isMultiple) render(); - else BxEvent.dispatch($select, "input"); - }; - $select.addEventListener("input", render), $btnPrev.addEventListener("click", onPrevNext), $btnNext.addEventListener("click", onPrevNext), new MutationObserver((mutationList, observer2) => { - mutationList.forEach((mutation) => { - if (mutation.type === "childList" || mutation.type === "attributes") render(); - }); - }).observe($select, { - subtree: !0, - childList: !0, - attributes: !0 - }), render(); - let $div = CE("div", { - class: "bx-select", - _nearby: { - orientation: "horizontal", - focus: $btnNext - } - }, $select, $btnPrev, $content, $btnNext); - return Object.defineProperty($div, "value", { - get() { - return $select.value; - }, - set(value) { - $div.setValue(value); - } - }), $div.addEventListener = function() { - $select.addEventListener.apply($select, arguments); - }, $div.removeEventListener = function() { - $select.removeEventListener.apply($select, arguments); - }, $div.dispatchEvent = function() { - return $select.dispatchEvent.apply($select, arguments); - }, $div.setValue = (value) => { - if ("setValue" in $select) $select.setValue(value); - else $select.value = value; - }, $div; - } -} function onChangeVideoPlayerType() { - let playerType = getPref("video_player_type"), $videoProcessing = document.getElementById(`bx_setting_${"video_processing"}`), $videoSharpness = document.getElementById(`bx_setting_${"video_sharpness"}`), $videoPowerPreference = document.getElementById(`bx_setting_${"video_power_preference"}`), $videoMaxFps = document.getElementById(`bx_setting_${"video_max_fps"}`); + let playerType = getPref("video.player.type"), $videoProcessing = document.getElementById(`bx_setting_${escapeCssSelector("video.processing")}`), $videoSharpness = document.getElementById(`bx_setting_${escapeCssSelector("video.processing.sharpness")}`), $videoPowerPreference = document.getElementById(`bx_setting_${escapeCssSelector("video.player.powerPreference")}`), $videoMaxFps = document.getElementById(`bx_setting_${escapeCssSelector("video.maxFps")}`); if (!$videoProcessing) return; let isDisabled = !1, $optCas = $videoProcessing.querySelector(`option[value=${"cas"}]`); if (playerType === "webgl2") $optCas && ($optCas.disabled = !1); - else if ($videoProcessing.value = "usm", setPref("video_processing", "usm"), $optCas && ($optCas.disabled = !0), UserAgent.isSafari()) isDisabled = !0; + else if ($videoProcessing.value = "usm", setPref("video.processing", "usm"), $optCas && ($optCas.disabled = !0), UserAgent.isSafari()) isDisabled = !0; $videoProcessing.disabled = isDisabled, $videoSharpness.dataset.disabled = isDisabled.toString(), $videoPowerPreference.closest(".bx-settings-row").classList.toggle("bx-gone", playerType !== "webgl2"), $videoMaxFps.closest(".bx-settings-row").classList.toggle("bx-gone", playerType !== "webgl2"), updateVideoPlayer(); } function limitVideoPlayerFps(targetFps) { @@ -2177,117 +2282,36 @@ function limitVideoPlayerFps(targetFps) { function updateVideoPlayer() { let streamPlayer = STATES.currentStream.streamPlayer; if (!streamPlayer) return; - limitVideoPlayerFps(getPref("video_max_fps")); + limitVideoPlayerFps(getPref("video.maxFps")); let options = { - processing: getPref("video_processing"), - sharpness: getPref("video_sharpness"), - saturation: getPref("video_saturation"), - contrast: getPref("video_contrast"), - brightness: getPref("video_brightness") + processing: getPref("video.processing"), + sharpness: getPref("video.processing.sharpness"), + saturation: getPref("video.saturation"), + contrast: getPref("video.contrast"), + brightness: getPref("video.brightness") }; - streamPlayer.setPlayerType(getPref("video_player_type")), streamPlayer.updateOptions(options), streamPlayer.refreshPlayer(); + streamPlayer.setPlayerType(getPref("video.player.type")), streamPlayer.updateOptions(options), streamPlayer.refreshPlayer(); } window.addEventListener("resize", updateVideoPlayer); -class MkbPreset { - static MOUSE_SETTINGS = { - map_to: { - label: t("map-mouse-to"), - type: "options", - default: MouseMapTo[2], - options: { - [MouseMapTo[2]]: t("right-stick"), - [MouseMapTo[1]]: t("left-stick"), - [MouseMapTo[0]]: t("off") - } - }, - sensitivity_y: { - label: t("horizontal-sensitivity"), - type: "number-stepper", - default: 50, - min: 1, - max: 300, - params: { - suffix: "%", - exactTicks: 50 - } - }, - sensitivity_x: { - label: t("vertical-sensitivity"), - type: "number-stepper", - default: 50, - min: 1, - max: 300, - params: { - suffix: "%", - exactTicks: 50 - } - }, - deadzone_counterweight: { - label: t("deadzone-counterweight"), - type: "number-stepper", - default: 20, - min: 1, - max: 50, - params: { - suffix: "%", - exactTicks: 10 - } - } - }; - static DEFAULT_PRESET = { - mapping: { - 12: ["ArrowUp"], - 13: ["ArrowDown"], - 14: ["ArrowLeft"], - 15: ["ArrowRight"], - 100: ["KeyW"], - 101: ["KeyS"], - 102: ["KeyA"], - 103: ["KeyD"], - 200: ["KeyI"], - 201: ["KeyK"], - 202: ["KeyJ"], - 203: ["KeyL"], - 0: ["Space", "KeyE"], - 2: ["KeyR"], - 1: ["ControlLeft", "Backspace"], - 3: ["KeyV"], - 9: ["Enter"], - 8: ["Tab"], - 4: ["KeyC", "KeyG"], - 5: ["KeyQ"], - 16: ["Backquote"], - 7: ["Mouse0"], - 6: ["Mouse2"], - 10: ["ShiftLeft"], - 11: ["KeyF"] - }, - mouse: { - map_to: MouseMapTo[2], - sensitivity_x: 100, - sensitivity_y: 100, - deadzone_counterweight: 20 - } - }; - static convert(preset) { - let obj = { - mapping: {}, - mouse: Object.assign({}, preset.mouse) - }; - for (let buttonIndex in preset.mapping) - for (let keyName of preset.mapping[parseInt(buttonIndex)]) - obj.mapping[keyName] = parseInt(buttonIndex); - let mouse = obj.mouse; - mouse["sensitivity_x"] *= EmulatedMkbHandler.DEFAULT_PANNING_SENSITIVITY, mouse["sensitivity_y"] *= EmulatedMkbHandler.DEFAULT_PANNING_SENSITIVITY, mouse["deadzone_counterweight"] *= EmulatedMkbHandler.DEFAULT_DEADZONE_COUNTERWEIGHT; - let mouseMapTo = MouseMapTo[mouse["map_to"]]; - if (typeof mouseMapTo !== "undefined") mouse["map_to"] = mouseMapTo; - else mouse["map_to"] = MkbPreset.MOUSE_SETTINGS["map_to"].default; - return obj; - } -} class KeyHelper { - static #NON_PRINTABLE_KEYS = { + static NON_PRINTABLE_KEYS = { Backquote: "`", + Minus: "-", + Equal: "=", + BracketLeft: "[", + BracketRight: "]", + Backslash: "\\", + Semicolon: ";", + Quote: "'", + Comma: ",", + Period: ".", + Slash: "/", + NumpadMultiply: "Numpad *", + NumpadAdd: "Numpad +", + NumpadSubtract: "Numpad -", + NumpadDecimal: "Numpad .", + NumpadDivide: "Numpad /", + NumpadEqual: "Numpad =", Mouse0: "Left Click", Mouse2: "Right Click", Mouse1: "Middle Click", @@ -2297,19 +2321,43 @@ class KeyHelper { ScrollRight: "Scroll Right" }; static getKeyFromEvent(e) { - let code, name; - if (e instanceof KeyboardEvent) code = e.code || e.key; + let code = null, modifiers; + if (e instanceof KeyboardEvent) code = e.code || e.key, modifiers = 0, modifiers ^= e.ctrlKey ? 1 : 0, modifiers ^= e.shiftKey ? 2 : 0, modifiers ^= e.altKey ? 4 : 0; else if (e instanceof WheelEvent) { if (e.deltaY < 0) code = "ScrollUp"; else if (e.deltaY > 0) code = "ScrollDown"; else if (e.deltaX < 0) code = "ScrollLeft"; else if (e.deltaX > 0) code = "ScrollRight"; } else if (e instanceof MouseEvent) code = "Mouse" + e.button; - if (code) name = KeyHelper.codeToKeyName(code); - return code ? { code, name } : null; + if (code) { + let results = { code }; + if (modifiers) results.modifiers = modifiers; + return results; + } + return null; } - static codeToKeyName(code) { - return KeyHelper.#NON_PRINTABLE_KEYS[code] || code.startsWith("Key") && code.substring(3) || code.startsWith("Digit") && code.substring(5) || code.startsWith("Numpad") && "Numpad " + code.substring(6) || code.startsWith("Arrow") && "Arrow " + code.substring(5) || code.endsWith("Lock") && code.replace("Lock", " Lock") || code.endsWith("Left") && "Left " + code.replace("Left", "") || code.endsWith("Right") && "Right " + code.replace("Right", "") || code; + static getFullKeyCodeFromEvent(e) { + let key = KeyHelper.getKeyFromEvent(e); + return key ? `${key.code}:${key.modifiers || 0}` : ""; + } + static parseFullKeyCode(str) { + if (!str) return null; + let tmp = str.split(":"), code = tmp[0], modifiers = parseInt(tmp[1]); + return { + code, + modifiers + }; + } + static codeToKeyName(key) { + let { code, modifiers } = key, text = [KeyHelper.NON_PRINTABLE_KEYS[code] || code.startsWith("Key") && code.substring(3) || code.startsWith("Digit") && code.substring(5) || code.startsWith("Numpad") && "Numpad " + code.substring(6) || code.startsWith("Arrow") && "Arrow " + code.substring(5) || code.endsWith("Lock") && code.replace("Lock", " Lock") || code.endsWith("Left") && "Left " + code.replace("Left", "") || code.endsWith("Right") && "Right " + code.replace("Right", "") || code]; + if (modifiers && modifiers !== 0) { + if (!code.startsWith("Control") && !code.startsWith("Shift") && !code.startsWith("Alt")) { + if (modifiers & 2) text.unshift("Shift"); + if (modifiers & 4) text.unshift("Alt"); + if (modifiers & 1) text.unshift("Ctrl"); + } + } + return text.join(" + "); } } class PointerClient { @@ -2387,162 +2435,429 @@ class MouseDataProvider { constructor(handler) { this.mkbHandler = handler; } + init() {} + destroy() {} } class MkbHandler {} -class LocalDb { - static DB_NAME = "BetterXcloud"; - static DB_VERSION = 2; - db; - open() { - return new Promise((resolve, reject) => { - if (this.db) { - resolve(); - return; - } - let request = window.indexedDB.open(LocalDb.DB_NAME, LocalDb.DB_VERSION); - request.onupgradeneeded = this.onUpgradeNeeded.bind(this), request.onerror = (e) => { - console.log(e), alert(e.target.error.message), reject && reject(); - }, request.onsuccess = (e) => { - this.db = e.target.result, resolve(); - }; - }); - } - table(name, type) { - let table = this.db.transaction(name, type || "readonly").objectStore(name); - return new Promise((resolve) => resolve(table)); - } - call(method) { - let table = arguments[1]; - return new Promise((resolve) => { - let request = method.call(table, ...Array.from(arguments).slice(2)); - request.onsuccess = (e) => { - resolve([table, e.target.result]); - }; - }); - } - count(table) { - return this.call(table.count, ...arguments); - } - add(table, data) { - return this.call(table.add, ...arguments); - } - put(table, data) { - return this.call(table.put, ...arguments); - } - delete(table, data) { - return this.call(table.delete, ...arguments); - } - get(table, id) { - return this.call(table.get, ...arguments); - } - getAll(table) { - return this.call(table.getAll, ...arguments); - } -} -class MkbPresetsDb extends LocalDb { +class ControllerShortcutsTable extends BasePresetsTable { static instance; - static getInstance = () => MkbPresetsDb.instance ?? (MkbPresetsDb.instance = new MkbPresetsDb); - LOG_TAG = "MkbPresetsDb"; - TABLE_PRESETS = "mkb_presets"; + static getInstance = () => ControllerShortcutsTable.instance ?? (ControllerShortcutsTable.instance = new ControllerShortcutsTable); + LOG_TAG = "ControllerShortcutsTable"; + TABLE_PRESETS = LocalDb.TABLE_CONTROLLER_SHORTCUTS; + DEFAULT_PRESETS = { + [-1]: { + id: -1, + name: "Type A", + data: { + mapping: { + 3: AppInterface ? "device.volume.inc" : "stream.volume.inc", + 0: AppInterface ? "device.volume.dec" : "stream.volume.dec", + 2: "stream.stats.toggle", + 1: AppInterface ? "device.sound.toggle" : "stream.sound.toggle", + 5: "stream.screenshot.capture", + 9: "stream.menu.show" + } + } + }, + [-2]: { + id: -2, + name: "Type B", + data: { + mapping: { + 12: AppInterface ? "device.volume.inc" : "stream.volume.inc", + 13: AppInterface ? "device.volume.dec" : "stream.volume.dec", + 15: "stream.stats.toggle", + 14: AppInterface ? "device.sound.toggle" : "stream.sound.toggle", + 4: "stream.screenshot.capture", + 8: "stream.menu.show" + } + } + } + }; + DEFAULT_PRESET_ID = -1; constructor() { - super(); + super(LocalDb.TABLE_CONTROLLER_SHORTCUTS); BxLogger.info(this.LOG_TAG, "constructor()"); } - createTable(db) { - db.createObjectStore(this.TABLE_PRESETS, { - keyPath: "id", - autoIncrement: !0 - }).createIndex("name_idx", "name"); +} +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 + }; + async getControllerData(id) { + let setting = await this.get(id); + if (!setting) return deepClone(ControllerSettingsTable.DEFAULT_DATA); + return setting.data; } - onUpgradeNeeded(e) { - let db = e.target.result; - if (db.objectStoreNames.contains("undefined")) db.deleteObjectStore("undefined"); - if (!db.objectStoreNames.contains(this.TABLE_PRESETS)) this.createTable(db); - } - async presetsTable() { - return await this.open(), await this.table(this.TABLE_PRESETS, "readwrite"); - } - async newPreset(name, data) { - let table = await this.presetsTable(), [, id] = await this.add(table, { name, data }); - return id; - } - async updatePreset(preset) { - let table = await this.presetsTable(), [, id] = await this.put(table, preset); - return id; - } - async deletePreset(id) { - let table = await this.presetsTable(); - return await this.delete(table, id), id; - } - async getPreset(id) { - let table = await this.presetsTable(), [, preset] = await this.get(table, id); - return preset; - } - async getPresets() { - let table = await this.presetsTable(), [, count] = await this.count(table); - if (count > 0) { - let [, items] = await this.getAll(table), presets = {}; - return items.forEach((item) => presets[item.id] = item), presets; + async getControllersData() { + let all = await this.getAll(), results = {}; + for (let key in all) { + let settings = all[key].data; + settings.vibrationIntensity /= 100, results[key] = settings; } - let preset = { - name: t("default"), - data: MkbPreset.DEFAULT_PRESET - }, [, id] = await this.add(table, preset); - return preset.id = id, setPref("mkb_default_preset_id", id), { - [id]: preset + return results; + } +} +function showGamepadToast(gamepad) { + if (gamepad.id === VIRTUAL_GAMEPAD_ID) return; + BxLogger.info("Gamepad", gamepad); + let text = "🎮"; + if (getPref("localCoOp.enabled")) text += ` #${gamepad.index + 1}`; + let gamepadId = gamepad.id.replace(/ \(.*?Vendor: \w+ Product: \w+\)$/, ""); + text += ` - ${gamepadId}`; + let status; + if (gamepad.connected) status = (gamepad.vibrationActuator ? "✅" : "❌") + " " + t("vibration-status"); + else status = t("disconnected"); + Toast.show(text, status, { instant: !1 }); +} +function hasGamepad() { + let gamepads = window.navigator.getGamepads(); + for (let gamepad of gamepads) + if (gamepad?.connected) return !0; + return !1; +} +class StreamSettings { + static settings = { + settings: {}, + xCloudPollingMode: "all", + deviceVibrationIntensity: 0, + controllerPollingRate: 4, + controllers: {}, + mkbPreset: null, + keyboardShortcuts: {} + }; + static getPref(key) { + return getPref(key); + } + static async refreshControllerSettings() { + let settings = StreamSettings.settings, controllers = {}, settingsTable = ControllerSettingsTable.getInstance(), shortcutsTable = ControllerShortcutsTable.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; + controllers[gamepad.id] = { + vibrationIntensity: settingsData.vibrationIntensity, + shortcuts: shortcutsMapping + }; + } + settings.controllers = controllers, settings.controllerPollingRate = StreamSettings.getPref("controller.pollingRate"), await StreamSettings.refreshDeviceVibration(); + } + static async refreshDeviceVibration() { + if (!STATES.browser.capabilities.deviceVibration) return; + let mode = StreamSettings.getPref("deviceVibration.mode"), intensity = 0; + if (mode === "on" || mode === "auto" && !hasGamepad()) intensity = StreamSettings.getPref("deviceVibration.intensity") / 100; + StreamSettings.settings.deviceVibrationIntensity = intensity, BxEvent.dispatch(window, BxEvent.DEVICE_VIBRATION_CHANGED); + } + static async refreshMkbSettings() { + let settings = StreamSettings.settings, presetId = StreamSettings.getPref("mkb.p1.preset.mappingId"), orgPreset = await MkbMappingPresetsTable.getInstance().getPreset(presetId), orgPresetData = orgPreset.data, converted = { + mapping: {}, + mouse: Object.assign({}, orgPresetData.mouse) + }, key; + for (key in orgPresetData.mapping) { + let buttonIndex = parseInt(key); + if (!orgPresetData.mapping[buttonIndex]) continue; + for (let keyName of orgPresetData.mapping[buttonIndex]) + if (typeof keyName === "string") converted.mapping[keyName] = buttonIndex; + } + let mouse = converted.mouse; + mouse["sensitivityX"] *= 0.001, mouse["sensitivityY"] *= 0.001, mouse["deadzoneCounterweight"] *= 0.01, settings.mkbPreset = converted, setPref("mkb.p1.preset.mappingId", orgPreset.id), BxEvent.dispatch(window, BxEvent.MKB_UPDATED); + } + static async refreshKeyboardShortcuts() { + let settings = StreamSettings.settings, presetId = StreamSettings.getPref("keyboardShortcuts.preset.inGameId"); + if (presetId === 0) { + settings.keyboardShortcuts = null, setPref("keyboardShortcuts.preset.inGameId", presetId), BxEvent.dispatch(window, BxEvent.KEYBOARD_SHORTCUTS_UPDATED); + return; + } + let orgPreset = await KeyboardShortcutsTable.getInstance().getPreset(presetId), orgPresetData = orgPreset.data.mapping, converted = {}, action; + for (action in orgPresetData) { + let info = orgPresetData[action], key = `${info.code}:${info.modifiers || 0}`; + converted[key] = action; + } + settings.keyboardShortcuts = converted, setPref("keyboardShortcuts.preset.inGameId", orgPreset.id), BxEvent.dispatch(window, BxEvent.KEYBOARD_SHORTCUTS_UPDATED); + } + static async refreshAllSettings() { + window.BX_STREAM_SETTINGS = StreamSettings.settings, await StreamSettings.refreshControllerSettings(), await StreamSettings.refreshMkbSettings(), await StreamSettings.refreshKeyboardShortcuts(); + } + static findKeyboardShortcut(targetAction) { + let shortcuts = StreamSettings.settings.keyboardShortcuts; + for (let codeStr in shortcuts) + if (shortcuts[codeStr] === targetAction) return KeyHelper.parseFullKeyCode(codeStr); + return null; + } + static setup() { + let listener = () => { + StreamSettings.refreshControllerSettings(); }; + window.addEventListener("gamepadconnected", listener), window.addEventListener("gamepaddisconnected", listener), StreamSettings.refreshAllSettings(); + } +} +class MkbPopup { + static instance; + static getInstance = () => MkbPopup.instance ?? (MkbPopup.instance = new MkbPopup); + popupType; + $popup; + $title; + $btnActivate; + mkbHandler; + constructor() { + this.render(), window.addEventListener(BxEvent.KEYBOARD_SHORTCUTS_UPDATED, (e) => { + let $newButton = this.createActivateButton(); + this.$btnActivate.replaceWith($newButton), this.$btnActivate = $newButton; + }); + } + attachMkbHandler(handler) { + this.mkbHandler = handler, this.popupType = handler instanceof NativeMkbHandler ? "native" : "virtual", this.$popup.dataset.type = this.popupType, this.$title.innerText = t(this.popupType === "native" ? "native-mkb" : "virtual-controller"); + } + toggleVisibility(show) { + this.$popup.classList.toggle("bx-gone", !show), show && this.moveOffscreen(!1); + } + moveOffscreen(doMove) { + this.$popup.classList.toggle("bx-offscreen", doMove); + } + createActivateButton() { + let options = { + style: 1 | 512 | 128, + label: t("activate"), + onClick: this.onActivate + }, shortcutKey = StreamSettings.findKeyboardShortcut("mkb.toggle"); + if (shortcutKey) options.secondaryText = t("press-key-to-toggle-mkb", { key: KeyHelper.codeToKeyName(shortcutKey) }); + return createButton(options); + } + onActivate = (e) => { + e.preventDefault(), this.mkbHandler.toggle(!0); + }; + render() { + this.$popup = CE("div", { class: "bx-mkb-pointer-lock-msg bx-gone" }, this.$title = CE("p"), this.$btnActivate = this.createActivateButton(), CE("div", {}, createButton({ + label: t("ignore"), + style: 8, + onClick: (e) => { + e.preventDefault(), this.mkbHandler.toggle(!1), this.mkbHandler.waitForMouseData(!1); + } + }), createButton({ + label: t("manage"), + style: 64, + onClick: () => { + let dialog = SettingsDialog.getInstance(); + dialog.focusTab("mkb"), dialog.show(); + } + }))), document.documentElement.appendChild(this.$popup); + } + reset() { + this.toggleVisibility(!0), this.moveOffscreen(!1); + } +} +class NativeMkbHandler extends MkbHandler { + static instance; + static getInstance() { + if (typeof NativeMkbHandler.instance === "undefined") if (NativeMkbHandler.isAllowed()) NativeMkbHandler.instance = new NativeMkbHandler; + else NativeMkbHandler.instance = null; + return NativeMkbHandler.instance; + } + LOG_TAG = "NativeMkbHandler"; + static isAllowed = () => { + return STATES.browser.capabilities.emulatedNativeMkb && getPref("nativeMkb.mode") === "on"; + }; + pointerClient; + enabled = !1; + mouseButtonsPressed = 0; + mouseWheelX = 0; + mouseWheelY = 0; + mouseVerticalMultiply = 0; + mouseHorizontalMultiply = 0; + inputSink; + popup; + constructor() { + super(); + BxLogger.info(this.LOG_TAG, "constructor()"), this.popup = MkbPopup.getInstance(), this.popup.attachMkbHandler(this); + } + onKeyboardEvent(e) { + if (e.type === "keyup" && e.code === "F8") { + e.preventDefault(), this.toggle(); + return; + } + } + onPointerLockRequested(e) { + AppInterface.requestPointerCapture(), this.start(); + } + onPointerLockExited(e) { + AppInterface.releasePointerCapture(), this.stop(); + } + onPollingModeChanged = (e) => { + let move = window.BX_STREAM_SETTINGS.xCloudPollingMode !== "none"; + this.popup.moveOffscreen(move); + }; + onDialogShown = () => { + document.pointerLockElement && document.exitPointerLock(); + }; + handleEvent(event) { + switch (event.type) { + case "keyup": + this.onKeyboardEvent(event); + break; + case BxEvent.XCLOUD_DIALOG_SHOWN: + this.onDialogShown(); + break; + case BxEvent.POINTER_LOCK_REQUESTED: + this.onPointerLockRequested(event); + break; + case BxEvent.POINTER_LOCK_EXITED: + this.onPointerLockExited(event); + break; + case BxEvent.XCLOUD_POLLING_MODE_CHANGED: + this.onPollingModeChanged(event); + break; + } + } + init() { + this.pointerClient = PointerClient.getInstance(), this.inputSink = window.BX_EXPOSED.inputSink, this.updateInputConfigurationAsync(!1); + try { + this.pointerClient.start(STATES.pointerServerPort, this); + } catch (e) { + Toast.show("Cannot enable Mouse & Keyboard feature"); + } + this.mouseVerticalMultiply = getPref("nativeMkb.scroll.sensitivityY"), this.mouseHorizontalMultiply = getPref("nativeMkb.scroll.sensitivityX"), window.addEventListener("keyup", this), window.addEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this), window.addEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.addEventListener(BxEvent.POINTER_LOCK_EXITED, this), window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this); + let shortcutKey = StreamSettings.findKeyboardShortcut("mkb.toggle"); + if (shortcutKey) { + let msg = t("press-key-to-toggle-mkb", { key: `${KeyHelper.codeToKeyName(shortcutKey)}` }); + Toast.show(msg, t("native-mkb"), { html: !0 }); + } + this.waitForMouseData(!1); + } + toggle(force) { + let setEnable; + if (typeof force !== "undefined") setEnable = force; + else setEnable = !this.enabled; + if (setEnable) document.documentElement.requestPointerLock(); + else document.exitPointerLock(); + } + updateInputConfigurationAsync(enabled) { + window.BX_EXPOSED.streamSession.updateInputConfigurationAsync({ + enableKeyboardInput: enabled, + enableMouseInput: enabled, + enableAbsoluteMouse: !1, + enableTouchInput: !1 + }); + } + start() { + this.resetMouseInput(), this.enabled = !0, this.updateInputConfigurationAsync(!0), window.BX_EXPOSED.stopTakRendering = !0, this.waitForMouseData(!1), Toast.show(t("native-mkb"), t("enabled"), { instant: !0 }); + } + stop() { + this.resetMouseInput(), this.enabled = !1, this.updateInputConfigurationAsync(!1), this.waitForMouseData(!0); + } + destroy() { + this.pointerClient?.stop(), window.removeEventListener("keyup", this), window.removeEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this), window.removeEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.removeEventListener(BxEvent.POINTER_LOCK_EXITED, this), window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this), this.waitForMouseData(!1); + } + handleMouseMove(data) { + this.sendMouseInput({ + X: data.movementX, + Y: data.movementY, + Buttons: this.mouseButtonsPressed, + WheelX: this.mouseWheelX, + WheelY: this.mouseWheelY + }); + } + handleMouseClick(data) { + let { pointerButton, pressed } = data; + if (pressed) this.mouseButtonsPressed |= pointerButton; + else this.mouseButtonsPressed ^= pointerButton; + this.mouseButtonsPressed = Math.max(0, this.mouseButtonsPressed), this.sendMouseInput({ + X: 0, + Y: 0, + Buttons: this.mouseButtonsPressed, + WheelX: this.mouseWheelX, + WheelY: this.mouseWheelY + }); + } + handleMouseWheel(data) { + let { vertical, horizontal } = data; + if (this.mouseWheelX = horizontal, this.mouseHorizontalMultiply && this.mouseHorizontalMultiply !== 1) this.mouseWheelX *= this.mouseHorizontalMultiply; + if (this.mouseWheelY = vertical, this.mouseVerticalMultiply && this.mouseVerticalMultiply !== 1) this.mouseWheelY *= this.mouseVerticalMultiply; + return this.sendMouseInput({ + X: 0, + Y: 0, + Buttons: this.mouseButtonsPressed, + WheelX: this.mouseWheelX, + WheelY: this.mouseWheelY + }), !0; + } + setVerticalScrollMultiplier(vertical) { + this.mouseVerticalMultiply = vertical; + } + setHorizontalScrollMultiplier(horizontal) { + this.mouseHorizontalMultiply = horizontal; + } + waitForMouseData(showPopup) { + this.popup.toggleVisibility(showPopup); + } + isEnabled() { + return this.enabled; + } + sendMouseInput(data) { + data.Type = 0, this.inputSink?.onMouseInput(data); + } + resetMouseInput() { + this.mouseButtonsPressed = 0, this.mouseWheelX = 0, this.mouseWheelY = 0, this.sendMouseInput({ + X: 0, + Y: 0, + Buttons: 0, + WheelX: 0, + WheelY: 0 + }); } } var PointerToMouseButton = { 1: 0, 2: 2, 4: 1 -}, VIRTUAL_GAMEPAD_ID = "Xbox 360 Controller"; +}, VIRTUAL_GAMEPAD_ID = "Better xCloud Virtual Controller"; class WebSocketMouseDataProvider extends MouseDataProvider { - #pointerClient; - #connected = !1; + pointerClient; + isConnected = !1; init() { - this.#pointerClient = PointerClient.getInstance(), this.#connected = !1; + this.pointerClient = PointerClient.getInstance(), this.isConnected = !1; try { - this.#pointerClient.start(STATES.pointerServerPort, this.mkbHandler), this.#connected = !0; + this.pointerClient.start(STATES.pointerServerPort, this.mkbHandler), this.isConnected = !0; } catch (e) { Toast.show("Cannot enable Mouse & Keyboard feature"); } } start() { - this.#connected && AppInterface.requestPointerCapture(); + this.isConnected && AppInterface.requestPointerCapture(); } stop() { - this.#connected && AppInterface.releasePointerCapture(); + this.isConnected && AppInterface.releasePointerCapture(); } destroy() { - this.#connected && this.#pointerClient?.stop(); + this.isConnected && this.pointerClient?.stop(); } } class PointerLockMouseDataProvider extends MouseDataProvider { - init() {} start() { - window.addEventListener("mousemove", this.#onMouseMoveEvent), window.addEventListener("mousedown", this.#onMouseEvent), window.addEventListener("mouseup", this.#onMouseEvent), window.addEventListener("wheel", this.#onWheelEvent, { passive: !1 }), window.addEventListener("contextmenu", this.#disableContextMenu); + window.addEventListener("mousemove", this.onMouseMoveEvent), window.addEventListener("mousedown", this.onMouseEvent), window.addEventListener("mouseup", this.onMouseEvent), window.addEventListener("wheel", this.onWheelEvent, { passive: !1 }), window.addEventListener("contextmenu", this.disableContextMenu); } stop() { - document.pointerLockElement && document.exitPointerLock(), window.removeEventListener("mousemove", this.#onMouseMoveEvent), window.removeEventListener("mousedown", this.#onMouseEvent), window.removeEventListener("mouseup", this.#onMouseEvent), window.removeEventListener("wheel", this.#onWheelEvent), window.removeEventListener("contextmenu", this.#disableContextMenu); + document.pointerLockElement && document.exitPointerLock(), window.removeEventListener("mousemove", this.onMouseMoveEvent), window.removeEventListener("mousedown", this.onMouseEvent), window.removeEventListener("mouseup", this.onMouseEvent), window.removeEventListener("wheel", this.onWheelEvent), window.removeEventListener("contextmenu", this.disableContextMenu); } - destroy() {} - #onMouseMoveEvent = (e) => { + onMouseMoveEvent = (e) => { this.mkbHandler.handleMouseMove({ movementX: e.movementX, movementY: e.movementY }); }; - #onMouseEvent = (e) => { + onMouseEvent = (e) => { e.preventDefault(); - let isMouseDown = e.type === "mousedown", data = { + let data = { mouseButton: e.button, - pressed: isMouseDown + pressed: e.type === "mousedown" }; this.mkbHandler.handleMouseClick(data); }; - #onWheelEvent = (e) => { + onWheelEvent = (e) => { if (!KeyHelper.getKeyFromEvent(e)) return; let data = { vertical: e.deltaY, @@ -2550,19 +2865,23 @@ class PointerLockMouseDataProvider extends MouseDataProvider { }; if (this.mkbHandler.handleMouseWheel(data)) e.preventDefault(); }; - #disableContextMenu = (e) => e.preventDefault(); + disableContextMenu = (e) => e.preventDefault(); } class EmulatedMkbHandler extends MkbHandler { static instance; - static getInstance = () => EmulatedMkbHandler.instance ?? (EmulatedMkbHandler.instance = new EmulatedMkbHandler); + static getInstance() { + if (typeof EmulatedMkbHandler.instance === "undefined") if (EmulatedMkbHandler.isAllowed()) EmulatedMkbHandler.instance = new EmulatedMkbHandler; + else EmulatedMkbHandler.instance = null; + return EmulatedMkbHandler.instance; + } static LOG_TAG = "EmulatedMkbHandler"; - #CURRENT_PRESET_DATA = MkbPreset.convert(MkbPreset.DEFAULT_PRESET); - static DEFAULT_PANNING_SENSITIVITY = 0.001; - static DEFAULT_DEADZONE_COUNTERWEIGHT = 0.01; - static MAXIMUM_STICK_RANGE = 1.1; - #VIRTUAL_GAMEPAD = { + static isAllowed() { + return getPref("mkb.enabled") && (AppInterface || !UserAgent.isMobile()); + } + PRESET; + VIRTUAL_GAMEPAD = { id: VIRTUAL_GAMEPAD_ID, - index: 3, + index: 0, connected: !1, hapticActuators: null, mapping: "standard", @@ -2571,244 +2890,225 @@ class EmulatedMkbHandler extends MkbHandler { timestamp: performance.now(), vibrationActuator: null }; - #nativeGetGamepads = window.navigator.getGamepads.bind(window.navigator); - #enabled = !1; - #mouseDataProvider; - #isPolling = !1; - #prevWheelCode = null; - #wheelStoppedTimeout; - #detectMouseStoppedTimeout; - #$message; - #escKeyDownTime = -1; - #STICK_MAP; - #LEFT_STICK_X = []; - #LEFT_STICK_Y = []; - #RIGHT_STICK_X = []; - #RIGHT_STICK_Y = []; + nativeGetGamepads; + initialized = !1; + enabled = !1; + mouseDataProvider; + isPolling = !1; + prevWheelCode = null; + wheelStoppedTimeoutId = null; + detectMouseStoppedTimeoutId = null; + escKeyDownTime = -1; + LEFT_STICK_X = []; + LEFT_STICK_Y = []; + RIGHT_STICK_X = []; + RIGHT_STICK_Y = []; + popup; + STICK_MAP = { + 102: [this.LEFT_STICK_X, 0, -1], + 103: [this.LEFT_STICK_X, 0, 1], + 100: [this.LEFT_STICK_Y, 1, -1], + 101: [this.LEFT_STICK_Y, 1, 1], + 202: [this.RIGHT_STICK_X, 2, -1], + 203: [this.RIGHT_STICK_X, 2, 1], + 200: [this.RIGHT_STICK_Y, 3, -1], + 201: [this.RIGHT_STICK_Y, 3, 1] + }; constructor() { super(); - BxLogger.info(EmulatedMkbHandler.LOG_TAG, "constructor()"), this.#STICK_MAP = { - 102: [this.#LEFT_STICK_X, 0, -1], - 103: [this.#LEFT_STICK_X, 0, 1], - 100: [this.#LEFT_STICK_Y, 1, -1], - 101: [this.#LEFT_STICK_Y, 1, 1], - 202: [this.#RIGHT_STICK_X, 2, -1], - 203: [this.#RIGHT_STICK_X, 2, 1], - 200: [this.#RIGHT_STICK_Y, 3, -1], - 201: [this.#RIGHT_STICK_Y, 3, 1] - }; + BxLogger.info(EmulatedMkbHandler.LOG_TAG, "constructor()"), this.nativeGetGamepads = window.navigator.getGamepads.bind(window.navigator), this.popup = MkbPopup.getInstance(), this.popup.attachMkbHandler(this); } - isEnabled = () => this.#enabled; - #patchedGetGamepads = () => { - let gamepads = this.#nativeGetGamepads() || []; - return gamepads[this.#VIRTUAL_GAMEPAD.index] = this.#VIRTUAL_GAMEPAD, gamepads; + isEnabled = () => this.enabled; + patchedGetGamepads = () => { + let gamepads = this.nativeGetGamepads() || []; + return gamepads[this.VIRTUAL_GAMEPAD.index] = this.VIRTUAL_GAMEPAD, gamepads; }; - #getVirtualGamepad = () => this.#VIRTUAL_GAMEPAD; - #updateStick(stick, x, y) { - let virtualGamepad = this.#getVirtualGamepad(); + getVirtualGamepad = () => this.VIRTUAL_GAMEPAD; + updateStick(stick, x, y) { + let virtualGamepad = this.getVirtualGamepad(); virtualGamepad.axes[stick * 2] = x, virtualGamepad.axes[stick * 2 + 1] = y, virtualGamepad.timestamp = performance.now(); } - #vectorLength = (x, y) => Math.sqrt(x ** 2 + y ** 2); - #resetGamepad = () => { - let gamepad = this.#getVirtualGamepad(); + vectorLength = (x, y) => Math.sqrt(x ** 2 + y ** 2); + resetGamepad() { + let gamepad = this.getVirtualGamepad(); gamepad.axes = [0, 0, 0, 0]; for (let button of gamepad.buttons) button.pressed = !1, button.value = 0; gamepad.timestamp = performance.now(); - }; - #pressButton = (buttonIndex, pressed) => { - let virtualGamepad = this.#getVirtualGamepad(); + } + pressButton(buttonIndex, pressed) { + let virtualGamepad = this.getVirtualGamepad(); if (buttonIndex >= 100) { - let [valueArr, axisIndex] = this.#STICK_MAP[buttonIndex]; + let [valueArr, axisIndex] = this.STICK_MAP[buttonIndex]; valueArr = valueArr, axisIndex = axisIndex; for (let i = valueArr.length - 1;i >= 0; i--) if (valueArr[i] === buttonIndex) valueArr.splice(i, 1); pressed && valueArr.push(buttonIndex); let value; - if (valueArr.length) value = this.#STICK_MAP[valueArr[valueArr.length - 1]][2]; + if (valueArr.length) value = this.STICK_MAP[valueArr[valueArr.length - 1]][2]; else value = 0; virtualGamepad.axes[axisIndex] = value; } else virtualGamepad.buttons[buttonIndex].pressed = pressed, virtualGamepad.buttons[buttonIndex].value = pressed ? 1 : 0; virtualGamepad.timestamp = performance.now(); - }; - #onKeyboardEvent = (e) => { + } + onKeyboardEvent = (e) => { let isKeyDown = e.type === "keydown"; - if (e.code === "F8") { - if (!isKeyDown) e.preventDefault(), this.toggle(); - return; - } if (e.code === "Escape") { - if (e.preventDefault(), this.#enabled && isKeyDown) { - if (this.#escKeyDownTime === -1) this.#escKeyDownTime = performance.now(); - else if (performance.now() - this.#escKeyDownTime >= 1000) this.stop(); - } else this.#escKeyDownTime = -1; + if (e.preventDefault(), this.enabled && isKeyDown) { + if (this.escKeyDownTime === -1) this.escKeyDownTime = performance.now(); + else if (performance.now() - this.escKeyDownTime >= 1000) this.stop(); + } else this.escKeyDownTime = -1; return; } - if (!this.#isPolling) return; - let buttonIndex = this.#CURRENT_PRESET_DATA.mapping[e.code || e.key]; + if (!this.isPolling || !this.PRESET) return; + if (window.BX_STREAM_SETTINGS.xCloudPollingMode !== "none") return; + let buttonIndex = this.PRESET.mapping[e.code || e.key]; if (typeof buttonIndex === "undefined") return; if (e.repeat) return; - e.preventDefault(), this.#pressButton(buttonIndex, isKeyDown); + e.preventDefault(), this.pressButton(buttonIndex, isKeyDown); }; - #onMouseStopped = () => { - this.#detectMouseStoppedTimeout = null; - let analog = this.#CURRENT_PRESET_DATA.mouse["map_to"] === 1 ? 0 : 1; - this.#updateStick(analog, 0, 0); + onMouseStopped = () => { + if (this.detectMouseStoppedTimeoutId = null, !this.PRESET) return; + let analog = this.PRESET.mouse["mapTo"] === 1 ? 0 : 1; + this.updateStick(analog, 0, 0); }; - handleMouseClick = (data) => { + handleMouseClick(data) { let mouseButton; if (typeof data.mouseButton !== "undefined") mouseButton = data.mouseButton; else if (typeof data.pointerButton !== "undefined") mouseButton = PointerToMouseButton[data.pointerButton]; - let keyCode = "Mouse" + mouseButton, key = { - code: keyCode, - name: KeyHelper.codeToKeyName(keyCode) + let key = { + code: "Mouse" + mouseButton }; - if (!key.name) return; - let buttonIndex = this.#CURRENT_PRESET_DATA.mapping[key.code]; + if (!this.PRESET) return; + let buttonIndex = this.PRESET.mapping[key.code]; if (typeof buttonIndex === "undefined") return; - this.#pressButton(buttonIndex, data.pressed); - }; - handleMouseMove = (data) => { - let mouseMapTo = this.#CURRENT_PRESET_DATA.mouse["map_to"]; + this.pressButton(buttonIndex, data.pressed); + } + handleMouseMove(data) { + let preset = this.PRESET; + if (!preset) return; + let mouseMapTo = preset.mouse["mapTo"]; if (mouseMapTo === 0) return; - this.#detectMouseStoppedTimeout && clearTimeout(this.#detectMouseStoppedTimeout), this.#detectMouseStoppedTimeout = window.setTimeout(this.#onMouseStopped.bind(this), 50); - let deadzoneCounterweight = this.#CURRENT_PRESET_DATA.mouse["deadzone_counterweight"], x = data.movementX * this.#CURRENT_PRESET_DATA.mouse["sensitivity_x"], y = data.movementY * this.#CURRENT_PRESET_DATA.mouse["sensitivity_y"], length = this.#vectorLength(x, y); + this.detectMouseStoppedTimeoutId && clearTimeout(this.detectMouseStoppedTimeoutId), this.detectMouseStoppedTimeoutId = window.setTimeout(this.onMouseStopped, 50); + let deadzoneCounterweight = preset.mouse["deadzoneCounterweight"], x = data.movementX * preset.mouse["sensitivityX"], y = data.movementY * preset.mouse["sensitivityY"], length = this.vectorLength(x, y); if (length !== 0 && length < deadzoneCounterweight) x *= deadzoneCounterweight / length, y *= deadzoneCounterweight / length; - else if (length > EmulatedMkbHandler.MAXIMUM_STICK_RANGE) x *= EmulatedMkbHandler.MAXIMUM_STICK_RANGE / length, y *= EmulatedMkbHandler.MAXIMUM_STICK_RANGE / length; + else if (length > 1.1) x *= 1.1 / length, y *= 1.1 / length; let analog = mouseMapTo === 1 ? 0 : 1; - this.#updateStick(analog, x, y); - }; - handleMouseWheel = (data) => { + this.updateStick(analog, x, y); + } + handleMouseWheel(data) { let code = ""; if (data.vertical < 0) code = "ScrollUp"; else if (data.vertical > 0) code = "ScrollDown"; else if (data.horizontal < 0) code = "ScrollLeft"; else if (data.horizontal > 0) code = "ScrollRight"; if (!code) return !1; + if (!this.PRESET) return !1; let key = { - code, - name: KeyHelper.codeToKeyName(code) - }, buttonIndex = this.#CURRENT_PRESET_DATA.mapping[key.code]; + code + }, buttonIndex = this.PRESET.mapping[key.code]; if (typeof buttonIndex === "undefined") return !1; - if (this.#prevWheelCode === null || this.#prevWheelCode === key.code) this.#wheelStoppedTimeout && clearTimeout(this.#wheelStoppedTimeout), this.#pressButton(buttonIndex, !0); - return this.#wheelStoppedTimeout = window.setTimeout(() => { - this.#prevWheelCode = null, this.#pressButton(buttonIndex, !1); + if (this.prevWheelCode === null || this.prevWheelCode === key.code) this.wheelStoppedTimeoutId && clearTimeout(this.wheelStoppedTimeoutId), this.pressButton(buttonIndex, !0); + return this.wheelStoppedTimeoutId = window.setTimeout(() => { + this.prevWheelCode = null, this.pressButton(buttonIndex, !1); }, 20), !0; - }; - toggle = (force) => { - if (typeof force !== "undefined") this.#enabled = force; - else this.#enabled = !this.#enabled; - if (this.#enabled) document.body.requestPointerLock(); + } + toggle(force) { + if (!this.initialized) return; + if (typeof force !== "undefined") this.enabled = force; + else this.enabled = !this.enabled; + if (this.enabled) document.body.requestPointerLock(); else document.pointerLockElement && document.exitPointerLock(); + } + refreshPresetData() { + this.PRESET = window.BX_STREAM_SETTINGS.mkbPreset, this.resetGamepad(); + } + waitForMouseData(showPopup) { + this.popup.toggleVisibility(showPopup); + } + onPollingModeChanged = (e) => { + let move = window.BX_STREAM_SETTINGS.xCloudPollingMode !== "none"; + this.popup.moveOffscreen(move); }; - #getCurrentPreset = () => { - return new Promise((resolve) => { - let presetId = getPref("mkb_default_preset_id"); - MkbPresetsDb.getInstance().getPreset(presetId).then((preset) => { - resolve(preset); - }); - }); - }; - refreshPresetData = () => { - this.#getCurrentPreset().then((preset) => { - this.#CURRENT_PRESET_DATA = MkbPreset.convert(preset ? preset.data : MkbPreset.DEFAULT_PRESET), this.#resetGamepad(); - }); - }; - waitForMouseData = (wait) => { - this.#$message && this.#$message.classList.toggle("bx-gone", !wait); - }; - #onPollingModeChanged = (e) => { - if (!this.#$message) return; - if (e.mode === "none") this.#$message.classList.remove("bx-offscreen"); - else this.#$message.classList.add("bx-offscreen"); - }; - #onDialogShown = () => { + onDialogShown = () => { document.pointerLockElement && document.exitPointerLock(); }; - #initMessage = () => { - if (!this.#$message) this.#$message = CE("div", { class: "bx-mkb-pointer-lock-msg bx-gone" }, CE("div", {}, CE("p", {}, t("virtual-controller")), CE("p", {}, t("press-key-to-toggle-mkb", { key: "F8" }))), CE("div", { "data-type": "virtual" }, createButton({ - style: 1 | 256 | 64, - label: t("activate"), - onClick: ((e) => { - e.preventDefault(), e.stopPropagation(), this.toggle(!0); - }).bind(this) - }), CE("div", {}, createButton({ - label: t("ignore"), - style: 4, - onClick: (e) => { - e.preventDefault(), e.stopPropagation(), this.toggle(!1), this.waitForMouseData(!1); - } - }), createButton({ - label: t("edit"), - onClick: (e) => { - e.preventDefault(), e.stopPropagation(); - let dialog = SettingsNavigationDialog.getInstance(); - dialog.focusTab("mkb"), NavigationDialogManager.getInstance().show(dialog); - } - })))); - if (!this.#$message.isConnected) document.documentElement.appendChild(this.#$message); - }; - #onPointerLockChange = () => { + onPointerLockChange = () => { if (document.pointerLockElement) this.start(); else this.stop(); }; - #onPointerLockError = (e) => { + onPointerLockError = (e) => { console.log(e), this.stop(); }; - #onPointerLockRequested = () => { + onPointerLockRequested = () => { this.start(); }; - #onPointerLockExited = () => { - this.#mouseDataProvider?.stop(); + onPointerLockExited = () => { + this.mouseDataProvider?.stop(); }; handleEvent(event) { switch (event.type) { case BxEvent.POINTER_LOCK_REQUESTED: - this.#onPointerLockRequested(); + this.onPointerLockRequested(); break; case BxEvent.POINTER_LOCK_EXITED: - this.#onPointerLockExited(); + this.onPointerLockExited(); break; } } - init = () => { - if (this.refreshPresetData(), this.#enabled = !1, AppInterface) this.#mouseDataProvider = new WebSocketMouseDataProvider(this); - else this.#mouseDataProvider = new PointerLockMouseDataProvider(this); - if (this.#mouseDataProvider.init(), window.addEventListener("keydown", this.#onKeyboardEvent), window.addEventListener("keyup", this.#onKeyboardEvent), window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.#onPollingModeChanged), window.addEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this.#onDialogShown), AppInterface) window.addEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.addEventListener(BxEvent.POINTER_LOCK_EXITED, this); - else document.addEventListener("pointerlockchange", this.#onPointerLockChange), document.addEventListener("pointerlockerror", this.#onPointerLockError); - if (this.#initMessage(), this.#$message?.classList.add("bx-gone"), AppInterface) Toast.show(t("press-key-to-toggle-mkb", { key: "F8" }), t("virtual-controller"), { html: !0 }), this.waitForMouseData(!1); + init() { + if (!STATES.browser.capabilities.mkb) { + this.initialized = !1; + return; + } + if (this.initialized = !0, this.refreshPresetData(), this.enabled = !1, AppInterface) this.mouseDataProvider = new WebSocketMouseDataProvider(this); + else this.mouseDataProvider = new PointerLockMouseDataProvider(this); + if (this.mouseDataProvider.init(), window.addEventListener("keydown", this.onKeyboardEvent), window.addEventListener("keyup", this.onKeyboardEvent), window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.onPollingModeChanged), window.addEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this.onDialogShown), AppInterface) window.addEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.addEventListener(BxEvent.POINTER_LOCK_EXITED, this); + else document.addEventListener("pointerlockchange", this.onPointerLockChange), document.addEventListener("pointerlockerror", this.onPointerLockError); + if (MkbPopup.getInstance().reset(), AppInterface) Toast.show(t("press-key-to-toggle-mkb", { key: "F8" }), t("virtual-controller"), { html: !0 }), this.waitForMouseData(!1); else this.waitForMouseData(!0); - }; - destroy = () => { - if (this.#isPolling = !1, this.#enabled = !1, this.stop(), this.waitForMouseData(!1), document.pointerLockElement && document.exitPointerLock(), window.removeEventListener("keydown", this.#onKeyboardEvent), window.removeEventListener("keyup", this.#onKeyboardEvent), AppInterface) window.removeEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.removeEventListener(BxEvent.POINTER_LOCK_EXITED, this); - else document.removeEventListener("pointerlockchange", this.#onPointerLockChange), document.removeEventListener("pointerlockerror", this.#onPointerLockError); - window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.#onPollingModeChanged), window.removeEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this.#onDialogShown), this.#mouseDataProvider?.destroy(), window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.#onPollingModeChanged); - }; - start = () => { - if (!this.#enabled) this.#enabled = !0, Toast.show(t("virtual-controller"), t("enabled"), { instant: !0 }); - this.#isPolling = !0, this.#escKeyDownTime = -1, this.#resetGamepad(), window.navigator.getGamepads = this.#patchedGetGamepads, this.waitForMouseData(!1), this.#mouseDataProvider?.start(); - let virtualGamepad = this.#getVirtualGamepad(); + } + destroy() { + if (!this.initialized) return; + if (this.initialized = !1, this.isPolling = !1, this.enabled = !1, this.stop(), this.waitForMouseData(!1), document.pointerLockElement && document.exitPointerLock(), window.removeEventListener("keydown", this.onKeyboardEvent), window.removeEventListener("keyup", this.onKeyboardEvent), AppInterface) window.removeEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.removeEventListener(BxEvent.POINTER_LOCK_EXITED, this); + else document.removeEventListener("pointerlockchange", this.onPointerLockChange), document.removeEventListener("pointerlockerror", this.onPointerLockError); + window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.onPollingModeChanged), window.removeEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this.onDialogShown), this.mouseDataProvider?.destroy(), window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.onPollingModeChanged); + } + updateGamepadSlots() { + this.VIRTUAL_GAMEPAD.index = getPref("mkb.p1.slot") - 1; + } + start() { + if (!this.enabled) this.enabled = !0, Toast.show(t("virtual-controller"), t("enabled"), { instant: !0 }); + this.isPolling = !0, this.escKeyDownTime = -1, this.resetGamepad(), this.updateGamepadSlots(), window.navigator.getGamepads = this.patchedGetGamepads, this.waitForMouseData(!1), this.mouseDataProvider?.start(); + let virtualGamepad = this.getVirtualGamepad(); virtualGamepad.connected = !0, virtualGamepad.timestamp = performance.now(), BxEvent.dispatch(window, "gamepadconnected", { gamepad: virtualGamepad }), window.BX_EXPOSED.stopTakRendering = !0, Toast.show(t("virtual-controller"), t("enabled"), { instant: !0 }); - }; - stop = () => { - this.#enabled = !1, this.#isPolling = !1, this.#escKeyDownTime = -1; - let virtualGamepad = this.#getVirtualGamepad(); - if (virtualGamepad.connected) this.#resetGamepad(), virtualGamepad.connected = !1, virtualGamepad.timestamp = performance.now(), BxEvent.dispatch(window, "gamepaddisconnected", { + } + stop() { + this.enabled = !1, this.isPolling = !1, this.escKeyDownTime = -1; + let virtualGamepad = this.getVirtualGamepad(); + if (virtualGamepad.connected) this.resetGamepad(), virtualGamepad.connected = !1, virtualGamepad.timestamp = performance.now(), BxEvent.dispatch(window, "gamepaddisconnected", { gamepad: virtualGamepad - }), window.navigator.getGamepads = this.#nativeGetGamepads; - this.waitForMouseData(!0), this.#mouseDataProvider?.stop(); - }; + }), window.navigator.getGamepads = this.nativeGetGamepads; + this.waitForMouseData(!0), this.mouseDataProvider?.stop(); + } static setupEvents() {} } class NavigationDialog { dialogManager; + onMountedCallbacks = []; constructor() { this.dialogManager = NavigationDialogManager.getInstance(); } - show() { - if (NavigationDialogManager.getInstance().show(this), !this.getFocusedElement()) this.focusIfNeeded(); + isCancellable() { + return !0; + } + isOverlayVisible() { + return !0; + } + show(configs = {}, clearStack = !1) { + if (NavigationDialogManager.getInstance().show(this, configs, clearStack), !this.getFocusedElement()) this.focusIfNeeded(); } hide() { NavigationDialogManager.getInstance().hide(); @@ -2819,8 +3119,11 @@ class NavigationDialog { if (this.$container.contains($activeElement)) return $activeElement; return null; } - onBeforeMount() {} - onMounted() {} + onBeforeMount(configs = {}) {} + onMounted(configs = {}) { + for (let callback of this.onMountedCallbacks) + callback.call(this); + } onBeforeUnmount() {} onUnmounted() {} handleKeyPress(key) { @@ -2873,10 +3176,11 @@ class NavigationDialogManager { $overlay; $container; 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) => { - e.preventDefault(), e.stopPropagation(), 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_controller_friendly")) + 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]; @@ -2900,9 +3204,13 @@ class NavigationDialogManager { $label.style.minWidth = width + "px", $parent.dataset.calculated = "true"; } } + updateActiveInput(input) { + document.documentElement.dataset.activeInput = input; + } handleEvent(event) { switch (event.type) { case "keydown": + this.updateActiveInput("keyboard"); let $target = event.target, keyboardEvent = event, keyCode = keyboardEvent.code || keyboardEvent.key, handled = this.dialog?.handleKeyPress(keyCode); if (handled) { event.preventDefault(), event.stopPropagation(); @@ -2921,7 +3229,7 @@ class NavigationDialogManager { isShowing() { return this.$container && !this.$container.classList.contains("bx-gone"); } - pollGamepad() { + pollGamepad = () => { let gamepads = window.navigator.getGamepads(); for (let gamepad of gamepads) { if (!gamepad || !gamepad.connected) continue; @@ -2965,6 +3273,7 @@ class NavigationDialogManager { continue; } if (this.gamepadLastStates[gamepad.index] = null, lastKeyPressed) return; + if (this.updateActiveInput("gamepad"), this.handleGamepad(gamepad, releasedButton)) return; if (releasedButton === 0) { document.activeElement && document.activeElement.dispatchEvent(new MouseEvent("click", { bubbles: !0 })); return; @@ -2972,9 +3281,8 @@ class NavigationDialogManager { this.hide(); return; } - if (this.handleGamepad(gamepad, releasedButton)) return; } - } + }; handleGamepad(gamepad, key) { let handled = this.dialog?.handleGamepad(key); if (handled) return !0; @@ -2990,12 +3298,15 @@ class NavigationDialogManager { clearGamepadHoldingInterval() { this.gamepadHoldingIntervalId && window.clearInterval(this.gamepadHoldingIntervalId), this.gamepadHoldingIntervalId = null; } - show(dialog) { - if (this.clearGamepadHoldingInterval(), BxEvent.dispatch(window, BxEvent.XCLOUD_DIALOG_SHOWN), window.BX_EXPOSED.disableGamepadPolling = !0, document.body.classList.add("bx-no-scroll"), this.$overlay.classList.remove("bx-gone"), STATES.isPlaying) this.$overlay.classList.add("bx-invisible"); - this.unmountCurrentDialog(), this.dialog = dialog, dialog.onBeforeMount(), this.$container.appendChild(dialog.getContent()), dialog.onMounted(), this.$container.classList.remove("bx-gone"), this.$container.addEventListener("keydown", this), this.startGamepadPolling(); + show(dialog, configs = {}, clearStack = !1) { + this.clearGamepadHoldingInterval(), BxEvent.dispatch(window, BxEvent.XCLOUD_DIALOG_SHOWN), window.BX_EXPOSED.disableGamepadPolling = !0, document.body.classList.add("bx-no-scroll"), this.unmountCurrentDialog(), this.dialogsStack.push(dialog), this.dialog = dialog, dialog.onBeforeMount(configs), this.$container.appendChild(dialog.getContent()), dialog.onMounted(configs), this.$overlay.classList.remove("bx-gone"), this.$overlay.classList.toggle("bx-invisible", !dialog.isOverlayVisible()), this.$container.classList.remove("bx-gone"), this.$container.addEventListener("keydown", this), this.startGamepadPolling(); } hide() { - this.clearGamepadHoldingInterval(), document.body.classList.remove("bx-no-scroll"), BxEvent.dispatch(window, BxEvent.XCLOUD_DIALOG_DISMISSED), this.$overlay.classList.add("bx-gone"), this.$overlay.classList.remove("bx-invisible"), this.$container.classList.add("bx-gone"), this.$container.removeEventListener("keydown", this), this.stopGamepadPolling(), this.unmountCurrentDialog(), window.BX_EXPOSED.disableGamepadPolling = !1; + if (this.clearGamepadHoldingInterval(), document.body.classList.remove("bx-no-scroll"), BxEvent.dispatch(window, BxEvent.XCLOUD_DIALOG_DISMISSED), this.$overlay.classList.add("bx-gone"), this.$overlay.classList.remove("bx-invisible"), this.$container.classList.add("bx-gone"), this.$container.removeEventListener("keydown", this), this.stopGamepadPolling(), this.dialog) { + let dialogIndex = this.dialogsStack.indexOf(this.dialog); + if (dialogIndex > -1) this.dialogsStack = this.dialogsStack.slice(0, dialogIndex); + } + if (this.unmountCurrentDialog(), window.BX_EXPOSED.disableGamepadPolling = !1, this.dialogsStack.length) this.dialogsStack[this.dialogsStack.length - 1].show(); } focus($elm) { if (!$elm) return !1; @@ -3060,7 +3371,7 @@ class NavigationDialogManager { return null; } startGamepadPolling() { - this.stopGamepadPolling(), this.gamepadPollingIntervalId = window.setInterval(this.pollGamepad.bind(this), NavigationDialogManager.GAMEPAD_POLLING_INTERVAL); + this.stopGamepadPolling(), this.gamepadPollingIntervalId = window.setInterval(this.pollGamepad, NavigationDialogManager.GAMEPAD_POLLING_INTERVAL); } stopGamepadPolling() { this.gamepadLastStates = [], this.gamepadPollingIntervalId && window.clearInterval(this.gamepadPollingIntervalId), this.gamepadPollingIntervalId = null; @@ -3084,7 +3395,6 @@ var BxIcon = { STREAM_SETTINGS: "", STREAM_STATS: "", CLOSE: "", - COMMAND: "", CONTROLLER: "", CREATE_SHORTCUT: "", DISPLAY: "", @@ -3099,7 +3409,6 @@ var BxIcon = { POWER: "", QUESTION: "", REFRESH: "", - VIRTUAL_CONTROLLER: "", REMOTE_PLAY: "", CARET_LEFT: "", CARET_RIGHT: "", @@ -3116,364 +3425,141 @@ var BxIcon = { UPLOAD: "", AUDIO: "" }; -class Dialog { - $dialog; - $title; - $content; - $overlay; - onClose; - constructor(options) { +class BxSelectElement extends HTMLSelectElement { + optionsList; + indicatorsList; + $indicators; + visibleIndex; + isMultiple; + $select; + $btnNext; + $btnPrev; + $label; + $checkBox; + static create($select, forceFriendly = !1) { + if (!forceFriendly && !getPref("ui.controllerFriendly")) 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 $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", { + 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) => { + self.$checkBox.click(); + }), self.$checkBox.addEventListener("input", (e) => { + let $option = BxSelectElement.getOptionAtIndex.call(self, self.visibleIndex); + $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) => { + 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); + }); + }).observe($select, { + subtree: !0, + childList: !0, + attributes: !0 + }), self.append($select, $btnPrev, $content, $btnNext), BxSelectElement.resetIndicators.call(self), BxSelectElement.render.call(self), Object.defineProperty(self, "value", { + get() { + return $select.value; + }, + set(value) { + self.optionsList = Array.from($select.querySelectorAll("option")), $select.value = value, self.visibleIndex = $select.selectedIndex, BxSelectElement.resetIndicators.call(self), BxSelectElement.render.call(self); + } + }), Object.defineProperty(self, "disabled", { + get() { + return $select.disabled; + }, + set(value) { + $select.disabled = value; + } + }), self.addEventListener = function() { + $select.addEventListener.apply($select, arguments); + }, self.removeEventListener = function() { + $select.removeEventListener.apply($select, arguments); + }, self.dispatchEvent = function() { + return $select.dispatchEvent.apply($select, arguments); + }, self.appendChild = function(node) { + return $select.appendChild(node), node; + }, self; + } + static resetIndicators() { let { - title, - className, - content, - hideCloseButton, - onClose, - helpUrl - } = options, $overlay = document.querySelector(".bx-dialog-overlay"); - if (!$overlay) this.$overlay = CE("div", { class: "bx-dialog-overlay bx-gone" }), this.$overlay.addEventListener("contextmenu", (e) => e.preventDefault()), document.documentElement.appendChild(this.$overlay); - else this.$overlay = $overlay; - let $close; - this.onClose = onClose, this.$dialog = CE("div", { class: `bx-dialog ${className || ""} bx-gone` }, this.$title = CE("h2", {}, CE("b", {}, title), helpUrl && createButton({ - icon: BxIcon.QUESTION, - style: 4, - title: t("help"), - url: helpUrl - })), this.$content = CE("div", { class: "bx-dialog-content" }, content), !hideCloseButton && ($close = CE("button", { type: "button" }, t("close")))), $close && $close.addEventListener("click", (e) => { - this.hide(e); - }), !title && this.$title.classList.add("bx-gone"), !content && this.$content.classList.add("bx-gone"), this.$dialog.addEventListener("contextmenu", (e) => e.preventDefault()), document.documentElement.appendChild(this.$dialog); - } - show(newOptions) { - if (document.activeElement && document.activeElement.blur(), newOptions && newOptions.title) this.$title.querySelector("b").textContent = newOptions.title, this.$title.classList.remove("bx-gone"); - this.$dialog.classList.remove("bx-gone"), this.$overlay.classList.remove("bx-gone"), document.body.classList.add("bx-no-scroll"); - } - hide(e) { - this.$dialog.classList.add("bx-gone"), this.$overlay.classList.add("bx-gone"), document.body.classList.remove("bx-no-scroll"), this.onClose && this.onClose(e); - } - toggle() { - this.$dialog.classList.toggle("bx-gone"), this.$overlay.classList.toggle("bx-gone"); - } -} -class MkbRemapper { - BUTTON_ORDERS = [ - 12, - 13, - 14, - 15, - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 16, - 10, - 100, - 101, - 102, - 103, - 11, - 200, - 201, - 202, - 203 - ]; - static instance; - static getInstance = () => MkbRemapper.instance ?? (MkbRemapper.instance = new MkbRemapper); - LOG_TAG = "MkbRemapper"; - states = { - currentPresetId: 0, - presets: {}, - editingPresetData: null, - isEditing: !1 - }; - $wrapper; - $presetsSelect; - $activateButton; - $currentBindingKey; - allKeyElements = []; - allMouseElements = {}; - bindingDialog; - constructor() { - BxLogger.info(this.LOG_TAG, "constructor()"), this.states.currentPresetId = getPref("mkb_default_preset_id"), this.bindingDialog = new Dialog({ - className: "bx-binding-dialog", - content: CE("div", {}, CE("p", {}, t("press-to-bind")), CE("i", {}, t("press-esc-to-cancel"))), - hideCloseButton: !0 - }); - } - clearEventListeners = () => { - window.removeEventListener("keydown", this.onKeyDown), window.removeEventListener("mousedown", this.onMouseDown), window.removeEventListener("wheel", this.onWheel); - }; - bindKey = ($elm, key) => { - let buttonIndex = parseInt($elm.dataset.buttonIndex), keySlot = parseInt($elm.dataset.keySlot); - if ($elm.dataset.keyCode === key.code) return; - for (let $otherElm of this.allKeyElements) - if ($otherElm.dataset.keyCode === key.code) this.unbindKey($otherElm); - this.states.editingPresetData.mapping[buttonIndex][keySlot] = key.code, $elm.textContent = key.name, $elm.dataset.keyCode = key.code; - }; - unbindKey = ($elm) => { - let buttonIndex = parseInt($elm.dataset.buttonIndex), keySlot = parseInt($elm.dataset.keySlot); - this.states.editingPresetData.mapping[buttonIndex][keySlot] = null, $elm.textContent = "", delete $elm.dataset.keyCode; - }; - onWheel = (e) => { - e.preventDefault(), this.clearEventListeners(), this.bindKey(this.$currentBindingKey, KeyHelper.getKeyFromEvent(e)), window.setTimeout(() => this.bindingDialog.hide(), 200); - }; - onMouseDown = (e) => { - e.preventDefault(), this.clearEventListeners(), this.bindKey(this.$currentBindingKey, KeyHelper.getKeyFromEvent(e)), window.setTimeout(() => this.bindingDialog.hide(), 200); - }; - onKeyDown = (e) => { - if (e.preventDefault(), e.stopPropagation(), this.clearEventListeners(), e.code !== "Escape") this.bindKey(this.$currentBindingKey, KeyHelper.getKeyFromEvent(e)); - window.setTimeout(() => this.bindingDialog.hide(), 200); - }; - onBindingKey = (e) => { - if (!this.states.isEditing || e.button !== 0) return; - console.log(e), this.$currentBindingKey = e.target, window.addEventListener("keydown", this.onKeyDown), window.addEventListener("mousedown", this.onMouseDown), window.addEventListener("wheel", this.onWheel), this.bindingDialog.show({ title: this.$currentBindingKey.dataset.prompt }); - }; - onContextMenu = (e) => { - if (e.preventDefault(), !this.states.isEditing) return; - this.unbindKey(e.target); - }; - getPreset = (presetId) => { - return this.states.presets[presetId]; - }; - getCurrentPreset = () => { - let preset = this.getPreset(this.states.currentPresetId); - if (!preset) { - let firstPresetId = parseInt(Object.keys(this.states.presets)[0]); - preset = this.states.presets[firstPresetId], this.states.currentPresetId = firstPresetId, setPref("mkb_default_preset_id", firstPresetId); - } - return preset; - }; - switchPreset = (presetId) => { - this.states.currentPresetId = presetId; - let presetData = this.getCurrentPreset().data; - for (let $elm of this.allKeyElements) { - let buttonIndex = parseInt($elm.dataset.buttonIndex), keySlot = parseInt($elm.dataset.keySlot), buttonKeys = presetData.mapping[buttonIndex]; - if (buttonKeys && buttonKeys[keySlot]) $elm.textContent = KeyHelper.codeToKeyName(buttonKeys[keySlot]), $elm.dataset.keyCode = buttonKeys[keySlot]; - else $elm.textContent = "", delete $elm.dataset.keyCode; - } - let key; - for (key in this.allMouseElements) { - let $elm = this.allMouseElements[key], value = presetData.mouse[key]; - if (typeof value === "undefined") value = MkbPreset.MOUSE_SETTINGS[key].default; - "setValue" in $elm && $elm.setValue(value); - } - let activated = getPref("mkb_default_preset_id") === this.states.currentPresetId; - this.$activateButton.disabled = activated, this.$activateButton.querySelector("span").textContent = activated ? t("activated") : t("activate"); - }; - async refresh() { - removeChildElements(this.$presetsSelect); - let presets = await MkbPresetsDb.getInstance().getPresets(); - this.states.presets = presets; - let fragment = document.createDocumentFragment(), defaultPresetId; - if (this.states.currentPresetId === 0) this.states.currentPresetId = parseInt(Object.keys(presets)[0]), defaultPresetId = this.states.currentPresetId, setPref("mkb_default_preset_id", defaultPresetId), EmulatedMkbHandler.getInstance().refreshPresetData(); - else defaultPresetId = getPref("mkb_default_preset_id"); - for (let id in presets) { - let name = presets[id].name; - if (id === defaultPresetId) name = "🎮 " + name; - let $options = CE("option", { value: id }, name); - $options.selected = parseInt(id) === this.states.currentPresetId, fragment.appendChild($options); - } - this.$presetsSelect.appendChild(fragment); - let activated = defaultPresetId === this.states.currentPresetId; - this.$activateButton.disabled = activated, this.$activateButton.querySelector("span").textContent = activated ? t("activated") : t("activate"), !this.states.isEditing && this.switchPreset(this.states.currentPresetId); - } - toggleEditing = (force) => { - if (this.states.isEditing = typeof force !== "undefined" ? force : !this.states.isEditing, this.$wrapper.classList.toggle("bx-editing", this.states.isEditing), this.states.isEditing) this.states.editingPresetData = deepClone(this.getCurrentPreset().data); - else this.states.editingPresetData = null; - let childElements = this.$wrapper.querySelectorAll("select, button, input"); - for (let $elm of Array.from(childElements)) { - if ($elm.parentElement.parentElement.classList.contains("bx-mkb-action-buttons")) continue; - let disable = !this.states.isEditing; - if ($elm.parentElement.classList.contains("bx-mkb-preset-tools")) disable = !disable; - $elm.disabled = disable; - } - }; - render() { - this.$wrapper = CE("div", { class: "bx-mkb-settings" }), this.$presetsSelect = CE("select", { tabindex: -1 }), this.$presetsSelect.addEventListener("change", (e) => { - this.switchPreset(parseInt(e.target.value)); - }); - let promptNewName = (value) => { - let newName = ""; - while (!newName) { - if (newName = prompt(t("prompt-preset-name"), value), newName === null) return !1; - newName = newName.trim(); + optionsList, + indicatorsList, + $indicators + } = this, targetSize = optionsList.length; + if (indicatorsList.length > targetSize) while (indicatorsList.length > targetSize) + indicatorsList.pop()?.remove(); + else if (indicatorsList.length < targetSize) while (indicatorsList.length < targetSize) { + let $indicator = CE("span", {}); + indicatorsList.push($indicator), $indicators.appendChild($indicator); } - return newName ? newName : !1; - }, $header = CE("div", { class: "bx-mkb-preset-tools" }, this.$presetsSelect, createButton({ - title: t("rename"), - icon: BxIcon.CURSOR_TEXT, - tabIndex: -1, - onClick: async () => { - let preset = this.getCurrentPreset(), newName = promptNewName(preset.name); - if (!newName || newName === preset.name) return; - preset.name = newName, await MkbPresetsDb.getInstance().updatePreset(preset), await this.refresh(); - } - }), createButton({ - icon: BxIcon.NEW, - title: t("new"), - tabIndex: -1, - onClick: (e) => { - let newName = promptNewName(""); - if (!newName) return; - MkbPresetsDb.getInstance().newPreset(newName, MkbPreset.DEFAULT_PRESET).then((id) => { - this.states.currentPresetId = id, this.refresh(); - }); - } - }), createButton({ - icon: BxIcon.COPY, - title: t("copy"), - tabIndex: -1, - onClick: (e) => { - let preset = this.getCurrentPreset(), newName = promptNewName(`${preset.name} (2)`); - if (!newName) return; - MkbPresetsDb.getInstance().newPreset(newName, preset.data).then((id) => { - this.states.currentPresetId = id, this.refresh(); - }); - } - }), createButton({ - icon: BxIcon.TRASH, - style: 2, - title: t("delete"), - tabIndex: -1, - onClick: (e) => { - if (!confirm(t("confirm-delete-preset"))) return; - MkbPresetsDb.getInstance().deletePreset(this.states.currentPresetId).then((id) => { - this.states.currentPresetId = 0, this.refresh(); - }); - } - })); - this.$wrapper.appendChild($header); - let $rows = CE("div", { class: "bx-mkb-settings-rows" }, CE("i", { class: "bx-mkb-note" }, t("right-click-to-unbind"))), keysPerButton = 2; - for (let buttonIndex of this.BUTTON_ORDERS) { - let [buttonName, buttonPrompt] = GamepadKeyName[buttonIndex], $elm, $fragment = document.createDocumentFragment(); - for (let i = 0;i < keysPerButton; i++) - $elm = CE("button", { - type: "button", - "data-prompt": buttonPrompt, - "data-button-index": buttonIndex, - "data-key-slot": i - }, " "), $elm.addEventListener("mouseup", this.onBindingKey), $elm.addEventListener("contextmenu", this.onContextMenu), $fragment.appendChild($elm), this.allKeyElements.push($elm); - let $keyRow = CE("div", { class: "bx-mkb-key-row" }, CE("label", { title: buttonName }, buttonPrompt), $fragment); - $rows.appendChild($keyRow); + for (let $indicator of indicatorsList) + clearDataSet($indicator); + $indicators.classList.toggle("bx-invisible", targetSize <= 1); + } + static getOptionAtIndex(index) { + return this.optionsList[index]; + } + static render(e) { + let { + $label, + $btnNext, + $btnPrev, + $checkBox, + visibleIndex, + optionsList, + indicatorsList + } = this; + if (e && e.manualTrigger) this.visibleIndex = this.$select.selectedIndex; + this.visibleIndex = BxSelectElement.normalizeIndex.call(this, this.visibleIndex); + 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) { + let groupLabel = $parent instanceof HTMLOptGroupElement ? $parent.label : " "; + $label.innerHTML = ""; + let fragment = document.createDocumentFragment(); + fragment.appendChild(CE("span", {}, groupLabel)), fragment.appendChild(document.createTextNode(content)), $label.appendChild(fragment); + } else $label.textContent = content; + } 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); + for (let i = 0;i < optionsList.length; i++) { + let $option2 = optionsList[i], $indicator = indicatorsList[i]; + if (clearDataSet($indicator), $option2.selected) $indicator.dataset.selected = "true"; + if ($option2.index === visibleIndex) $indicator.dataset.highlighted = "true"; } - $rows.appendChild(CE("i", { class: "bx-mkb-note" }, t("mkb-adjust-ingame-settings"))); - let $mouseSettings = document.createDocumentFragment(); - for (let key in MkbPreset.MOUSE_SETTINGS) { - let setting = MkbPreset.MOUSE_SETTINGS[key], value = setting.default, $elm, onChange = (e, value2) => { - this.states.editingPresetData.mouse[key] = value2; - }, $row = CE("label", { - class: "bx-settings-row", - for: `bx_setting_${key}` - }, CE("span", { class: "bx-settings-label" }, setting.label), $elm = SettingElement.render(setting.type, key, setting, value, onChange, setting.params)); - $mouseSettings.appendChild($row), this.allMouseElements[key] = $elm; - } - $rows.appendChild($mouseSettings), this.$wrapper.appendChild($rows); - let $actionButtons = CE("div", { class: "bx-mkb-action-buttons" }, CE("div", {}, createButton({ - label: t("edit"), - tabIndex: -1, - onClick: (e) => this.toggleEditing(!0) - }), this.$activateButton = createButton({ - label: t("activate"), - style: 1, - tabIndex: -1, - onClick: (e) => { - setPref("mkb_default_preset_id", this.states.currentPresetId), EmulatedMkbHandler.getInstance().refreshPresetData(), this.refresh(); - } - })), CE("div", {}, createButton({ - label: t("cancel"), - style: 4, - tabIndex: -1, - onClick: (e) => { - this.switchPreset(this.states.currentPresetId), this.toggleEditing(!1); - } - }), createButton({ - label: t("save"), - style: 1, - tabIndex: -1, - onClick: (e) => { - let updatedPreset = deepClone(this.getCurrentPreset()); - updatedPreset.data = this.states.editingPresetData, MkbPresetsDb.getInstance().updatePreset(updatedPreset).then((id) => { - if (id === getPref("mkb_default_preset_id")) EmulatedMkbHandler.getInstance().refreshPresetData(); - this.toggleEditing(!1), this.refresh(); - }); - } - }))); - return this.$wrapper.appendChild($actionButtons), this.toggleEditing(!1), this.refresh(), this.$wrapper; } -} -var VIBRATION_DATA_MAP = { - gamepadIndex: 8, - leftMotorPercent: 8, - rightMotorPercent: 8, - leftTriggerMotorPercent: 8, - rightTriggerMotorPercent: 8, - durationMs: 16 -}; -class VibrationManager { - static #playDeviceVibration(data) { - if (AppInterface) { - AppInterface.vibrate(JSON.stringify(data), window.BX_VIBRATION_INTENSITY); - return; - } - let intensity = Math.min(100, data.leftMotorPercent + data.rightMotorPercent / 2) * window.BX_VIBRATION_INTENSITY; - if (intensity === 0 || intensity === 100) { - window.navigator.vibrate(intensity ? data.durationMs : 0); - return; - } - let pulseDuration = 200, onDuration = Math.floor(pulseDuration * intensity / 100), offDuration = pulseDuration - onDuration, repeats = Math.ceil(data.durationMs / pulseDuration), pulses = Array(repeats).fill([onDuration, offDuration]).flat(); - window.navigator.vibrate(pulses); + static normalizeIndex(index) { + return Math.min(Math.max(index, 0), this.optionsList.length - 1); } - static supportControllerVibration() { - return Gamepad.prototype.hasOwnProperty("vibrationActuator"); - } - static supportDeviceVibration() { - return !!window.navigator.vibrate; - } - static updateGlobalVars(stopVibration = !0) { - if (window.BX_ENABLE_CONTROLLER_VIBRATION = VibrationManager.supportControllerVibration() ? getPref("controller_enable_vibration") : !1, window.BX_VIBRATION_INTENSITY = getPref("controller_vibration_intensity") / 100, !VibrationManager.supportDeviceVibration()) { - window.BX_ENABLE_DEVICE_VIBRATION = !1; - return; - } - stopVibration && window.navigator.vibrate(0); - let value = getPref("controller_device_vibration"), enabled; - if (value === "on") enabled = !0; - else if (value === "auto") { - enabled = !0; - let gamepads = window.navigator.getGamepads(); - for (let gamepad of gamepads) - if (gamepad) { - enabled = !1; - break; - } - } else enabled = !1; - window.BX_ENABLE_DEVICE_VIBRATION = enabled; - } - static #onMessage(e) { - if (!window.BX_ENABLE_DEVICE_VIBRATION) return; - if (typeof e !== "object" || !(e.data instanceof ArrayBuffer)) return; - let dataView = new DataView(e.data), offset = 0, messageType; - if (dataView.byteLength === 13) messageType = dataView.getUint16(offset, !0), offset += Uint16Array.BYTES_PER_ELEMENT; - else messageType = dataView.getUint8(offset), offset += Uint8Array.BYTES_PER_ELEMENT; - if (!(messageType & 128)) return; - let vibrationType = dataView.getUint8(offset); - if (offset += Uint8Array.BYTES_PER_ELEMENT, vibrationType !== 0) return; - let data = {}, key; - for (key in VIBRATION_DATA_MAP) - if (VIBRATION_DATA_MAP[key] === 16) data[key] = dataView.getUint16(offset, !0), offset += Uint16Array.BYTES_PER_ELEMENT; - else data[key] = dataView.getUint8(offset), offset += Uint8Array.BYTES_PER_ELEMENT; - VibrationManager.#playDeviceVibration(data); - } - static initialSetup() { - window.addEventListener("gamepadconnected", (e) => VibrationManager.updateGlobalVars()), window.addEventListener("gamepaddisconnected", (e) => VibrationManager.updateGlobalVars()), VibrationManager.updateGlobalVars(!1), window.addEventListener(BxEvent.DATA_CHANNEL_CREATED, (e) => { - let dataChannel = e.dataChannel; - if (!dataChannel || dataChannel.label !== "input") return; - dataChannel.addEventListener("message", VibrationManager.#onMessage); - }); + static onPrevNext(e) { + if (!e.target) return; + let { + $btnNext, + $select, + isMultiple, + visibleIndex: currentIndex + } = this, newIndex = e.target.closest("button") === $btnNext ? currentIndex + 1 : currentIndex - 1; + if (newIndex > this.optionsList.length - 1) newIndex = 0; + else if (newIndex < 0) newIndex = this.optionsList.length - 1; + if (newIndex = BxSelectElement.normalizeIndex.call(this, newIndex), this.visibleIndex = newIndex, !isMultiple && newIndex !== currentIndex) $select.selectedIndex = newIndex; + if (isMultiple) BxSelectElement.render.call(this); + else BxEvent.dispatch($select, "input"); } } var FeatureGates = { @@ -3481,9 +3567,220 @@ var FeatureGates = { EnableWifiWarnings: !1, EnableUpdateRequiredPage: !1, ShowForcedUpdateScreen: !1 -}; -if (getPref("block_social_features")) FeatureGates.EnableGuideChatTab = !1; +}, nativeMkbMode = getPref("nativeMkb.mode"); +if (nativeMkbMode !== "default") FeatureGates.EnableMouseAndKeyboard = nativeMkbMode === "on"; +if (getPref("block.social")) FeatureGates.EnableGuideChatTab = !1; +if (getPref("feature.byog.disabled")) FeatureGates.EnableBYOG = !1, FeatureGates.EnableBYOGPurchase = !1; if (BX_FLAGS.FeatureGates) FeatureGates = Object.assign(BX_FLAGS.FeatureGates, FeatureGates); +class BxNumberStepper extends HTMLInputElement { + intervalId = null; + isHolding; + controlValue; + controlMin; + controlMax; + uiMin; + uiMax; + steps; + options; + onChange; + $text; + $btnInc; + $btnDec; + $range; + onInput; + onRangeInput; + onClick; + onPointerUp; + onPointerDown; + setValue; + normalizeValue; + static create(key, value, min, max, options = {}, onChange) { + options = options || {}, options.suffix = options.suffix || "", options.disabled = !!options.disabled, options.hideSlider = !!options.hideSlider; + let $text, $btnInc, $btnDec, $range, self = CE("div", { + class: "bx-number-stepper", + id: `bx_setting_${escapeCssSelector(key)}` + }, CE("div", {}, $btnDec = CE("button", { + _dataset: { + type: "dec" + }, + type: "button", + class: options.hideSlider ? "bx-focusable" : "", + tabindex: options.hideSlider ? 0 : -1 + }, "-"), $text = CE("span"), $btnInc = CE("button", { + _dataset: { + type: "inc" + }, + type: "button", + class: options.hideSlider ? "bx-focusable" : "", + tabindex: options.hideSlider ? 0 : -1 + }, "+"))); + if (self.$text = $text, self.$btnInc = $btnInc, self.$btnDec = $btnDec, self.onChange = onChange, self.onInput = BxNumberStepper.onInput.bind(self), self.onRangeInput = BxNumberStepper.onRangeInput.bind(self), self.onClick = BxNumberStepper.onClick.bind(self), self.onPointerUp = BxNumberStepper.onPointerUp.bind(self), self.onPointerDown = BxNumberStepper.onPointerDown.bind(self), self.controlMin = min, self.controlMax = max, self.isHolding = !1, self.options = options, self.uiMin = options.reverse ? -max : min, self.uiMax = options.reverse ? -min : max, self.steps = Math.max(options.steps || 1, 1), BxNumberStepper.setValue.call(self, value), options.disabled) return $btnInc.disabled = !0, $btnInc.classList.add("bx-inactive"), $btnDec.disabled = !0, $btnDec.classList.add("bx-inactive"), self.disabled = !0, self; + if ($range = CE("input", { + id: `bx_inp_setting_${key}`, + type: "range", + min: self.uiMin, + max: self.uiMax, + value: options.reverse ? -value : value, + step: self.steps, + tabindex: 0 + }), self.$range = $range, options.hideSlider && $range.classList.add("bx-gone"), $range.addEventListener("input", self.onRangeInput), self.addEventListener("input", self.onInput), self.appendChild($range), options.ticks || options.exactTicks) { + let markersId = `markers-${key}`, $markers = CE("datalist", { id: markersId }); + if ($range.setAttribute("list", markersId), options.exactTicks) { + let start = Math.max(Math.floor(min / options.exactTicks), 1) * options.exactTicks; + if (start === min) start += options.exactTicks; + for (let i = start;i < max; i += options.exactTicks) + $markers.appendChild(CE("option", { + value: options.reverse ? -i : i + })); + } else for (let i = self.uiMin + options.ticks;i < self.uiMax; i += options.ticks) + $markers.appendChild(CE("option", { value: i })); + self.appendChild($markers); + } + return BxNumberStepper.updateButtonsVisibility.call(self), self.addEventListener("click", self.onClick), self.addEventListener("pointerdown", self.onPointerDown), self.addEventListener("contextmenu", BxNumberStepper.onContextMenu), setNearby(self, { + focus: options.hideSlider ? $btnInc : $range + }), Object.defineProperty(self, "value", { + get() { + return self.controlValue; + }, + set(value2) { + BxNumberStepper.setValue.call(self, value2); + } + }), self; + } + static setValue(value) { + if (this.controlValue = BxNumberStepper.normalizeValue.call(this, value), this.$text.textContent = BxNumberStepper.updateTextValue.call(this), this.$range) this.$range.value = this.options.reverse ? -value : value; + BxNumberStepper.updateButtonsVisibility.call(this); + } + static normalizeValue(value) { + return value = parseInt(value), value = Math.max(this.controlMin, value), value = Math.min(this.controlMax, value), value; + } + static onInput(e) { + BxEvent.dispatch(this.$range, "input"); + } + static onRangeInput(e) { + let value = parseInt(e.target.value); + if (this.options.reverse) value *= -1; + if (BxNumberStepper.setValue.call(this, value), BxNumberStepper.updateButtonsVisibility.call(this), !e.ignoreOnChange && this.onChange) this.onChange(e, value); + } + static onClick(e) { + if (e.preventDefault(), this.isHolding) return; + let $btn = e.target.closest("button"); + $btn && BxNumberStepper.buttonPressed.call(this, e, $btn), BxNumberStepper.clearIntervalId.call(this), this.isHolding = !1; + } + static onPointerDown(e) { + BxNumberStepper.clearIntervalId.call(this); + let $btn = e.target.closest("button"); + if (!$btn) return; + this.isHolding = !0, e.preventDefault(), this.intervalId = window.setInterval((e2) => { + BxNumberStepper.buttonPressed.call(this, e2, $btn); + }, 200), window.addEventListener("pointerup", this.onPointerUp, { once: !0 }), window.addEventListener("pointercancel", this.onPointerUp, { once: !0 }); + } + static onPointerUp(e) { + BxNumberStepper.clearIntervalId.call(this), this.isHolding = !1; + } + static onContextMenu(e) { + e.preventDefault(); + } + static updateTextValue() { + let value = this.controlValue, textContent = null; + if (this.options.customTextValue) textContent = this.options.customTextValue(value, this.controlMin, this.controlMax); + if (textContent === null) textContent = value.toString() + this.options.suffix; + return textContent; + } + static buttonPressed(e, $btn) { + let value = this.controlValue; + if (value = this.options.reverse ? -value : value, $btn.dataset.type === "dec") value = Math.max(this.uiMin, value - this.steps); + else value = Math.min(this.uiMax, value + this.steps); + value = this.options.reverse ? -value : value, BxNumberStepper.setValue.call(this, value), BxNumberStepper.updateButtonsVisibility.call(this), this.onChange && this.onChange(e, value); + } + static clearIntervalId() { + this.intervalId && clearInterval(this.intervalId), this.intervalId = null; + } + static updateButtonsVisibility() { + if (this.$btnDec.classList.toggle("bx-inactive", this.controlValue === this.uiMin), this.$btnInc.classList.toggle("bx-inactive", this.controlValue === this.uiMax), this.controlValue === this.uiMin || this.controlValue === this.uiMax) BxNumberStepper.clearIntervalId.call(this); + } +} +class SettingElement { + static renderOptions(key, setting, currentValue, onChange) { + let $control = CE("select", { + tabindex: 0 + }), $parent; + if (setting.optionsGroup) $parent = CE("optgroup", { + label: setting.optionsGroup + }), $control.appendChild($parent); + else $parent = $control; + for (let value in setting.options) { + let label = setting.options[value], $option = CE("option", { value }, label); + $parent.appendChild($option); + } + return $control.value = currentValue, onChange && $control.addEventListener("input", (e) => { + let target = e.target, value = setting.type && setting.type === "number" ? parseInt(target.value) : target.value; + !e.ignoreOnChange && onChange(e, value); + }), $control.setValue = (value) => { + $control.value = value; + }, $control; + } + static renderMultipleOptions(key, setting, currentValue, onChange, params = {}) { + let $control = CE("select", { + multiple: !0, + tabindex: 0 + }), size = params.size ? params.size : Object.keys(setting.multipleOptions).length; + $control.setAttribute("size", size.toString()); + for (let value in setting.multipleOptions) { + let label = setting.multipleOptions[value], $option = CE("option", { value }, label); + $option.selected = currentValue.indexOf(value) > -1, $option.addEventListener("mousedown", function(e) { + e.preventDefault(); + let target = e.target; + target.selected = !target.selected; + let $parent = target.parentElement; + $parent.focus(), BxEvent.dispatch($parent, "input"); + }), $control.appendChild($option); + } + return $control.addEventListener("mousedown", function(e) { + let self = this, orgScrollTop = self.scrollTop; + window.setTimeout(() => self.scrollTop = orgScrollTop, 0); + }), $control.addEventListener("mousemove", (e) => e.preventDefault()), onChange && $control.addEventListener("input", (e) => { + let target = e.target, values = Array.from(target.selectedOptions).map((i) => i.value); + !e.ignoreOnChange && onChange(e, values); + }), $control; + } + static renderCheckbox(key, setting, currentValue, onChange) { + let $control = CE("input", { type: "checkbox", tabindex: 0 }); + return $control.checked = currentValue, onChange && $control.addEventListener("input", (e) => { + !e.ignoreOnChange && onChange(e, e.target.checked); + }), $control.setValue = (value) => { + $control.checked = !!value; + }, $control; + } + static renderNumberStepper(key, setting, value, onChange, options = {}) { + return BxNumberStepper.create(key, value, setting.min, setting.max, options, onChange); + } + static METHOD_MAP = { + options: SettingElement.renderOptions, + "multiple-options": SettingElement.renderMultipleOptions, + "number-stepper": SettingElement.renderNumberStepper, + checkbox: SettingElement.renderCheckbox + }; + static render(type, key, setting, currentValue, onChange, options) { + let method = SettingElement.METHOD_MAP[type], $control = method(...Array.from(arguments).slice(1)); + if (type !== "number-stepper") $control.id = `bx_setting_${escapeCssSelector(key)}`; + if (type === "options" || type === "multiple-options") $control.name = $control.id; + return $control; + } + static fromPref(key, storage, onChange, overrideParams = {}) { + let definition = storage.getDefinition(key), currentValue = storage.getSetting(key), type; + if ("options" in definition) type = "options"; + else if ("multipleOptions" in definition) type = "multiple-options"; + else if (typeof definition.default === "number") type = "number-stepper"; + else type = "checkbox"; + let params = {}; + if ("params" in definition) params = Object.assign(overrideParams, definition.params || {}); + if (params.disabled) currentValue = definition.default; + return SettingElement.render(type, key, definition, currentValue, (e, value) => { + storage.setSetting(key, value), onChange && onChange(e, value); + }, params); + } +} class FullscreenText { static instance; static getInstance = () => FullscreenText.instance ?? (FullscreenText.instance = new FullscreenText); @@ -3501,21 +3798,159 @@ class FullscreenText { document.body.classList.remove("bx-no-scroll"), this.$text.classList.add("bx-gone"); } } -function showGamepadToast(gamepad) { - if (gamepad.id === VIRTUAL_GAMEPAD_ID) return; - BxLogger.info("Gamepad", gamepad); - let text = "🎮"; - if (getPref("local_co_op_enabled")) text += ` #${gamepad.index + 1}`; - let gamepadId = gamepad.id.replace(/ \(.*?Vendor: \w+ Product: \w+\)$/, ""); - text += ` - ${gamepadId}`; - let status; - if (gamepad.connected) status = (gamepad.vibrationActuator ? "✅" : "❌") + " " + t("vibration-status"); - else status = t("disconnected"); - Toast.show(text, status, { instant: !1 }); +class SuggestionsSetting { + static async renderSuggestions(e) { + let $btnSuggest = e.target.closest("div"); + $btnSuggest.toggleAttribute("bx-open"); + let $content = $btnSuggest.nextElementSibling; + if ($content) { + BxEvent.dispatch($content.querySelector("select"), "input"); + return; + } + let settingTabGroup; + for (settingTabGroup in this.SETTINGS_UI) { + let settingTab = this.SETTINGS_UI[settingTabGroup]; + if (!settingTab || !settingTab.items || typeof settingTab.items === "function") continue; + for (let settingTabContent of settingTab.items) { + if (!settingTabContent || settingTabContent instanceof HTMLElement || !settingTabContent.items) continue; + for (let setting of settingTabContent.items) { + let prefKey; + if (typeof setting === "string") prefKey = setting; + else if (typeof setting === "object") prefKey = setting.pref; + if (prefKey) this.suggestedSettingLabels[prefKey] = settingTabContent.label; + } + } + } + let recommendedDevice = ""; + if (BX_FLAGS.DeviceInfo.deviceType.includes("android")) { + if (BX_FLAGS.DeviceInfo.androidInfo) recommendedDevice = await SuggestionsSetting.getRecommendedSettings.call(this, BX_FLAGS.DeviceInfo.androidInfo); + } + let hasRecommendedSettings = Object.keys(this.suggestedSettings.recommended).length > 0, deviceType = BX_FLAGS.DeviceInfo.deviceType; + if (deviceType === "android-handheld") SuggestionsSetting.addDefaultSuggestedSetting.call(this, "touchController.mode", "off"), SuggestionsSetting.addDefaultSuggestedSetting.call(this, "deviceVibration.mode", "on"); + else if (deviceType === "android") SuggestionsSetting.addDefaultSuggestedSetting.call(this, "deviceVibration.mode", "auto"); + else if (deviceType === "android-tv") SuggestionsSetting.addDefaultSuggestedSetting.call(this, "touchController.mode", "off"); + SuggestionsSetting.generateDefaultSuggestedSettings.call(this); + let $suggestedSettings = CE("div", { class: "bx-suggest-wrapper" }), $select = CE("select", {}, hasRecommendedSettings && CE("option", { value: "recommended" }, t("recommended")), !hasRecommendedSettings && CE("option", { value: "highest" }, t("highest-quality")), CE("option", { value: "default" }, t("default")), CE("option", { value: "lowest" }, t("lowest-quality"))); + $select.addEventListener("input", (e2) => { + let profile = $select.value; + removeChildElements($suggestedSettings); + let fragment = document.createDocumentFragment(), note; + if (profile === "recommended") note = t("recommended-settings-for-device", { device: recommendedDevice }); + else if (profile === "highest") note = "⚠️ " + t("highest-quality-note"); + note && fragment.appendChild(CE("div", { class: "bx-suggest-note" }, note)); + let settings = this.suggestedSettings[profile], prefKey; + for (prefKey in settings) { + let suggestedValue, definition = getPrefDefinition(prefKey); + if (definition && definition.transformValue) suggestedValue = definition.transformValue.get.call(definition, settings[prefKey]); + else suggestedValue = settings[prefKey]; + let currentValue = getPref(prefKey, !1), currentValueText = STORAGE.Global.getValueText(prefKey, currentValue), isSameValue = currentValue === suggestedValue, $child, $value; + if (isSameValue) $value = currentValueText; + else { + let suggestedValueText = STORAGE.Global.getValueText(prefKey, suggestedValue); + $value = currentValueText + " ➔ " + suggestedValueText; + } + let $checkbox, breadcrumb = this.suggestedSettingLabels[prefKey] + " ❯ " + STORAGE.Global.getLabel(prefKey), id = escapeCssSelector(`bx_suggest_${prefKey}`); + if ($child = CE("div", { + class: `bx-suggest-row ${isSameValue ? "bx-suggest-ok" : "bx-suggest-change"}` + }, $checkbox = CE("input", { + type: "checkbox", + tabindex: 0, + checked: !0, + id + }), CE("label", { + for: id + }, CE("div", { + class: "bx-suggest-label" + }, breadcrumb), CE("div", { + class: "bx-suggest-value" + }, $value))), isSameValue) + $checkbox.disabled = !0, $checkbox.checked = !0; + fragment.appendChild($child); + } + $suggestedSettings.appendChild(fragment); + }), BxEvent.dispatch($select, "input"); + let onClickApply = () => { + let profile = $select.value, settings = this.suggestedSettings[profile], prefKey; + for (prefKey in settings) { + let suggestedValue = settings[prefKey], $checkBox = $content.querySelector(`#bx_suggest_${escapeCssSelector(prefKey)}`); + if (!$checkBox.checked || $checkBox.disabled) continue; + let $control = this.settingElements[prefKey]; + if (!$control) { + setPref(prefKey, suggestedValue); + continue; + } + let settingDefinition = getPrefDefinition(prefKey); + if (settingDefinition.transformValue) suggestedValue = settingDefinition.transformValue.get.call(settingDefinition, suggestedValue); + if ("setValue" in $control) $control.setValue(suggestedValue); + else $control.value = suggestedValue; + BxEvent.dispatch($control, "input", { + manualTrigger: !0 + }); + } + BxEvent.dispatch($select, "input"); + }, $btnApply = createButton({ + label: t("apply"), + style: 128 | 64, + onClick: onClickApply + }); + $content = CE("div", { + class: "bx-sub-content-box bx-suggest-box", + _nearby: { + orientation: "vertical" + } + }, BxSelectElement.create($select, !0), $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", + tabindex: 0 + }, "🤓 " + t("how-to-improve-app-performance")), BX_FLAGS.DeviceInfo.deviceType.includes("android") && !hasRecommendedSettings && CE("a", { + class: "bx-suggest-link bx-focusable", + href: "https://github.com/redphx/better-xcloud-devices", + target: "_blank", + tabindex: 0 + }, t("suggest-settings-link"))), $btnSuggest.insertAdjacentElement("afterend", $content); + } + static async getRecommendedSettings(androidInfo) { + function normalize(str) { + return str.toLowerCase().trim().replaceAll(/\s+/g, "-").replaceAll(/-+/g, "-"); + } + try { + let { brand, board, model } = androidInfo; + brand = normalize(brand), board = normalize(board), model = normalize(model); + let url = GhPagesUtils.getUrl(`devices/${brand}/${board}-${model}.json`), json = await (await NATIVE_FETCH(url)).json(), recommended = {}; + if (json.schema_version !== 2) return null; + let scriptSettings = json.settings.script; + if (scriptSettings._base) { + let base = typeof scriptSettings._base === "string" ? [scriptSettings._base] : scriptSettings._base; + for (let profile of base) + Object.assign(recommended, this.suggestedSettings[profile]); + delete scriptSettings._base; + } + let key; + for (key in scriptSettings) + recommended[key] = scriptSettings[key]; + return BX_FLAGS.DeviceInfo.deviceType = json.device_type, this.suggestedSettings.recommended = recommended, json.device_name; + } catch (e) {} + return null; + } + static addDefaultSuggestedSetting(prefKey, value) { + let key; + for (key in this.suggestedSettings) + if (key !== "default" && !(prefKey in this.suggestedSettings)) this.suggestedSettings[key][prefKey] = value; + } + static generateDefaultSuggestedSettings() { + let key; + for (key in this.suggestedSettings) { + if (key === "default") continue; + let prefKey; + for (prefKey in this.suggestedSettings[key]) + if (!(prefKey in this.suggestedSettings.default)) this.suggestedSettings.default[prefKey] = getPrefDefinition(prefKey).default; + } + } } -class SettingsNavigationDialog extends NavigationDialog { +class SettingsDialog extends NavigationDialog { static instance; - static getInstance = () => SettingsNavigationDialog.instance ?? (SettingsNavigationDialog.instance = new SettingsNavigationDialog); + static getInstance = () => SettingsDialog.instance ?? (SettingsDialog.instance = new SettingsDialog); LOG_TAG = "SettingsNavigationDialog"; $container; $tabs; @@ -3539,11 +3974,11 @@ class SettingsNavigationDialog extends NavigationDialog { helpUrl: "https://better-xcloud.github.io/features/", items: [ ($parent) => { - let PREF_LATEST_VERSION = getPref("version_latest"), topButtons = []; + let PREF_LATEST_VERSION = getPref("version.latest"), topButtons = []; if (!SCRIPT_VERSION.includes("beta") && PREF_LATEST_VERSION && PREF_LATEST_VERSION != SCRIPT_VERSION) { let opts = { label: "🌟 " + t("new-version-available", { version: PREF_LATEST_VERSION }), - style: 1 | 32 | 64 + style: 1 | 64 | 128 }; if (AppInterface && AppInterface.updateLatestScript) opts.onClick = (e) => AppInterface.updateLatestScript(); else opts.url = "https://github.com/redphx/better-xcloud/releases/latest"; @@ -3552,20 +3987,20 @@ class SettingsNavigationDialog extends NavigationDialog { if (AppInterface) topButtons.push(createButton({ label: t("app-settings"), icon: BxIcon.STREAM_SETTINGS, - style: 64 | 32, + style: 128 | 64, onClick: (e) => { AppInterface.openAppSettings && AppInterface.openAppSettings(), this.hide(); } })); else if (UserAgent.getDefault().toLowerCase().includes("android")) topButtons.push(createButton({ label: "🔥 " + t("install-android"), - style: 64 | 32, + style: 128 | 64, url: "https://better-xcloud.github.io/android" })); this.$btnGlobalReload = createButton({ label: t("settings-reload"), classes: ["bx-settings-reload-button", "bx-gone"], - style: 32 | 64, + style: 64 | 128, onClick: (e) => { this.reloadPage(); } @@ -3574,7 +4009,7 @@ class SettingsNavigationDialog extends NavigationDialog { }, t("settings-reload-note")), topButtons.push(this.$noteGlobalReload), this.$btnSuggestion = CE("div", { class: "bx-suggest-toggler bx-focusable", tabindex: 0 - }, CE("label", {}, t("suggest-settings")), CE("span", {}, "❯")), this.$btnSuggestion.addEventListener("click", this.renderSuggestions.bind(this)), topButtons.push(this.$btnSuggestion); + }, CE("label", {}, t("suggest-settings")), CE("span", {}, "❯")), this.$btnSuggestion.addEventListener("click", SuggestionsSetting.renderSuggestions.bind(this)), topButtons.push(this.$btnSuggestion); let $div = CE("div", { class: "bx-top-buttons", _nearby: { @@ -3583,113 +4018,129 @@ class SettingsNavigationDialog extends NavigationDialog { }, ...topButtons); $parent.appendChild($div); }, - "bx_locale", - "server_bypass_restriction", - "ui_controller_friendly", - "xhome_enabled" + { + pref: "bx.locale", + multiLines: !0 + }, + "server.bypassRestriction", + "ui.controllerFriendly", + "xhome.enabled" ] }, { group: "server", label: t("server"), items: [ - "server_region", - "stream_preferred_locale", - "prefer_ipv6_server" + { + pref: "server.region", + multiLines: !0 + }, + { + pref: "stream.locale", + multiLines: !0 + }, + "server.ipv6.prefer" ] }, { group: "stream", label: t("stream"), items: [ - "stream_target_resolution", - "stream_codec_profile", - "bitrate_video_max", - "audio_enable_volume_control", - "stream_disable_feedback_dialog", - "screenshot_apply_filters", - "audio_mic_on_playing", - "game_fortnite_force_console", - "stream_combine_sources" - ] - }, { - requiredVariants: "full", - group: "co-op", - label: t("local-co-op"), - items: [ - "local_co_op_enabled" + "stream.video.resolution", + "stream.video.codecProfile", + "stream.video.maxBitrate", + "audio.volume.booster.enabled", + "screenshot.applyFilters", + "audio.mic.onPlaying", + "game.fortnite.forceConsole", + "stream.video.combineAudio" ] }, { requiredVariants: "full", group: "mkb", label: t("mouse-and-keyboard"), - unsupportedNote: !STATES.userAgent.capabilities.mkb ? CE("a", { - href: "https://github.com/redphx/better-xcloud/issues/206#issuecomment-1920475657", - target: "_blank" - }, "⚠️ " + t("browser-unsupported-feature")) : null, - unsupported: !STATES.userAgent.capabilities.mkb, items: [ - "native_mkb_enabled", - "game_msfs2020_force_native_mkb", - "mkb_enabled", - "mkb_hide_idle_cursor" - ] + "nativeMkb.mode", + { + pref: "nativeMkb.forcedGames", + multiLines: !0 + }, + "mkb.enabled", + "mkb.cursor.hideIdle" + ], + ...!STATES.browser.capabilities.emulatedNativeMkb && (!STATES.userAgent.capabilities.mkb || !STATES.browser.capabilities.mkb) ? { + unsupported: !0, + unsupportedNote: CE("a", { + href: "https://github.com/redphx/better-xcloud/issues/206#issuecomment-1920475657", + target: "_blank" + }, "⚠️ " + t("browser-unsupported-feature")) + } : {} }, { requiredVariants: "full", group: "touch-control", label: t("touch-controller"), - unsupported: !STATES.userAgent.capabilities.touch, - unsupportedNote: !STATES.userAgent.capabilities.touch ? "⚠️ " + t("device-unsupported-touch") : null, items: [ - "stream_touch_controller", - "stream_touch_controller_auto_off", - "stream_touch_controller_default_opacity", - "stream_touch_controller_style_standard", - "stream_touch_controller_style_custom" - ] + { + pref: "touchController.mode", + note: CE("a", { href: "https://github.com/redphx/better-xcloud/discussions/241", target: "_blank" }, t("unofficial-game-list")) + }, + "touchController.autoOff", + "touchController.opacity.default", + "touchController.style.standard", + "touchController.style.custom" + ], + ...!STATES.userAgent.capabilities.touch ? { + unsupported: !0, + unsupportedNote: "⚠️ " + t("device-unsupported-touch") + } : {} }, { group: "ui", label: t("ui"), items: [ - "ui_layout", - "ui_game_card_show_wait_time", - "controller_show_connection_status", - "stream_simplify_menu", - "skip_splash_video", - !AppInterface && "ui_scrollbar_hide", - "hide_dots_icon", - "reduce_animations", - "block_social_features", - "ui_hide_sections" + "ui.layout", + "ui.gameCard.waitTime.show", + "ui.controllerStatus.show", + "ui.streamMenu.simplify", + "ui.splashVideo.skip", + !AppInterface && "ui.hideScrollbar", + "ui.systemMenu.hideHandle", + "ui.feedbackDialog.disabled", + "ui.reduceAnimations", + "block.social", + "feature.byog.disabled", + { + pref: "ui.hideSections", + multiLines: !0 + } ] }, { requiredVariants: "full", group: "game-bar", label: t("game-bar"), items: [ - "game_bar_position" + "gameBar.position" ] }, { group: "loading-screen", label: t("loading-screen"), items: [ - "ui_loading_screen_game_art", - "ui_loading_screen_wait_time", - "ui_loading_screen_rocket" + "loadingScreen.gameArt.show", + "loadingScreen.waitTime.show", + "loadingScreen.rocket" ] }, { group: "other", label: t("other"), items: [ - "block_tracking" + "block.tracking" ] }, { group: "advanced", label: t("advanced"), items: [ { - pref: "user_agent_profile", + pref: "userAgent.profile", + multiLines: !0, onCreated: (setting, $control) => { let defaultUserAgent = window.navigator.orgUserAgent || window.navigator.userAgent, $inpCustomUserAgent = CE("input", { - id: `bx_setting_inp_${setting.pref}`, type: "text", placeholder: defaultUserAgent, autocomplete: "off", @@ -3708,14 +4159,6 @@ class SettingsNavigationDialog extends NavigationDialog { }, { group: "footer", items: [ - ($parent) => { - $parent.appendChild(CE("a", { - class: "bx-donation-link", - href: "https://ko-fi.com/redphx", - target: "_blank", - tabindex: 0 - }, `❤️ ${t("support-better-xcloud")}`)); - }, ($parent) => { try { let appVersion = document.querySelector("meta[name=gamepass-app-version]").content, appDate = new Date(document.querySelector("meta[name=gamepass-app-date]").content).toISOString().substring(0, 10); @@ -3725,25 +4168,45 @@ class SettingsNavigationDialog extends NavigationDialog { } catch (e) {} }, ($parent) => { - let debugInfo = deepClone(BX_FLAGS.DeviceInfo); - debugInfo.settings = JSON.parse(window.localStorage.getItem("better_xcloud") || "{}"); - let $debugInfo = CE("div", { class: "bx-debug-info" }, createButton({ - label: "Debug info", - style: 4 | 64 | 32, + $parent.appendChild(CE("a", { + class: "bx-donation-link", + href: "https://ko-fi.com/redphx", + target: "_blank", + tabindex: 0 + }, `❤️ ${t("support-better-xcloud")}`)); + }, + ($parent) => { + $parent.appendChild(createButton({ + label: t("clear-data"), + style: 8 | 128 | 64, onClick: (e) => { - let $pre = e.target.closest("button")?.nextElementSibling; + if (confirm(t("clear-data-confirm"))) clearAllData(); + } + })); + }, + ($parent) => { + $parent.appendChild(CE("div", { class: "bx-debug-info" }, createButton({ + label: "Debug info", + style: 8 | 128 | 64, + onClick: (e) => { + let $button = e.target.closest("button"); + if (!$button) return; + let $pre = $button.nextElementSibling; + if (!$pre) { + let debugInfo = deepClone(BX_FLAGS.DeviceInfo); + debugInfo.settings = JSON.parse(window.localStorage.getItem("BetterXcloud") || "{}"), $pre = CE("pre", { + class: "bx-focusable bx-gone", + tabindex: 0, + _on: { + click: async (e2) => { + await copyToClipboard(e2.target.innerText); + } + } + }, "```\n" + JSON.stringify(debugInfo, null, " ") + "\n```"), $button.insertAdjacentElement("afterend", $pre); + } $pre.classList.toggle("bx-gone"), $pre.scrollIntoView(); } - }), CE("pre", { - class: "bx-focusable bx-gone", - tabindex: 0, - on: { - click: async (e) => { - await copyToClipboard(e.target.innerText); - } - } - }, "```\n" + JSON.stringify(debugInfo, null, " ") + "\n```")); - $parent.appendChild($debugInfo); + }))); } ] }]; @@ -3753,18 +4216,18 @@ class SettingsNavigationDialog extends NavigationDialog { label: t("audio"), helpUrl: "https://better-xcloud.github.io/ingame-features/#audio", items: [{ - pref: "audio_volume", + pref: "audio.volume", onChange: (e, value) => { SoundShortcut.setGainNodeVolume(value); }, params: { - disabled: !getPref("audio_enable_volume_control") + disabled: !getPref("audio.volume.booster.enabled") }, onCreated: (setting, $elm) => { let $range = $elm.querySelector("input[type=range"); window.addEventListener(BxEvent.SETTINGS_CHANGED, (e) => { let { storageKey, settingKey, settingValue } = e; - if (storageKey !== "better_xcloud" || settingKey !== "audio_volume") return; + if (storageKey !== "BetterXcloud" || settingKey !== "audio.volume") return; $range.value = settingValue, BxEvent.dispatch($range, "input", { ignoreOnChange: !0 }); @@ -3776,116 +4239,95 @@ class SettingsNavigationDialog extends NavigationDialog { label: t("video"), helpUrl: "https://better-xcloud.github.io/ingame-features/#video", items: [{ - pref: "video_player_type", + pref: "video.player.type", onChange: onChangeVideoPlayerType }, { - pref: "video_max_fps", + pref: "video.maxFps", onChange: (e) => { limitVideoPlayerFps(parseInt(e.target.value)); } }, { - pref: "video_power_preference", + pref: "video.player.powerPreference", onChange: () => { let streamPlayer = STATES.currentStream.streamPlayer; if (!streamPlayer) return; streamPlayer.reloadPlayer(), updateVideoPlayer(); } }, { - pref: "video_processing", + pref: "video.processing", onChange: updateVideoPlayer }, { - pref: "video_ratio", + pref: "video.ratio", onChange: updateVideoPlayer }, { - pref: "video_sharpness", + pref: "video.processing.sharpness", onChange: updateVideoPlayer }, { - pref: "video_saturation", + pref: "video.saturation", onChange: updateVideoPlayer }, { - pref: "video_contrast", + pref: "video.contrast", onChange: updateVideoPlayer }, { - pref: "video_brightness", + pref: "video.brightness", onChange: updateVideoPlayer }] }]; TAB_CONTROLLER_ITEMS = [ + !1, { group: "controller", label: t("controller"), helpUrl: "https://better-xcloud.github.io/ingame-features/#controller", - items: [{ - pref: "controller_enable_vibration", - unsupported: !VibrationManager.supportControllerVibration(), - onChange: () => VibrationManager.updateGlobalVars() - }, { - pref: "controller_device_vibration", - unsupported: !VibrationManager.supportDeviceVibration(), - onChange: () => VibrationManager.updateGlobalVars() - }, (VibrationManager.supportControllerVibration() || VibrationManager.supportDeviceVibration()) && { - pref: "controller_vibration_intensity", - unsupported: !VibrationManager.supportDeviceVibration(), - onChange: () => VibrationManager.updateGlobalVars() - }, !1] + items: [ + !1, + !1, + !1 + ] }, !1 ]; - TAB_VIRTUAL_CONTROLLER_ITEMS = () => [{ - group: "mkb", - label: t("virtual-controller"), - helpUrl: "https://better-xcloud.github.io/mouse-and-keyboard/", - content: MkbRemapper.getInstance().render() - }]; - TAB_NATIVE_MKB_ITEMS = [{ - requiredVariants: "full", - group: "native-mkb", - label: t("native-mkb"), - items: [] - }]; - TAB_SHORTCUTS_ITEMS = () => [{ - requiredVariants: "full", - group: "controller-shortcuts", - label: t("controller-shortcuts"), - content: !1 - }]; + TAB_MKB_ITEMS = () => [ + !1, + !1 + ]; TAB_STATS_ITEMS = [{ group: "stats", label: t("stream-stats"), helpUrl: "https://better-xcloud.github.io/stream-stats/", items: [ { - pref: "stats_show_when_playing" + pref: "stats.showWhenPlaying" }, { - pref: "stats_quick_glance", + pref: "stats.quickGlance.enabled", onChange: (e) => { let streamStats = StreamStats.getInstance(); e.target.checked ? streamStats.quickGlanceSetup() : streamStats.quickGlanceStop(); } }, { - pref: "stats_items", + pref: "stats.items", onChange: StreamStats.refreshStyles }, { - pref: "stats_position", + pref: "stats.position", onChange: StreamStats.refreshStyles }, { - pref: "stats_text_size", + pref: "stats.textSize", onChange: StreamStats.refreshStyles }, { - pref: "stats_opacity", + pref: "stats.opacity", onChange: StreamStats.refreshStyles }, { - pref: "stats_transparent", + pref: "stats.transparent", onChange: StreamStats.refreshStyles }, { - pref: "stats_conditional_formatting", + pref: "stats.colors", onChange: StreamStats.refreshStyles } ] @@ -3908,14 +4350,6 @@ class SettingsNavigationDialog extends NavigationDialog { requiredVariants: "full" }, mkb: !1, - "native-mkb": !1, - shortcuts: { - group: "shortcuts", - icon: BxIcon.COMMAND, - items: this.TAB_SHORTCUTS_ITEMS, - lazyContent: !0, - requiredVariants: "full" - }, stats: { group: "stats", icon: BxIcon.STREAM_STATS, @@ -3924,7 +4358,11 @@ class SettingsNavigationDialog extends NavigationDialog { }; constructor() { super(); - BxLogger.info(this.LOG_TAG, "constructor()"), this.renderFullSettings = STATES.supportedRegion && STATES.isSignedIn, this.setupDialog(); + BxLogger.info(this.LOG_TAG, "constructor()"), this.renderFullSettings = STATES.supportedRegion && STATES.isSignedIn, this.setupDialog(), this.onMountedCallbacks.push(() => { + if (onChangeVideoPlayerType(), STATES.userAgent.capabilities.touch) BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED); + let $selectUserAgent = document.querySelector(`#bx_setting_${escapeCssSelector("userAgent.profile")}`); + if ($selectUserAgent) $selectUserAgent.disabled = !0, BxEvent.dispatch($selectUserAgent, "input", {}), $selectUserAgent.disabled = !1; + }); } getDialog() { return this; @@ -3933,186 +4371,45 @@ class SettingsNavigationDialog extends NavigationDialog { return this.$container; } onMounted() { - if (!this.renderFullSettings) return; - if (onChangeVideoPlayerType(), STATES.userAgent.capabilities.touch) BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED); - let $selectUserAgent = document.querySelector(`#bx_setting_${"user_agent_profile"}`); - if ($selectUserAgent) $selectUserAgent.disabled = !0, BxEvent.dispatch($selectUserAgent, "input", {}), $selectUserAgent.disabled = !1; + super.onMounted(); + } + isOverlayVisible() { + return !STATES.isPlaying; } reloadPage() { this.$btnGlobalReload.disabled = !0, this.$btnGlobalReload.firstElementChild.textContent = t("settings-reloading"), this.hide(), FullscreenText.getInstance().show(t("settings-reloading")), window.location.reload(); } - async getRecommendedSettings(androidInfo) { - function normalize(str) { - return str.toLowerCase().trim().replaceAll(/\s+/g, "-").replaceAll(/-+/g, "-"); - } - try { - let { brand, board, model } = androidInfo; - brand = normalize(brand), board = normalize(board), model = normalize(model); - let url = `https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/devices/${brand}/${board}-${model}.json`, json = await (await NATIVE_FETCH(url)).json(), recommended = {}; - if (json.schema_version !== 1) return null; - let scriptSettings = json.settings.script; - if (scriptSettings._base) { - let base = typeof scriptSettings._base === "string" ? [scriptSettings._base] : scriptSettings._base; - for (let profile of base) - Object.assign(recommended, this.suggestedSettings[profile]); - delete scriptSettings._base; - } - let key; - for (key in scriptSettings) - recommended[key] = scriptSettings[key]; - return BX_FLAGS.DeviceInfo.deviceType = json.device_type, this.suggestedSettings.recommended = recommended, json.device_name; - } catch (e) {} - return null; - } - addDefaultSuggestedSetting(prefKey, value) { - let key; - for (key in this.suggestedSettings) - if (key !== "default" && !(prefKey in this.suggestedSettings)) this.suggestedSettings[key][prefKey] = value; - } - generateDefaultSuggestedSettings() { - let key; - for (key in this.suggestedSettings) { - if (key === "default") continue; - let prefKey; - for (prefKey in this.suggestedSettings[key]) - if (!(prefKey in this.suggestedSettings.default)) this.suggestedSettings.default[prefKey] = getPrefDefinition(prefKey).default; - } - } isSupportedVariant(requiredVariants) { if (typeof requiredVariants === "undefined") return !0; return requiredVariants = typeof requiredVariants === "string" ? [requiredVariants] : requiredVariants, requiredVariants.includes(SCRIPT_VARIANT); } - async renderSuggestions(e) { - let $btnSuggest = e.target.closest("div"); - $btnSuggest.toggleAttribute("bx-open"); - let $content = $btnSuggest.nextElementSibling; - if ($content) { - BxEvent.dispatch($content.querySelector("select"), "input"); - return; - } - let settingTabGroup; - for (settingTabGroup in this.SETTINGS_UI) { - let settingTab = this.SETTINGS_UI[settingTabGroup]; - if (!settingTab || !settingTab.items || typeof settingTab.items === "function") continue; - for (let settingTabContent of settingTab.items) { - if (!settingTabContent || !settingTabContent.items) continue; - for (let setting of settingTabContent.items) { - let prefKey; - if (typeof setting === "string") prefKey = setting; - else if (typeof setting === "object") prefKey = setting.pref; - if (prefKey) this.suggestedSettingLabels[prefKey] = settingTabContent.label; - } - } - } - let recommendedDevice = ""; - if (BX_FLAGS.DeviceInfo.deviceType.includes("android")) { - if (BX_FLAGS.DeviceInfo.androidInfo) recommendedDevice = await this.getRecommendedSettings(BX_FLAGS.DeviceInfo.androidInfo); - } - let hasRecommendedSettings = Object.keys(this.suggestedSettings.recommended).length > 0, deviceType = BX_FLAGS.DeviceInfo.deviceType; - if (deviceType === "android-handheld") this.addDefaultSuggestedSetting("stream_touch_controller", "off"), this.addDefaultSuggestedSetting("controller_device_vibration", "on"); - else if (deviceType === "android") this.addDefaultSuggestedSetting("controller_device_vibration", "auto"); - else if (deviceType === "android-tv") this.addDefaultSuggestedSetting("stream_touch_controller", "off"); - this.generateDefaultSuggestedSettings(); - let $suggestedSettings = CE("div", { class: "bx-suggest-wrapper" }), $select = CE("select", {}, hasRecommendedSettings && CE("option", { value: "recommended" }, t("recommended")), !hasRecommendedSettings && CE("option", { value: "highest" }, t("highest-quality")), CE("option", { value: "default" }, t("default")), CE("option", { value: "lowest" }, t("lowest-quality"))); - $select.addEventListener("input", (e2) => { - let profile = $select.value; - removeChildElements($suggestedSettings); - let fragment = document.createDocumentFragment(), note; - if (profile === "recommended") note = t("recommended-settings-for-device", { device: recommendedDevice }); - else if (profile === "highest") note = "⚠️ " + t("highest-quality-note"); - note && fragment.appendChild(CE("div", { class: "bx-suggest-note" }, note)); - let settings = this.suggestedSettings[profile], prefKey; - for (prefKey in settings) { - let currentValue = getPref(prefKey, !1), suggestedValue = settings[prefKey], currentValueText = STORAGE.Global.getValueText(prefKey, currentValue), isSameValue = currentValue === suggestedValue, $child, $value; - if (isSameValue) $value = currentValueText; - else { - let suggestedValueText = STORAGE.Global.getValueText(prefKey, suggestedValue); - $value = currentValueText + " ➔ " + suggestedValueText; - } - let $checkbox, breadcrumb = this.suggestedSettingLabels[prefKey] + " ❯ " + STORAGE.Global.getLabel(prefKey); - if ($child = CE("div", { - class: `bx-suggest-row ${isSameValue ? "bx-suggest-ok" : "bx-suggest-change"}` - }, $checkbox = CE("input", { - type: "checkbox", - tabindex: 0, - checked: !0, - id: `bx_suggest_${prefKey}` - }), CE("label", { - for: `bx_suggest_${prefKey}` - }, CE("div", { - class: "bx-suggest-label" - }, breadcrumb), CE("div", { - class: "bx-suggest-value" - }, $value))), isSameValue) - $checkbox.disabled = !0, $checkbox.checked = !0; - fragment.appendChild($child); - } - $suggestedSettings.appendChild(fragment); - }), BxEvent.dispatch($select, "input"); - let onClickApply = () => { - let profile = $select.value, settings = this.suggestedSettings[profile], prefKey; - for (prefKey in settings) { - let suggestedValue = settings[prefKey], $checkBox = $content.querySelector(`#bx_suggest_${prefKey}`); - if (!$checkBox.checked || $checkBox.disabled) continue; - let $control = this.settingElements[prefKey]; - if (!$control) { - setPref(prefKey, suggestedValue); - continue; - } - if ("setValue" in $control) $control.setValue(suggestedValue); - else $control.value = suggestedValue; - BxEvent.dispatch($control, "input", { - manualTrigger: !0 - }); - } - BxEvent.dispatch($select, "input"); - }, $btnApply = createButton({ - label: t("apply"), - style: 64 | 32, - onClick: onClickApply - }); - $content = CE("div", { - class: "bx-suggest-box", - _nearby: { - orientation: "vertical" - } - }, BxSelectElement.wrap($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", - tabindex: 0 - }, "🤓 " + t("how-to-improve-app-performance")), BX_FLAGS.DeviceInfo.deviceType.includes("android") && !hasRecommendedSettings && CE("a", { - class: "bx-suggest-link bx-focusable", - href: "https://github.com/redphx/better-xcloud-devices", - target: "_blank", - tabindex: 0 - }, t("suggest-settings-link"))), $btnSuggest.insertAdjacentElement("afterend", $content); - } - onTabClicked(e) { + onTabClicked = (e) => { let $svg = e.target.closest("svg"); if ($svg.dataset.lazy) { delete $svg.dataset.lazy; - let settingTab = this.SETTINGS_UI[$svg.dataset.group], items = settingTab.items(), $tabContent = this.renderTabContent.call(this, settingTab, items); + let settingTab = this.SETTINGS_UI[$svg.dataset.group]; + if (!settingTab) return; + let items = settingTab.items(), $tabContent = this.renderSettingsSection.call(this, settingTab, items); this.$tabContents.appendChild($tabContent); } 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_controller_friendly")) this.dialogManager.calculateSelectBoxes($child); + if ($child.classList.remove("bx-gone"), getPref("ui.controllerFriendly")) this.dialogManager.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"); - } + }; renderTab(settingTab) { let $svg = createSvgIcon(settingTab.icon); - return $svg.dataset.group = settingTab.group, $svg.tabIndex = 0, settingTab.lazyContent && ($svg.dataset.lazy = settingTab.lazyContent.toString()), $svg.addEventListener("click", this.onTabClicked.bind(this)), $svg; + return $svg.dataset.group = settingTab.group, $svg.tabIndex = 0, settingTab.lazyContent && ($svg.dataset.lazy = settingTab.lazyContent.toString()), $svg.addEventListener("click", this.onTabClicked), $svg; } - onGlobalSettingChanged(e) { + onGlobalSettingChanged = (e) => { this.$btnReload.classList.add("bx-danger"), this.$noteGlobalReload.classList.add("bx-gone"), this.$btnGlobalReload.classList.remove("bx-gone"), this.$btnGlobalReload.classList.add("bx-danger"); - } + }; renderServerSetting(setting) { - let selectedValue = getPref("server_region"), continents = { + let selectedValue = getPref("server.region"), continents = { "america-north": { label: t("continent-north-america") }, @@ -4132,7 +4429,7 @@ class SettingsNavigationDialog extends NavigationDialog { label: t("other") } }, $control = CE("select", { - id: `bx_setting_${setting.pref}`, + id: `bx_setting_${escapeCssSelector(setting.pref)}`, title: setting.label, tabindex: 0 }); @@ -4167,10 +4464,10 @@ class SettingsNavigationDialog extends NavigationDialog { if (setting.content) if (typeof setting.content === "function") $control = setting.content.apply(this); else $control = setting.content; else if (!setting.unsupported) { - if (pref === "server_region") $control = this.renderServerSetting(setting); - else if (pref === "bx_locale") $control = SettingElement.fromPref(pref, STORAGE.Global, async (e) => { + if (pref === "server.region") $control = this.renderServerSetting(setting); + else if (pref === "bx.locale") $control = SettingElement.fromPref(pref, STORAGE.Global, async (e) => { let newLocale = e.target.value; - if (getPref("ui_controller_friendly")) { + if (getPref("ui.controllerFriendly")) { let timeoutId = e.target.timeoutId; timeoutId && window.clearTimeout(timeoutId), e.target.timeoutId = window.setTimeout(() => { Translations.refreshLocale(newLocale), Translations.updateTranslations(); @@ -4178,7 +4475,7 @@ class SettingsNavigationDialog extends NavigationDialog { } else Translations.refreshLocale(newLocale), Translations.updateTranslations(); this.onGlobalSettingChanged(e); }); - else if (pref === "user_agent_profile") $control = SettingElement.fromPref("user_agent_profile", STORAGE.Global, (e) => { + else if (pref === "userAgent.profile") $control = SettingElement.fromPref("userAgent.profile", STORAGE.Global, (e) => { let value = e.target.value, isCustom = value === "custom", userAgent2 = UserAgent.get(value); UserAgent.updateStorage(value); let $inp = $control.nextElementSibling; @@ -4186,16 +4483,16 @@ class SettingsNavigationDialog extends NavigationDialog { }); else { let onChange = setting.onChange; - if (!onChange && settingTab.group === "global") onChange = this.onGlobalSettingChanged.bind(this); + if (!onChange && settingTab.group === "global") onChange = this.onGlobalSettingChanged; $control = SettingElement.fromPref(pref, STORAGE.Global, onChange, setting.params); } - if ($control instanceof HTMLSelectElement && getPref("ui_controller_friendly")) $control = BxSelectElement.wrap($control); + if ($control instanceof HTMLSelectElement) $control = BxSelectElement.create($control); pref && (this.settingElements[pref] = $control); } let prefDefinition = null; if (pref) prefDefinition = getPrefDefinition(pref); if (prefDefinition && !this.isSupportedVariant(prefDefinition.requiredVariants)) return; - let label = prefDefinition?.label || setting.label, note = prefDefinition?.note || setting.note, unsupportedNote = prefDefinition?.unsupportedNote || setting.unsupportedNote, experimental = prefDefinition?.experimental || setting.experimental; + let label = prefDefinition?.label || setting.label || "", note = prefDefinition?.note || setting.note, unsupportedNote = prefDefinition?.unsupportedNote || setting.unsupportedNote, experimental = prefDefinition?.experimental || setting.experimental; if (typeof note === "function") note = note(); if (typeof unsupportedNote === "function") unsupportedNote = unsupportedNote(); if (settingTabContent.label && setting.pref) { @@ -4206,35 +4503,32 @@ class SettingsNavigationDialog extends NavigationDialog { let $note; if (unsupportedNote) $note = CE("div", { class: "bx-settings-dialog-note" }, unsupportedNote); else if (note) $note = CE("div", { class: "bx-settings-dialog-note" }, note); - let $label, $row = CE("label", { - class: "bx-settings-row", - for: `bx_setting_${pref}`, - "data-type": settingTabContent.group, - _nearby: { - orientation: "horizontal" - } - }, $label = CE("span", { class: "bx-settings-label" }, label, $note), !prefDefinition?.unsupported && $control), $link = $label.querySelector("a"); - if ($link) $link.classList.add("bx-focusable"), setNearby($label, { - focus: $link - }); - $tabContent.appendChild($row), !prefDefinition?.unsupported && setting.onCreated && setting.onCreated(setting, $control); + let $row = createSettingRow(label, !prefDefinition?.unsupported && $control, { + $note, + multiLines: setting.multiLines + }); + $row.htmlFor = `bx_setting_${escapeCssSelector(pref)}`, $row.dataset.type = settingTabContent.group, $tabContent.appendChild($row), !prefDefinition?.unsupported && setting.onCreated && setting.onCreated(setting, $control); } - renderTabContent(settingTab, items) { + renderSettingsSection(settingTab, sections) { let $tabContent = CE("div", { class: "bx-gone", "data-tab-group": settingTab.group }); - for (let settingTabContent of items) { - if (!settingTabContent) continue; - if (!this.isSupportedVariant(settingTabContent.requiredVariants)) continue; - if (!this.renderFullSettings && settingTab.group === "global" && settingTabContent.group !== "general" && settingTabContent.group !== "footer") continue; - let label = settingTabContent.label; + for (let section of sections) { + if (!section) continue; + if (section instanceof HTMLElement) { + $tabContent.appendChild(section); + continue; + } + if (!this.isSupportedVariant(section.requiredVariants)) continue; + if (!this.renderFullSettings && settingTab.group === "global" && section.group !== "general" && section.group !== "footer") continue; + let label = section.label; if (label === t("better-xcloud")) { if (label += " " + SCRIPT_VERSION, SCRIPT_VARIANT === "lite") label += " (Lite)"; label = createButton({ label, url: "https://github.com/redphx/better-xcloud/releases", - style: 1024 | 8 | 32 + style: 2048 | 16 | 64 }); } if (label) { @@ -4242,31 +4536,31 @@ class SettingsNavigationDialog extends NavigationDialog { _nearby: { orientation: "horizontal" } - }, CE("span", {}, label), settingTabContent.helpUrl && createButton({ + }, CE("span", {}, label), section.helpUrl && createButton({ icon: BxIcon.QUESTION, - style: 4 | 32, - url: settingTabContent.helpUrl, + style: 8 | 64, + url: section.helpUrl, title: t("help") })); $tabContent.appendChild($title); } - if (settingTabContent.unsupportedNote) { - let $note = CE("b", { class: "bx-note-unsupported" }, settingTabContent.unsupportedNote); + if (section.unsupportedNote) { + let $note = CE("b", { class: "bx-note-unsupported" }, section.unsupportedNote); $tabContent.appendChild($note); } - if (settingTabContent.unsupported) continue; - if (settingTabContent.content) { - $tabContent.appendChild(settingTabContent.content); + if (section.unsupported) continue; + if (section.content) { + $tabContent.appendChild(section.content); continue; } - settingTabContent.items = settingTabContent.items || []; - for (let setting of settingTabContent.items) { + section.items = section.items || []; + for (let setting of section.items) { if (setting === !1) continue; if (typeof setting === "function") { setting.apply(this, [$tabContent]); continue; } - this.renderSettingRow(settingTab, $tabContent, settingTabContent, setting); + this.renderSettingRow(settingTab, $tabContent, section, setting); } } return $tabContent; @@ -4296,13 +4590,13 @@ class SettingsNavigationDialog extends NavigationDialog { } }), CE("div", {}, this.$btnReload = createButton({ icon: BxIcon.REFRESH, - style: 32 | 16, + style: 64 | 32, onClick: (e) => { this.reloadPage(); } }), createButton({ icon: BxIcon.CLOSE, - style: 32 | 16, + style: 64 | 32, onClick: (e) => { this.dialogManager.hide(); } @@ -4328,7 +4622,7 @@ class SettingsNavigationDialog extends NavigationDialog { if (settingTab.group !== "global" && !this.renderFullSettings) continue; let $svg = this.renderTab(settingTab); if ($tabs.appendChild($svg), typeof settingTab.items === "function") continue; - let $tabContent = this.renderTabContent.call(this, settingTab, settingTab.items); + let $tabContent = this.renderSettingsSection.call(this, settingTab, settingTab.items); $tabContents.appendChild($tabContent); } $tabs.firstElementChild.dispatchEvent(new Event("click")); @@ -4414,6 +4708,11 @@ class SettingsNavigationDialog extends NavigationDialog { handleGamepad(button) { let handled = !0; switch (button) { + case 1: + let $focusing = document.activeElement; + if ($focusing && this.$tabs.contains($focusing)) this.hide(); + else this.focusActiveTab(); + break; case 4: case 5: this.focusActiveTab(); @@ -4474,7 +4773,8 @@ var BxExposed = { /[;,/?:@&=+_`~$%#^*()!^™\xae\xa9]/g, / {2,}/g, / /g - ] + ], + toggleLocalCoOp: (enable) => {} }; function localRedirect(path) { let url = window.location.href.substring(0, 31) + path, $pageContent = document.getElementById("PageContent"); @@ -4491,11 +4791,11 @@ function localRedirect(path) { } window.localRedirect = localRedirect; function getPreferredServerRegion(shortName = !1) { - let preferredRegion = getPref("server_region"); - if (preferredRegion in STATES.serverRegions) if (shortName && STATES.serverRegions[preferredRegion].shortName) return STATES.serverRegions[preferredRegion].shortName; + let preferredRegion = getPref("server.region"), serverRegions = STATES.serverRegions; + if (preferredRegion in serverRegions) if (shortName && serverRegions[preferredRegion].shortName) return serverRegions[preferredRegion].shortName; else return preferredRegion; - for (let regionName in STATES.serverRegions) { - let region = STATES.serverRegions[regionName]; + for (let regionName in serverRegions) { + let region = serverRegions[regionName]; if (!region.isDefault) continue; if (shortName && region.shortName) return region.shortName; else return regionName; @@ -4516,32 +4816,32 @@ class HeaderSection { classes: ["bx-header-remote-play-button", "bx-gone"], icon: BxIcon.REMOTE_PLAY, title: t("remote-play"), - style: 4 | 32 | 512, - onClick: (e) => RemotePlayManager.getInstance().togglePopup() + style: 8 | 64 | 1024, + onClick: (e) => RemotePlayManager.getInstance()?.togglePopup() }), this.$btnSettings = createButton({ classes: ["bx-header-settings-button"], label: "???", - style: 8 | 16 | 32 | 128, - onClick: (e) => SettingsNavigationDialog.getInstance().show() - }), this.$buttonsWrapper = CE("div", {}, getPref("xhome_enabled") ? this.$btnRemotePlay : null, this.$btnSettings); + style: 16 | 32 | 64 | 256, + onClick: (e) => SettingsDialog.getInstance().show() + }), this.$buttonsWrapper = CE("div", {}, getPref("xhome.enabled") ? this.$btnRemotePlay : null, this.$btnSettings); } injectSettingsButton($parent) { if (!$parent) return; - let PREF_LATEST_VERSION = getPref("version_latest"), $btnSettings = this.$btnSettings; + let PREF_LATEST_VERSION = getPref("version.latest"), $btnSettings = this.$btnSettings; if (isElementVisible(this.$buttonsWrapper)) return; if ($btnSettings.querySelector("span").textContent = getPreferredServerRegion(!0) || t("better-xcloud"), !SCRIPT_VERSION.includes("beta") && PREF_LATEST_VERSION && PREF_LATEST_VERSION !== SCRIPT_VERSION) $btnSettings.setAttribute("data-update-available", "true"); $parent.appendChild(this.$buttonsWrapper); } - checkHeader() { + checkHeader = () => { let $target = document.querySelector("#PageContent div[class*=EdgewaterHeader-module__rightSectionSpacing]"); if (!$target) $target = document.querySelector("div[class^=UnsupportedMarketPage-module__buttons]"); $target && this.injectSettingsButton($target); - } + }; watchHeader() { let $root = document.querySelector("#PageContent header") || document.querySelector("#root"); if (!$root) return; this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = null, this.observer && this.observer.disconnect(), this.observer = new MutationObserver((mutationList) => { - this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = window.setTimeout(this.checkHeader.bind(this), 2000); + this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = window.setTimeout(this.checkHeader, 2000); }), this.observer.observe($root, { subtree: !0, childList: !0 }), this.checkHeader(); } showRemotePlayButton() { @@ -4551,9 +4851,9 @@ class HeaderSection { HeaderSection.getInstance().watchHeader(); } } -class RemotePlayNavigationDialog extends NavigationDialog { +class RemotePlayDialog extends NavigationDialog { static instance; - static getInstance = () => RemotePlayNavigationDialog.instance ?? (RemotePlayNavigationDialog.instance = new RemotePlayNavigationDialog); + static getInstance = () => RemotePlayDialog.instance ?? (RemotePlayDialog.instance = new RemotePlayDialog); LOG_TAG = "RemotePlayNavigationDialog"; STATE_LABELS = { On: t("powered-on"), @@ -4567,11 +4867,10 @@ class RemotePlayNavigationDialog extends NavigationDialog { BxLogger.info(this.LOG_TAG, "constructor()"), this.setupDialog(); } setupDialog() { - let $fragment = CE("div", { class: "bx-remote-play-container" }), $settingNote = CE("p", {}), currentResolution = getPref("xhome_resolution"), $resolutions = CE("select", {}, CE("option", { value: "1080p" }, "1080p"), CE("option", { value: "720p" }, "720p")); - if (getPref("ui_controller_friendly")) $resolutions = BxSelectElement.wrap($resolutions); - $resolutions.addEventListener("input", (e) => { + let $fragment = CE("div", { class: "bx-remote-play-container" }), $settingNote = CE("p", {}), currentResolution = getPref("xhome.video.resolution"), $resolutions = CE("select", {}, CE("option", { value: "720p" }, "720p"), CE("option", { value: "1080p" }, "1080p")); + $resolutions = BxSelectElement.create($resolutions), $resolutions.addEventListener("input", (e) => { let value = e.target.value; - $settingNote.textContent = value === "1080p" ? "✅ " + t("can-stream-xbox-360-games") : "❌ " + t("cant-stream-xbox-360-games"), setPref("xhome_resolution", value); + $settingNote.textContent = value === "1080p" ? "✅ " + t("can-stream-xbox-360-games") : "❌ " + t("cant-stream-xbox-360-games"), setPref("xhome.video.resolution", value); }), $resolutions.value = currentResolution, BxEvent.dispatch($resolutions, "input", { manualTrigger: !0 }); @@ -4584,7 +4883,7 @@ class RemotePlayNavigationDialog extends NavigationDialog { let $child = CE("div", { class: "bx-remote-play-device-wrapper" }, CE("div", { class: "bx-remote-play-device-info" }, CE("div", {}, CE("span", { class: "bx-remote-play-device-name" }, con.deviceName), CE("span", { class: "bx-remote-play-console-type" }, con.consoleType.replace("Xbox", ""))), CE("div", { class: "bx-remote-play-power-state" }, this.STATE_LABELS[con.powerState])), createButton({ classes: ["bx-remote-play-connect-button"], label: t("console-connect"), - style: 1 | 32, + style: 1 | 64, onClick: (e) => manager.play(con.serverId) })); $fragment.appendChild($child); @@ -4596,11 +4895,11 @@ class RemotePlayNavigationDialog extends NavigationDialog { } }, createButton({ icon: BxIcon.QUESTION, - style: 4 | 32, + style: 8 | 64, url: "https://better-xcloud.github.io/remote-play", label: t("help") }), createButton({ - style: 4 | 32, + style: 8 | 64, label: t("close"), onClick: (e) => this.hide() }))), this.$container = $fragment; @@ -4618,7 +4917,11 @@ class RemotePlayNavigationDialog extends NavigationDialog { } class RemotePlayManager { static instance; - static getInstance = () => RemotePlayManager.instance ?? (RemotePlayManager.instance = new RemotePlayManager); + static getInstance() { + if (typeof RemotePlayManager.instance === "undefined") if (getPref("xhome.enabled")) RemotePlayManager.instance = new RemotePlayManager; + else RemotePlayManager.instance = null; + return RemotePlayManager.instance; + } LOG_TAG = "RemotePlayManager"; isInitialized = !1; XCLOUD_TOKEN; @@ -4630,25 +4933,25 @@ class RemotePlayManager { } initialize() { if (this.isInitialized) return; - this.isInitialized = !0, this.getXhomeToken(() => { + this.isInitialized = !0, this.requestXhomeToken(() => { this.getConsolesList(() => { BxLogger.info(this.LOG_TAG, "Consoles", this.consoles), STATES.supportedRegion && HeaderSection.getInstance().showRemotePlayButton(), BxEvent.dispatch(window, BxEvent.REMOTE_PLAY_READY); }); }); } - get xcloudToken() { + getXcloudToken() { return this.XCLOUD_TOKEN; } - set xcloudToken(token) { + setXcloudToken(token) { this.XCLOUD_TOKEN = token; } - get xhomeToken() { + getXhomeToken() { return this.XHOME_TOKEN; } getConsoles() { return this.consoles; } - getXhomeToken(callback) { + requestXhomeToken(callback) { if (this.XHOME_TOKEN) { callback(); return; @@ -4705,7 +5008,7 @@ class RemotePlayManager { callback(); } play(serverId, resolution) { - if (resolution) setPref("xhome_resolution", resolution); + if (resolution) setPref("xhome.video.resolution", resolution); STATES.remotePlay.config = { serverId }, window.BX_REMOTE_PLAY_CONFIG = STATES.remotePlay.config, localRedirect("/launch/fortnite/BT5P2X999VH2#remote-play"); @@ -4719,14 +5022,10 @@ class RemotePlayManager { Toast.show(t("no-consoles-found"), "", { instant: !0 }); return; } - if (AppInterface && AppInterface.showRemotePlayDialog) { - AppInterface.showRemotePlayDialog(JSON.stringify(this.consoles)), document.activeElement.blur(); - return; - } - RemotePlayNavigationDialog.getInstance().show(); + RemotePlayDialog.getInstance().show(); } static detect() { - if (!getPref("xhome_enabled")) return; + if (!getPref("xhome.enabled")) return; if (STATES.remotePlay.isPlaying = window.location.pathname.includes("/launch/") && window.location.hash.startsWith("#remote-play"), STATES.remotePlay?.isPlaying) window.BX_REMOTE_PLAY_CONFIG = STATES.remotePlay.config, window.history.replaceState({ origin: "better-xcloud" }, "", "https://www.xbox.com/" + location.pathname.substring(1, 6) + "/play"); else window.BX_REMOTE_PLAY_CONFIG = null; } @@ -4750,7 +5049,7 @@ class LoadingScreen { let $bgStyle = CE("style"); document.documentElement.appendChild($bgStyle), LoadingScreen.$bgStyle = $bgStyle; } - if (LoadingScreen.setBackground(titleInfo.product.heroImageUrl || titleInfo.product.titledHeroImageUrl || titleInfo.product.tileImageUrl), getPref("ui_loading_screen_rocket") === "hide") LoadingScreen.hideRocket(); + if (LoadingScreen.setBackground(titleInfo.product.heroImageUrl || titleInfo.product.titledHeroImageUrl || titleInfo.product.tileImageUrl), getPref("loadingScreen.rocket") === "hide") LoadingScreen.hideRocket(); } static hideRocket() { let $bgStyle = LoadingScreen.$bgStyle; @@ -4765,7 +5064,7 @@ class LoadingScreen { }, bg.src = imageUrl; } static setupWaitTime(waitTime) { - if (getPref("ui_loading_screen_rocket") === "hide-queue") LoadingScreen.hideRocket(); + if (getPref("loadingScreen.rocket") === "hide-queue") LoadingScreen.hideRocket(); let secondsLeft = waitTime, $countDown, $estimated; LoadingScreen.orgWebTitle = document.title; let endDate = new Date, timeZoneOffsetSeconds = endDate.getTimezoneOffset() * 60; @@ -4780,7 +5079,7 @@ class LoadingScreen { }, 1000); } static hide() { - if (LoadingScreen.orgWebTitle && (document.title = LoadingScreen.orgWebTitle), LoadingScreen.$waitTimeBox && LoadingScreen.$waitTimeBox.classList.add("bx-gone"), getPref("ui_loading_screen_game_art") && LoadingScreen.$bgStyle) { + if (LoadingScreen.orgWebTitle && (document.title = LoadingScreen.orgWebTitle), LoadingScreen.$waitTimeBox && LoadingScreen.$waitTimeBox.classList.add("bx-gone"), getPref("loadingScreen.gameArt.show") && LoadingScreen.$bgStyle) { let $rocketBg = document.querySelector('#game-stream rect[width="800"]'); $rocketBg && $rocketBg.addEventListener("transitionend", (e) => { LoadingScreen.$bgStyle.textContent += "#game-stream{background:#000 !important}"; @@ -4809,18 +5108,18 @@ class GuideMenu { let buttons = { scriptSettings: createButton({ label: t("better-xcloud"), - style: 64 | 32 | 1, - onClick: (() => { + style: 128 | 64 | 1, + onClick: () => { window.addEventListener(BxEvent.XCLOUD_DIALOG_DISMISSED, (e) => { - setTimeout(() => SettingsNavigationDialog.getInstance().show(), 50); + setTimeout(() => SettingsDialog.getInstance().show(), 50); }, { once: !0 }), this.closeGuideMenu(); - }).bind(this) + } }), closeApp: AppInterface && createButton({ icon: BxIcon.POWER, label: t("close-app"), title: t("close-app"), - style: 64 | 32 | 2, + style: 128 | 64 | 4, onClick: (e) => { AppInterface.closeApp(); }, @@ -4832,20 +5131,20 @@ class GuideMenu { icon: BxIcon.REFRESH, label: t("reload-page"), title: t("reload-page"), - style: 64 | 32, - onClick: (() => { + style: 128 | 64, + onClick: () => { if (this.closeGuideMenu(), STATES.isPlaying) confirm(t("confirm-reload-stream")) && window.location.reload(); else window.location.reload(); - }).bind(this) + } }), backToHome: createButton({ icon: BxIcon.HOME, label: t("back-to-home"), title: t("back-to-home"), - style: 64 | 32, - onClick: (() => { + style: 128 | 64, + onClick: () => { this.closeGuideMenu(), confirm(t("back-to-home-confirm")) && (window.location.href = window.location.href.substring(0, 31)); - }).bind(this), + }, attributes: { "data-state": "playing" } @@ -4886,14 +5185,14 @@ class GuideMenu { let $buttons = this.renderButtons(); $buttons.dataset.isPlaying = isPlaying.toString(), $target.insertAdjacentElement("afterend", $buttons); } - async onShown(e) { + onShown = async (e) => { if (e.where === "home") { let $root = document.querySelector("#gamepass-dialog-root div[role=dialog] div[role=tabpanel] div[class*=HomeLandingPage]"); $root && this.injectHome($root, STATES.isPlaying); } - } + }; addEventListeners() { - window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, this.onShown.bind(this)); + window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, this.onShown); } observe($addedElm) { let className = $addedElm.className; @@ -4968,7 +5267,7 @@ class StreamBadges { if ($badge = CE("div", { class: "bx-badge", title: badgeInfo.name }, CE("span", { class: "bx-badge-name" }, createSvgIcon(badgeInfo.icon)), CE("span", { class: "bx-badge-value", style: `background-color: ${badgeInfo.color}` }, value)), name === "battery") $badge.classList.add("bx-badge-battery"); return this.badges[name].$element = $badge, $badge; } - async updateBadges(forceUpdate = !1) { + updateBadges = async (forceUpdate = !1) => { if (!this.$container || !forceUpdate && !this.$container.isConnected) { this.stop(); return; @@ -4989,9 +5288,9 @@ class StreamBadges { if ($elm.lastElementChild.textContent = value, name === "battery") if (batt.current === 100 && batt.start === 100) $elm.classList.add("bx-gone"); else $elm.dataset.charging = batt.isCharging.toString(), $elm.classList.remove("bx-gone"); } - } + }; async start() { - await this.updateBadges(!0), this.stop(), this.intervalId = window.setInterval(this.updateBadges.bind(this), this.REFRESH_INTERVAL); + await this.updateBadges(!0), this.stop(), this.intervalId = window.setInterval(this.updateBadges, this.REFRESH_INTERVAL); } stop() { this.intervalId && clearInterval(this.intervalId), this.intervalId = null; @@ -5094,8 +5393,46 @@ class XcloudInterceptor { UKSouth: ["🇬🇧", "europe"], WestEurope: ["🇪🇺", "europe"] }; + static BASE_DEVICE_INFO = { + appInfo: { + env: { + clientAppId: window.location.host, + clientAppType: "browser", + clientAppVersion: "24.17.36", + clientSdkVersion: "10.1.14", + httpEnvironment: "prod", + sdkInstallId: "" + } + }, + dev: { + displayInfo: { + dimensions: { + widthInPixels: 1920, + heightInPixels: 1080 + }, + pixelDensity: { + dpiX: 1, + dpiY: 1 + } + }, + hw: { + make: "Microsoft", + model: "unknown", + sdktype: "web" + }, + os: { + name: "windows", + ver: "22631.2715", + platform: "desktop" + }, + browser: { + browserName: "chrome", + browserVersion: "125.0" + } + } + }; static async handleLogin(request, init) { - let bypassServer = getPref("server_bypass_restriction"); + let bypassServer = getPref("server.bypassRestriction"); if (bypassServer !== "off") { let ip = BypassServerIps[bypassServer]; ip && request.headers.set("X-Forwarded-For", ip); @@ -5103,7 +5440,7 @@ class XcloudInterceptor { let response = await NATIVE_FETCH(request, init); if (response.status !== 200) return BxEvent.dispatch(window, BxEvent.XCLOUD_SERVERS_UNAVAILABLE), response; let obj = await response.clone().json(); - RemotePlayManager.getInstance().xcloudToken = obj.gsToken; + RemotePlayManager.getInstance()?.setXcloudToken(obj.gsToken); let serverRegex = /\/\/(\w+)\./, serverExtra = XcloudInterceptor.SERVER_EXTRA_INFO, region; for (region of obj.offeringSettings.regions) { let { name: regionName, name: shortName } = region; @@ -5123,7 +5460,7 @@ class XcloudInterceptor { } static async handlePlay(request, init) { BxEvent.dispatch(window, BxEvent.STREAM_LOADING); - let PREF_STREAM_TARGET_RESOLUTION = getPref("stream_target_resolution"), PREF_STREAM_PREFERRED_LOCALE = getPref("stream_preferred_locale"), url = typeof request === "string" ? request : request.url, parsedUrl = new URL(url), badgeRegion = parsedUrl.host.split(".", 1)[0]; + let PREF_STREAM_TARGET_RESOLUTION = getPref("stream.video.resolution"), PREF_STREAM_PREFERRED_LOCALE = getPref("stream.locale"), url = typeof request === "string" ? request : request.url, parsedUrl = new URL(url), badgeRegion = parsedUrl.host.split(".", 1)[0]; for (let regionName in STATES.serverRegions) { let region = STATES.serverRegions[regionName]; if (parsedUrl.origin == region.baseUri) { @@ -5132,20 +5469,36 @@ class XcloudInterceptor { } } StreamBadges.getInstance().setRegion(badgeRegion); - let body = await request.clone().json(); + let clone = request.clone(), body = await clone.json(), headers = {}; + for (let pair of clone.headers.entries()) + headers[pair[0]] = pair[1]; if (PREF_STREAM_TARGET_RESOLUTION !== "auto") { - let osName = PREF_STREAM_TARGET_RESOLUTION === "720p" ? "android" : "windows"; + let osName; + switch (PREF_STREAM_TARGET_RESOLUTION) { + case "1080p-hq": + osName = "tizen"; + let deviceInfo = XcloudInterceptor.BASE_DEVICE_INFO; + deviceInfo.dev.os.name = "tizen", headers["x-ms-device-info"] = JSON.stringify(deviceInfo); + break; + case "1080p": + osName = "windows"; + break; + default: + osName = "android"; + break; + } body.settings.osName = osName; } if (PREF_STREAM_PREFERRED_LOCALE !== "default") body.settings.locale = PREF_STREAM_PREFERRED_LOCALE; let newRequest = new Request(request, { - body: JSON.stringify(body) + body: JSON.stringify(body), + headers }); return NATIVE_FETCH(newRequest); } static async handleWaitTime(request, init) { let response = await NATIVE_FETCH(request, init); - if (getPref("ui_loading_screen_wait_time")) { + if (getPref("loadingScreen.waitTime.show")) { let json = await response.clone().json(); if (json.estimatedAllocationTimeInSeconds > 0) LoadingScreen.setupWaitTime(json.estimatedTotalWaitTimeInSeconds); } @@ -5159,13 +5512,13 @@ class XcloudInterceptor { let obj = JSON.parse(text), overrides = JSON.parse(obj.clientStreamingConfigOverrides || "{}") || {}; overrides.inputConfiguration = overrides.inputConfiguration || {}, overrides.inputConfiguration.enableVibration = !0; let overrideMkb = null; - if (getPref("native_mkb_enabled") === "on" || STATES.currentStream.titleInfo && BX_FLAGS.ForceNativeMkbTitles?.includes(STATES.currentStream.titleInfo.details.productId)) overrideMkb = !0; - if (getPref("native_mkb_enabled") === "off") overrideMkb = !1; + if (getPref("nativeMkb.mode") === "on" || STATES.currentStream.titleInfo && BX_FLAGS.ForceNativeMkbTitles?.includes(STATES.currentStream.titleInfo.details.productId)) overrideMkb = !0; + if (getPref("nativeMkb.mode") === "off") overrideMkb = !1; if (overrideMkb !== null) overrides.inputConfiguration = Object.assign(overrides.inputConfiguration, { enableMouseInput: overrideMkb, enableKeyboardInput: overrideMkb }); - if (getPref("audio_mic_on_playing")) overrides.audioConfiguration = overrides.audioConfiguration || {}, overrides.audioConfiguration.enableMicrophone = !0; + if (getPref("audio.mic.onPlaying")) overrides.audioConfiguration = overrides.audioConfiguration || {}, overrides.audioConfiguration.enableMicrophone = !0; return obj.clientStreamingConfigOverrides = JSON.stringify(overrides), response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response; } static async handle(request, init) { @@ -5225,21 +5578,21 @@ async function patchIceCandidates(request, consoleAddrs) { let response = await NATIVE_FETCH(request), text = await response.clone().text(); if (!text.length) return response; let options = { - preferIpv6Server: getPref("prefer_ipv6_server"), + preferIpv6Server: getPref("server.ipv6.prefer"), consoleAddrs }, obj = JSON.parse(text), exchangeResponse = JSON.parse(obj.exchangeResponse); return exchangeResponse = updateIceCandidates(exchangeResponse, options), obj.exchangeResponse = JSON.stringify(exchangeResponse), response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response; } function interceptHttpRequests() { let BLOCKED_URLS = []; - if (getPref("block_tracking")) clearAllLogs(), BLOCKED_URLS = BLOCKED_URLS.concat([ + if (getPref("block.tracking")) clearAllLogs(), BLOCKED_URLS = BLOCKED_URLS.concat([ "https://arc.msn.com", "https://browser.events.data.microsoft.com", "https://dc.services.visualstudio.com", "https://2c06dea3f26c40c69b8456d319791fd0@o427368.ingest.sentry.io", "https://mscom.demdex.net" ]); - if (getPref("block_social_features")) BLOCKED_URLS = BLOCKED_URLS.concat([ + if (getPref("block.social")) BLOCKED_URLS = BLOCKED_URLS.concat([ "https://peoplehub.xboxlive.com/users/me/people/social", "https://peoplehub.xboxlive.com/users/me/people/recommendations", "https://xblmessaging.xboxlive.com/network/xbox/users/me/inbox" @@ -5310,18 +5663,19 @@ function interceptHttpRequests() { }; } 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-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:99999;--bx-toast-z-index:60000;--bx-dialog-z-index:50000;--bx-dialog-overlay-z-index:40200;--bx-stats-bar-z-index:40100;--bx-mkb-pointer-lock-msg-z-index:40000;--bx-navigation-dialog-z-index:30100;--bx-navigation-dialog-overlay-z-index:30000;--bx-game-bar-z-index:10000;--bx-screenshot-animation-z-index:9000;--bx-wait-time-box-z-index:1000}@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)}.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]{overflow:auto}#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.549);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-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-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-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);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-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-dialog-overlay{position:fixed;inset:0;z-index:var(--bx-dialog-overlay-z-index);background:#000;opacity:50%}.bx-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:20px;border-radius:8px;z-index:var(--bx-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-dialog *:focus{outline:none !important}.bx-dialog h2{display:flex;margin-bottom:12px}.bx-dialog h2 b{flex:1;color:#fff;display:block;font-family:var(--bx-title-font);font-size:26px;font-weight:400;line-height:var(--bx-button-height)}.bx-dialog.bx-binding-dialog h2 b{font-family:var(--bx-promptfont-font) !important}.bx-dialog > div{overflow:auto;padding:2px 0}.bx-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-dialog > button:hover{background-color:#515863}}.bx-dialog > button:focus{background-color:#515863}@media screen and (max-width:450px){.bx-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-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-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-dialog input{accent-color:var(--bx-primary-button-color)}.bx-settings-dialog input:focus{accent-color:var(--bx-danger-button-color)}.bx-settings-dialog select:disabled{-webkit-appearance:none;background:transparent;text-align-last:right;border:none;color:#fff}.bx-settings-dialog select option:disabled{display:none}.bx-settings-dialog input[type=checkbox]: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-settings-dialog a{color:#1c9d1c;text-decoration:none}.bx-settings-dialog a:hover,.bx-settings-dialog a:focus{color:#5dc21e}.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 > div[data-tab-group=shortcuts] > div[data-has-gamepad=true] > div:first-of-type{display:none}.bx-settings-tab-contents > div[data-tab-group=shortcuts] > div[data-has-gamepad=true] > div:last-of-type{display:block}.bx-settings-tab-contents > div[data-tab-group=shortcuts] > div[data-has-gamepad=false] > div:first-of-type{display:block}.bx-settings-tab-contents > div[data-tab-group=shortcuts] > div[data-has-gamepad=false] > div:last-of-type{display:none}.bx-settings-tab-contents > div[data-tab-group=shortcuts] .bx-shortcut-profile{width:100%;height:36px;display:block}.bx-settings-tab-contents > div[data-tab-group=shortcuts] .bx-shortcut-note{margin-top:10px;font-size:14px}.bx-settings-tab-contents > div[data-tab-group=shortcuts] .bx-shortcut-row{display:flex;margin-bottom:10px}.bx-settings-tab-contents > div[data-tab-group=shortcuts] .bx-shortcut-row label.bx-prompt{flex:1;font-size:26px;margin-bottom:0}.bx-settings-tab-contents > div[data-tab-group=shortcuts] .bx-shortcut-row .bx-shortcut-actions{flex:2;position:relative}.bx-settings-tab-contents > div[data-tab-group=shortcuts] .bx-shortcut-row .bx-shortcut-actions select{position:absolute;width:100%;height:100%;display:block}.bx-settings-tab-contents > div[data-tab-group=shortcuts] .bx-shortcut-row .bx-shortcut-actions select:last-of-type{opacity:0;z-index:calc(var(--bx-settings-z-index) + 1)}.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}@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-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}.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:10px;border-top-right-radius:10px}.bx-settings-tab-contents > div .bx-settings-row:not(:has(+ .bx-settings-row)){border:none;border-bottom-left-radius:10px;border-bottom-right-radius:10px}.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:10px}.bx-suggest-toggler{text-align:left;display:flex;border-radius:4px;overflow:hidden;background:#003861}.bx-suggest-toggler label{flex:1;margin-bottom:0;padding:10px;background:#004f87}.bx-suggest-toggler span{display:inline-block;align-self:center;padding:10px;width:40px;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;background:#161616;padding:10px;box-shadow:0 0 12px #0f0f0f inset;border-radius:10px}.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-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:20px}.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}.bx-select{display:flex;align-items:center;flex:0 1 auto}.bx-select select{position:absolute !important;top:-9999px !important;left:-9999px !important;visibility:hidden !important}.bx-select > div,.bx-select button.bx-select-value{min-width:120px;text-align:left;margin:0 8px;line-height:24px;vertical-align:middle;background:#fff;color:#000;border-radius:4px;padding:2px 8px;flex:1}.bx-select > div{display:inline-block}.bx-select > div input{display:inline-block;margin-right:8px}.bx-select > div label{margin-bottom:0;font-size:14px;width:100%}.bx-select > div label span{display:block;font-size:10px;font-weight:bold;text-align:left;line-height:initial}.bx-select button.bx-select-value{border:none;display:inline-flex;cursor:pointer;min-height:30px;font-size:.9rem;align-items:center}.bx-select button.bx-select-value span{flex:1;text-align:left;display:inline-block}.bx-select button.bx-select-value input{margin:0 4px;accent-color:var(--bx-primary-button-color);pointer-events:none}.bx-select button.bx-select-value:hover input,.bx-select button.bx-select-value:focus input{accent-color:var(--bx-danger-button-color)}.bx-select button.bx-select-value:hover::after,.bx-select button.bx-select-value:focus::after{border-color:#4d4d4d !important}.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}.bx-select button.bx-button span{line-height:unset}.bx-guide-home-achievements-progress{display:flex;gap:10px;flex-direction:row}.bx-guide-home-achievements-progress .bx-button{margin-bottom:0 !important}html[data-xds-platform=tv] .bx-guide-home-achievements-progress{flex-direction:column}html:not([data-xds-platform=tv]) .bx-guide-home-achievements-progress{flex-direction:row}html:not([data-xds-platform=tv]) .bx-guide-home-achievements-progress > button:first-of-type{flex:1}html:not([data-xds-platform=tv]) .bx-guide-home-achievements-progress > button:last-of-type{width:40px}html:not([data-xds-platform=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}html[data-xds-platform=tv] .bx-guide-home-buttons > div{flex-direction:column}html[data-xds-platform=tv] .bx-guide-home-buttons > div button{margin-bottom:0 !important}html:not([data-xds-platform=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]{display:flex}div[data-testid=media-container].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)}#game-stream video{margin:auto;align-self:center;background:#000}#game-stream canvas{position:absolute;align-self:center;margin:auto;left:0;right:0}#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 span{display:inline-block;min-width:40px;font-family:var(--bx-monospaced-font);font-size:13px;margin:0 4px}.bx-number-stepper button{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 button:hover{background-color:var(--bx-default-button-hover-color)}}.bx-number-stepper button:active{background-color:var(--bx-default-button-hover-color)}.bx-number-stepper button:disabled + span{font-family:var(--bx-title-font)}.bx-number-stepper input[type="range"]{display:block;margin:12px auto 2px;width:180px;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[data-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-transparent=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{min-width:60px;display:inline-block;text-align:right;vertical-align:middle}.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-stats-bar span:first-of-type{min-width:22px}.bx-mkb-settings{display:flex;flex-direction:column;flex:1;padding-bottom:10px;overflow:hidden}.bx-mkb-settings select:disabled{-webkit-appearance:none;background:transparent;text-align-last:right;text-align:right;border:none;color:#fff}.bx-mkb-pointer-lock-msg{user-select:none;-webkit-user-select:none;position:fixed;left:50%;top:50%;transform:translateX(-50%) translateY(-50%);margin:auto;background:#151515;z-index:var(--bx-mkb-pointer-lock-msg-z-index);color:#fff;text-align:center;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:220px;opacity:.9}.bx-mkb-pointer-lock-msg:hover{opacity:1}.bx-mkb-pointer-lock-msg > div:first-of-type{display:flex;flex-direction:column;text-align:left}.bx-mkb-pointer-lock-msg p{margin:0}.bx-mkb-pointer-lock-msg p:first-child{font-size:22px;margin-bottom:4px;font-weight:bold}.bx-mkb-pointer-lock-msg p:last-child{font-size:12px;font-style:italic}.bx-mkb-pointer-lock-msg > div:last-of-type{margin-top:10px}.bx-mkb-pointer-lock-msg > div:last-of-type[data-type=\'native\'] button:first-of-type{margin-bottom:8px}.bx-mkb-pointer-lock-msg > div:last-of-type[data-type=\'virtual\'] div{display:flex;flex-flow:row;margin-top:8px}.bx-mkb-pointer-lock-msg > div:last-of-type[data-type=\'virtual\'] div button{flex:1}.bx-mkb-pointer-lock-msg > div:last-of-type[data-type=\'virtual\'] div button:first-of-type{margin-right:5px}.bx-mkb-pointer-lock-msg > div:last-of-type[data-type=\'virtual\'] div button:last-of-type{margin-left:5px}.bx-mkb-preset-tools{display:flex;margin-bottom:12px}.bx-mkb-preset-tools select{flex:1}.bx-mkb-preset-tools button{margin-left:6px}.bx-mkb-settings-rows{flex:1;overflow:scroll}.bx-mkb-key-row{display:flex;margin-bottom:10px;align-items:center}.bx-mkb-key-row label{margin-bottom:0;font-family:var(--bx-promptfont-font);font-size:26px;text-align:center;width:26px;height:32px;line-height:32px}.bx-mkb-key-row button{flex:1;height:32px;line-height:32px;margin:0 0 0 10px;background:transparent;border:none;color:#fff;border-radius:0;border-left:1px solid #373737}.bx-mkb-key-row button:hover{background:transparent;cursor:default}.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:16px 0 10px;font-size:12px}.bx-mkb-note:first-of-type{margin-top:0}.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_hide_sections"), 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")}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]{overflow:auto}#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.549);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-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:20px;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:20px;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 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}.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}.bx-suggest-toggler label{flex:1;padding:10px;background:#004f87}.bx-suggest-toggler span{display:inline-block;align-self:center;padding:10px;width:40px;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:20px}.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}div.bx-select > div label span{display:block;font-size:10px;font-weight:bold;text-align:left;line-height:initial;white-space:pre}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}html[data-xds-platform=tv] .bx-guide-home-achievements-progress{flex-direction:column}html:not([data-xds-platform=tv]) .bx-guide-home-achievements-progress{flex-direction:row}html:not([data-xds-platform=tv]) .bx-guide-home-achievements-progress > button:first-of-type{flex:1}html:not([data-xds-platform=tv]) .bx-guide-home-achievements-progress > button:last-of-type{width:40px}html:not([data-xds-platform=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}html[data-xds-platform=tv] .bx-guide-home-buttons > div{flex-direction:column}html[data-xds-platform=tv] .bx-guide-home-buttons > div button{margin-bottom:0 !important}html:not([data-xds-platform=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]{display:flex}div[data-testid=media-container].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)}#game-stream video{margin:auto;align-self:center;background:#000}#game-stream canvas{position:absolute;align-self:center;margin:auto;left:0;right:0}#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-transparent=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{min-width:60px;display:inline-block;text-align:right;vertical-align:middle}.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-stats-bar span:first-of-type{min-width:22px}.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 = []; if (PREF_HIDE_SECTIONS.includes("news")) selectorToHide.push("#BodyContent > div[class*=CarouselRow-module]"); + if (getPref("feature.byog.disabled")) 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]"); if (PREF_HIDE_SECTIONS.includes("most-popular")) selectorToHide.push('#BodyContent div[class*=HomePage-module__bottomSpacing]:has(a[href="/play/gallery/popular"])'); if (PREF_HIDE_SECTIONS.includes("touch")) selectorToHide.push('#BodyContent div[class*=HomePage-module__bottomSpacing]:has(a[href="/play/gallery/touch"])'); - if (getPref("block_social_features")) selectorToHide.push("#gamepass-dialog-root div[class^=AchievementsPreview-module__container] + button[class*=HomeLandingPage-module__button]"); + if (getPref("block.social")) selectorToHide.push("#gamepass-dialog-root div[class^=AchievementsPreview-module__container] + button[class*=HomeLandingPage-module__button]"); if (selectorToHide) css += selectorToHide.join(",") + "{ display: none; }"; - if (getPref("reduce_animations")) css += "div[class*=GameCard-module__gameTitleInnerWrapper],div[class*=GameCard-module__card],div[class*=ScrollArrows-module]{transition:none !important}"; - if (getPref("hide_dots_icon")) css += "div[class*=Grip-module__container]{visibility:hidden}@media (hover:hover){button[class*=GripHandle-module__container]:hover div[class*=Grip-module__container]{visibility:visible}}button[class*=GripHandle-module__container][aria-expanded=true] div[class*=Grip-module__container]{visibility:visible}button[class*=GripHandle-module__container][aria-expanded=false]{background-color:transparent !important}div[class*=StreamHUD-module__buttonsContainer]{padding:0 !important}"; - if (css += "div[class*=StreamMenu-module__menu]{min-width:100vw !important}", getPref("stream_simplify_menu")) css += "div[class*=Menu-module__scrollable]{--bxStreamMenuItemSize:80px;--streamMenuItemSize:calc(var(--bxStreamMenuItemSize) + 40px) !important}.bx-badges{top:calc(var(--streamMenuItemSize) - 20px)}body[data-media-type=tv] .bx-badges{top:calc(var(--streamMenuItemSize) - 10px) !important}button[class*=MenuItem-module__container]{min-width:auto !important;min-height:auto !important;width:var(--bxStreamMenuItemSize) !important;height:var(--bxStreamMenuItemSize) !important}div[class*=MenuItem-module__label]{display:none !important}svg[class*=MenuItem-module__icon]{width:36px;height:100% !important;padding:0 !important;margin:0 !important}"; + if (getPref("ui.reduceAnimations")) css += "div[class*=GameCard-module__gameTitleInnerWrapper],div[class*=GameCard-module__card],div[class*=ScrollArrows-module]{transition:none !important}"; + if (getPref("ui.systemMenu.hideHandle")) css += "div[class*=Grip-module__container]{visibility:hidden}@media (hover:hover){button[class*=GripHandle-module__container]:hover div[class*=Grip-module__container]{visibility:visible}}button[class*=GripHandle-module__container][aria-expanded=true] div[class*=Grip-module__container]{visibility:visible}button[class*=GripHandle-module__container][aria-expanded=false]{background-color:transparent !important}div[class*=StreamHUD-module__buttonsContainer]{padding:0 !important}"; + if (css += "div[class*=StreamMenu-module__menu]{min-width:100vw !important}", getPref("ui.streamMenu.simplify")) css += "div[class*=Menu-module__scrollable]{--bxStreamMenuItemSize:80px;--streamMenuItemSize:calc(var(--bxStreamMenuItemSize) + 40px) !important}.bx-badges{top:calc(var(--streamMenuItemSize) - 20px)}body[data-media-type=tv] .bx-badges{top:calc(var(--streamMenuItemSize) - 10px) !important}button[class*=MenuItem-module__container]{min-width:auto !important;min-height:auto !important;width:var(--bxStreamMenuItemSize) !important;height:var(--bxStreamMenuItemSize) !important}div[class*=MenuItem-module__label]{display:none !important}svg[class*=MenuItem-module__icon]{width:36px;height:100% !important;padding:0 !important;margin:0 !important}"; else css += "body[data-media-type=tv] .bx-badges{top:calc(var(--streamMenuItemSize) + 30px)}body:not([data-media-type=tv]) .bx-badges{top:calc(var(--streamMenuItemSize) + 20px)}body:not([data-media-type=tv]) button[class*=MenuItem-module__container]{min-width:auto !important;width:100px !important}body:not([data-media-type=tv]) button[class*=MenuItem-module__container]:nth-child(n+2){margin-left:10px !important}body:not([data-media-type=tv]) div[class*=MenuItem-module__label]{margin-left:8px !important;margin-right:8px !important}"; - if (getPref("ui_scrollbar_hide")) css += "html{scrollbar-width:none}body::-webkit-scrollbar{display:none}"; + if (getPref("ui.hideScrollbar")) css += "html{scrollbar-width:none}body::-webkit-scrollbar{display:none}"; let $style = CE("style", {}, css); document.documentElement.appendChild($style); } @@ -5335,25 +5689,6 @@ function preloadFonts() { }); document.querySelector("head")?.appendChild($link); } -class MouseCursorHider { - static #timeout; - static #cursorVisible = !0; - static show() { - document.body && (document.body.style.cursor = "unset"), MouseCursorHider.#cursorVisible = !0; - } - static hide() { - document.body && (document.body.style.cursor = "none"), MouseCursorHider.#timeout = null, MouseCursorHider.#cursorVisible = !1; - } - static onMouseMove(e) { - !MouseCursorHider.#cursorVisible && MouseCursorHider.show(), MouseCursorHider.#timeout && clearTimeout(MouseCursorHider.#timeout), MouseCursorHider.#timeout = window.setTimeout(MouseCursorHider.hide, 3000); - } - static start() { - MouseCursorHider.show(), document.addEventListener("mousemove", MouseCursorHider.onMouseMove); - } - static stop() { - MouseCursorHider.#timeout && clearTimeout(MouseCursorHider.#timeout), document.removeEventListener("mousemove", MouseCursorHider.onMouseMove), MouseCursorHider.show(); - } -} function patchHistoryMethod(type) { let orig = window.history[type]; return function(...args) { @@ -5537,6 +5872,7 @@ class WebGL2Player { } else frameCallback = requestAnimationFrame; let animate = () => { if (this.stopped) return; + this.animFrameId = frameCallback(animate); let draw = !0; if (this.targetFps === 0) draw = !1; else if (this.targetFps < 60) { @@ -5548,17 +5884,16 @@ class WebGL2Player { let gl = this.gl; gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, this.$video), gl.drawArrays(gl.TRIANGLES, 0, 6); } - this.animFrameId = frameCallback(animate); }; this.animFrameId = frameCallback(animate); } setupShaders() { - BxLogger.info(this.LOG_TAG, "Setting up", getPref("video_power_preference")); + BxLogger.info(this.LOG_TAG, "Setting up", getPref("video.player.powerPreference")); let gl = this.$canvas.getContext("webgl2", { isBx: !0, antialias: !0, alpha: !1, - powerPreference: getPref("video_power_preference") + powerPreference: getPref("video.player.powerPreference") }); this.gl = gl, gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferWidth); let vShader = gl.createShader(gl.VERTEX_SHADER); @@ -5645,7 +5980,7 @@ class StreamPlayer { return filters.join(" "); } resizePlayer() { - let PREF_RATIO = getPref("video_ratio"), $video = this.$video, isNativeTouchGame = STATES.currentStream.titleInfo?.details.hasNativeTouchSupport, $webGL2Canvas; + let PREF_RATIO = getPref("video.ratio"), $video = this.$video, isNativeTouchGame = STATES.currentStream.titleInfo?.details.hasNativeTouchSupport, $webGL2Canvas; if (this.playerType == "webgl2") $webGL2Canvas = this.webGL2Player?.getCanvas(); let targetWidth, targetHeight, targetObjectFit; if (PREF_RATIO.includes(":")) { @@ -5708,16 +6043,16 @@ class StreamPlayer { } } function patchVideoApi() { - let PREF_SKIP_SPLASH_VIDEO = getPref("skip_splash_video"), showFunc = function() { + let PREF_SKIP_SPLASH_VIDEO = getPref("ui.splashVideo.skip"), showFunc = function() { if (this.style.visibility = "visible", !this.videoWidth) return; let playerOptions = { - processing: getPref("video_processing"), - sharpness: getPref("video_sharpness"), - saturation: getPref("video_saturation"), - contrast: getPref("video_contrast"), - brightness: getPref("video_brightness") + processing: getPref("video.processing"), + sharpness: getPref("video.processing.sharpness"), + saturation: getPref("video.saturation"), + contrast: getPref("video.contrast"), + brightness: getPref("video.brightness") }; - STATES.currentStream.streamPlayer = new StreamPlayer(this, getPref("video_player_type"), playerOptions), BxEvent.dispatch(window, BxEvent.STREAM_PLAYING, { + STATES.currentStream.streamPlayer = new StreamPlayer(this, getPref("video.player.type"), playerOptions), BxEvent.dispatch(window, BxEvent.STREAM_PLAYING, { $video: this }); }, nativePlay = HTMLMediaElement.prototype.play; @@ -5732,7 +6067,7 @@ function patchVideoApi() { }; } function patchRtcCodecs() { - if (getPref("stream_codec_profile") === "default") return; + if (getPref("stream.video.codecProfile") === "default") return; if (typeof RTCRtpTransceiver === "undefined" || !("setCodecPreferences" in RTCRtpTransceiver.prototype)) return !1; } function patchRtcPeerConnection() { @@ -5743,13 +6078,13 @@ function patchRtcPeerConnection() { dataChannel }), dataChannel; }; - let maxVideoBitrate = getPref("bitrate_video_max"), codec = getPref("stream_codec_profile"); - if (codec !== "default" || maxVideoBitrate > 0) { + let maxVideoBitrateDef = getPrefDefinition("stream.video.maxBitrate"), maxVideoBitrate = getPref("stream.video.maxBitrate"), codec = getPref("stream.video.codecProfile"); + if (codec !== "default" || maxVideoBitrate < maxVideoBitrateDef.max) { let nativeSetLocalDescription = RTCPeerConnection.prototype.setLocalDescription; RTCPeerConnection.prototype.setLocalDescription = function(description) { if (codec !== "default") arguments[0].sdp = setCodecPreferences(arguments[0].sdp, codec); try { - if (maxVideoBitrate > 0 && description) arguments[0].sdp = patchSdpBitrate(arguments[0].sdp, Math.round(maxVideoBitrate / 1000)); + if (maxVideoBitrate < maxVideoBitrateDef.max && description) arguments[0].sdp = patchSdpBitrate(arguments[0].sdp, Math.round(maxVideoBitrate / 1000)); } catch (e) { BxLogger.error("setLocalDescription", e); } @@ -5771,7 +6106,7 @@ function patchAudioContext() { let ctx = new OrgAudioContext(options); return BxLogger.info("patchAudioContext", ctx, options), ctx.createGain = function() { let gainNode = nativeCreateGain.apply(this); - return gainNode.gain.value = getPref("audio_volume") / 100, STATES.currentStream.audioGainNode = gainNode, gainNode; + return gainNode.gain.value = getPref("audio.volume") / 100, STATES.currentStream.audioGainNode = gainNode, gainNode; }, STATES.currentStream.audioContext = ctx, ctx; }; } @@ -5827,8 +6162,7 @@ class ProductDetailsPage { static $btnShortcut = AppInterface && createButton({ icon: BxIcon.CREATE_SHORTCUT, label: t("create-shortcut"), - style: 32, - tabIndex: 0, + style: 64, onClick: (e) => { AppInterface.createShortcut(window.location.pathname.substring(6)); } @@ -5836,8 +6170,7 @@ class ProductDetailsPage { static $btnWallpaper = AppInterface && createButton({ icon: BxIcon.DOWNLOAD, label: t("wallpaper"), - style: 32, - tabIndex: 0, + style: 64, onClick: (e) => { let details = parseDetailsPath(window.location.pathname); details && AppInterface.downloadWallpapers(details.titleSlug, details.productId); @@ -5914,7 +6247,7 @@ class StreamUiHandler { if ($gripHandle && $gripHandle.ariaExpanded === "true") $gripHandle.dispatchEvent(new PointerEvent("pointerdown")), $gripHandle.click(), $gripHandle.dispatchEvent(new PointerEvent("pointerdown")), $gripHandle.click(); }, $btnStreamSettings = StreamUiHandler.$btnStreamSettings; if (typeof $btnStreamSettings === "undefined") $btnStreamSettings = StreamUiHandler.cloneStreamHudButton($orgButton, t("better-xcloud"), BxIcon.BETTER_XCLOUD), $btnStreamSettings?.addEventListener("click", (e) => { - hideGripHandle(), e.preventDefault(), SettingsNavigationDialog.getInstance().show(); + hideGripHandle(), e.preventDefault(), SettingsDialog.getInstance().show(); }), StreamUiHandler.$btnStreamSettings = $btnStreamSettings; let streamStats = StreamStats.getInstance(), $btnStreamStats = StreamUiHandler.$btnStreamStats; if (typeof $btnStreamStats === "undefined") $btnStreamStats = StreamUiHandler.cloneStreamHudButton($orgButton, t("stream-stats"), BxIcon.STREAM_STATS), $btnStreamStats?.addEventListener("click", async (e) => { @@ -5978,8 +6311,7 @@ class RootDialogObserver { static $btnShortcut = AppInterface && createButton({ icon: BxIcon.CREATE_SHORTCUT, label: t("create-shortcut"), - style: 32 | 4 | 64 | 1024 | 2048, - tabIndex: 0, + style: 64 | 8 | 128 | 2048 | 4096, onClick: (e) => { window.BX_EXPOSED.dialogRoutes?.closeAll(); let $btn = e.target.closest("button"); @@ -5989,8 +6321,7 @@ class RootDialogObserver { static $btnWallpaper = AppInterface && createButton({ icon: BxIcon.DOWNLOAD, label: t("wallpaper"), - style: 32 | 4 | 64 | 1024 | 2048, - tabIndex: 0, + style: 64 | 8 | 128 | 2048 | 4096, onClick: (e) => { window.BX_EXPOSED.dialogRoutes?.closeAll(); let $btn = e.target.closest("button"), details = parseDetailsPath($btn.dataset.path); @@ -6057,9 +6388,9 @@ window.addEventListener("load", (e) => { }); document.addEventListener("readystatechange", (e) => { if (document.readyState !== "interactive") return; - if (STATES.isSignedIn = !!window.xbcUser?.isSignedIn, STATES.isSignedIn) getPref("xhome_enabled") && RemotePlayManager.getInstance().initialize(); + if (STATES.isSignedIn = !!window.xbcUser?.isSignedIn, STATES.isSignedIn) RemotePlayManager.getInstance()?.initialize(); else window.setTimeout(HeaderSection.watchHeader, 2000); - if (getPref("ui_hide_sections").includes("friends")) { + if (getPref("ui.hideSections").includes("friends")) { let $parent = document.querySelector("div[class*=PlayWithFriendsSkeleton]")?.closest("div[class*=HomePage-module]"); $parent && ($parent.style.display = "none"); } @@ -6071,7 +6402,7 @@ window.addEventListener("popstate", onHistoryChanged); window.history.pushState = patchHistoryMethod("pushState"); window.history.replaceState = patchHistoryMethod("replaceState"); window.addEventListener(BxEvent.XCLOUD_SERVERS_UNAVAILABLE, (e) => { - if (STATES.supportedRegion = !1, window.setTimeout(HeaderSection.watchHeader, 2000), document.querySelector("div[class^=UnsupportedMarketPage-module__container]")) SettingsNavigationDialog.getInstance().show(); + if (STATES.supportedRegion = !1, window.setTimeout(HeaderSection.watchHeader, 2000), document.querySelector("div[class^=UnsupportedMarketPage-module__container]")) SettingsDialog.getInstance().show(); }, { once: !0 }); window.addEventListener(BxEvent.XCLOUD_SERVERS_READY, (e) => { STATES.isSignedIn = !0, window.setTimeout(HeaderSection.watchHeader, 2000); @@ -6080,12 +6411,12 @@ window.addEventListener(BxEvent.STREAM_LOADING, (e) => { if (window.location.pathname.includes("/launch/") && STATES.currentStream.titleInfo) STATES.currentStream.titleSlug = productTitleToSlug(STATES.currentStream.titleInfo.product.title); else STATES.currentStream.titleSlug = "remote-play"; }); -getPref("ui_loading_screen_game_art") && window.addEventListener(BxEvent.TITLE_INFO_READY, LoadingScreen.setup); +getPref("loadingScreen.gameArt.show") && window.addEventListener(BxEvent.TITLE_INFO_READY, LoadingScreen.setup); window.addEventListener(BxEvent.STREAM_STARTING, (e) => { - if (LoadingScreen.hide(), !getPref("mkb_enabled") && getPref("mkb_hide_idle_cursor")) MouseCursorHider.start(), MouseCursorHider.hide(); + LoadingScreen.hide(); }); window.addEventListener(BxEvent.STREAM_PLAYING, (e) => { - STATES.isPlaying = !0, StreamUiHandler.observe(), updateVideoPlayer(); + window.BX_STREAM_SETTINGS = StreamSettings.settings, StreamSettings.refreshAllSettings(), STATES.isPlaying = !0, StreamUiHandler.observe(), updateVideoPlayer(); }); window.addEventListener(BxEvent.STREAM_ERROR_PAGE, (e) => { BxEvent.dispatch(window, BxEvent.STREAM_STOPPED); @@ -6118,8 +6449,11 @@ window.addEventListener("pagehide", (e) => { BxEvent.dispatch(window, BxEvent.STREAM_STOPPED); }); function main() { - if (getPref("game_msfs2020_force_native_mkb")) BX_FLAGS.ForceNativeMkbTitles.push("9PMQDM08SNK9"); - if (patchRtcPeerConnection(), patchRtcCodecs(), interceptHttpRequests(), patchVideoApi(), patchCanvasContext(), getPref("audio_enable_volume_control") && patchAudioContext(), getPref("block_tracking")) patchMeControl(), disableAdobeAudienceManager(); - if (RootDialogObserver.waitForRootDialog(), addCss(), GuideMenu.getInstance().addEventListeners(), StreamStatsCollector.setupEvents(), StreamBadges.setupEvents(), StreamStats.setupEvents(), getPref("controller_show_connection_status")) window.addEventListener("gamepadconnected", (e) => showGamepadToast(e.gamepad)), window.addEventListener("gamepaddisconnected", (e) => showGamepadToast(e.gamepad)); + if (GhPagesUtils.fetchLatestCommit(), getPref("nativeMkb.mode") === "on") { + let customList = getPref("nativeMkb.forcedGames"); + BX_FLAGS.ForceNativeMkbTitles.push(...customList); + } + if (StreamSettings.setup(), patchRtcPeerConnection(), patchRtcCodecs(), interceptHttpRequests(), patchVideoApi(), patchCanvasContext(), getPref("audio.volume.booster.enabled") && patchAudioContext(), getPref("block.tracking")) patchMeControl(), disableAdobeAudienceManager(); + if (RootDialogObserver.waitForRootDialog(), addCss(), GuideMenu.getInstance().addEventListeners(), StreamStatsCollector.setupEvents(), StreamBadges.setupEvents(), StreamStats.setupEvents(), getPref("ui.controllerStatus.show")) window.addEventListener("gamepadconnected", (e) => showGamepadToast(e.gamepad)), window.addEventListener("gamepaddisconnected", (e) => showGamepadToast(e.gamepad)); } main(); diff --git a/dist/better-xcloud.user.js b/dist/better-xcloud.user.js old mode 100644 new mode 100755 index 62f10bf..c5ab15d --- a/dist/better-xcloud.user.js +++ b/dist/better-xcloud.user.js @@ -143,7 +143,7 @@ function deepClone(obj) { } var BxEvent; ((BxEvent) => { - BxEvent.JUMP_BACK_IN_READY = "bx-jump-back-in-ready", BxEvent.POPSTATE = "bx-popstate", BxEvent.TITLE_INFO_READY = "bx-title-info-ready", BxEvent.SETTINGS_CHANGED = "bx-settings-changed", BxEvent.STREAM_LOADING = "bx-stream-loading", BxEvent.STREAM_STARTING = "bx-stream-starting", BxEvent.STREAM_STARTED = "bx-stream-started", BxEvent.STREAM_PLAYING = "bx-stream-playing", BxEvent.STREAM_STOPPED = "bx-stream-stopped", BxEvent.STREAM_ERROR_PAGE = "bx-stream-error-page", BxEvent.STREAM_WEBRTC_CONNECTED = "bx-stream-webrtc-connected", BxEvent.STREAM_WEBRTC_DISCONNECTED = "bx-stream-webrtc-disconnected", BxEvent.MKB_UPDATED = "bx-mkb-updated", BxEvent.KEYBOARD_SHORTCUTS_UPDATED = "bx-keyboard-shortcuts-updated", BxEvent.STREAM_SESSION_READY = "bx-stream-session-ready", BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED = "bx-custom-touch-layouts-loaded", BxEvent.TOUCH_LAYOUT_MANAGER_READY = "bx-touch-layout-manager-ready", BxEvent.REMOTE_PLAY_READY = "bx-remote-play-ready", BxEvent.REMOTE_PLAY_FAILED = "bx-remote-play-failed", BxEvent.XCLOUD_SERVERS_READY = "bx-servers-ready", BxEvent.XCLOUD_SERVERS_UNAVAILABLE = "bx-servers-unavailable", BxEvent.DATA_CHANNEL_CREATED = "bx-data-channel-created", BxEvent.DEVICE_VIBRATION_CHANGED = "bx-device-vibration-changed", BxEvent.GAME_BAR_ACTION_ACTIVATED = "bx-game-bar-action-activated", BxEvent.MICROPHONE_STATE_CHANGED = "bx-microphone-state-changed", BxEvent.SPEAKER_STATE_CHANGED = "bx-speaker-state-changed", BxEvent.VIDEO_VISIBILITY_CHANGED = "bx-video-visibility-changed", BxEvent.CAPTURE_SCREENSHOT = "bx-capture-screenshot", BxEvent.POINTER_LOCK_REQUESTED = "bx-pointer-lock-requested", BxEvent.POINTER_LOCK_EXITED = "bx-pointer-lock-exited", BxEvent.NAVIGATION_FOCUS_CHANGED = "bx-nav-focus-changed", BxEvent.XCLOUD_DIALOG_SHOWN = "bx-xcloud-dialog-shown", BxEvent.XCLOUD_DIALOG_DISMISSED = "bx-xcloud-dialog-dismissed", BxEvent.XCLOUD_GUIDE_MENU_SHOWN = "bx-xcloud-guide-menu-shown", BxEvent.XCLOUD_POLLING_MODE_CHANGED = "bx-xcloud-polling-mode-changed", BxEvent.XCLOUD_RENDERING_COMPONENT = "bx-xcloud-rendering-component", BxEvent.XCLOUD_ROUTER_HISTORY_READY = "bx-xcloud-router-history-ready"; + BxEvent.JUMP_BACK_IN_READY = "bx-jump-back-in-ready", BxEvent.POPSTATE = "bx-popstate", BxEvent.TITLE_INFO_READY = "bx-title-info-ready", BxEvent.SETTINGS_CHANGED = "bx-settings-changed", BxEvent.STREAM_LOADING = "bx-stream-loading", BxEvent.STREAM_STARTING = "bx-stream-starting", BxEvent.STREAM_STARTED = "bx-stream-started", BxEvent.STREAM_PLAYING = "bx-stream-playing", BxEvent.STREAM_STOPPED = "bx-stream-stopped", BxEvent.STREAM_ERROR_PAGE = "bx-stream-error-page", BxEvent.STREAM_WEBRTC_CONNECTED = "bx-stream-webrtc-connected", BxEvent.STREAM_WEBRTC_DISCONNECTED = "bx-stream-webrtc-disconnected", BxEvent.MKB_UPDATED = "bx-mkb-updated", BxEvent.KEYBOARD_SHORTCUTS_UPDATED = "bx-keyboard-shortcuts-updated", BxEvent.STREAM_SESSION_READY = "bx-stream-session-ready", BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED = "bx-custom-touch-layouts-loaded", BxEvent.TOUCH_LAYOUT_MANAGER_READY = "bx-touch-layout-manager-ready", BxEvent.REMOTE_PLAY_READY = "bx-remote-play-ready", BxEvent.REMOTE_PLAY_FAILED = "bx-remote-play-failed", BxEvent.XCLOUD_SERVERS_READY = "bx-servers-ready", BxEvent.XCLOUD_SERVERS_UNAVAILABLE = "bx-servers-unavailable", BxEvent.DATA_CHANNEL_CREATED = "bx-data-channel-created", BxEvent.DEVICE_VIBRATION_CHANGED = "bx-device-vibration-changed", BxEvent.GAME_BAR_ACTION_ACTIVATED = "bx-game-bar-action-activated", BxEvent.MICROPHONE_STATE_CHANGED = "bx-microphone-state-changed", BxEvent.SPEAKER_STATE_CHANGED = "bx-speaker-state-changed", BxEvent.VIDEO_VISIBILITY_CHANGED = "bx-video-visibility-changed", BxEvent.CAPTURE_SCREENSHOT = "bx-capture-screenshot", BxEvent.POINTER_LOCK_REQUESTED = "bx-pointer-lock-requested", BxEvent.POINTER_LOCK_EXITED = "bx-pointer-lock-exited", BxEvent.NAVIGATION_FOCUS_CHANGED = "bx-nav-focus-changed", BxEvent.GH_PAGES_FORCE_NATIVE_MKB_UPDATED = "bx-gh-pages-force-native-mkb-updated", BxEvent.XCLOUD_DIALOG_SHOWN = "bx-xcloud-dialog-shown", BxEvent.XCLOUD_DIALOG_DISMISSED = "bx-xcloud-dialog-dismissed", BxEvent.XCLOUD_GUIDE_MENU_SHOWN = "bx-xcloud-guide-menu-shown", BxEvent.XCLOUD_POLLING_MODE_CHANGED = "bx-xcloud-polling-mode-changed", BxEvent.XCLOUD_RENDERING_COMPONENT = "bx-xcloud-rendering-component", BxEvent.XCLOUD_ROUTER_HISTORY_READY = "bx-xcloud-router-history-ready"; function dispatch(target, eventName, data) { if (!target) return; if (!eventName) { @@ -205,11 +205,10 @@ class GhPagesUtils { if (latestCommitHash) return `${prefix}/${latestCommitHash}/${path}`; else return `${prefix}/refs/heads/gh-pages/${path}`; } - static getNativeMkbCustomList() { + static getNativeMkbCustomList(update = !1) { let key = "BetterXcloud.GhPages.ForceNativeMkb"; - NATIVE_FETCH(GhPagesUtils.getUrl("native-mkb/ids.json")).then((response) => response.json()).then((json) => { - if (json.$schemaVersion !== 1) return; - window.localStorage.setItem(key, JSON.stringify(json)); + update && NATIVE_FETCH(GhPagesUtils.getUrl("native-mkb/ids.json")).then((response) => response.json()).then((json) => { + if (json.$schemaVersion === 1) window.localStorage.setItem(key, JSON.stringify(json)), BxEvent.dispatch(window, BxEvent.GH_PAGES_FORCE_NATIVE_MKB_UPDATED); }); let info = JSON.parse(window.localStorage.getItem(key) || "{}"); if (info.$schemaVersion !== 1) return window.localStorage.removeItem(key), {}; @@ -524,6 +523,7 @@ var SUPPORTED_LANGUAGES = { small: "Small", "smart-tv": "Smart TV", sound: "Sound", + standard: "Standard", standby: "Standby", "stat-bitrate": "Bitrate", "stat-decode-time": "Decode time", @@ -602,6 +602,7 @@ var SUPPORTED_LANGUAGES = { "vibration-status": "Vibration", video: "Video", "virtual-controller": "Virtual controller", + "virtual-controller-slot": "Virtual controller slot", "visual-quality": "Visual quality", "visual-quality-high": "High", "visual-quality-low": "Low", @@ -678,9 +679,7 @@ class Translations { localStorage.setItem(Translations.KEY_LOCALE, locale); } } -var t = Translations.get, ut = (text) => { - return BxLogger.warning("Untranslated text", text), text; -}; +var t = Translations.get; Translations.init(); class NavigationUtils { static setNearby($elm, nearby) { @@ -798,7 +797,7 @@ function clearDataSet($elm) { function renderPresetsList($select, allPresets, selectedValue, addOffValue = !1) { if (removeChildElements($select), addOffValue) { let $option = CE("option", { value: 0 }, t("off")); - $select.appendChild($option); + $option.selected = selectedValue === 0, $select.appendChild($option); } let groups = { default: t("default"), @@ -808,11 +807,10 @@ function renderPresetsList($select, allPresets, selectedValue, addOffValue = !1) let $optGroup = CE("optgroup", { label: groups[key] }); for (let id of allPresets[key]) { let record = allPresets.data[id], $option = CE("option", { value: record.id }, record.name); - $optGroup.appendChild($option); + $option.selected = selectedValue === record.id, $optGroup.appendChild($option); } if ($optGroup.hasChildNodes()) $select.appendChild($optGroup); } - if (selectedValue !== null) $select.value = selectedValue.toString(), BxEvent.dispatch($select, "input", { manualTrigger: !0 }); } var FILE_SIZE_UNITS = ["B", "KB", "MB", "GB", "TB"]; function humanFileSize(size) { @@ -832,6 +830,9 @@ function secondsToHms(seconds) { if (h > 0 && output.push(`${h}h`), m > 0 && output.push(`${m}m`), s > 0 || output.length === 0) output.push(`${s}s`); return output.join(" "); } +function escapeCssSelector(name) { + return name.replaceAll(".", "-"); +} var CE = createElement; window.BX_CE = createElement; class Toast { @@ -1139,6 +1140,45 @@ class MkbMappingPresetsTable extends BasePresetsTable { DEFAULT_PRESETS = { [-1]: { id: -1, + name: t("standard"), + data: { + mapping: { + 16: ["Backquote"], + 12: ["ArrowUp", "Digit1"], + 13: ["ArrowDown", "Digit2"], + 14: ["ArrowLeft", "Digit3"], + 15: ["ArrowRight", "Digit4"], + 100: ["KeyW"], + 101: ["KeyS"], + 102: ["KeyA"], + 103: ["KeyD"], + 200: ["KeyU"], + 201: ["KeyJ"], + 202: ["KeyH"], + 203: ["KeyK"], + 0: ["Space", "KeyE"], + 2: ["KeyR"], + 1: ["KeyC", "Backspace"], + 3: ["KeyE"], + 9: ["Enter"], + 8: ["Tab"], + 4: ["KeyQ"], + 5: ["KeyF"], + 7: ["Mouse0"], + 6: ["Mouse2"], + 10: ["KeyX"], + 11: ["KeyZ"] + }, + mouse: { + mapTo: 2, + sensitivityX: 100, + sensitivityY: 100, + deadzoneCounterweight: 20 + } + } + }, + [-2]: { + id: -2, name: "Shooter", data: { mapping: { @@ -1191,13 +1231,13 @@ class KeyboardShortcutsTable extends BasePresetsTable { DEFAULT_PRESETS = { [-1]: { id: -1, - name: t("default"), + name: t("standard"), data: { mapping: { - "mkb-toggle": { + "mkb.toggle": { code: "F8" }, - "stream-screenshot-capture": { + "stream.screenshot.capture": { code: "Slash" } } @@ -1233,26 +1273,26 @@ function getSupportedCodecProfiles() { } class GlobalSettingsStorage extends BaseSettingsStore { static DEFINITIONS = { - versionLastCheck: { + "version.lastCheck": { default: 0 }, - versionLatest: { + "version.latest": { default: "" }, - versionCurrent: { + "version.current": { default: "" }, - bxLocale: { + "bx.locale": { label: t("language"), default: localStorage.getItem("BetterXcloud.Locale") || "en-US", options: SUPPORTED_LANGUAGES }, - serverRegion: { + "server.region": { label: t("region"), note: CE("a", { target: "_blank", href: "https://umap.openstreetmap.fr/en/map/xbox-cloud-gaming-servers_1135022" }, t("server-locations")), default: "default" }, - serverBypassRestriction: { + "server.bypassRestriction": { label: t("bypass-region-restriction"), note: "⚠️ " + t("use-this-at-your-own-risk"), default: "off", @@ -1261,7 +1301,7 @@ class GlobalSettingsStorage extends BaseSettingsStore { off: t("off") }, BypassServers) }, - streamLocale: { + "stream.locale": { label: t("preferred-game-language"), default: "default", options: { @@ -1298,7 +1338,7 @@ class GlobalSettingsStorage extends BaseSettingsStore { "zh-TW": "中文 (繁體)" } }, - streamResolution: { + "stream.video.resolution": { label: t("target-resolution"), default: "auto", options: { @@ -1309,10 +1349,10 @@ class GlobalSettingsStorage extends BaseSettingsStore { }, suggest: { lowest: "720p", - highest: "1080p" + highest: "1080p-hq" } }, - streamCodecProfile: { + "stream.video.codecProfile": { label: t("visual-quality"), default: "default", options: getSupportedCodecProfiles(), @@ -1325,31 +1365,31 @@ class GlobalSettingsStorage extends BaseSettingsStore { }; } }, - serverPreferIpv6: { + "server.ipv6.prefer": { label: t("prefer-ipv6-server"), default: !1 }, - screenshotApplyFilters: { + "screenshot.applyFilters": { requiredVariants: "full", label: t("screenshot-apply-filters"), default: !1 }, - uiSkipSplashVideo: { + "ui.splashVideo.skip": { label: t("skip-splash-video"), default: !1 }, - uiHideSystemMenuIcon: { + "ui.systemMenu.hideHandle": { label: t("hide-system-menu-icon"), default: !1 }, - streamCombineSources: { + "stream.video.combineAudio": { requiredVariants: "full", label: t("combine-audio-video-streams"), default: !1, experimental: !0, note: t("combine-audio-video-streams-summary") }, - touchControllerMode: { + "touchController.mode": { requiredVariants: "full", label: t("tc-availability"), default: "all", @@ -1361,13 +1401,13 @@ class GlobalSettingsStorage extends BaseSettingsStore { unsupported: !STATES.userAgent.capabilities.touch, unsupportedValue: "default" }, - touchControllerAutoOff: { + "touchController.autoOff": { requiredVariants: "full", label: t("tc-auto-off"), default: !1, unsupported: !STATES.userAgent.capabilities.touch }, - touchControllerDefaultOpacity: { + "touchController.opacity.default": { requiredVariants: "full", label: t("tc-default-opacity"), default: 100, @@ -1381,7 +1421,7 @@ class GlobalSettingsStorage extends BaseSettingsStore { }, unsupported: !STATES.userAgent.capabilities.touch }, - touchControllerStyleStandard: { + "touchController.style.standard": { requiredVariants: "full", label: t("tc-standard-layout-style"), default: "default", @@ -1392,7 +1432,7 @@ class GlobalSettingsStorage extends BaseSettingsStore { }, unsupported: !STATES.userAgent.capabilities.touch }, - touchControllerStyleCustom: { + "touchController.style.custom": { requiredVariants: "full", label: t("tc-custom-layout-style"), default: "default", @@ -1402,21 +1442,21 @@ class GlobalSettingsStorage extends BaseSettingsStore { }, unsupported: !STATES.userAgent.capabilities.touch }, - uiSimplifyStreamMenu: { + "ui.streamMenu.simplify": { label: t("simplify-stream-menu"), default: !1 }, - mkbHideIdleCursor: { + "mkb.cursor.hideIdle": { requiredVariants: "full", label: t("hide-idle-cursor"), default: !1 }, - uiDisableFeedbackDialog: { + "ui.feedbackDialog.disabled": { requiredVariants: "full", label: t("disable-post-stream-feedback-dialog"), default: !1 }, - streamMaxVideoBitrate: { + "stream.video.maxBitrate": { requiredVariants: "full", label: t("bitrate-video-maximum"), note: "⚠️ " + t("unexpected-behavior"), @@ -1443,7 +1483,7 @@ class GlobalSettingsStorage extends BaseSettingsStore { highest: 0 } }, - gameBarPosition: { + "gameBar.position": { requiredVariants: "full", label: t("position"), default: "bottom-left", @@ -1453,7 +1493,7 @@ class GlobalSettingsStorage extends BaseSettingsStore { "bottom-right": t("bottom-right") } }, - localCoOpEnabled: { + "localCoOp.enabled": { requiredVariants: "full", label: t("enable-local-co-op-support"), default: !1, @@ -1462,11 +1502,11 @@ class GlobalSettingsStorage extends BaseSettingsStore { target: "_blank" }, t("enable-local-co-op-support-note")) }, - uiShowControllerStatus: { + "ui.controllerStatus.show": { label: t("show-controller-connection-status"), default: !0 }, - deviceVibrationMode: { + "deviceVibration.mode": { requiredVariants: "full", label: t("device-vibration"), default: "off", @@ -1476,7 +1516,7 @@ class GlobalSettingsStorage extends BaseSettingsStore { auto: t("device-vibration-not-using-gamepad") } }, - deviceVibrationIntensity: { + "deviceVibration.intensity": { requiredVariants: "full", label: t("vibration-intensity"), default: 50, @@ -1488,7 +1528,7 @@ class GlobalSettingsStorage extends BaseSettingsStore { exactTicks: 20 } }, - controllerPollingRate: { + "controller.pollingRate": { requiredVariants: "full", label: t("polling-rate"), default: 4, @@ -1506,7 +1546,7 @@ class GlobalSettingsStorage extends BaseSettingsStore { } } }, - mkbEnabled: { + "mkb.enabled": { requiredVariants: "full", label: t("enable-mkb"), default: !1, @@ -1521,7 +1561,7 @@ class GlobalSettingsStorage extends BaseSettingsStore { }, "⚠️ " + note); } }, - nativeMkbMode: { + "nativeMkb.mode": { requiredVariants: "full", label: t("native-mkb"), default: "default", @@ -1536,14 +1576,17 @@ class GlobalSettingsStorage extends BaseSettingsStore { else delete setting.options["on"]; } }, - forceNativeMkbGames: { + "nativeMkb.forcedGames": { label: t("force-native-mkb-games"), default: [], + unsupported: !AppInterface && UserAgent.isMobile(), ready: (setting) => { - setting.multipleOptions = GhPagesUtils.getNativeMkbCustomList(), setting.unsupported = !AppInterface && UserAgent.isMobile(); + if (!setting.unsupported) setting.multipleOptions = GhPagesUtils.getNativeMkbCustomList(!0), window.addEventListener(BxEvent.GH_PAGES_FORCE_NATIVE_MKB_UPDATED, (e) => { + setting.multipleOptions = GhPagesUtils.getNativeMkbCustomList(); + }); } }, - nativeMkbScrollXSensitivity: { + "nativeMkb.scroll.sensitivityX": { requiredVariants: "full", label: t("horizontal-scroll-sensitivity"), default: 0, @@ -1558,7 +1601,7 @@ class GlobalSettingsStorage extends BaseSettingsStore { } } }, - nativeMkbScrollYSensitivity: { + "nativeMkb.scroll.sensitivityY": { requiredVariants: "full", label: t("vertical-scroll-sensitivity"), default: 0, @@ -1573,11 +1616,11 @@ class GlobalSettingsStorage extends BaseSettingsStore { } } }, - mkbMappingPresetIdP1: { + "mkb.p1.preset.mappingId": { requiredVariants: "full", default: -1 }, - mkbSlotP1: { + "mkb.p1.slot": { requiredVariants: "full", default: 1, min: 1, @@ -1586,11 +1629,11 @@ class GlobalSettingsStorage extends BaseSettingsStore { hideSlider: !0 } }, - mkbMappingPresetIdP2: { + "mkb.p2.preset.mappingId": { requiredVariants: "full", default: 0 }, - mkbSlotP2: { + "mkb.p2.slot": { requiredVariants: "full", default: 0, min: 0, @@ -1602,24 +1645,24 @@ class GlobalSettingsStorage extends BaseSettingsStore { } } }, - keyboardShortcutsInGamePresetId: { + "keyboardShortcuts.preset.inGameId": { requiredVariants: "full", default: -1 }, - uiReduceAnimations: { + "ui.reduceAnimations": { label: t("reduce-animations"), default: !1 }, - loadingScreenGameArt: { + "loadingScreen.gameArt.show": { requiredVariants: "full", label: t("show-game-art"), default: !0 }, - loadingScreenShowWaitTime: { + "loadingScreen.waitTime.show": { label: t("show-wait-time"), default: !0 }, - loadingScreenRocket: { + "loadingScreen.rocket": { label: t("rocket-animation"), default: "show", options: { @@ -1628,11 +1671,11 @@ class GlobalSettingsStorage extends BaseSettingsStore { hide: t("rocket-always-hide") } }, - uiControllerFriendly: { + "ui.controllerFriendly": { label: t("controller-friendly-ui"), default: BX_FLAGS.DeviceInfo.deviceType !== "unknown" }, - uiLayout: { + "ui.layout": { requiredVariants: "full", label: t("layout"), default: "default", @@ -1642,11 +1685,11 @@ class GlobalSettingsStorage extends BaseSettingsStore { tv: t("smart-tv") } }, - uiHideScrollbar: { + "ui.hideScrollbar": { label: t("hide-scrollbar"), default: !1 }, - uiHideSections: { + "ui.hideSections": { requiredVariants: "full", label: t("hide-sections"), default: [], @@ -1662,24 +1705,24 @@ class GlobalSettingsStorage extends BaseSettingsStore { size: 0 } }, - byogDisabled: { + "feature.byog.disabled": { label: t("disable-byog-feature"), default: !1 }, - uiGameCardShowWaitTime: { + "ui.gameCard.waitTime.show": { requiredVariants: "full", label: t("show-wait-time-in-game-card"), default: !0 }, - blockSocialFeatures: { + "block.social": { label: t("disable-social-features"), default: !1 }, - blockTracking: { + "block.tracking": { label: t("disable-xcloud-analytics"), default: !1 }, - userAgentProfile: { + "userAgent.profile": { label: t("user-agent-profile"), note: "⚠️ " + t("unexpected-behavior"), default: BX_FLAGS.DeviceInfo.deviceType === "android-tv" || BX_FLAGS.DeviceInfo.deviceType === "webos" ? "vr-oculus" : "default", @@ -1693,7 +1736,7 @@ class GlobalSettingsStorage extends BaseSettingsStore { custom: t("custom") } }, - videoPlayerType: { + "video.player.type": { label: t("renderer"), default: "default", options: { @@ -1705,7 +1748,7 @@ class GlobalSettingsStorage extends BaseSettingsStore { highest: "webgl2" } }, - videoProcessing: { + "video.processing": { label: t("clarity-boost"), default: "usm", options: { @@ -1717,7 +1760,7 @@ class GlobalSettingsStorage extends BaseSettingsStore { highest: "cas" } }, - videoPowerPreference: { + "video.player.powerPreference": { label: t("renderer-configuration"), default: "default", options: { @@ -1729,7 +1772,7 @@ class GlobalSettingsStorage extends BaseSettingsStore { highest: "low-power" } }, - videoMaxFps: { + "video.maxFps": { label: t("max-fps"), default: 60, min: 10, @@ -1742,7 +1785,7 @@ class GlobalSettingsStorage extends BaseSettingsStore { } } }, - videoSharpness: { + "video.processing.sharpness": { label: t("sharpness"), default: 0, min: 0, @@ -1758,7 +1801,7 @@ class GlobalSettingsStorage extends BaseSettingsStore { highest: 2 } }, - videoRatio: { + "video.ratio": { label: t("aspect-ratio"), note: t("aspect-ratio-note"), default: "16:9", @@ -1771,7 +1814,7 @@ class GlobalSettingsStorage extends BaseSettingsStore { fill: t("stretch") } }, - videoSaturation: { + "video.saturation": { label: t("saturation"), default: 100, min: 50, @@ -1781,7 +1824,7 @@ class GlobalSettingsStorage extends BaseSettingsStore { ticks: 25 } }, - videoContrast: { + "video.contrast": { label: t("contrast"), default: 100, min: 50, @@ -1791,7 +1834,7 @@ class GlobalSettingsStorage extends BaseSettingsStore { ticks: 25 } }, - videoBrightness: { + "video.brightness": { label: t("brightness"), default: 100, min: 50, @@ -1801,16 +1844,16 @@ class GlobalSettingsStorage extends BaseSettingsStore { ticks: 25 } }, - audioMicOnPlaying: { + "audio.mic.onPlaying": { label: t("enable-mic-on-startup"), default: !1 }, - audioEnableVolumeControl: { + "audio.volume.booster.enabled": { requiredVariants: "full", label: t("enable-volume-control"), default: !1 }, - audioVolume: { + "audio.volume": { label: t("volume"), default: 100, min: 0, @@ -1821,7 +1864,7 @@ class GlobalSettingsStorage extends BaseSettingsStore { ticks: 100 } }, - statsItems: { + "stats.items": { label: t("stats"), default: ["ping", "fps", "btr", "dt", "pl", "fl"], multipleOptions: { @@ -1848,15 +1891,15 @@ class GlobalSettingsStorage extends BaseSettingsStore { multipleOptions[key] = key.toUpperCase() + ": " + multipleOptions[key]; } }, - statsShowWhenPlaying: { + "stats.showWhenPlaying": { label: t("show-stats-on-startup"), default: !1 }, - statsQuickGlance: { + "stats.quickGlance.enabled": { label: "👀 " + t("enable-quick-glance-mode"), default: !0 }, - statsPosition: { + "stats.position": { label: t("position"), default: "top-right", options: { @@ -1865,7 +1908,7 @@ class GlobalSettingsStorage extends BaseSettingsStore { "top-right": t("top-right") } }, - statsTextSize: { + "stats.textSize": { label: t("text-size"), default: "0.9rem", options: { @@ -1874,11 +1917,11 @@ class GlobalSettingsStorage extends BaseSettingsStore { "1.1rem": t("large") } }, - statsTransparent: { + "stats.transparent": { label: t("transparent-background"), default: !1 }, - statsOpacity: { + "stats.opacity": { label: t("opacity"), default: 80, min: 50, @@ -1889,16 +1932,16 @@ class GlobalSettingsStorage extends BaseSettingsStore { ticks: 10 } }, - statsConditionalFormatting: { + "stats.colors": { label: t("conditional-formatting"), default: !1 }, - xhomeEnabled: { + "xhome.enabled": { requiredVariants: "full", label: t("enable-remote-play-feature"), default: !1 }, - xhomeStreamResolution: { + "xhome.video.resolution": { requiredVariants: "full", default: "1080p", options: { @@ -1907,7 +1950,7 @@ class GlobalSettingsStorage extends BaseSettingsStore { "1080p-hq": "1080p (HQ)" } }, - gameFortniteForceConsole: { + "game.fortnite.forceConsole": { requiredVariants: "full", label: "🎮 " + t("fortnite-force-console-version"), default: !1, @@ -1922,10 +1965,10 @@ var globalSettings = new GlobalSettingsStorage, getPrefDefinition = globalSettin STORAGE.Global = globalSettings; function checkForUpdate() { if (SCRIPT_VERSION.includes("beta")) return; - let CHECK_INTERVAL_SECONDS = 7200, currentVersion = getPref("versionCurrent"), lastCheck = getPref("versionLastCheck"), now = Math.round(+new Date / 1000); + let CHECK_INTERVAL_SECONDS = 7200, currentVersion = getPref("version.current"), lastCheck = getPref("version.lastCheck"), now = Math.round(+new Date / 1000); if (currentVersion === SCRIPT_VERSION && now - lastCheck < CHECK_INTERVAL_SECONDS) return; - setPref("versionLastCheck", now), fetch("https://api.github.com/repos/redphx/better-xcloud/releases/latest").then((response) => response.json()).then((json) => { - setPref("versionLatest", json.tag_name.substring(1)), setPref("versionCurrent", SCRIPT_VERSION); + setPref("version.lastCheck", now), fetch("https://api.github.com/repos/redphx/better-xcloud/releases/latest").then((response) => response.json()).then((json) => { + setPref("version.latest", json.tag_name.substring(1)), setPref("version.current", SCRIPT_VERSION); }), Translations.updateTranslations(currentVersion === SCRIPT_VERSION); } function disablePwa() { @@ -1985,22 +2028,22 @@ function clearAllData() { } class SoundShortcut { static adjustGainNodeVolume(amount) { - if (!getPref("audioEnableVolumeControl")) return 0; - let currentValue = getPref("audioVolume"), nearestValue; + if (!getPref("audio.volume.booster.enabled")) return 0; + let currentValue = getPref("audio.volume"), nearestValue; if (amount > 0) nearestValue = ceilToNearest(currentValue, amount); else nearestValue = floorToNearest(currentValue, -1 * amount); let newValue; if (currentValue !== nearestValue) newValue = nearestValue; else newValue = currentValue + amount; - return newValue = setPref("audioVolume", newValue, !0), SoundShortcut.setGainNodeVolume(newValue), Toast.show(`${t("stream")} ❯ ${t("volume")}`, newValue + "%", { instant: !0 }), newValue; + return newValue = setPref("audio.volume", newValue, !0), SoundShortcut.setGainNodeVolume(newValue), Toast.show(`${t("stream")} ❯ ${t("volume")}`, newValue + "%", { instant: !0 }), newValue; } static setGainNodeVolume(value) { STATES.currentStream.audioGainNode && (STATES.currentStream.audioGainNode.gain.value = value / 100); } static muteUnmute() { - if (getPref("audioEnableVolumeControl") && STATES.currentStream.audioGainNode) { - let gainValue = STATES.currentStream.audioGainNode.gain.value, settingValue = getPref("audioVolume"), targetValue; - if (settingValue === 0) targetValue = 100, setPref("audioVolume", targetValue, !0); + if (getPref("audio.volume.booster.enabled") && STATES.currentStream.audioGainNode) { + let gainValue = STATES.currentStream.audioGainNode.gain.value, settingValue = getPref("audio.volume"), targetValue; + if (settingValue === 0) targetValue = 100, setPref("audio.volume", targetValue, !0); else if (gainValue === 0) targetValue = settingValue; else targetValue = 0; let status; @@ -2052,7 +2095,7 @@ class StreamStatsCollector { fps: { current: 0, toString() { - let maxFps = getPref("videoMaxFps"); + let maxFps = getPref("video.maxFps"); return maxFps < 60 ? `${maxFps}/${this.current}` : this.current.toString(); } }, @@ -2294,7 +2337,7 @@ class StreamStats { this.destroy(); return; } - let PREF_STATS_CONDITIONAL_FORMATTING = getPref("statsConditionalFormatting"), grade = "", statsCollector = StreamStatsCollector.getInstance(); + let PREF_STATS_CONDITIONAL_FORMATTING = getPref("stats.colors"), grade = "", statsCollector = StreamStatsCollector.getInstance(); await statsCollector.collect(); let statKey; for (statKey in this.stats) { @@ -2305,11 +2348,11 @@ class StreamStats { } }; refreshStyles() { - let PREF_ITEMS = getPref("statsItems"), $container = this.$container; - $container.dataset.stats = "[" + PREF_ITEMS.join("][") + "]", $container.dataset.position = getPref("statsPosition"), $container.dataset.transparent = getPref("statsTransparent"), $container.style.opacity = getPref("statsOpacity") + "%", $container.style.fontSize = getPref("statsTextSize"); + let PREF_ITEMS = getPref("stats.items"), $container = this.$container; + $container.dataset.stats = "[" + PREF_ITEMS.join("][") + "]", $container.dataset.position = getPref("stats.position"), $container.dataset.transparent = getPref("stats.transparent"), $container.style.opacity = getPref("stats.opacity") + "%", $container.style.fontSize = getPref("stats.textSize"); } hideSettingsUi() { - if (this.isGlancing() && !getPref("statsQuickGlance")) this.stop(); + if (this.isGlancing() && !getPref("stats.quickGlance.enabled")) this.stop(); } async render() { this.$container = CE("div", { class: "bx-stats-bar bx-gone" }); @@ -2325,7 +2368,7 @@ class StreamStats { } static setupEvents() { window.addEventListener(BxEvent.STREAM_PLAYING, (e) => { - let PREF_STATS_QUICK_GLANCE = getPref("statsQuickGlance"), PREF_STATS_SHOW_WHEN_PLAYING = getPref("statsShowWhenPlaying"), streamStats = StreamStats.getInstance(); + let PREF_STATS_QUICK_GLANCE = getPref("stats.quickGlance.enabled"), PREF_STATS_SHOW_WHEN_PLAYING = getPref("stats.showWhenPlaying"), streamStats = StreamStats.getInstance(); if (PREF_STATS_SHOW_WHEN_PLAYING) streamStats.start(); else if (PREF_STATS_QUICK_GLANCE) streamStats.quickGlanceSetup(), !PREF_STATS_SHOW_WHEN_PLAYING && streamStats.start(!0); }); @@ -2335,11 +2378,11 @@ class StreamStats { } } function onChangeVideoPlayerType() { - let playerType = getPref("videoPlayerType"), $videoProcessing = document.getElementById(`bx_setting_${"videoProcessing"}`), $videoSharpness = document.getElementById(`bx_setting_${"videoSharpness"}`), $videoPowerPreference = document.getElementById(`bx_setting_${"videoPowerPreference"}`), $videoMaxFps = document.getElementById(`bx_setting_${"videoMaxFps"}`); + let playerType = getPref("video.player.type"), $videoProcessing = document.getElementById(`bx_setting_${escapeCssSelector("video.processing")}`), $videoSharpness = document.getElementById(`bx_setting_${escapeCssSelector("video.processing.sharpness")}`), $videoPowerPreference = document.getElementById(`bx_setting_${escapeCssSelector("video.player.powerPreference")}`), $videoMaxFps = document.getElementById(`bx_setting_${escapeCssSelector("video.maxFps")}`); if (!$videoProcessing) return; let isDisabled = !1, $optCas = $videoProcessing.querySelector(`option[value=${"cas"}]`); if (playerType === "webgl2") $optCas && ($optCas.disabled = !1); - else if ($videoProcessing.value = "usm", setPref("videoProcessing", "usm"), $optCas && ($optCas.disabled = !0), UserAgent.isSafari()) isDisabled = !0; + else if ($videoProcessing.value = "usm", setPref("video.processing", "usm"), $optCas && ($optCas.disabled = !0), UserAgent.isSafari()) isDisabled = !0; $videoProcessing.disabled = isDisabled, $videoSharpness.dataset.disabled = isDisabled.toString(), $videoPowerPreference.closest(".bx-settings-row").classList.toggle("bx-gone", playerType !== "webgl2"), $videoMaxFps.closest(".bx-settings-row").classList.toggle("bx-gone", playerType !== "webgl2"), updateVideoPlayer(); } function limitVideoPlayerFps(targetFps) { @@ -2348,15 +2391,15 @@ function limitVideoPlayerFps(targetFps) { function updateVideoPlayer() { let streamPlayer = STATES.currentStream.streamPlayer; if (!streamPlayer) return; - limitVideoPlayerFps(getPref("videoMaxFps")); + limitVideoPlayerFps(getPref("video.maxFps")); let options = { - processing: getPref("videoProcessing"), - sharpness: getPref("videoSharpness"), - saturation: getPref("videoSaturation"), - contrast: getPref("videoContrast"), - brightness: getPref("videoBrightness") + processing: getPref("video.processing"), + sharpness: getPref("video.processing.sharpness"), + saturation: getPref("video.saturation"), + contrast: getPref("video.contrast"), + brightness: getPref("video.brightness") }; - streamPlayer.setPlayerType(getPref("videoPlayerType")), streamPlayer.updateOptions(options), streamPlayer.refreshPlayer(); + streamPlayer.setPlayerType(getPref("video.player.type")), streamPlayer.updateOptions(options), streamPlayer.refreshPlayer(); } window.addEventListener("resize", updateVideoPlayer); class KeyHelper { @@ -2516,12 +2559,12 @@ class ControllerShortcutsTable extends BasePresetsTable { name: "Type A", data: { mapping: { - 3: AppInterface ? "device-volume-inc" : "stream-volume-inc", - 0: AppInterface ? "device-volume-dec" : "stream-volume-dec", - 2: "stream-stats-toggle", - 1: AppInterface ? "device-sound-toggle" : "stream-sound-toggle", - 5: "stream-screenshot-capture", - 9: "stream-menu-show" + 3: AppInterface ? "device.volume.inc" : "stream.volume.inc", + 0: AppInterface ? "device.volume.dec" : "stream.volume.dec", + 2: "stream.stats.toggle", + 1: AppInterface ? "device.sound.toggle" : "stream.sound.toggle", + 5: "stream.screenshot.capture", + 9: "stream.menu.show" } } }, @@ -2530,12 +2573,12 @@ class ControllerShortcutsTable extends BasePresetsTable { name: "Type B", data: { mapping: { - 12: AppInterface ? "device-volume-inc" : "stream-volume-inc", - 13: AppInterface ? "device-volume-dec" : "stream-volume-dec", - 15: "stream-stats-toggle", - 14: AppInterface ? "device-sound-toggle" : "stream-sound-toggle", - 4: "stream-screenshot-capture", - 8: "stream-menu-show" + 12: AppInterface ? "device.volume.inc" : "stream.volume.inc", + 13: AppInterface ? "device.volume.dec" : "stream.volume.dec", + 15: "stream.stats.toggle", + 14: AppInterface ? "device.sound.toggle" : "stream.sound.toggle", + 4: "stream.screenshot.capture", + 8: "stream.menu.show" } } } @@ -2571,7 +2614,7 @@ function showGamepadToast(gamepad) { if (gamepad.id === VIRTUAL_GAMEPAD_ID) return; BxLogger.info("Gamepad", gamepad); let text = "🎮"; - if (getPref("localCoOpEnabled")) text += ` #${gamepad.index + 1}`; + if (getPref("localCoOp.enabled")) text += ` #${gamepad.index + 1}`; let gamepadId = gamepad.id.replace(/ \(.*?Vendor: \w+ Product: \w+\)$/, ""); text += ` - ${gamepadId}`; let status; @@ -2617,16 +2660,16 @@ class StreamSettings { shortcuts: shortcutsMapping }; } - settings.controllers = controllers, settings.controllerPollingRate = StreamSettings.getPref("controllerPollingRate"), await StreamSettings.refreshDeviceVibration(); + settings.controllers = controllers, settings.controllerPollingRate = StreamSettings.getPref("controller.pollingRate"), await StreamSettings.refreshDeviceVibration(); } static async refreshDeviceVibration() { if (!STATES.browser.capabilities.deviceVibration) return; - let mode = StreamSettings.getPref("deviceVibrationMode"), intensity = 0; - if (mode === "on" || mode === "auto" && !hasGamepad()) intensity = StreamSettings.getPref("deviceVibrationIntensity") / 100; + let mode = StreamSettings.getPref("deviceVibration.mode"), intensity = 0; + if (mode === "on" || mode === "auto" && !hasGamepad()) intensity = StreamSettings.getPref("deviceVibration.intensity") / 100; StreamSettings.settings.deviceVibrationIntensity = intensity, BxEvent.dispatch(window, BxEvent.DEVICE_VIBRATION_CHANGED); } static async refreshMkbSettings() { - let settings = StreamSettings.settings, presetId = StreamSettings.getPref("mkbMappingPresetIdP1"), orgPreset = await MkbMappingPresetsTable.getInstance().getPreset(presetId), orgPresetData = orgPreset.data, converted = { + let settings = StreamSettings.settings, presetId = StreamSettings.getPref("mkb.p1.preset.mappingId"), orgPreset = await MkbMappingPresetsTable.getInstance().getPreset(presetId), orgPresetData = orgPreset.data, converted = { mapping: {}, mouse: Object.assign({}, orgPresetData.mouse) }, key; @@ -2637,12 +2680,12 @@ class StreamSettings { if (typeof keyName === "string") converted.mapping[keyName] = buttonIndex; } let mouse = converted.mouse; - mouse["sensitivityX"] *= 0.001, mouse["sensitivityY"] *= 0.001, mouse["deadzoneCounterweight"] *= 0.01, settings.mkbPreset = converted, setPref("mkbMappingPresetIdP1", orgPreset.id), BxEvent.dispatch(window, BxEvent.MKB_UPDATED); + mouse["sensitivityX"] *= 0.001, mouse["sensitivityY"] *= 0.001, mouse["deadzoneCounterweight"] *= 0.01, settings.mkbPreset = converted, setPref("mkb.p1.preset.mappingId", orgPreset.id), BxEvent.dispatch(window, BxEvent.MKB_UPDATED); } static async refreshKeyboardShortcuts() { - let settings = StreamSettings.settings, presetId = StreamSettings.getPref("keyboardShortcutsInGamePresetId"); + let settings = StreamSettings.settings, presetId = StreamSettings.getPref("keyboardShortcuts.preset.inGameId"); if (presetId === 0) { - settings.keyboardShortcuts = null, setPref("keyboardShortcutsInGamePresetId", presetId), BxEvent.dispatch(window, BxEvent.KEYBOARD_SHORTCUTS_UPDATED); + settings.keyboardShortcuts = null, setPref("keyboardShortcuts.preset.inGameId", presetId), BxEvent.dispatch(window, BxEvent.KEYBOARD_SHORTCUTS_UPDATED); return; } let orgPreset = await KeyboardShortcutsTable.getInstance().getPreset(presetId), orgPresetData = orgPreset.data.mapping, converted = {}, action; @@ -2650,7 +2693,7 @@ class StreamSettings { let info = orgPresetData[action], key = `${info.code}:${info.modifiers || 0}`; converted[key] = action; } - settings.keyboardShortcuts = converted, setPref("keyboardShortcutsInGamePresetId", orgPreset.id), BxEvent.dispatch(window, BxEvent.KEYBOARD_SHORTCUTS_UPDATED); + settings.keyboardShortcuts = converted, setPref("keyboardShortcuts.preset.inGameId", orgPreset.id), BxEvent.dispatch(window, BxEvent.KEYBOARD_SHORTCUTS_UPDATED); } static async refreshAllSettings() { window.BX_STREAM_SETTINGS = StreamSettings.settings, await StreamSettings.refreshControllerSettings(), await StreamSettings.refreshMkbSettings(), await StreamSettings.refreshKeyboardShortcuts(); @@ -2696,7 +2739,7 @@ class MkbPopup { style: 1 | 512 | 128, label: t("activate"), onClick: this.onActivate - }, shortcutKey = StreamSettings.findKeyboardShortcut("mkb-toggle"); + }, shortcutKey = StreamSettings.findKeyboardShortcut("mkb.toggle"); if (shortcutKey) options.secondaryText = t("press-key-to-toggle-mkb", { key: KeyHelper.codeToKeyName(shortcutKey) }); return createButton(options); } @@ -2732,7 +2775,7 @@ class NativeMkbHandler extends MkbHandler { } LOG_TAG = "NativeMkbHandler"; static isAllowed = () => { - return STATES.browser.capabilities.emulatedNativeMkb && getPref("nativeMkbMode") === "on"; + return STATES.browser.capabilities.emulatedNativeMkb && getPref("nativeMkb.mode") === "on"; }; pointerClient; enabled = !1; @@ -2792,8 +2835,8 @@ class NativeMkbHandler extends MkbHandler { } catch (e) { Toast.show("Cannot enable Mouse & Keyboard feature"); } - this.mouseVerticalMultiply = getPref("nativeMkbScrollYSensitivity"), this.mouseHorizontalMultiply = getPref("nativeMkbScrollXSensitivity"), window.addEventListener("keyup", this), window.addEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this), window.addEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.addEventListener(BxEvent.POINTER_LOCK_EXITED, this), window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this); - let shortcutKey = StreamSettings.findKeyboardShortcut("mkb-toggle"); + this.mouseVerticalMultiply = getPref("nativeMkb.scroll.sensitivityY"), this.mouseHorizontalMultiply = getPref("nativeMkb.scroll.sensitivityX"), window.addEventListener("keyup", this), window.addEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this), window.addEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.addEventListener(BxEvent.POINTER_LOCK_EXITED, this), window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this); + let shortcutKey = StreamSettings.findKeyboardShortcut("mkb.toggle"); if (shortcutKey) { let msg = t("press-key-to-toggle-mkb", { key: `${KeyHelper.codeToKeyName(shortcutKey)}` }); Toast.show(msg, t("native-mkb"), { html: !0 }); @@ -2948,7 +2991,7 @@ class EmulatedMkbHandler extends MkbHandler { } static LOG_TAG = "EmulatedMkbHandler"; static isAllowed() { - return getPref("mkbEnabled") && (AppInterface || !UserAgent.isMobile()); + return getPref("mkb.enabled") && (AppInterface || !UserAgent.isMobile()); } PRESET; VIRTUAL_GAMEPAD = { @@ -3147,7 +3190,7 @@ class EmulatedMkbHandler extends MkbHandler { window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.onPollingModeChanged), window.removeEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this.onDialogShown), this.mouseDataProvider?.destroy(), window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.onPollingModeChanged); } updateGamepadSlots() { - this.VIRTUAL_GAMEPAD.index = getPref("mkbSlotP1") - 1; + this.VIRTUAL_GAMEPAD.index = getPref("mkb.p1.slot") - 1; } start() { if (!this.enabled) this.enabled = !0, Toast.show(t("virtual-controller"), t("enabled"), { instant: !0 }); @@ -3260,7 +3303,7 @@ class NavigationDialogManager { constructor() { if (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("uiControllerFriendly")) + }), 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]; @@ -3284,9 +3327,13 @@ class NavigationDialogManager { $label.style.minWidth = width + "px", $parent.dataset.calculated = "true"; } } + updateActiveInput(input) { + document.documentElement.dataset.activeInput = input; + } handleEvent(event) { switch (event.type) { case "keydown": + this.updateActiveInput("keyboard"); let $target = event.target, keyboardEvent = event, keyCode = keyboardEvent.code || keyboardEvent.key, handled = this.dialog?.handleKeyPress(keyCode); if (handled) { event.preventDefault(), event.stopPropagation(); @@ -3349,6 +3396,7 @@ class NavigationDialogManager { continue; } if (this.gamepadLastStates[gamepad.index] = null, lastKeyPressed) return; + if (this.updateActiveInput("gamepad"), this.handleGamepad(gamepad, releasedButton)) return; if (releasedButton === 0) { document.activeElement && document.activeElement.dispatchEvent(new MouseEvent("click", { bubbles: !0 })); return; @@ -3356,7 +3404,6 @@ class NavigationDialogManager { this.hide(); return; } - if (this.handleGamepad(gamepad, releasedButton)) return; } }; handleGamepad(gamepad, key) { @@ -3616,7 +3663,7 @@ class TouchController { }; let $style = document.createElement("style"); document.documentElement.appendChild($style), TouchController.#$style = $style; - let PREF_STYLE_STANDARD = getPref("touchControllerStyleStandard"), PREF_STYLE_CUSTOM = getPref("touchControllerStyleCustom"); + let PREF_STYLE_STANDARD = getPref("touchController.style.standard"), PREF_STYLE_CUSTOM = getPref("touchController.style.custom"); window.addEventListener(BxEvent.DATA_CHANNEL_CREATED, (e) => { let dataChannel = e.dataChannel; if (!dataChannel || dataChannel.label !== "message") return; @@ -3699,7 +3746,7 @@ class BxSelectElement extends HTMLSelectElement { $label; $checkBox; static create($select, forceFriendly = !1) { - if (!forceFriendly && !getPref("uiControllerFriendly")) return $select.classList.add("bx-select"), $select; + if (!forceFriendly && !getPref("ui.controllerFriendly")) return $select.classList.add("bx-select"), $select; $select.removeAttribute("tabindex"); let $wrapper = CE("div", { class: "bx-select" }), $btnPrev = createButton({ label: "<", @@ -3726,13 +3773,13 @@ class BxSelectElement extends HTMLSelectElement { 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) => { mutationList.forEach((mutation) => { - if (mutation.type === "childList" || mutation.type === "attributes") self.optionsList = Array.from($select.querySelectorAll("option")), BxSelectElement.resetIndicators.call(self), BxSelectElement.render.call(self); + 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); }); }).observe($select, { subtree: !0, childList: !0, attributes: !0 - }), BxSelectElement.resetIndicators.call(self), BxSelectElement.render.call(self), self.append($select, $btnPrev, $content, $btnNext), 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; }, @@ -3994,10 +4041,10 @@ var FeatureGates = { EnableWifiWarnings: !1, EnableUpdateRequiredPage: !1, ShowForcedUpdateScreen: !1 -}, nativeMkbMode = getPref("nativeMkbMode"); +}, nativeMkbMode = getPref("nativeMkb.mode"); if (nativeMkbMode !== "default") FeatureGates.EnableMouseAndKeyboard = nativeMkbMode === "on"; -if (getPref("blockSocialFeatures")) FeatureGates.EnableGuideChatTab = !1; -if (getPref("byogDisabled")) FeatureGates.EnableBYOG = !1, FeatureGates.EnableBYOGPurchase = !1; +if (getPref("block.social")) FeatureGates.EnableGuideChatTab = !1; +if (getPref("feature.byog.disabled")) FeatureGates.EnableBYOG = !1, FeatureGates.EnableBYOGPurchase = !1; if (BX_FLAGS.FeatureGates) FeatureGates = Object.assign(BX_FLAGS.FeatureGates, FeatureGates); class PatcherUtils { static indexOf(txt, searchString, startIndex, maxRange) { @@ -4053,7 +4100,7 @@ var ENDING_CHUNKS_PATCH_NAME = "loadingEndingChunks", LOG_TAG2 = "Patcher", PATC websiteLayout(str) { let text = '?"tv":"default"'; if (!str.includes(text)) return !1; - let layout = getPref("uiLayout") === "tv" ? "tv" : "default"; + let layout = getPref("ui.layout") === "tv" ? "tv" : "default"; return str.replace(text, `?"${layout}":"${layout}"`); }, remotePlayDirectConnectUrl(str) { @@ -4099,7 +4146,7 @@ var ENDING_CHUNKS_PATCH_NAME = "loadingEndingChunks", LOG_TAG2 = "Patcher", PATC let setTimeoutIndex = str.indexOf("setTimeout(this.pollGamepads", index); 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("blockTracking")) codeBlock = codeBlock.replace("this.inputPollingIntervalStats.addValue", ""), codeBlock = codeBlock.replace("this.inputPollingDurationStats.addValue", ""); + 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, { @@ -4202,8 +4249,8 @@ if (window.BX_EXPOSED.stopTakRendering) { let text = "const{TakRenderer:"; if (!str.includes(text)) return !1; let autoOffCode = ""; - if (getPref("touchControllerMode") === "off") autoOffCode = "return;"; - else if (getPref("touchControllerAutoOff")) autoOffCode = ` + if (getPref("touchController.mode") === "off") autoOffCode = "return;"; + else if (getPref("touchController.autoOff")) autoOffCode = ` const gamepads = window.navigator.getGamepads(); let gamepadFound = false; for (let gamepad of gamepads) { @@ -4239,7 +4286,7 @@ window.BX_EXPOSED.showStreamMenu = e.onShowStreamMenu; // Restore the "..." button e.guideUI = null; `; - if (getPref("touchControllerMode") === "off") newCode += "e.canShowTakHUD = false;"; + if (getPref("touchController.mode") === "off") newCode += "e.canShowTakHUD = false;"; return str = str.replace(text, newCode + text), str; }, broadcastPollingMode(str) { @@ -4293,7 +4340,7 @@ BxLogger.info('patchRemotePlayMkb', ${configsVar}); patchTouchControlDefaultOpacity(str) { let text = "opacityMultiplier:1"; if (!str.includes(text)) return !1; - let newCode = `opacityMultiplier: ${(getPref("touchControllerDefaultOpacity") / 100).toFixed(1)}`; + let newCode = `opacityMultiplier: ${(getPref("touchController.opacity.default") / 100).toFixed(1)}`; return str = str.replace(text, newCode), str; }, patchShowSensorControls(str) { @@ -4380,7 +4427,7 @@ true` + text; let index = str.indexOf("SiglRow-module__heroCard___"); if (index < 0) return !1; if (index = PatcherUtils.lastIndexOf(str, "const[", index, 300), index < 0) return !1; - let PREF_HIDE_SECTIONS = getPref("uiHideSections"), siglIds = [], sections = { + let PREF_HIDE_SECTIONS = getPref("ui.hideSections"), siglIds = [], sections = { "native-mkb": "8fa264dd-124f-4af3-97e8-596fcdf4b486", "most-popular": "e7590b22-e299-44db-ae22-25c61405454c" }; @@ -4456,7 +4503,7 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) { return str = str.replace(text, "=window.BX_EXPOSED.modifyPreloadedState(window.__PRELOADED_STATE__);"), str; } }, PATCH_ORDERS = [ - ...getPref("nativeMkbMode") === "on" ? [ + ...getPref("nativeMkb.mode") === "on" ? [ "enableNativeMkb", "exposeInputSink" ] : [], @@ -4474,24 +4521,24 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) { "enableTvRoutes", "supportLocalCoOp", "overrideStorageGetSettings", - getPref("uiGameCardShowWaitTime") && "patchSetCurrentlyFocusedInteractable", - getPref("uiLayout") !== "default" && "websiteLayout", - getPref("gameFortniteForceConsole") && "forceFortniteConsole", - getPref("uiHideSections").includes("friends") && "ignorePlayWithFriendsSection", - getPref("uiHideSections").includes("all-games") && "ignoreAllGamesSection", - getPref("uiHideSections").includes("touch") && "ignorePlayWithTouchSection", - (getPref("uiHideSections").includes("native-mkb") || getPref("uiHideSections").includes("most-popular")) && "ignoreSiglSections", + getPref("ui.gameCard.waitTime.show") && "patchSetCurrentlyFocusedInteractable", + getPref("ui.layout") !== "default" && "websiteLayout", + getPref("game.fortnite.forceConsole") && "forceFortniteConsole", + getPref("ui.hideSections").includes("friends") && "ignorePlayWithFriendsSection", + getPref("ui.hideSections").includes("all-games") && "ignoreAllGamesSection", + getPref("ui.hideSections").includes("touch") && "ignorePlayWithTouchSection", + (getPref("ui.hideSections").includes("native-mkb") || getPref("ui.hideSections").includes("most-popular")) && "ignoreSiglSections", ...STATES.userAgent.capabilities.touch ? [ "disableTouchContextMenu" ] : [], - ...getPref("blockTracking") ? [ + ...getPref("block.tracking") ? [ "disableAiTrack", "disableTelemetry", "blockWebRtcStatsCollector", "disableIndexDbLogging", "disableTelemetryProvider" ] : [], - ...getPref("xhomeEnabled") ? [ + ...getPref("xhome.enabled") ? [ "remotePlayKeepAlive", "remotePlayDirectConnectUrl", "remotePlayDisableAchievementToast", @@ -4509,24 +4556,24 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) { "patchStreamHud", "playVibration", "alwaysShowStreamHud", - getPref("audioEnableVolumeControl") && !getPref("streamCombineSources") && "patchAudioMediaStream", - getPref("audioEnableVolumeControl") && getPref("streamCombineSources") && "patchCombinedAudioVideoMediaStream", - getPref("uiDisableFeedbackDialog") && "skipFeedbackDialog", + getPref("audio.volume.booster.enabled") && !getPref("stream.video.combineAudio") && "patchAudioMediaStream", + getPref("audio.volume.booster.enabled") && getPref("stream.video.combineAudio") && "patchCombinedAudioVideoMediaStream", + getPref("ui.feedbackDialog.disabled") && "skipFeedbackDialog", ...STATES.userAgent.capabilities.touch ? [ - getPref("touchControllerMode") === "all" && "patchShowSensorControls", - getPref("touchControllerMode") === "all" && "exposeTouchLayoutManager", - (getPref("touchControllerMode") === "off" || getPref("touchControllerAutoOff") || !STATES.userAgent.capabilities.touch) && "disableTakRenderer", - getPref("touchControllerDefaultOpacity") !== 100 && "patchTouchControlDefaultOpacity", + getPref("touchController.mode") === "all" && "patchShowSensorControls", + getPref("touchController.mode") === "all" && "exposeTouchLayoutManager", + (getPref("touchController.mode") === "off" || getPref("touchController.autoOff") || !STATES.userAgent.capabilities.touch) && "disableTakRenderer", + getPref("touchController.opacity.default") !== 100 && "patchTouchControlDefaultOpacity", "patchBabylonRendererClass" ] : [], BX_FLAGS.EnableXcloudLogging && "enableConsoleLogging", "patchPollGamepads", - getPref("streamCombineSources") && "streamCombineSources", - ...getPref("xhomeEnabled") ? [ + getPref("stream.video.combineAudio") && "streamCombineSources", + ...getPref("xhome.enabled") ? [ "patchRemotePlayMkb", "remotePlayConnectMode" ] : [], - ...getPref("nativeMkbMode") === "on" ? [ + ...getPref("nativeMkb.mode") === "on" ? [ "patchMouseAndKeyboardEnabled", "disableNativeRequestPointerLock" ] : [] @@ -4581,7 +4628,7 @@ class PatcherCache { static instance; static getInstance = () => PatcherCache.instance ?? (PatcherCache.instance = new PatcherCache); KEY_CACHE = "BetterXcloud.Patches.Cache"; - KEY_SIGNATURE = "BetterXcloud.Patches.CacheSignature"; + KEY_SIGNATURE = "BetterXcloud.Patches.Cache.Signature"; CACHE; isInitialized = !1; getSignature() { @@ -4652,7 +4699,7 @@ class BxNumberStepper extends HTMLInputElement { options = options || {}, options.suffix = options.suffix || "", options.disabled = !!options.disabled, options.hideSlider = !!options.hideSlider; let $text, $btnInc, $btnDec, $range, self = CE("div", { class: "bx-number-stepper", - id: `bx_setting_${key}` + id: `bx_setting_${escapeCssSelector(key)}` }, CE("div", {}, $btnDec = CE("button", { _dataset: { type: "dec" @@ -4817,7 +4864,7 @@ class SettingElement { }; static render(type, key, setting, currentValue, onChange, options) { let method = SettingElement.METHOD_MAP[type], $control = method(...Array.from(arguments).slice(1)); - if (type !== "number-stepper") $control.id = `bx_setting_${key}`; + if (type !== "number-stepper") $control.id = `bx_setting_${escapeCssSelector(key)}`; if (type === "options" || type === "multiple-options") $control.name = $control.id; return $control; } @@ -4868,7 +4915,7 @@ class BaseProfileManagerDialog extends NavigationDialog { this.title = title, this.presetsDb = presetsDb; } updateButtonStates() { - let isDefaultPreset = this.currentPresetId < 0; + let isDefaultPreset = this.currentPresetId <= 0; this.$btnRename.disabled = isDefaultPreset, this.$btnDelete.disabled = isDefaultPreset; } async renderPresetsList() { @@ -4902,7 +4949,7 @@ class BaseProfileManagerDialog extends NavigationDialog { onClick: async () => { let preset = this.allPresets.data[this.currentPresetId], newName = this.promptNewName(t("rename"), preset.name); if (!newName) return; - preset.name = newName, await this.presetsDb.updatePreset(preset), await this.renderPresetsList(); + preset.name = newName, await this.presetsDb.updatePreset(preset), await this.refresh(); } }), this.$btnDelete = createButton({ icon: BxIcon.TRASH, @@ -4910,7 +4957,7 @@ class BaseProfileManagerDialog extends NavigationDialog { style: 4 | 64, onClick: async (e) => { if (!confirm(t("confirm-delete-preset"))) return; - await this.presetsDb.deletePreset(this.currentPresetId), delete this.allPresets.data[this.currentPresetId], this.currentPresetId = parseInt(Object.keys(this.allPresets.data)[0]), await this.renderPresetsList(); + await this.presetsDb.deletePreset(this.currentPresetId), delete this.allPresets.data[this.currentPresetId], this.currentPresetId = parseInt(Object.keys(this.allPresets.data)[0]), await this.refresh(); } }), createButton({ icon: BxIcon.NEW, @@ -4920,7 +4967,7 @@ class BaseProfileManagerDialog extends NavigationDialog { let newName = this.promptNewName(t("new")); if (!newName) return; let newId = await this.presetsDb.newPreset(newName, this.BLANK_PRESET_DATA); - this.currentPresetId = newId, await this.renderPresetsList(); + this.currentPresetId = newId, await this.refresh(); } }), createButton({ icon: BxIcon.COPY, @@ -4930,7 +4977,7 @@ class BaseProfileManagerDialog extends NavigationDialog { let preset = this.allPresets.data[this.currentPresetId], newName = this.promptNewName(t("copy"), `${preset.name} (2)`); if (!newName) return; let newId = await this.presetsDb.newPreset(newName, preset.data); - this.currentPresetId = newId, await this.renderPresetsList(); + this.currentPresetId = newId, await this.refresh(); } })); this.$header = $header, this.$container = CE("div", { class: "bx-centered-dialog" }, CE("div", { class: "bx-dialog-title" }, CE("p", {}, this.title), createButton({ @@ -4959,36 +5006,36 @@ class BaseProfileManagerDialog extends NavigationDialog { } var SHORTCUT_ACTIONS = { [t("better-xcloud")]: { - "bx-settings-show": [t("settings"), t("show")] + "bx.settings.show": [t("settings"), t("show")] }, ...AppInterface ? { [t("device")]: { - "device-sound-toggle": [t("sound"), t("toggle")], - "device-volume-inc": [t("volume"), t("increase")], - "device-volume-dec": [t("volume"), t("decrease")], - "device-brightness-inc": [t("brightness"), t("increase")], - "device-brightness-dec": [t("brightness"), t("decrease")] + "device.sound.toggle": [t("sound"), t("toggle")], + "device.volume.inc": [t("volume"), t("increase")], + "device.volume.dec": [t("volume"), t("decrease")], + "device.brightness.inc": [t("brightness"), t("increase")], + "device.brightness.dec": [t("brightness"), t("decrease")] } } : {}, [t("stream")]: { - "stream-screenshot-capture": [t("take-screenshot")], - "stream-video-toggle": [t("video"), t("toggle")], - "stream-sound-toggle": [t("sound"), t("toggle")], - ...getPref("audioEnableVolumeControl") ? { - "stream-volume-inc": [t("volume"), t("increase")], - "stream-volume-dec": [t("volume"), t("decrease")] + "stream.screenshot.capture": [t("take-screenshot")], + "stream.video.toggle": [t("video"), t("toggle")], + "stream.sound.toggle": [t("sound"), t("toggle")], + ...getPref("audio.volume.booster.enabled") ? { + "stream.volume.inc": [t("volume"), t("increase")], + "stream.volume.dec": [t("volume"), t("decrease")] } : {}, - "stream-menu-show": [t("menu"), t("show")], - "stream-stats-toggle": [t("stats"), t("show-hide")], - "stream-microphone-toggle": [t("microphone"), t("toggle")] + "stream.menu.show": [t("menu"), t("show")], + "stream.stats.toggle": [t("stats"), t("show-hide")], + "stream.microphone.toggle": [t("microphone"), t("toggle")] }, ...STATES.browser.capabilities.mkb ? { [t("mouse-and-keyboard")]: { - "mkb-toggle": [t("toggle")] + "mkb.toggle": [t("toggle")] } } : {}, [t("other")]: { - "ta-open": [t("true-achievements"), t("show")] + "ta.open": [t("true-achievements"), t("show")] } }; class ControllerShortcutsManagerDialog extends BaseProfileManagerDialog { @@ -5019,7 +5066,7 @@ class ControllerShortcutsManagerDialog extends BaseProfileManagerDialog { ]; constructor(title) { super(title, ControllerShortcutsTable.getInstance()); - let PREF_CONTROLLER_FRIENDLY_UI = getPref("uiControllerFriendly"), $baseSelect = CE("select", { autocomplete: "off" }, CE("option", { value: "" }, "---")); + let PREF_CONTROLLER_FRIENDLY_UI = getPref("ui.controllerFriendly"), $baseSelect = CE("select", { autocomplete: "off" }, CE("option", { value: "" }, "---")); for (let groupLabel in SHORTCUT_ACTIONS) { let items = SHORTCUT_ACTIONS[groupLabel]; if (!items) continue; @@ -5069,7 +5116,7 @@ class ControllerShortcutsManagerDialog extends BaseProfileManagerDialog { return; } this.currentPresetId = id; - let isDefaultPreset = id < 0, actions = preset.data, button; + 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", { @@ -5213,9 +5260,9 @@ class SuggestionsSetting { if (BX_FLAGS.DeviceInfo.androidInfo) recommendedDevice = await SuggestionsSetting.getRecommendedSettings.call(this, BX_FLAGS.DeviceInfo.androidInfo); } let hasRecommendedSettings = Object.keys(this.suggestedSettings.recommended).length > 0, deviceType = BX_FLAGS.DeviceInfo.deviceType; - if (deviceType === "android-handheld") SuggestionsSetting.addDefaultSuggestedSetting.call(this, "touchControllerMode", "off"), SuggestionsSetting.addDefaultSuggestedSetting.call(this, "deviceVibrationMode", "on"); - else if (deviceType === "android") SuggestionsSetting.addDefaultSuggestedSetting.call(this, "deviceVibrationMode", "auto"); - else if (deviceType === "android-tv") SuggestionsSetting.addDefaultSuggestedSetting.call(this, "touchControllerMode", "off"); + if (deviceType === "android-handheld") SuggestionsSetting.addDefaultSuggestedSetting.call(this, "touchController.mode", "off"), SuggestionsSetting.addDefaultSuggestedSetting.call(this, "deviceVibration.mode", "on"); + else if (deviceType === "android") SuggestionsSetting.addDefaultSuggestedSetting.call(this, "deviceVibration.mode", "auto"); + else if (deviceType === "android-tv") SuggestionsSetting.addDefaultSuggestedSetting.call(this, "touchController.mode", "off"); SuggestionsSetting.generateDefaultSuggestedSettings.call(this); let $suggestedSettings = CE("div", { class: "bx-suggest-wrapper" }), $select = CE("select", {}, hasRecommendedSettings && CE("option", { value: "recommended" }, t("recommended")), !hasRecommendedSettings && CE("option", { value: "highest" }, t("highest-quality")), CE("option", { value: "default" }, t("default")), CE("option", { value: "lowest" }, t("lowest-quality"))); $select.addEventListener("input", (e2) => { @@ -5236,16 +5283,16 @@ class SuggestionsSetting { let suggestedValueText = STORAGE.Global.getValueText(prefKey, suggestedValue); $value = currentValueText + " ➔ " + suggestedValueText; } - let $checkbox, breadcrumb = this.suggestedSettingLabels[prefKey] + " ❯ " + STORAGE.Global.getLabel(prefKey); + let $checkbox, breadcrumb = this.suggestedSettingLabels[prefKey] + " ❯ " + STORAGE.Global.getLabel(prefKey), id = escapeCssSelector(`bx_suggest_${prefKey}`); if ($child = CE("div", { class: `bx-suggest-row ${isSameValue ? "bx-suggest-ok" : "bx-suggest-change"}` }, $checkbox = CE("input", { type: "checkbox", tabindex: 0, checked: !0, - id: `bx_suggest_${prefKey}` + id }), CE("label", { - for: `bx_suggest_${prefKey}` + for: id }, CE("div", { class: "bx-suggest-label" }, breadcrumb), CE("div", { @@ -5259,7 +5306,7 @@ class SuggestionsSetting { let onClickApply = () => { let profile = $select.value, settings = this.suggestedSettings[profile], prefKey; for (prefKey in settings) { - let suggestedValue = settings[prefKey], $checkBox = $content.querySelector(`#bx_suggest_${prefKey}`); + let suggestedValue = settings[prefKey], $checkBox = $content.querySelector(`#bx_suggest_${escapeCssSelector(prefKey)}`); if (!$checkBox.checked || $checkBox.disabled) continue; let $control = this.settingElements[prefKey]; if (!$control) { @@ -5305,7 +5352,7 @@ class SuggestionsSetting { let { brand, board, model } = androidInfo; brand = normalize(brand), board = normalize(board), model = normalize(model); let url = GhPagesUtils.getUrl(`devices/${brand}/${board}-${model}.json`), json = await (await NATIVE_FETCH(url)).json(), recommended = {}; - if (json.schema_version !== 1) return null; + if (json.schema_version !== 2) return null; let scriptSettings = json.settings.script; if (scriptSettings._base) { let base = typeof scriptSettings._base === "string" ? [scriptSettings._base] : scriptSettings._base; @@ -5549,7 +5596,7 @@ class MkbMappingManagerDialog extends BaseProfileManagerDialog { } let presetData = preset.data; this.currentPresetId = id; - let isDefaultPreset = id < 0; + let isDefaultPreset = id <= 0; this.updateButtonStates(); for (let $elm of this.allKeyElements) { let { buttonIndex, keySlot } = this.parseDataset($elm), buttonKeys = presetData.mapping[buttonIndex]; @@ -5633,7 +5680,7 @@ class KeyboardShortcutsManagerDialog extends BaseProfileManagerDialog { } let presetData = preset.data; this.currentPresetId = id; - let isDefaultPreset = id < 0; + let isDefaultPreset = id <= 0; this.updateButtonStates(); for (let $elm of this.allKeyElements) { let { action } = this.parseDataset($elm), keyInfo = presetData.mapping[action]; @@ -5676,7 +5723,7 @@ class MkbExtraSettings extends HTMLElement { input: $container.saveShortcutsSettings } })); - return $container.append(...getPref("mkbEnabled") ? [ + return $container.append(...getPref("mkb.enabled") ? [ createSettingRow(t("virtual-controller"), CE("div", { class: "bx-preset-row", _nearby: { @@ -5689,7 +5736,7 @@ class MkbExtraSettings extends HTMLElement { id: parseInt($container.$mappingPresets.value) }) })), { multiLines: !0 }), - createSettingRow(ut("Virtual controller slot"), SettingElement.fromPref("mkbSlotP1", STORAGE.Global, () => { + createSettingRow(t("virtual-controller-slot"), SettingElement.fromPref("mkb.p1.slot", STORAGE.Global, () => { EmulatedMkbHandler.getInstance()?.updateGamepadSlots(); })) ] : [], createSettingRow(t("keyboard-shortcuts-in-game"), CE("div", { @@ -5709,17 +5756,17 @@ class MkbExtraSettings extends HTMLElement { } static async updateLayout() { let mappingPresets = await MkbMappingPresetsTable.getInstance().getPresets(); - renderPresetsList(this.$mappingPresets, mappingPresets, getPref("mkbMappingPresetIdP1"), !1); + renderPresetsList(this.$mappingPresets, mappingPresets, getPref("mkb.p1.preset.mappingId"), !1); let shortcutsPresets = await KeyboardShortcutsTable.getInstance().getPresets(); - renderPresetsList(this.$shortcutsPresets, shortcutsPresets, getPref("keyboardShortcutsInGamePresetId"), !0); + renderPresetsList(this.$shortcutsPresets, shortcutsPresets, getPref("keyboardShortcuts.preset.inGameId"), !0); } static async saveMkbSettings() { let presetId = parseInt(this.$mappingPresets.value); - setPref("mkbMappingPresetIdP1", presetId), StreamSettings.refreshMkbSettings(); + setPref("mkb.p1.preset.mappingId", presetId), StreamSettings.refreshMkbSettings(); } static async saveShortcutsSettings() { let presetId = parseInt(this.$shortcutsPresets.value); - setPref("keyboardShortcutsInGamePresetId", presetId), StreamSettings.refreshKeyboardShortcuts(); + setPref("keyboardShortcuts.preset.inGameId", presetId), StreamSettings.refreshKeyboardShortcuts(); } } class SettingsDialog extends NavigationDialog { @@ -5748,7 +5795,7 @@ class SettingsDialog extends NavigationDialog { helpUrl: "https://better-xcloud.github.io/features/", items: [ ($parent) => { - let PREF_LATEST_VERSION = getPref("versionLatest"), topButtons = []; + let PREF_LATEST_VERSION = getPref("version.latest"), topButtons = []; if (!SCRIPT_VERSION.includes("beta") && PREF_LATEST_VERSION && PREF_LATEST_VERSION != SCRIPT_VERSION) { let opts = { label: "🌟 " + t("new-version-available", { version: PREF_LATEST_VERSION }), @@ -5793,53 +5840,52 @@ class SettingsDialog extends NavigationDialog { $parent.appendChild($div); }, { - pref: "bxLocale", + pref: "bx.locale", multiLines: !0 }, - "serverBypassRestriction", - "uiControllerFriendly", - "xhomeEnabled" + "server.bypassRestriction", + "ui.controllerFriendly", + "xhome.enabled" ] }, { group: "server", label: t("server"), items: [ { - pref: "serverRegion", + pref: "server.region", multiLines: !0 }, { - pref: "streamLocale", + pref: "stream.locale", multiLines: !0 }, - "serverPreferIpv6" + "server.ipv6.prefer" ] }, { group: "stream", label: t("stream"), items: [ - "streamResolution", - "streamCodecProfile", - "streamMaxVideoBitrate", - "audioEnableVolumeControl", - "uiDisableFeedbackDialog", - "screenshotApplyFilters", - "audioMicOnPlaying", - "gameFortniteForceConsole", - "streamCombineSources" + "stream.video.resolution", + "stream.video.codecProfile", + "stream.video.maxBitrate", + "audio.volume.booster.enabled", + "screenshot.applyFilters", + "audio.mic.onPlaying", + "game.fortnite.forceConsole", + "stream.video.combineAudio" ] }, { requiredVariants: "full", group: "mkb", label: t("mouse-and-keyboard"), items: [ - "nativeMkbMode", + "nativeMkb.mode", { - pref: "forceNativeMkbGames", + pref: "nativeMkb.forcedGames", multiLines: !0 }, - "mkbEnabled", - "mkbHideIdleCursor" + "mkb.enabled", + "mkb.cursor.hideIdle" ], ...!STATES.browser.capabilities.emulatedNativeMkb && (!STATES.userAgent.capabilities.mkb || !STATES.browser.capabilities.mkb) ? { unsupported: !0, @@ -5854,13 +5900,13 @@ class SettingsDialog extends NavigationDialog { label: t("touch-controller"), items: [ { - pref: "touchControllerMode", + pref: "touchController.mode", note: CE("a", { href: "https://github.com/redphx/better-xcloud/discussions/241", target: "_blank" }, t("unofficial-game-list")) }, - "touchControllerAutoOff", - "touchControllerDefaultOpacity", - "touchControllerStyleStandard", - "touchControllerStyleCustom" + "touchController.autoOff", + "touchController.opacity.default", + "touchController.style.standard", + "touchController.style.custom" ], ...!STATES.userAgent.capabilities.touch ? { unsupported: !0, @@ -5870,18 +5916,19 @@ class SettingsDialog extends NavigationDialog { group: "ui", label: t("ui"), items: [ - "uiLayout", - "uiGameCardShowWaitTime", - "uiShowControllerStatus", - "uiSimplifyStreamMenu", - "uiSkipSplashVideo", - !AppInterface && "uiHideScrollbar", - "uiHideSystemMenuIcon", - "uiReduceAnimations", - "blockSocialFeatures", - "byogDisabled", + "ui.layout", + "ui.gameCard.waitTime.show", + "ui.controllerStatus.show", + "ui.streamMenu.simplify", + "ui.splashVideo.skip", + !AppInterface && "ui.hideScrollbar", + "ui.systemMenu.hideHandle", + "ui.feedbackDialog.disabled", + "ui.reduceAnimations", + "block.social", + "feature.byog.disabled", { - pref: "uiHideSections", + pref: "ui.hideSections", multiLines: !0 } ] @@ -5890,32 +5937,31 @@ class SettingsDialog extends NavigationDialog { group: "game-bar", label: t("game-bar"), items: [ - "gameBarPosition" + "gameBar.position" ] }, { group: "loading-screen", label: t("loading-screen"), items: [ - "loadingScreenGameArt", - "loadingScreenShowWaitTime", - "loadingScreenRocket" + "loadingScreen.gameArt.show", + "loadingScreen.waitTime.show", + "loadingScreen.rocket" ] }, { group: "other", label: t("other"), items: [ - "blockTracking" + "block.tracking" ] }, { group: "advanced", label: t("advanced"), items: [ { - pref: "userAgentProfile", + pref: "userAgent.profile", multiLines: !0, onCreated: (setting, $control) => { let defaultUserAgent = window.navigator.orgUserAgent || window.navigator.userAgent, $inpCustomUserAgent = CE("input", { - id: `bx_setting_inp_${setting.pref}`, type: "text", placeholder: defaultUserAgent, autocomplete: "off", @@ -5991,18 +6037,18 @@ class SettingsDialog extends NavigationDialog { label: t("audio"), helpUrl: "https://better-xcloud.github.io/ingame-features/#audio", items: [{ - pref: "audioVolume", + pref: "audio.volume", onChange: (e, value) => { SoundShortcut.setGainNodeVolume(value); }, params: { - disabled: !getPref("audioEnableVolumeControl") + disabled: !getPref("audio.volume.booster.enabled") }, onCreated: (setting, $elm) => { let $range = $elm.querySelector("input[type=range"); window.addEventListener(BxEvent.SETTINGS_CHANGED, (e) => { let { storageKey, settingKey, settingValue } = e; - if (storageKey !== "BetterXcloud" || settingKey !== "audioVolume") return; + if (storageKey !== "BetterXcloud" || settingKey !== "audio.volume") return; $range.value = settingValue, BxEvent.dispatch($range, "input", { ignoreOnChange: !0 }); @@ -6014,37 +6060,37 @@ class SettingsDialog extends NavigationDialog { label: t("video"), helpUrl: "https://better-xcloud.github.io/ingame-features/#video", items: [{ - pref: "videoPlayerType", + pref: "video.player.type", onChange: onChangeVideoPlayerType }, { - pref: "videoMaxFps", + pref: "video.maxFps", onChange: (e) => { limitVideoPlayerFps(parseInt(e.target.value)); } }, { - pref: "videoPowerPreference", + pref: "video.player.powerPreference", onChange: () => { let streamPlayer = STATES.currentStream.streamPlayer; if (!streamPlayer) return; streamPlayer.reloadPlayer(), updateVideoPlayer(); } }, { - pref: "videoProcessing", + pref: "video.processing", onChange: updateVideoPlayer }, { - pref: "videoRatio", + pref: "video.ratio", onChange: updateVideoPlayer }, { - pref: "videoSharpness", + pref: "video.processing.sharpness", onChange: updateVideoPlayer }, { - pref: "videoSaturation", + pref: "video.saturation", onChange: updateVideoPlayer }, { - pref: "videoContrast", + pref: "video.contrast", onChange: updateVideoPlayer }, { - pref: "videoBrightness", + pref: "video.brightness", onChange: updateVideoPlayer }] }]; @@ -6053,12 +6099,12 @@ class SettingsDialog extends NavigationDialog { group: "device", label: t("device"), items: [{ - pref: "deviceVibrationMode", + pref: "deviceVibration.mode", multiLines: !0, unsupported: !STATES.browser.capabilities.deviceVibration, onChange: () => StreamSettings.refreshControllerSettings() }, { - pref: "deviceVibrationIntensity", + pref: "deviceVibration.intensity", unsupported: !STATES.browser.capabilities.deviceVibration, onChange: () => StreamSettings.refreshControllerSettings() }] @@ -6069,13 +6115,13 @@ class SettingsDialog extends NavigationDialog { helpUrl: "https://better-xcloud.github.io/ingame-features/#controller", items: [ { - pref: "localCoOpEnabled", + pref: "localCoOp.enabled", onChange: () => { - BxExposed.toggleLocalCoOp(getPref("localCoOpEnabled")); + BxExposed.toggleLocalCoOp(getPref("localCoOp.enabled")); } }, { - pref: "controllerPollingRate", + pref: "controller.pollingRate", onChange: () => StreamSettings.refreshControllerSettings() }, ($parent) => { @@ -6133,12 +6179,12 @@ class SettingsDialog extends NavigationDialog { group: "native-mkb", label: t("native-mkb"), items: [{ - pref: "nativeMkbScrollYSensitivity", + pref: "nativeMkb.scroll.sensitivityY", onChange: (e, value) => { NativeMkbHandler.getInstance()?.setVerticalScrollMultiplier(value / 100); } }, { - pref: "nativeMkbScrollXSensitivity", + pref: "nativeMkb.scroll.sensitivityX", onChange: (e, value) => { NativeMkbHandler.getInstance()?.setHorizontalScrollMultiplier(value / 100); } @@ -6151,37 +6197,37 @@ class SettingsDialog extends NavigationDialog { helpUrl: "https://better-xcloud.github.io/stream-stats/", items: [ { - pref: "statsShowWhenPlaying" + pref: "stats.showWhenPlaying" }, { - pref: "statsQuickGlance", + pref: "stats.quickGlance.enabled", onChange: (e) => { let streamStats = StreamStats.getInstance(); e.target.checked ? streamStats.quickGlanceSetup() : streamStats.quickGlanceStop(); } }, { - pref: "statsItems", + pref: "stats.items", onChange: StreamStats.refreshStyles }, { - pref: "statsPosition", + pref: "stats.position", onChange: StreamStats.refreshStyles }, { - pref: "statsTextSize", + pref: "stats.textSize", onChange: StreamStats.refreshStyles }, { - pref: "statsOpacity", + pref: "stats.opacity", onChange: StreamStats.refreshStyles }, { - pref: "statsTransparent", + pref: "stats.transparent", onChange: StreamStats.refreshStyles }, { - pref: "statsConditionalFormatting", + pref: "stats.colors", onChange: StreamStats.refreshStyles } ] @@ -6220,7 +6266,7 @@ class SettingsDialog extends NavigationDialog { super(); BxLogger.info(this.LOG_TAG, "constructor()"), this.renderFullSettings = STATES.supportedRegion && STATES.isSignedIn, this.setupDialog(), this.onMountedCallbacks.push(() => { if (onChangeVideoPlayerType(), STATES.userAgent.capabilities.touch) BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED); - let $selectUserAgent = document.querySelector(`#bx_setting_${"userAgentProfile"}`); + let $selectUserAgent = document.querySelector(`#bx_setting_${escapeCssSelector("userAgent.profile")}`); if ($selectUserAgent) $selectUserAgent.disabled = !0, BxEvent.dispatch($selectUserAgent, "input", {}), $selectUserAgent.disabled = !1; }); } @@ -6255,7 +6301,7 @@ 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("uiControllerFriendly")) this.dialogManager.calculateSelectBoxes($child); + if ($child.classList.remove("bx-gone"), getPref("ui.controllerFriendly")) this.dialogManager.calculateSelectBoxes($child); } else $child.classList.add("bx-gone"); for (let $child2 of Array.from(this.$tabs.children)) $child2.classList.remove("bx-active"); @@ -6269,7 +6315,7 @@ class SettingsDialog extends NavigationDialog { PatcherCache.getInstance().clear(), this.$btnReload.classList.add("bx-danger"), this.$noteGlobalReload.classList.add("bx-gone"), this.$btnGlobalReload.classList.remove("bx-gone"), this.$btnGlobalReload.classList.add("bx-danger"); }; renderServerSetting(setting) { - let selectedValue = getPref("serverRegion"), continents = { + let selectedValue = getPref("server.region"), continents = { "america-north": { label: t("continent-north-america") }, @@ -6289,7 +6335,7 @@ class SettingsDialog extends NavigationDialog { label: t("other") } }, $control = CE("select", { - id: `bx_setting_${setting.pref}`, + id: `bx_setting_${escapeCssSelector(setting.pref)}`, title: setting.label, tabindex: 0 }); @@ -6324,10 +6370,10 @@ class SettingsDialog extends NavigationDialog { if (setting.content) if (typeof setting.content === "function") $control = setting.content.apply(this); else $control = setting.content; else if (!setting.unsupported) { - if (pref === "serverRegion") $control = this.renderServerSetting(setting); - else if (pref === "bxLocale") $control = SettingElement.fromPref(pref, STORAGE.Global, async (e) => { + if (pref === "server.region") $control = this.renderServerSetting(setting); + else if (pref === "bx.locale") $control = SettingElement.fromPref(pref, STORAGE.Global, async (e) => { let newLocale = e.target.value; - if (getPref("uiControllerFriendly")) { + if (getPref("ui.controllerFriendly")) { let timeoutId = e.target.timeoutId; timeoutId && window.clearTimeout(timeoutId), e.target.timeoutId = window.setTimeout(() => { Translations.refreshLocale(newLocale), Translations.updateTranslations(); @@ -6335,7 +6381,7 @@ class SettingsDialog extends NavigationDialog { } else Translations.refreshLocale(newLocale), Translations.updateTranslations(); this.onGlobalSettingChanged(e); }); - else if (pref === "userAgentProfile") $control = SettingElement.fromPref("userAgentProfile", STORAGE.Global, (e) => { + else if (pref === "userAgent.profile") $control = SettingElement.fromPref("userAgent.profile", STORAGE.Global, (e) => { let value = e.target.value, isCustom = value === "custom", userAgent2 = UserAgent.get(value); UserAgent.updateStorage(value); let $inp = $control.nextElementSibling; @@ -6367,7 +6413,7 @@ class SettingsDialog extends NavigationDialog { $note, multiLines: setting.multiLines }); - $row.htmlFor = `bx_setting_${pref}`, $row.dataset.type = settingTabContent.group, $tabContent.appendChild($row), !prefDefinition?.unsupported && setting.onCreated && setting.onCreated(setting, $control); + $row.htmlFor = `bx_setting_${escapeCssSelector(pref)}`, $row.dataset.type = settingTabContent.group, $tabContent.appendChild($row), !prefDefinition?.unsupported && setting.onCreated && setting.onCreated(setting, $control); } renderSettingsSection(settingTab, sections) { let $tabContent = CE("div", { @@ -6568,6 +6614,11 @@ class SettingsDialog extends NavigationDialog { handleGamepad(button) { let handled = !0; switch (button) { + case 1: + let $focusing = document.activeElement; + if ($focusing && this.$tabs.contains($focusing)) this.hide(); + else this.focusActiveTab(); + break; case 4: case 5: this.focusActiveTab(); @@ -6611,7 +6662,7 @@ class ScreenshotManager { let currentStream = STATES.currentStream, streamPlayer = currentStream.streamPlayer, $canvas = this.$canvas; if (!streamPlayer || !$canvas) return; let $player; - if (getPref("screenshotApplyFilters")) $player = streamPlayer.getPlayerElement(); + if (getPref("screenshot.applyFilters")) $player = streamPlayer.getPlayerElement(); else $player = streamPlayer.getPlayerElement("default"); if (!$player || !$player.isConnected) return; $player.parentElement.addEventListener("animationend", this.onAnimationEnd, { once: !0 }), $player.parentElement.classList.add("bx-taking-screenshot"); @@ -6638,7 +6689,7 @@ class RendererShortcut { } $mediaContainer.classList.toggle("bx-gone"); let isShowing = !$mediaContainer.classList.contains("bx-gone"); - limitVideoPlayerFps(isShowing ? getPref("videoMaxFps") : 0), BxEvent.dispatch(window, BxEvent.VIDEO_VISIBILITY_CHANGED, { isShowing }); + limitVideoPlayerFps(isShowing ? getPref("video.maxFps") : 0), BxEvent.dispatch(window, BxEvent.VIDEO_VISIBILITY_CHANGED, { isShowing }); } } class TrueAchievements { @@ -6723,45 +6774,45 @@ class TrueAchievements { class ShortcutHandler { static runAction(action) { switch (action) { - case "bx-settings-show": + case "bx.settings.show": SettingsDialog.getInstance().show(); break; - case "stream-screenshot-capture": + case "stream.screenshot.capture": ScreenshotManager.getInstance().takeScreenshot(); break; - case "stream-video-toggle": + case "stream.video.toggle": RendererShortcut.toggleVisibility(); break; - case "stream-stats-toggle": + case "stream.stats.toggle": StreamStats.getInstance().toggle(); break; - case "stream-microphone-toggle": + case "stream.microphone.toggle": MicrophoneShortcut.toggle(); break; - case "stream-menu-show": + case "stream.menu.show": StreamUiShortcut.showHideStreamMenu(); break; - case "stream-sound-toggle": + case "stream.sound.toggle": SoundShortcut.muteUnmute(); break; - case "stream-volume-inc": + case "stream.volume.inc": SoundShortcut.adjustGainNodeVolume(10); break; - case "stream-volume-dec": + case "stream.volume.dec": SoundShortcut.adjustGainNodeVolume(-10); break; - case "device-brightness-inc": - case "device-brightness-dec": - case "device-sound-toggle": - case "device-volume-inc": - case "device-volume-dec": + case "device.brightness.inc": + case "device.brightness.dec": + case "device.sound.toggle": + case "device.volume.inc": + case "device.volume.dec": AppInterface && AppInterface.runShortcut && AppInterface.runShortcut(action); break; - case "mkb-toggle": + case "mkb.toggle": if (STATES.currentStream.titleInfo?.details.hasMkbSupport) NativeMkbHandler.getInstance()?.toggle(); else EmulatedMkbHandler.getInstance()?.toggle(); break; - case "ta-open": + case "ta.open": TrueAchievements.getInstance().open(!1); break; } @@ -6827,10 +6878,10 @@ var BxExposed = { titleInfo = deepClone(titleInfo); let supportedInputTypes = titleInfo.details.supportedInputTypes; if (BX_FLAGS.ForceNativeMkbTitles?.includes(titleInfo.details.productId)) supportedInputTypes.push("MKB"); - if (getPref("nativeMkbMode") === "off") supportedInputTypes = supportedInputTypes.filter((i) => i !== "MKB"); + if (getPref("nativeMkb.mode") === "off") supportedInputTypes = supportedInputTypes.filter((i) => i !== "MKB"); if (titleInfo.details.hasMkbSupport = supportedInputTypes.includes("MKB"), STATES.userAgent.capabilities.touch) { - let touchControllerAvailability = getPref("touchControllerMode"); - if (touchControllerAvailability !== "off" && getPref("touchControllerAutoOff")) { + let touchControllerAvailability = getPref("touchController.mode"); + if (touchControllerAvailability !== "off" && getPref("touchController.autoOff")) { let gamepads = window.navigator.getGamepads(), gamepadFound = !1; for (let gamepad of gamepads) if (gamepad && gamepad.connected) { @@ -6901,7 +6952,7 @@ function localRedirect(path) { } window.localRedirect = localRedirect; function getPreferredServerRegion(shortName = !1) { - let preferredRegion = getPref("serverRegion"), serverRegions = STATES.serverRegions; + let preferredRegion = getPref("server.region"), serverRegions = STATES.serverRegions; if (preferredRegion in serverRegions) if (shortName && serverRegions[preferredRegion].shortName) return serverRegions[preferredRegion].shortName; else return preferredRegion; for (let regionName in serverRegions) { @@ -6933,11 +6984,11 @@ class HeaderSection { label: "???", style: 16 | 32 | 64 | 256, onClick: (e) => SettingsDialog.getInstance().show() - }), this.$buttonsWrapper = CE("div", {}, getPref("xhomeEnabled") ? this.$btnRemotePlay : null, this.$btnSettings); + }), this.$buttonsWrapper = CE("div", {}, getPref("xhome.enabled") ? this.$btnRemotePlay : null, this.$btnSettings); } injectSettingsButton($parent) { if (!$parent) return; - let PREF_LATEST_VERSION = getPref("versionLatest"), $btnSettings = this.$btnSettings; + let PREF_LATEST_VERSION = getPref("version.latest"), $btnSettings = this.$btnSettings; if (isElementVisible(this.$buttonsWrapper)) return; if ($btnSettings.querySelector("span").textContent = getPreferredServerRegion(!0) || t("better-xcloud"), !SCRIPT_VERSION.includes("beta") && PREF_LATEST_VERSION && PREF_LATEST_VERSION !== SCRIPT_VERSION) $btnSettings.setAttribute("data-update-available", "true"); $parent.appendChild(this.$buttonsWrapper); @@ -6977,10 +7028,10 @@ class RemotePlayDialog extends NavigationDialog { BxLogger.info(this.LOG_TAG, "constructor()"), this.setupDialog(); } setupDialog() { - let $fragment = CE("div", { class: "bx-remote-play-container" }), $settingNote = CE("p", {}), currentResolution = getPref("xhomeStreamResolution"), $resolutions = CE("select", {}, CE("option", { value: "720p" }, "720p"), CE("option", { value: "1080p" }, "1080p")); + let $fragment = CE("div", { class: "bx-remote-play-container" }), $settingNote = CE("p", {}), currentResolution = getPref("xhome.video.resolution"), $resolutions = CE("select", {}, CE("option", { value: "720p" }, "720p"), CE("option", { value: "1080p" }, "1080p")); $resolutions = BxSelectElement.create($resolutions), $resolutions.addEventListener("input", (e) => { let value = e.target.value; - $settingNote.textContent = value === "1080p" ? "✅ " + t("can-stream-xbox-360-games") : "❌ " + t("cant-stream-xbox-360-games"), setPref("xhomeStreamResolution", value); + $settingNote.textContent = value === "1080p" ? "✅ " + t("can-stream-xbox-360-games") : "❌ " + t("cant-stream-xbox-360-games"), setPref("xhome.video.resolution", value); }), $resolutions.value = currentResolution, BxEvent.dispatch($resolutions, "input", { manualTrigger: !0 }); @@ -7028,7 +7079,7 @@ class RemotePlayDialog extends NavigationDialog { class RemotePlayManager { static instance; static getInstance() { - if (typeof RemotePlayManager.instance === "undefined") if (getPref("xhomeEnabled")) RemotePlayManager.instance = new RemotePlayManager; + if (typeof RemotePlayManager.instance === "undefined") if (getPref("xhome.enabled")) RemotePlayManager.instance = new RemotePlayManager; else RemotePlayManager.instance = null; return RemotePlayManager.instance; } @@ -7118,7 +7169,7 @@ class RemotePlayManager { callback(); } play(serverId, resolution) { - if (resolution) setPref("xhomeStreamResolution", resolution); + if (resolution) setPref("xhome.video.resolution", resolution); STATES.remotePlay.config = { serverId }, window.BX_REMOTE_PLAY_CONFIG = STATES.remotePlay.config, localRedirect("/launch/fortnite/BT5P2X999VH2#remote-play"); @@ -7132,14 +7183,10 @@ class RemotePlayManager { Toast.show(t("no-consoles-found"), "", { instant: !0 }); return; } - if (AppInterface && AppInterface.showRemotePlayDialog) { - AppInterface.showRemotePlayDialog(JSON.stringify(this.consoles)), document.activeElement.blur(); - return; - } RemotePlayDialog.getInstance().show(); } static detect() { - if (!getPref("xhomeEnabled")) return; + if (!getPref("xhome.enabled")) return; if (STATES.remotePlay.isPlaying = window.location.pathname.includes("/launch/") && window.location.hash.startsWith("#remote-play"), STATES.remotePlay?.isPlaying) window.BX_REMOTE_PLAY_CONFIG = STATES.remotePlay.config, window.history.replaceState({ origin: "better-xcloud" }, "", "https://www.xbox.com/" + location.pathname.substring(1, 6) + "/play"); else window.BX_REMOTE_PLAY_CONFIG = null; } @@ -7221,7 +7268,7 @@ class XhomeInterceptor { } static async handleInputConfigs(request, opts) { let response = await NATIVE_FETCH(request); - if (getPref("touchControllerMode") !== "all") return response; + if (getPref("touchController.mode") !== "all") return response; let obj = await response.clone().json(), xboxTitleId = JSON.parse(opts.body).titleIds[0]; TouchController.setXboxTitleId(xboxTitleId); let inputConfigs = obj[0], hasTouchSupport = inputConfigs.supportedTabs.length > 0; @@ -7261,13 +7308,16 @@ class XhomeInterceptor { headers[pair[0]] = pair[1]; headers.authorization = `Bearer ${RemotePlayManager.getInstance().getXhomeToken()}`; let deviceInfo = XhomeInterceptor.BASE_DEVICE_INFO; - switch (getPref("xhomeStreamResolution")) { + switch (getPref("xhome.video.resolution")) { case "1080p-hq": deviceInfo.dev.os.name = "tizen"; break; case "720p": deviceInfo.dev.os.name = "android"; break; + default: + deviceInfo.dev.os.name = "windows"; + break; } headers["x-ms-device-info"] = JSON.stringify(deviceInfo); let opts = { @@ -7305,7 +7355,7 @@ class LoadingScreen { let $bgStyle = CE("style"); document.documentElement.appendChild($bgStyle), LoadingScreen.$bgStyle = $bgStyle; } - if (LoadingScreen.setBackground(titleInfo.product.heroImageUrl || titleInfo.product.titledHeroImageUrl || titleInfo.product.tileImageUrl), getPref("loadingScreenRocket") === "hide") LoadingScreen.hideRocket(); + if (LoadingScreen.setBackground(titleInfo.product.heroImageUrl || titleInfo.product.titledHeroImageUrl || titleInfo.product.tileImageUrl), getPref("loadingScreen.rocket") === "hide") LoadingScreen.hideRocket(); } static hideRocket() { let $bgStyle = LoadingScreen.$bgStyle; @@ -7320,7 +7370,7 @@ class LoadingScreen { }, bg.src = imageUrl; } static setupWaitTime(waitTime) { - if (getPref("loadingScreenRocket") === "hide-queue") LoadingScreen.hideRocket(); + if (getPref("loadingScreen.rocket") === "hide-queue") LoadingScreen.hideRocket(); let secondsLeft = waitTime, $countDown, $estimated; LoadingScreen.orgWebTitle = document.title; let endDate = new Date, timeZoneOffsetSeconds = endDate.getTimezoneOffset() * 60; @@ -7335,7 +7385,7 @@ class LoadingScreen { }, 1000); } static hide() { - if (LoadingScreen.orgWebTitle && (document.title = LoadingScreen.orgWebTitle), LoadingScreen.$waitTimeBox && LoadingScreen.$waitTimeBox.classList.add("bx-gone"), getPref("loadingScreenGameArt") && LoadingScreen.$bgStyle) { + if (LoadingScreen.orgWebTitle && (document.title = LoadingScreen.orgWebTitle), LoadingScreen.$waitTimeBox && LoadingScreen.$waitTimeBox.classList.add("bx-gone"), getPref("loadingScreen.gameArt.show") && LoadingScreen.$bgStyle) { let $rocketBg = document.querySelector('#game-stream rect[width="800"]'); $rocketBg && $rocketBg.addEventListener("transitionend", (e) => { LoadingScreen.$bgStyle.textContent += "#game-stream{background:#000 !important}"; @@ -7697,7 +7747,7 @@ class XcloudInterceptor { } }; static async handleLogin(request, init) { - let bypassServer = getPref("serverBypassRestriction"); + let bypassServer = getPref("server.bypassRestriction"); if (bypassServer !== "off") { let ip = BypassServerIps[bypassServer]; ip && request.headers.set("X-Forwarded-For", ip); @@ -7725,7 +7775,7 @@ class XcloudInterceptor { } static async handlePlay(request, init) { BxEvent.dispatch(window, BxEvent.STREAM_LOADING); - let PREF_STREAM_TARGET_RESOLUTION = getPref("streamResolution"), PREF_STREAM_PREFERRED_LOCALE = getPref("streamLocale"), url = typeof request === "string" ? request : request.url, parsedUrl = new URL(url), badgeRegion = parsedUrl.host.split(".", 1)[0]; + let PREF_STREAM_TARGET_RESOLUTION = getPref("stream.video.resolution"), PREF_STREAM_PREFERRED_LOCALE = getPref("stream.locale"), url = typeof request === "string" ? request : request.url, parsedUrl = new URL(url), badgeRegion = parsedUrl.host.split(".", 1)[0]; for (let regionName in STATES.serverRegions) { let region = STATES.serverRegions[regionName]; if (parsedUrl.origin == region.baseUri) { @@ -7763,7 +7813,7 @@ class XcloudInterceptor { } static async handleWaitTime(request, init) { let response = await NATIVE_FETCH(request, init); - if (getPref("loadingScreenShowWaitTime")) { + if (getPref("loadingScreen.waitTime.show")) { let json = await response.clone().json(); if (json.estimatedAllocationTimeInSeconds > 0) LoadingScreen.setupWaitTime(json.estimatedTotalWaitTimeInSeconds); } @@ -7771,7 +7821,7 @@ class XcloudInterceptor { } static async handleConfiguration(request, init) { if (request.method !== "GET") return NATIVE_FETCH(request, init); - if (getPref("touchControllerMode") === "all") if (STATES.currentStream.titleInfo?.details.hasTouchSupport) TouchController.disable(); + if (getPref("touchController.mode") === "all") if (STATES.currentStream.titleInfo?.details.hasTouchSupport) TouchController.disable(); else TouchController.enable(); let response = await NATIVE_FETCH(request, init), text = await response.clone().text(); if (!text.length) return response; @@ -7779,14 +7829,14 @@ class XcloudInterceptor { let obj = JSON.parse(text), overrides = JSON.parse(obj.clientStreamingConfigOverrides || "{}") || {}; overrides.inputConfiguration = overrides.inputConfiguration || {}, overrides.inputConfiguration.enableVibration = !0; let overrideMkb = null; - if (getPref("nativeMkbMode") === "on" || STATES.currentStream.titleInfo && BX_FLAGS.ForceNativeMkbTitles?.includes(STATES.currentStream.titleInfo.details.productId)) overrideMkb = !0; - if (getPref("nativeMkbMode") === "off") overrideMkb = !1; + if (getPref("nativeMkb.mode") === "on" || STATES.currentStream.titleInfo && BX_FLAGS.ForceNativeMkbTitles?.includes(STATES.currentStream.titleInfo.details.productId)) overrideMkb = !0; + if (getPref("nativeMkb.mode") === "off") overrideMkb = !1; if (overrideMkb !== null) overrides.inputConfiguration = Object.assign(overrides.inputConfiguration, { enableMouseInput: overrideMkb, enableKeyboardInput: overrideMkb }); if (TouchController.isEnabled()) overrides.inputConfiguration.enableTouchInput = !0, overrides.inputConfiguration.maxTouchPoints = 10; - if (getPref("audioMicOnPlaying")) overrides.audioConfiguration = overrides.audioConfiguration || {}, overrides.audioConfiguration.enableMicrophone = !0; + if (getPref("audio.mic.onPlaying")) overrides.audioConfiguration = overrides.audioConfiguration || {}, overrides.audioConfiguration.enableMicrophone = !0; return obj.clientStreamingConfigOverrides = JSON.stringify(overrides), response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response; } static async handle(request, init) { @@ -7846,21 +7896,21 @@ async function patchIceCandidates(request, consoleAddrs) { let response = await NATIVE_FETCH(request), text = await response.clone().text(); if (!text.length) return response; let options = { - preferIpv6Server: getPref("serverPreferIpv6"), + preferIpv6Server: getPref("server.ipv6.prefer"), consoleAddrs }, obj = JSON.parse(text), exchangeResponse = JSON.parse(obj.exchangeResponse); return exchangeResponse = updateIceCandidates(exchangeResponse, options), obj.exchangeResponse = JSON.stringify(exchangeResponse), response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response; } function interceptHttpRequests() { let BLOCKED_URLS = []; - if (getPref("blockTracking")) clearAllLogs(), BLOCKED_URLS = BLOCKED_URLS.concat([ + if (getPref("block.tracking")) clearAllLogs(), BLOCKED_URLS = BLOCKED_URLS.concat([ "https://arc.msn.com", "https://browser.events.data.microsoft.com", "https://dc.services.visualstudio.com", "https://2c06dea3f26c40c69b8456d319791fd0@o427368.ingest.sentry.io", "https://mscom.demdex.net" ]); - if (getPref("blockSocialFeatures")) BLOCKED_URLS = BLOCKED_URLS.concat([ + if (getPref("block.social")) BLOCKED_URLS = BLOCKED_URLS.concat([ "https://peoplehub.xboxlive.com/users/me/people/social", "https://peoplehub.xboxlive.com/users/me/people/recommendations", "https://xblmessaging.xboxlive.com/network/xbox/users/me/inbox" @@ -7939,19 +7989,19 @@ function interceptHttpRequests() { }; } 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]{overflow:auto}#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.549);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-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:20px;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:20px;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 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}.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}.bx-suggest-toggler label{flex:1;padding:10px;background:#004f87}.bx-suggest-toggler span{display:inline-block;align-self:center;padding:10px;width:40px;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:20px}.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}div.bx-select > div label span{display:block;font-size:10px;font-weight:bold;text-align:left;line-height:initial;white-space:pre}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}html[data-xds-platform=tv] .bx-guide-home-achievements-progress{flex-direction:column}html:not([data-xds-platform=tv]) .bx-guide-home-achievements-progress{flex-direction:row}html:not([data-xds-platform=tv]) .bx-guide-home-achievements-progress > button:first-of-type{flex:1}html:not([data-xds-platform=tv]) .bx-guide-home-achievements-progress > button:last-of-type{width:40px}html:not([data-xds-platform=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}html[data-xds-platform=tv] .bx-guide-home-buttons > div{flex-direction:column}html[data-xds-platform=tv] .bx-guide-home-buttons > div button{margin-bottom:0 !important}html:not([data-xds-platform=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]{display:flex}div[data-testid=media-container].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)}#game-stream video{margin:auto;align-self:center;background:#000}#game-stream canvas{position:absolute;align-self:center;margin:auto;left:0;right:0}#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-transparent=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{min-width:60px;display:inline-block;text-align:right;vertical-align:middle}.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-stats-bar span:first-of-type{min-width:22px}.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("uiHideSections"), 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")}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]{overflow:auto}#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.549);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-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:20px;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:20px;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 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}.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}.bx-suggest-toggler label{flex:1;padding:10px;background:#004f87}.bx-suggest-toggler span{display:inline-block;align-self:center;padding:10px;width:40px;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:20px}.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}div.bx-select > div label span{display:block;font-size:10px;font-weight:bold;text-align:left;line-height:initial;white-space:pre}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}html[data-xds-platform=tv] .bx-guide-home-achievements-progress{flex-direction:column}html:not([data-xds-platform=tv]) .bx-guide-home-achievements-progress{flex-direction:row}html:not([data-xds-platform=tv]) .bx-guide-home-achievements-progress > button:first-of-type{flex:1}html:not([data-xds-platform=tv]) .bx-guide-home-achievements-progress > button:last-of-type{width:40px}html:not([data-xds-platform=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}html[data-xds-platform=tv] .bx-guide-home-buttons > div{flex-direction:column}html[data-xds-platform=tv] .bx-guide-home-buttons > div button{margin-bottom:0 !important}html:not([data-xds-platform=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]{display:flex}div[data-testid=media-container].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)}#game-stream video{margin:auto;align-self:center;background:#000}#game-stream canvas{position:absolute;align-self:center;margin:auto;left:0;right:0}#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-transparent=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{min-width:60px;display:inline-block;text-align:right;vertical-align:middle}.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-stats-bar span:first-of-type{min-width:22px}.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 = []; if (PREF_HIDE_SECTIONS.includes("news")) selectorToHide.push("#BodyContent > div[class*=CarouselRow-module]"); - if (getPref("byogDisabled")) selectorToHide.push("#BodyContent > div[class*=ByogRow-module__container___]"); + if (getPref("feature.byog.disabled")) 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]"); if (PREF_HIDE_SECTIONS.includes("most-popular")) selectorToHide.push('#BodyContent div[class*=HomePage-module__bottomSpacing]:has(a[href="/play/gallery/popular"])'); if (PREF_HIDE_SECTIONS.includes("touch")) selectorToHide.push('#BodyContent div[class*=HomePage-module__bottomSpacing]:has(a[href="/play/gallery/touch"])'); - if (getPref("blockSocialFeatures")) selectorToHide.push("#gamepass-dialog-root div[class^=AchievementsPreview-module__container] + button[class*=HomeLandingPage-module__button]"); + if (getPref("block.social")) selectorToHide.push("#gamepass-dialog-root div[class^=AchievementsPreview-module__container] + button[class*=HomeLandingPage-module__button]"); if (selectorToHide) css += selectorToHide.join(",") + "{ display: none; }"; - if (getPref("uiReduceAnimations")) css += "div[class*=GameCard-module__gameTitleInnerWrapper],div[class*=GameCard-module__card],div[class*=ScrollArrows-module]{transition:none !important}"; - if (getPref("uiHideSystemMenuIcon")) css += "div[class*=Grip-module__container]{visibility:hidden}@media (hover:hover){button[class*=GripHandle-module__container]:hover div[class*=Grip-module__container]{visibility:visible}}button[class*=GripHandle-module__container][aria-expanded=true] div[class*=Grip-module__container]{visibility:visible}button[class*=GripHandle-module__container][aria-expanded=false]{background-color:transparent !important}div[class*=StreamHUD-module__buttonsContainer]{padding:0 !important}"; - if (css += "div[class*=StreamMenu-module__menu]{min-width:100vw !important}", getPref("uiSimplifyStreamMenu")) css += "div[class*=Menu-module__scrollable]{--bxStreamMenuItemSize:80px;--streamMenuItemSize:calc(var(--bxStreamMenuItemSize) + 40px) !important}.bx-badges{top:calc(var(--streamMenuItemSize) - 20px)}body[data-media-type=tv] .bx-badges{top:calc(var(--streamMenuItemSize) - 10px) !important}button[class*=MenuItem-module__container]{min-width:auto !important;min-height:auto !important;width:var(--bxStreamMenuItemSize) !important;height:var(--bxStreamMenuItemSize) !important}div[class*=MenuItem-module__label]{display:none !important}svg[class*=MenuItem-module__icon]{width:36px;height:100% !important;padding:0 !important;margin:0 !important}"; + if (getPref("ui.reduceAnimations")) css += "div[class*=GameCard-module__gameTitleInnerWrapper],div[class*=GameCard-module__card],div[class*=ScrollArrows-module]{transition:none !important}"; + if (getPref("ui.systemMenu.hideHandle")) css += "div[class*=Grip-module__container]{visibility:hidden}@media (hover:hover){button[class*=GripHandle-module__container]:hover div[class*=Grip-module__container]{visibility:visible}}button[class*=GripHandle-module__container][aria-expanded=true] div[class*=Grip-module__container]{visibility:visible}button[class*=GripHandle-module__container][aria-expanded=false]{background-color:transparent !important}div[class*=StreamHUD-module__buttonsContainer]{padding:0 !important}"; + if (css += "div[class*=StreamMenu-module__menu]{min-width:100vw !important}", getPref("ui.streamMenu.simplify")) css += "div[class*=Menu-module__scrollable]{--bxStreamMenuItemSize:80px;--streamMenuItemSize:calc(var(--bxStreamMenuItemSize) + 40px) !important}.bx-badges{top:calc(var(--streamMenuItemSize) - 20px)}body[data-media-type=tv] .bx-badges{top:calc(var(--streamMenuItemSize) - 10px) !important}button[class*=MenuItem-module__container]{min-width:auto !important;min-height:auto !important;width:var(--bxStreamMenuItemSize) !important;height:var(--bxStreamMenuItemSize) !important}div[class*=MenuItem-module__label]{display:none !important}svg[class*=MenuItem-module__icon]{width:36px;height:100% !important;padding:0 !important;margin:0 !important}"; else css += "body[data-media-type=tv] .bx-badges{top:calc(var(--streamMenuItemSize) + 30px)}body:not([data-media-type=tv]) .bx-badges{top:calc(var(--streamMenuItemSize) + 20px)}body:not([data-media-type=tv]) button[class*=MenuItem-module__container]{min-width:auto !important;width:100px !important}body:not([data-media-type=tv]) button[class*=MenuItem-module__container]:nth-child(n+2){margin-left:10px !important}body:not([data-media-type=tv]) div[class*=MenuItem-module__label]{margin-left:8px !important;margin-right:8px !important}"; - if (getPref("uiHideScrollbar")) css += "html{scrollbar-width:none}body::-webkit-scrollbar{display:none}"; + if (getPref("ui.hideScrollbar")) css += "html{scrollbar-width:none}body::-webkit-scrollbar{display:none}"; let $style = CE("style", {}, css); document.documentElement.appendChild($style); } @@ -7968,7 +8018,7 @@ function preloadFonts() { class MouseCursorHider { static instance; static getInstance() { - if (typeof MouseCursorHider.instance === "undefined") if (!getPref("mkbEnabled") && getPref("mkbHideIdleCursor")) MouseCursorHider.instance = new MouseCursorHider; + if (typeof MouseCursorHider.instance === "undefined") if (!getPref("mkb.enabled") && getPref("mkb.cursor.hideIdle")) MouseCursorHider.instance = new MouseCursorHider; else MouseCursorHider.instance = null; return MouseCursorHider.instance; } @@ -8189,12 +8239,12 @@ class WebGL2Player { this.animFrameId = frameCallback(animate); } setupShaders() { - BxLogger.info(this.LOG_TAG, "Setting up", getPref("videoPowerPreference")); + BxLogger.info(this.LOG_TAG, "Setting up", getPref("video.player.powerPreference")); let gl = this.$canvas.getContext("webgl2", { isBx: !0, antialias: !0, alpha: !1, - powerPreference: getPref("videoPowerPreference") + powerPreference: getPref("video.player.powerPreference") }); this.gl = gl, gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferWidth); let vShader = gl.createShader(gl.VERTEX_SHADER); @@ -8281,7 +8331,7 @@ class StreamPlayer { return filters.join(" "); } resizePlayer() { - let PREF_RATIO = getPref("videoRatio"), $video = this.$video, isNativeTouchGame = STATES.currentStream.titleInfo?.details.hasNativeTouchSupport, $webGL2Canvas; + let PREF_RATIO = getPref("video.ratio"), $video = this.$video, isNativeTouchGame = STATES.currentStream.titleInfo?.details.hasNativeTouchSupport, $webGL2Canvas; if (this.playerType == "webgl2") $webGL2Canvas = this.webGL2Player?.getCanvas(); let targetWidth, targetHeight, targetObjectFit; if (PREF_RATIO.includes(":")) { @@ -8327,7 +8377,7 @@ class StreamPlayer { } else { let filters = this.getVideoPlayerFilterStyle(), videoCss = ""; if (filters) videoCss += `filter: ${filters} !important;`; - if (getPref("screenshotApplyFilters")) ScreenshotManager.getInstance().updateCanvasFilters(filters); + if (getPref("screenshot.applyFilters")) ScreenshotManager.getInstance().updateCanvasFilters(filters); let css = ""; if (videoCss) css = `#game-stream video { ${videoCss} }`; this.$videoCss.textContent = css; @@ -8345,16 +8395,16 @@ class StreamPlayer { } } function patchVideoApi() { - let PREF_SKIP_SPLASH_VIDEO = getPref("uiSkipSplashVideo"), showFunc = function() { + let PREF_SKIP_SPLASH_VIDEO = getPref("ui.splashVideo.skip"), showFunc = function() { if (this.style.visibility = "visible", !this.videoWidth) return; let playerOptions = { - processing: getPref("videoProcessing"), - sharpness: getPref("videoSharpness"), - saturation: getPref("videoSaturation"), - contrast: getPref("videoContrast"), - brightness: getPref("videoBrightness") + processing: getPref("video.processing"), + sharpness: getPref("video.processing.sharpness"), + saturation: getPref("video.saturation"), + contrast: getPref("video.contrast"), + brightness: getPref("video.brightness") }; - STATES.currentStream.streamPlayer = new StreamPlayer(this, getPref("videoPlayerType"), playerOptions), BxEvent.dispatch(window, BxEvent.STREAM_PLAYING, { + STATES.currentStream.streamPlayer = new StreamPlayer(this, getPref("video.player.type"), playerOptions), BxEvent.dispatch(window, BxEvent.STREAM_PLAYING, { $video: this }); }, nativePlay = HTMLMediaElement.prototype.play; @@ -8369,7 +8419,7 @@ function patchVideoApi() { }; } function patchRtcCodecs() { - if (getPref("streamCodecProfile") === "default") return; + if (getPref("stream.video.codecProfile") === "default") return; if (typeof RTCRtpTransceiver === "undefined" || !("setCodecPreferences" in RTCRtpTransceiver.prototype)) return !1; } function patchRtcPeerConnection() { @@ -8380,7 +8430,7 @@ function patchRtcPeerConnection() { dataChannel }), dataChannel; }; - let maxVideoBitrateDef = getPrefDefinition("streamMaxVideoBitrate"), maxVideoBitrate = getPref("streamMaxVideoBitrate"), codec = getPref("streamCodecProfile"); + let maxVideoBitrateDef = getPrefDefinition("stream.video.maxBitrate"), maxVideoBitrate = getPref("stream.video.maxBitrate"), codec = getPref("stream.video.codecProfile"); if (codec !== "default" || maxVideoBitrate < maxVideoBitrateDef.max) { let nativeSetLocalDescription = RTCPeerConnection.prototype.setLocalDescription; RTCPeerConnection.prototype.setLocalDescription = function(description) { @@ -8408,7 +8458,7 @@ function patchAudioContext() { let ctx = new OrgAudioContext(options); return BxLogger.info("patchAudioContext", ctx, options), ctx.createGain = function() { let gainNode = nativeCreateGain.apply(this); - return gainNode.gain.value = getPref("audioVolume") / 100, STATES.currentStream.audioGainNode = gainNode, gainNode; + return gainNode.gain.value = getPref("audio.volume") / 100, STATES.currentStream.audioGainNode = gainNode, gainNode; }, STATES.currentStream.audioContext = ctx, ctx; }; } @@ -8630,7 +8680,7 @@ class RendererAction extends BaseGameBarAction { class GameBar { static instance; static getInstance() { - if (typeof GameBar.instance === "undefined") if (getPref("gameBarPosition") !== "off") GameBar.instance = new GameBar; + if (typeof GameBar.instance === "undefined") if (getPref("gameBar.position") !== "off") GameBar.instance = new GameBar; else GameBar.instance = null; return GameBar.instance; } @@ -8642,10 +8692,10 @@ class GameBar { actions = []; constructor() { BxLogger.info(this.LOG_TAG, "constructor()"); - let $container, position = getPref("gameBarPosition"), $gameBar = CE("div", { id: "bx-game-bar", class: "bx-gone", "data-position": position }, $container = CE("div", { class: "bx-game-bar-container bx-offscreen" }), createSvgIcon(position === "bottom-left" ? BxIcon.CARET_RIGHT : BxIcon.CARET_LEFT)); + let $container, position = getPref("gameBar.position"), $gameBar = CE("div", { id: "bx-game-bar", class: "bx-gone", "data-position": position }, $container = CE("div", { class: "bx-game-bar-container bx-offscreen" }), createSvgIcon(position === "bottom-left" ? BxIcon.CARET_RIGHT : BxIcon.CARET_LEFT)); if (this.actions = [ new ScreenshotAction, - ...STATES.userAgent.capabilities.touch && getPref("touchControllerMode") !== "off" ? [new TouchControlAction] : [], + ...STATES.userAgent.capabilities.touch && getPref("touchController.mode") !== "off" ? [new TouchControlAction] : [], new SpeakerAction, new RendererAction, new MicrophoneAction, @@ -9119,7 +9169,7 @@ document.addEventListener("readystatechange", (e) => { if (document.readyState !== "interactive") return; if (STATES.isSignedIn = !!window.xbcUser?.isSignedIn, STATES.isSignedIn) RemotePlayManager.getInstance()?.initialize(); else window.setTimeout(HeaderSection.watchHeader, 2000); - if (getPref("uiHideSections").includes("friends")) { + if (getPref("ui.hideSections").includes("friends")) { let $parent = document.querySelector("div[class*=PlayWithFriendsSkeleton]")?.closest("div[class*=HomePage-module]"); $parent && ($parent.style.display = "none"); } @@ -9140,7 +9190,7 @@ window.addEventListener(BxEvent.STREAM_LOADING, (e) => { if (window.location.pathname.includes("/launch/") && STATES.currentStream.titleInfo) STATES.currentStream.titleSlug = productTitleToSlug(STATES.currentStream.titleInfo.product.title); else STATES.currentStream.titleSlug = "remote-play"; }); -getPref("loadingScreenGameArt") && window.addEventListener(BxEvent.TITLE_INFO_READY, LoadingScreen.setup); +getPref("loadingScreen.gameArt.show") && window.addEventListener(BxEvent.TITLE_INFO_READY, LoadingScreen.setup); window.addEventListener(BxEvent.STREAM_STARTING, (e) => { LoadingScreen.hide(); { @@ -9155,7 +9205,7 @@ window.addEventListener(BxEvent.STREAM_PLAYING, (e) => { if (gameBar) gameBar.reset(), gameBar.enable(), gameBar.showBar(); KeyboardShortcutHandler.getInstance().start(); let $video = e.$video; - ScreenshotManager.getInstance().updateCanvasSize($video.videoWidth, $video.videoHeight), getPref("localCoOpEnabled") && BxExposed.toggleLocalCoOp(getPref("localCoOpEnabled")); + ScreenshotManager.getInstance().updateCanvasSize($video.videoWidth, $video.videoHeight), getPref("localCoOp.enabled") && BxExposed.toggleLocalCoOp(getPref("localCoOp.enabled")); } updateVideoPlayer(); }); @@ -9193,14 +9243,14 @@ window.addEventListener(BxEvent.CAPTURE_SCREENSHOT, (e) => { ScreenshotManager.getInstance().takeScreenshot(); }); function main() { - if (GhPagesUtils.fetchLatestCommit(), getPref("nativeMkbMode") === "on") { - let customList = getPref("forceNativeMkbGames"); + if (GhPagesUtils.fetchLatestCommit(), getPref("nativeMkb.mode") === "on") { + let customList = getPref("nativeMkb.forcedGames"); BX_FLAGS.ForceNativeMkbTitles.push(...customList); } - if (StreamSettings.setup(), patchRtcPeerConnection(), patchRtcCodecs(), interceptHttpRequests(), patchVideoApi(), patchCanvasContext(), AppInterface && patchPointerLockApi(), getPref("audioEnableVolumeControl") && patchAudioContext(), getPref("blockTracking")) patchMeControl(), disableAdobeAudienceManager(); - if (RootDialogObserver.waitForRootDialog(), addCss(), GuideMenu.getInstance().addEventListeners(), StreamStatsCollector.setupEvents(), StreamBadges.setupEvents(), StreamStats.setupEvents(), STATES.userAgent.capabilities.touch && TouchController.updateCustomList(), DeviceVibrationManager.getInstance(), BX_FLAGS.CheckForUpdate && checkForUpdate(), Patcher.init(), disablePwa(), getPref("xhomeEnabled")) RemotePlayManager.detect(); - if (getPref("touchControllerMode") === "all") TouchController.setup(); - if (getPref("mkbEnabled") && AppInterface) STATES.pointerServerPort = AppInterface.startPointerServer() || 9269, BxLogger.info("startPointerServer", "Port", STATES.pointerServerPort.toString()); - if (getPref("uiGameCardShowWaitTime") && GameTile.setup(), EmulatedMkbHandler.setupEvents(), getPref("uiShowControllerStatus")) window.addEventListener("gamepadconnected", (e) => showGamepadToast(e.gamepad)), window.addEventListener("gamepaddisconnected", (e) => showGamepadToast(e.gamepad)); + if (StreamSettings.setup(), patchRtcPeerConnection(), patchRtcCodecs(), interceptHttpRequests(), patchVideoApi(), patchCanvasContext(), AppInterface && patchPointerLockApi(), getPref("audio.volume.booster.enabled") && patchAudioContext(), getPref("block.tracking")) patchMeControl(), disableAdobeAudienceManager(); + if (RootDialogObserver.waitForRootDialog(), addCss(), GuideMenu.getInstance().addEventListeners(), StreamStatsCollector.setupEvents(), StreamBadges.setupEvents(), StreamStats.setupEvents(), STATES.userAgent.capabilities.touch && TouchController.updateCustomList(), DeviceVibrationManager.getInstance(), BX_FLAGS.CheckForUpdate && checkForUpdate(), Patcher.init(), disablePwa(), getPref("xhome.enabled")) RemotePlayManager.detect(); + if (getPref("touchController.mode") === "all") TouchController.setup(); + if (getPref("mkb.enabled") && AppInterface) STATES.pointerServerPort = AppInterface.startPointerServer() || 9269, BxLogger.info("startPointerServer", "Port", STATES.pointerServerPort.toString()); + if (getPref("ui.gameCard.waitTime.show") && GameTile.setup(), EmulatedMkbHandler.setupEvents(), getPref("ui.controllerStatus.show")) window.addEventListener("gamepadconnected", (e) => showGamepadToast(e.gamepad)), window.addEventListener("gamepaddisconnected", (e) => showGamepadToast(e.gamepad)); } main(); diff --git a/eslint.config.mjs b/eslint.config.mjs old mode 100644 new mode 100755 diff --git a/package.json b/package.json old mode 100644 new mode 100755 index e3453cf..f082668 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "@types/bun": "^1.1.14", "@types/node": "^22.10.1", "@types/stylus": "^0.48.43", - "eslint": "^9.15.0", + "eslint": "^9.16.0", "eslint-plugin-compat": "^6.0.1", "stylus": "^0.64.0" }, diff --git a/scripts/custom-flags.user.js b/scripts/custom-flags.user.js old mode 100644 new mode 100755 diff --git a/src/assets/css/button.styl b/src/assets/css/button.styl old mode 100644 new mode 100755 index 9c913d7..12b02a9 --- a/src/assets/css/button.styl +++ b/src/assets/css/button.styl @@ -67,6 +67,24 @@ } } + &.bx-warning { + --button-rgb: var(--bx-warning-button-rgb); + + &:not([disabled]):active { + --button-active-rgb: var(--bx-warning-button-active-rgb); + } + + &:not([disabled]):not(:active) { + &:hover, &.bx-focusable:focus { + --button-hover-rgb: var(--bx-warning-button-hover-rgb); + } + } + + &:disabled { + --button-disabled-rgb: var(--bx-warning-button-disabled-rgb); + } + } + &.bx-danger { --button-rgb: var(--bx-danger-button-rgb); @@ -107,6 +125,7 @@ &.bx-circular { border-radius: var(--bx-button-height); + width: var(--bx-button-height); height: var(--bx-button-height); } @@ -130,6 +149,25 @@ margin-left: 10px; } } + + &.bx-button-multi-lines { + height: auto; + text-align: left; + padding: 10px 0; + + span { + line-height: unset; + display: block; + + &:last-of-type { + text-transform: none; + font-weight: normal; + font-family: "Segoe Sans Variable Text"; + font-size: 12px; + margin-top: 4px; + } + } + } } .bx-focusable { diff --git a/src/assets/css/game-bar.styl b/src/assets/css/game-bar.styl old mode 100644 new mode 100755 diff --git a/src/assets/css/guide-menu.styl b/src/assets/css/guide-menu.styl old mode 100644 new mode 100755 diff --git a/src/assets/css/header.styl b/src/assets/css/header.styl old mode 100644 new mode 100755 diff --git a/src/assets/css/dialog.styl b/src/assets/css/key-binding-dialog.styl old mode 100644 new mode 100755 similarity index 66% rename from src/assets/css/dialog.styl rename to src/assets/css/key-binding-dialog.styl index 381988b..3504dbe --- a/src/assets/css/dialog.styl +++ b/src/assets/css/key-binding-dialog.styl @@ -1,12 +1,12 @@ -.bx-dialog-overlay { +.bx-key-binding-dialog-overlay { position: fixed; inset: 0; - z-index: var(--bx-dialog-overlay-z-index); + z-index: var(--bx-key-binding-dialog-overlay-z-index); background: black; opacity: 50%; } -.bx-dialog { +.bx-key-binding-dialog { display: flex; flex-flow: column; max-height: 90vh; @@ -18,7 +18,7 @@ min-width: 420px; padding: 20px; border-radius: 8px; - z-index: var(--bx-dialog-z-index); + z-index: var(--bx-key-binding-dialog-z-index); background: #1a1b1e; color: #fff; font-weight: 400; @@ -33,26 +33,13 @@ } h2 { - display: flex; margin-bottom: 12px; - - b { - flex: 1; - color: #fff; - display: block; - font-family: var(--bx-title-font); - font-size: 26px; - font-weight: 400; - line-height: var(--bx-button-height); - } - } - - &.bx-binding-dialog { - h2 { - b { - font-family: var(--bx-promptfont-font) !important; - } - } + color: #fff; + display: block; + font-family: var(--bx-title-font); + font-size: 32px; + font-weight: 400; + line-height: var(--bx-button-height); } > div { @@ -85,11 +72,26 @@ background-color: #515863; } } + + ul { + margin-bottom: 1rem; + + li { + display: none; + } + + &[data-flags*="[1]"] > li[data-flag="1"], + &[data-flags*="[2]"] > li[data-flag="2"], + &[data-flags*="[4]"] > li[data-flag="4"], + &[data-flags*="[8]"] > li[data-flag="8"] { + display: list-item; + } + } } @media screen and (max-width: 450px) { - .bx-dialog { + .bx-key-binding-dialog { min-width: 100%; } } diff --git a/src/assets/css/loading-screen.styl b/src/assets/css/loading-screen.styl old mode 100644 new mode 100755 diff --git a/src/assets/css/misc.styl b/src/assets/css/misc.styl old mode 100644 new mode 100755 diff --git a/src/assets/css/mkb.styl b/src/assets/css/mkb.styl old mode 100644 new mode 100755 index 7c9b16a..a76f0bf --- a/src/assets/css/mkb.styl +++ b/src/assets/css/mkb.styl @@ -4,15 +4,6 @@ flex: 1; padding-bottom: 10px; overflow: hidden; - - select:disabled { - -webkit-appearance: none; - background: transparent; - text-align-last: right; - text-align: right; - border: none; - color: #fff; - } } .bx-mkb-pointer-lock-msg { @@ -20,13 +11,12 @@ -webkit-user-select: none; position: fixed; left: 50%; - top: 50%; - transform: translateX(-50%) translateY(-50%); + bottom: 40px; + transform: translateX(-50%); margin: auto; background: #151515; z-index: var(--bx-mkb-pointer-lock-msg-z-index); color: #fff; - text-align: center; font-weight: 400; font-family: "Segoe UI", Arial, Helvetica, sans-serif; font-size: 1.3rem; @@ -34,117 +24,55 @@ border-radius: 8px; align-items: center; box-shadow: 0 0 6px #000; - min-width: 220px; + min-width: 300px; opacity: 0.9; + display: flex; + flex-direction: column; + gap: 10px; &:hover { opacity: 1; } - > div:first-of-type { - display: flex; - flex-direction: column; + > p { + margin: 0; + width: 100%; + font-size: 22px; + margin-bottom: 4px; + font-weight: bold; text-align: left; } - p { - margin: 0; + > div { + width: 100%; + display: flex; + flex-direction: row; - &:first-child { - font-size: 22px; - margin-bottom: 4px; - font-weight: bold; - } - - &:last-child { - font-size: 12px; - font-style: italic; - } - } - - > div:last-of-type { - margin-top: 10px; - - &[data-type='native'] { - button { - &:first-of-type { - margin-bottom: 8px; - } + gap: 10px; + button { + &:first-of-type { + flex-shrink: 1; } - } - &[data-type='virtual'] { - div { - display: flex; - flex-flow: row; - margin-top: 8px; - - button { - flex: 1; - - &:first-of-type { - margin-right: 5px; - } - - &:last-of-type { - margin-left: 5px; - } - } + &:last-of-type { + flex-grow: 1; } } } } -.bx-mkb-preset-tools { - display: flex; - margin-bottom: 12px; - - select { - flex: 1; - } - - button { - margin-left: 6px; - } -} - - -.bx-mkb-settings-rows { - flex: 1; - overflow: scroll; -} - .bx-mkb-key-row { display: flex; margin-bottom: 10px; align-items: center; + gap: 20px; label { margin-bottom: 0; font-family: var(--bx-promptfont-font); - font-size: 26px; + font-size: 32px; text-align: center; - width: 26px; - height: 32px; - line-height: 32px; - } - - button { - flex: 1; - height: 32px; - line-height: 32px; - margin: 0 0 0 10px; - background: transparent; - border: none; - color: white; - border-radius: 0; - border-left: 1px solid #373737; - - &:hover { - background: transparent; - cursor: default; - } } } @@ -181,10 +109,58 @@ .bx-mkb-note { display: block; - margin: 16px 0 10px; + margin: 0 0 10px; font-size: 12px; + text-align: center; +} - &:first-of-type { - margin-top: 0; +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; + + &:disabled { + background: #131416; + padding: 0 8px; + } + + &:not(:disabled) { + border: 2px solid transparent; + border-top: none; + border-bottom: 4px solid #252525; + background: #3b3b3b; + cursor: pointer; + + &:hover, &.bx-focusable:focus { + background: #20b217; + border-bottom-color: #186c13; + } + + &:active { + background: #16900f; + border-bottom: 3px solid #0c4e08; + border-left-width: 2px; + border-right-width: 2px; + } + + &.bx-focusable:focus { + &::after { + top: -6px; + left: -8px; + right: -8px; + bottom: -10px; + } + } + } + + .bx-settings-row .bx-binding-button-wrapper & { + min-width: 60px; } } diff --git a/src/assets/css/navigation-dialog.styl b/src/assets/css/navigation-dialog.styl old mode 100644 new mode 100755 index b1b1d4a..b0fae7d --- a/src/assets/css/navigation-dialog.styl +++ b/src/assets/css/navigation-dialog.styl @@ -6,6 +6,17 @@ *:focus { outline: none !important; } + + 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 { @@ -21,3 +32,187 @@ background: transparent; } } + +.bx-centered-dialog { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + + color: white; + background: #1a1b1e; + border-radius: 10px; + width: 450px; + max-width: calc(100vw - 20px); + margin: 0 0 0 auto; + padding: 20px; + + max-height: 95vh; + flex-direction: column; + overflow: hidden; + display: flex; + flex-direction: column; + + .bx-dialog-title { + display: flex; + flex-direction: row; + align-items: center; + margin-bottom: 10px; + + p { + padding: 0; + margin: 0; + flex: 1; + font-size: 1.2rem; + font-weight: bold; + } + + button { + flex-shrink: 0; + } + } + + .bx-dialog-content { + flex: 1; + overflow: auto; + overflow-x: hidden; + + > div { + } + } + + .bx-dialog-preset-tools { + display: flex; + margin-bottom: 12px; + gap: 6px; + + select { + flex: 1; + } + } +} + +.bx-centered-dialog, +.bx-settings-dialog { + input { + accent-color: var(--bx-primary-button-color); + + &:focus { + accent-color: var(--bx-danger-button-color); + } + } + + select:disabled { + -webkit-appearance: none; + background: transparent; + text-align-last: right; + border: none; + color: #fff; + } + + select option:disabled { + display: none; + } + + input[type=checkbox], + 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); + } + } + + a { + color: #1c9d1c; + text-decoration: none; + + &:hover, &:focus { + color: #5dc21e; + } + } + + label { + margin: 0; + } +} + +.bx-controller-shortcuts-manager-container { + .bx-shortcut-note { + margin-top: 10px; + font-size: 14px; + text-align: center; + } + + .bx-shortcut-row { + display: flex; + gap: 10px; + margin-bottom: 10px; + align-items: center; + + label.bx-prompt { + flex-shrink: 0; + font-size: 32px; + margin: 0; + + &::first-letter { + 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 { + text-align: left; + text-align-last: left; + } +} + +.bx-keyboard-shortcuts-manager-container { + display: flex; + flex-direction: column; + gap: 16px; + + fieldset { + background: #2a2a2a; + border: 1px solid #2a2a2a; + border-radius: 4px; + padding: 4px; + } + + legend { + width: auto; + padding: 4px 8px; + margin: 0 4px 4px; + background: #004f87; + box-shadow: 0px 2px 0px #071e3d; + border-radius: 4px; + font-size: 14px; + font-weight: bold; + text-transform: uppercase; + } + + .bx-settings-row { + background: none; + } +} diff --git a/src/assets/css/number-stepper.styl b/src/assets/css/number-stepper.styl old mode 100644 new mode 100755 index d72272d..b017633 --- a/src/assets/css/number-stepper.styl +++ b/src/assets/css/number-stepper.styl @@ -1,46 +1,54 @@ .bx-number-stepper { text-align: center; - span { - display: inline-block; - min-width: 40px; - font-family: var(--bx-monospaced-font); - font-size: 13px; - margin: 0 4px; - } + > div { + display: flex; + align-items: center; - button { - 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); + span { + flex: 1; + display: inline-block; + min-width: 40px; + font-family: var(--bx-monospaced-font); + font-size: 13px; + margin: 0 4px; + } - &:hover { - @media (hover: hover) { + 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); + + &:hover { + @media (hover: hover) { + background-color: var(--bx-default-button-hover-color); + } + } + + &:active { background-color: var(--bx-default-button-hover-color); } - } - &:active { - background-color: var(--bx-default-button-hover-color); - } - - &:disabled + span { - font-family: var(--bx-title-font); + &:disabled + span { + font-family: var(--bx-title-font); + } } } input[type="range"] { display: block; - margin: 12px auto 2px; - width: 180px; + margin: 8px 0 2px auto; + min-width: 180px; + width: 100%; color: #959595 !important; } @@ -48,7 +56,7 @@ display: none; } - &[data-disabled=true] { + &[data-disabled=true], &[disabled=true] { input[type=range], button { display: none; } diff --git a/src/assets/css/remote-play.styl b/src/assets/css/remote-play.styl old mode 100644 new mode 100755 diff --git a/src/assets/css/root.styl b/src/assets/css/root.styl old mode 100644 new mode 100755 index a5aa734..6df92ad --- a/src/assets/css/root.styl +++ b/src/assets/css/root.styl @@ -24,22 +24,24 @@ button_color(name, normal, hover, active, disabled) button_color('default', #2d3036, #515863, #222428, #8e8e8e); button_color('primary', #008746, #04b358, #044e2a, #448262); + button_color('warning', #c16e04, #fa9005, #965603, #a2816c); button_color('danger', #c10404, #e61d1d, #a26c6c, #df5656); - --bx-fullscreen-text-z-index: 99999; - --bx-toast-z-index: 60000; - --bx-dialog-z-index: 50000; + --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-dialog-overlay-z-index: 40200; - --bx-stats-bar-z-index: 40100; - --bx-mkb-pointer-lock-msg-z-index: 40000; + --bx-stats-bar-z-index: 4010; - --bx-navigation-dialog-z-index: 30100; - --bx-navigation-dialog-overlay-z-index: 30000; + --bx-navigation-dialog-z-index: 3010; + --bx-navigation-dialog-overlay-z-index: 3000; - --bx-game-bar-z-index: 10000; - --bx-screenshot-animation-z-index: 9000; - --bx-wait-time-box-z-index: 1000; + --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 { @@ -120,7 +122,7 @@ div[class^=HUDButton-module__hiddenContainer] ~ div:not([class^=HUDButton-module } .bx-prompt { - font-family: var(--bx-promptfont-font); + font-family: var(--bx-promptfont-font) !important; } .bx-line-through { @@ -226,3 +228,13 @@ div[class*=SupportedInputsBadge] { display: none; } } + +.bx-blink-me { + animation: bx-blinker 1s linear infinite; +} + +@keyframes bx-blinker { + 100% { + opacity: 0; + } +} diff --git a/src/assets/css/settings-dialog.styl b/src/assets/css/settings-dialog.styl old mode 100644 new mode 100755 index b51147e..b5e4752 --- a/src/assets/css/settings-dialog.styl +++ b/src/assets/css/settings-dialog.styl @@ -31,42 +31,6 @@ font-weight: normal; height: var(--bx-button-height); } - - input { - accent-color: var(--bx-primary-button-color); - - &:focus { - accent-color: var(--bx-danger-button-color); - } - } - - select:disabled { - -webkit-appearance: none; - background: transparent; - text-align-last: right; - border: none; - color: #fff; - } - - select option:disabled { - display: none; - } - - input[type=checkbox], - 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); - } - } - - a { - color: #1c9d1c; - text-decoration: none; - - &:hover, &:focus { - color: #5dc21e; - } - } } .bx-settings-tabs-container { @@ -170,69 +134,6 @@ overflow: hidden; } - > div[data-tab-group=shortcuts] { - > div { - &[data-has-gamepad=true] { - > div:first-of-type { - display: none; - } - - > div:last-of-type { - display: block; - } - } - - &[data-has-gamepad=false] { - > div:first-of-type { - display: block; - } - - > div:last-of-type { - display: none; - } - } - } - - .bx-shortcut-profile { - width: 100%; - height: 36px; - display: block; - } - - .bx-shortcut-note { - margin-top: 10px; - font-size: 14px; - } - - .bx-shortcut-row { - display: flex; - margin-bottom: 10px; - - label.bx-prompt { - flex: 1; - font-size: 26px; - margin-bottom: 0; - } - - .bx-shortcut-actions { - flex: 2; - position: relative; - - select { - position: absolute; - width: 100%; - height: 100%; - display: block; - - &:last-of-type { - opacity: 0; - z-index: calc(var(--bx-settings-z-index) + 1); - } - } - } - } - } - .bx-top-buttons { display: flex; flex-direction: column; @@ -262,6 +163,8 @@ text-overflow: ellipsis; overflow: hidden; white-space: nowrap; + min-height: var(--bx-button-height); + align-content: center; } } } @@ -306,6 +209,18 @@ margin: 0 0 0 auto; } } + + &[data-multi-lines="true"] { + flex-direction: column; + + > span.bx-settings-label { + align-self: start; + + + * { + margin: unset; + } + } + } } .bx-settings-dialog-note { @@ -339,6 +254,7 @@ line-height: 20px; font-size: 14px; margin-top: 10px; + margin-bottom: 10px; } .bx-debug-info { @@ -378,24 +294,26 @@ } .bx-settings-tab-contents { + border-radius-size = 6px; + > div { // Label at the beginning *:not(.bx-settings-row):has(+ .bx-settings-row) + .bx-settings-row:has(+ .bx-settings-row) { - border-top-left-radius: 10px; - border-top-right-radius: 10px; + border-top-left-radius: border-radius-size; + border-top-right-radius: border-radius-size; } // Label at the end .bx-settings-row:not(:has(+ .bx-settings-row)) { border: none; - border-bottom-left-radius: 10px; - border-bottom-right-radius: 10px; + border-bottom-left-radius: border-radius-size; + border-bottom-right-radius: border-radius-size; } // Single label *:not(.bx-settings-row):has(+ .bx-settings-row) + .bx-settings-row:not(:has(+ .bx-settings-row)) { border: none; - border-radius: 10px; + border-radius: border-radius-size; } } } @@ -409,7 +327,6 @@ label { flex: 1; - margin-bottom: 0; padding: 10px; background: #004f87; } @@ -444,10 +361,6 @@ .bx-suggest-box { display: none; - background: #161616; - padding: 10px; - box-shadow: 0px 0px 12px #0f0f0f inset; - border-radius: 10px; } .bx-suggest-wrapper { @@ -563,3 +476,65 @@ } } } + +.bx-sub-content-box { + background: #161616; + padding: 10px; + box-shadow: 0px 0px 12px #0f0f0f inset; + border-radius: 10px; + + .bx-settings-row & { + background: #202020; + padding: 12px; + box-shadow: 0 0 4px #000000 inset; + border-radius: 6px; + } +} + +.bx-controller-extra-settings { + &[data-has-gamepad=true] { + > :first-child { + display: none; + } + + > :last-child { + display: block; + } + } + + &[data-has-gamepad=false] { + > :first-child { + display: block; + } + + > :last-child { + display: none; + } + } + + .bx-controller-extra-wrapper { + flex: 1; + min-width: 1px; + } + + .bx-sub-content-box { + flex: 1; + text-align: left; + display: flex; + flex-direction: column; + margin-top: 10px; + + > label { + font-size: 14px; + } + } +} + +.bx-preset-row { + display: flex; + gap: 8px; + + .bx-select { + flex: 1; + } +} diff --git a/src/assets/css/stream-stats.styl b/src/assets/css/stream-stats.styl old mode 100644 new mode 100755 diff --git a/src/assets/css/stream.styl b/src/assets/css/stream.styl old mode 100644 new mode 100755 diff --git a/src/assets/css/styles.styl b/src/assets/css/styles.styl old mode 100644 new mode 100755 index d3b9929..b8c1a15 --- a/src/assets/css/styles.styl +++ b/src/assets/css/styles.styl @@ -2,7 +2,7 @@ @import 'button.styl'; @import 'header.styl'; -@import 'dialog.styl'; +@import 'key-binding-dialog.styl'; @import 'navigation-dialog.styl'; @import 'settings-dialog.styl'; @import 'toast.styl'; diff --git a/src/assets/css/toast.styl b/src/assets/css/toast.styl old mode 100644 new mode 100755 diff --git a/src/assets/css/web-components.styl b/src/assets/css/web-components.styl old mode 100644 new mode 100755 index 2ef47f4..92e1985 --- a/src/assets/css/web-components.styl +++ b/src/assets/css/web-components.styl @@ -1,7 +1,12 @@ -.bx-select { +select.bx-select { + min-height: 30px; +} + +div.bx-select { display: flex; align-items: center; flex: 0 1 auto; + gap: 8px; select { // Render offscreen instead of "display: none" so we could get its size @@ -9,23 +14,41 @@ top: -9999px !important; left: -9999px !important; visibility: hidden !important; + + &:disabled { + & ~ button { + display: none; + } + + & ~ div { + background: #131416; + color: white; + pointer-events: none; + + .bx-select-indicators { + visibility: hidden; + } + } + } } > div, button.bx-select-value { min-width: 120px; text-align: left; - margin: 0 8px; line-height: 24px; vertical-align: middle; background: #fff; color: #000; border-radius: 4px; padding: 2px 8px; + display: flex; flex: 1; + flex-direction: column; } > div { - display: inline-block; + min-height: 24px; + box-sizing: content-box; input { display: inline-block; @@ -36,6 +59,9 @@ margin-bottom: 0; font-size: 14px; width: 100%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; span { display: block; @@ -43,18 +69,23 @@ font-weight: bold; text-align: left; line-height: initial; + white-space: pre; } } } button.bx-select-value { border: none; - display: inline-flex; cursor: pointer; min-height: 30px; font-size: 0.9rem; align-items: center; + > div { + display: flex; + width: 100%; + } + span { flex: 1; text-align: left; @@ -97,3 +128,30 @@ } } } + +.bx-select-indicators { + display: flex; + height: 4px; + gap: 2px; + margin-bottom: 2px; + + span { + content: ' '; + display: inline-block; + flex: 1; + background: #cfcfcf; + border-radius: 4px; + + &[data-highlighted] { + background: #9c9c9c; + } + + &[data-selected] { + background: #aacfe7; + } + + &[data-highlighted][data-selected] { + background: #5fa3d0; + } + } +} diff --git a/src/assets/header_meta.txt b/src/assets/header_meta.txt old mode 100644 new mode 100755 diff --git a/src/assets/header_script.lite.txt b/src/assets/header_script.lite.txt old mode 100644 new mode 100755 diff --git a/src/assets/header_script.txt b/src/assets/header_script.txt old mode 100644 new mode 100755 diff --git a/src/assets/svg/battery-full.svg b/src/assets/svg/battery-full.svg old mode 100644 new mode 100755 diff --git a/src/assets/svg/better-xcloud.svg b/src/assets/svg/better-xcloud.svg old mode 100644 new mode 100755 diff --git a/src/assets/svg/camera.svg b/src/assets/svg/camera.svg old mode 100644 new mode 100755 diff --git a/src/assets/svg/caret-left.svg b/src/assets/svg/caret-left.svg old mode 100644 new mode 100755 diff --git a/src/assets/svg/caret-right.svg b/src/assets/svg/caret-right.svg old mode 100644 new mode 100755 diff --git a/src/assets/svg/clock.svg b/src/assets/svg/clock.svg old mode 100644 new mode 100755 diff --git a/src/assets/svg/close.svg b/src/assets/svg/close.svg old mode 100644 new mode 100755 diff --git a/src/assets/svg/cloud.svg b/src/assets/svg/cloud.svg old mode 100644 new mode 100755 diff --git a/src/assets/svg/command.svg b/src/assets/svg/command.svg old mode 100644 new mode 100755 diff --git a/src/assets/svg/controller.svg b/src/assets/svg/controller.svg old mode 100644 new mode 100755 diff --git a/src/assets/svg/copy.svg b/src/assets/svg/copy.svg old mode 100644 new mode 100755 diff --git a/src/assets/svg/create-shortcut.svg b/src/assets/svg/create-shortcut.svg old mode 100644 new mode 100755 diff --git a/src/assets/svg/cursor-text.svg b/src/assets/svg/cursor-text.svg old mode 100644 new mode 100755 diff --git a/src/assets/svg/display.svg b/src/assets/svg/display.svg old mode 100644 new mode 100755 diff --git a/src/assets/svg/download.svg b/src/assets/svg/download.svg old mode 100644 new mode 100755 diff --git a/src/assets/svg/eye-slash.svg b/src/assets/svg/eye-slash.svg old mode 100644 new mode 100755 diff --git a/src/assets/svg/eye.svg b/src/assets/svg/eye.svg old mode 100644 new mode 100755 diff --git a/src/assets/svg/home.svg b/src/assets/svg/home.svg old mode 100644 new mode 100755 diff --git a/src/assets/svg/microphone-slash.svg b/src/assets/svg/microphone-slash.svg old mode 100644 new mode 100755 diff --git a/src/assets/svg/microphone.svg b/src/assets/svg/microphone.svg old mode 100644 new mode 100755 diff --git a/src/assets/svg/mouse-settings.svg b/src/assets/svg/mouse-settings.svg old mode 100644 new mode 100755 diff --git a/src/assets/svg/mouse.svg b/src/assets/svg/mouse.svg old mode 100644 new mode 100755 diff --git a/src/assets/svg/native-mkb.svg b/src/assets/svg/native-mkb.svg old mode 100644 new mode 100755 diff --git a/src/assets/svg/new.svg b/src/assets/svg/new.svg old mode 100644 new mode 100755 diff --git a/src/assets/svg/power.svg b/src/assets/svg/power.svg old mode 100644 new mode 100755 diff --git a/src/assets/svg/question.svg b/src/assets/svg/question.svg old mode 100644 new mode 100755 diff --git a/src/assets/svg/refresh.svg b/src/assets/svg/refresh.svg old mode 100644 new mode 100755 diff --git a/src/assets/svg/remote-play.svg b/src/assets/svg/remote-play.svg old mode 100644 new mode 100755 diff --git a/src/assets/svg/speaker-high.svg b/src/assets/svg/speaker-high.svg old mode 100644 new mode 100755 diff --git a/src/assets/svg/speaker-slash.svg b/src/assets/svg/speaker-slash.svg old mode 100644 new mode 100755 diff --git a/src/assets/svg/stream-settings.svg b/src/assets/svg/stream-settings.svg old mode 100644 new mode 100755 diff --git a/src/assets/svg/stream-stats.svg b/src/assets/svg/stream-stats.svg old mode 100644 new mode 100755 diff --git a/src/assets/svg/touch-control-disable.svg b/src/assets/svg/touch-control-disable.svg old mode 100644 new mode 100755 diff --git a/src/assets/svg/touch-control-enable.svg b/src/assets/svg/touch-control-enable.svg old mode 100644 new mode 100755 diff --git a/src/assets/svg/trash.svg b/src/assets/svg/trash.svg old mode 100644 new mode 100755 diff --git a/src/assets/svg/true-achievements.svg b/src/assets/svg/true-achievements.svg old mode 100644 new mode 100755 diff --git a/src/assets/svg/upload.svg b/src/assets/svg/upload.svg old mode 100644 new mode 100755 diff --git a/src/assets/svg/virtual-controller.svg b/src/assets/svg/virtual-controller.svg old mode 100644 new mode 100755 diff --git a/src/build-config.ts b/src/build-config.ts old mode 100644 new mode 100755 diff --git a/src/enums/bypass-servers.ts b/src/enums/bypass-servers.ts old mode 100644 new mode 100755 index d7df700..fb41185 --- a/src/enums/bypass-servers.ts +++ b/src/enums/bypass-servers.ts @@ -1,17 +1,17 @@ import { t } from "@/utils/translation" export const BypassServers = { - 'br': t('brazil'), - 'jp': t('japan'), - 'kr': t('korea'), - 'pl': t('poland'), - 'us': t('united-states'), -} + br: t('brazil'), + jp: t('japan'), + kr: t('korea'), + pl: t('poland'), + us: t('united-states'), +} as const; export const BypassServerIps: Record = { - 'br': '169.150.198.66', - 'kr': '121.125.60.151', - 'jp': '138.199.21.239', - 'pl': '45.134.212.66', - 'us': '143.244.47.65', -} + br: '169.150.198.66', + kr: '121.125.60.151', + jp: '138.199.21.239', + pl: '45.134.212.66', + us: '143.244.47.65', +} as const; diff --git a/src/enums/game-pass-gallery.ts b/src/enums/game-pass-gallery.ts old mode 100644 new mode 100755 diff --git a/src/enums/gamepad.ts b/src/enums/gamepad.ts new file mode 100755 index 0000000..1f19674 --- /dev/null +++ b/src/enums/gamepad.ts @@ -0,0 +1,71 @@ +import { PrompFont } from "./prompt-font"; + +export enum GamepadKey { + A = 0, + B = 1, + X = 2, + Y = 3, + LB = 4, + RB = 5, + LT = 6, + RT = 7, + SELECT = 8, + START = 9, + L3 = 10, + R3 = 11, + UP = 12, + DOWN = 13, + LEFT = 14, + RIGHT = 15, + HOME = 16, + SHARE = 17, + + LS_UP = 100, + LS_DOWN = 101, + LS_LEFT = 102, + LS_RIGHT = 103, + + RS_UP = 200, + RS_DOWN = 201, + RS_LEFT = 202, + RS_RIGHT = 203, +}; + +export const GamepadKeyName: Record = { + [GamepadKey.A]: ['A', PrompFont.A], + [GamepadKey.B]: ['B', PrompFont.B], + [GamepadKey.X]: ['X', PrompFont.X], + [GamepadKey.Y]: ['Y', PrompFont.Y], + + [GamepadKey.LB]: ['LB', PrompFont.LB], + [GamepadKey.RB]: ['RB', PrompFont.RB], + [GamepadKey.LT]: ['LT', PrompFont.LT], + [GamepadKey.RT]: ['RT', PrompFont.RT], + + [GamepadKey.SELECT]: ['Select', PrompFont.SELECT], + [GamepadKey.START]: ['Start', PrompFont.START], + [GamepadKey.HOME]: ['Home', PrompFont.HOME], + + [GamepadKey.UP]: ['D-Pad Up', PrompFont.UP], + [GamepadKey.DOWN]: ['D-Pad Down', PrompFont.DOWN], + [GamepadKey.LEFT]: ['D-Pad Left', PrompFont.LEFT], + [GamepadKey.RIGHT]: ['D-Pad Right', PrompFont.RIGHT], + + [GamepadKey.L3]: ['L3', PrompFont.L3], + [GamepadKey.LS_UP]: ['Left Stick Up', PrompFont.LS_UP], + [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.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], +}; + + +export enum GamepadStick { + LEFT = 0, + RIGHT = 1, +}; diff --git a/src/enums/mkb.ts b/src/enums/mkb.ts old mode 100644 new mode 100755 index 8140eb2..e48ec4f --- a/src/enums/mkb.ts +++ b/src/enums/mkb.ts @@ -1,102 +1,169 @@ -import type { GamepadKeyNameType } from "@/types/mkb"; -import { PrompFont } from "@enums/prompt-font"; - -export enum GamepadKey { - A = 0, - B = 1, - X = 2, - Y = 3, - LB = 4, - RB = 5, - LT = 6, - RT = 7, - SELECT = 8, - START = 9, - L3 = 10, - R3 = 11, - UP = 12, - DOWN = 13, - LEFT = 14, - RIGHT = 15, - HOME = 16, - SHARE = 17, - - LS_UP = 100, - LS_DOWN = 101, - LS_LEFT = 102, - LS_RIGHT = 103, - - RS_UP = 200, - RS_DOWN = 201, - RS_LEFT = 202, - RS_RIGHT = 203, -}; +export const enum MouseConstant { + DEFAULT_PANNING_SENSITIVITY = 0.0010, + DEFAULT_DEADZONE_COUNTERWEIGHT = 0.01, + MAXIMUM_STICK_RANGE = 1.1, +} -export const GamepadKeyName: GamepadKeyNameType = { - [GamepadKey.A]: ['A', PrompFont.A], - [GamepadKey.B]: ['B', PrompFont.B], - [GamepadKey.X]: ['X', PrompFont.X], - [GamepadKey.Y]: ['Y', PrompFont.Y], - - [GamepadKey.LB]: ['LB', PrompFont.LB], - [GamepadKey.RB]: ['RB', PrompFont.RB], - [GamepadKey.LT]: ['LT', PrompFont.LT], - [GamepadKey.RT]: ['RT', PrompFont.RT], - - [GamepadKey.SELECT]: ['Select', PrompFont.SELECT], - [GamepadKey.START]: ['Start', PrompFont.START], - [GamepadKey.HOME]: ['Home', PrompFont.HOME], - - [GamepadKey.UP]: ['D-Pad Up', PrompFont.UP], - [GamepadKey.DOWN]: ['D-Pad Down', PrompFont.DOWN], - [GamepadKey.LEFT]: ['D-Pad Left', PrompFont.LEFT], - [GamepadKey.RIGHT]: ['D-Pad Right', PrompFont.RIGHT], - - [GamepadKey.L3]: ['L3', PrompFont.L3], - [GamepadKey.LS_UP]: ['Left Stick Up', PrompFont.LS_UP], - [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.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], -}; - - -export enum GamepadStick { - LEFT = 0, - RIGHT = 1, -}; - -export enum MouseButtonCode { +export const enum MouseButtonCode { LEFT_CLICK = 'Mouse0', RIGHT_CLICK = 'Mouse2', MIDDLE_CLICK = 'Mouse1', }; -export enum MouseMapTo { + +export const enum MouseMapTo { OFF = 0, LS = 1, RS = 2, } -export enum WheelCode { +export const enum WheelCode { SCROLL_UP = 'ScrollUp', SCROLL_DOWN = 'ScrollDown', SCROLL_LEFT = 'ScrollLeft', SCROLL_RIGHT = 'ScrollRight', }; -export enum MkbPresetKey { - MOUSE_MAP_TO = 'map_to', - MOUSE_SENSITIVITY_X = 'sensitivity_x', - MOUSE_SENSITIVITY_Y = 'sensitivity_y', +export const enum MkbPresetKey { + MOUSE_MAP_TO = 'mapTo', - MOUSE_DEADZONE_COUNTERWEIGHT = 'deadzone_counterweight', + MOUSE_SENSITIVITY_X = 'sensitivityX', + MOUSE_SENSITIVITY_Y = 'sensitivityY', + + MOUSE_DEADZONE_COUNTERWEIGHT = 'deadzoneCounterweight', +} + + +export type KeyCode = + | 'Backspace' + | 'Tab' + | 'Enter' + | 'ShiftLeft' + | 'ShiftRight' + | 'ControlLeft' + | 'ControlRight' + | 'AltLeft' + | 'AltRight' + | 'Pause' + | 'CapsLock' + | 'Escape' + | 'Space' + | 'PageUp' + | 'PageDown' + | 'End' + | 'Home' + | 'ArrowLeft' + | 'ArrowUp' + | 'ArrowRight' + | 'ArrowDown' + | 'PrintScreen' + | 'Insert' + | 'Delete' + | 'Digit0' + | 'Digit1' + | 'Digit2' + | 'Digit3' + | 'Digit4' + | 'Digit5' + | 'Digit6' + | 'Digit7' + | 'Digit8' + | 'Digit9' + | 'KeyA' + | 'KeyB' + | 'KeyC' + | 'KeyD' + | 'KeyE' + | 'KeyF' + | 'KeyG' + | 'KeyH' + | 'KeyI' + | 'KeyJ' + | 'KeyK' + | 'KeyL' + | 'KeyM' + | 'KeyN' + | 'KeyO' + | 'KeyP' + | 'KeyQ' + | 'KeyR' + | 'KeyS' + | 'KeyT' + | 'KeyU' + | 'KeyV' + | 'KeyW' + | 'KeyX' + | 'KeyY' + | 'KeyZ' + | 'MetaLeft' + | 'MetaRight' + | 'ContextMenu' + | 'F1' + | 'F2' + | 'F3' + | 'F4' + | 'F5' + | 'F6' + | 'F7' + | 'F8' + | 'F9' + | 'F10' + | 'F11' + | 'F12' + | 'NumLock' + | 'ScrollLock' + | 'AudioVolumeMute' + | 'AudioVolumeDown' + | 'AudioVolumeUp' + | 'MediaTrackNext' + | 'MediaTrackPrevious' + | 'MediaStop' + | 'MediaPlayPause' + | 'LaunchMail' + | 'LaunchMediaPlayer' + | 'LaunchApplication1' + | 'LaunchApplication2' + | 'Semicolon' + | 'Equal' + | 'Comma' + | 'Minus' + | 'Period' + | 'Slash' + | 'Backquote' + | 'BracketLeft' + | 'Backslash' + | 'BracketRight' + | 'Quote' + | 'Numpad0' + | 'Numpad1' + | 'Numpad2' + | 'Numpad3' + | 'Numpad4' + | 'Numpad5' + | 'Numpad6' + | 'Numpad7' + | 'Numpad8' + | 'Numpad9' + | 'NumpadMultiply' + | 'NumpadAdd' + | 'NumpadSubtract' + | 'NumpadDecimal' + | 'NumpadDivide'; + +export type KeyCodeExcludeModifiers = Exclude + +export const enum KeyModifier { + CTRL = 1, + ALT = 2, + SHIFT = 4, } diff --git a/src/enums/pref-keys.ts b/src/enums/pref-keys.ts old mode 100644 new mode 100755 index 56ad2c2..61a1a03 --- a/src/enums/pref-keys.ts +++ b/src/enums/pref-keys.ts @@ -1,102 +1,114 @@ -export enum StorageKey { - GLOBAL = 'better_xcloud', +export const enum StorageKey { + GLOBAL = 'BetterXcloud', + + LOCALE = 'BetterXcloud.Locale', + LOCALE_TRANSLATIONS = 'BetterXcloud.Locale.Translations', + PATCHES_CACHE = 'BetterXcloud.Patches.Cache', + PATCHES_SIGNATURE = 'BetterXcloud.Patches.Cache.Signature', + USER_AGENT = 'BetterXcloud.UserAgent', + + GH_PAGES_COMMIT_HASH = 'BetterXcloud.GhPages.CommitHash', + LIST_CUSTOM_TOUCH_LAYOUTS = 'BetterXcloud.GhPages.CustomTouchLayouts', + LIST_FORCE_NATIVE_MKB = 'BetterXcloud.GhPages.ForceNativeMkb', } -export enum PrefKey { - LAST_UPDATE_CHECK = 'version_last_check', - LATEST_VERSION = 'version_latest', - CURRENT_VERSION = 'version_current', +export const enum PrefKey { + VERSION_LAST_CHECK = 'version.lastCheck', + VERSION_LATEST = 'version.latest', + VERSION_CURRENT = 'version.current', - BETTER_XCLOUD_LOCALE = 'bx_locale', + SCRIPT_LOCALE = 'bx.locale', - SERVER_REGION = 'server_region', - SERVER_BYPASS_RESTRICTION = 'server_bypass_restriction', + SERVER_REGION = 'server.region', + SERVER_BYPASS_RESTRICTION = 'server.bypassRestriction', + SERVER_PREFER_IPV6 = 'server.ipv6.prefer', - PREFER_IPV6_SERVER = 'prefer_ipv6_server', - STREAM_TARGET_RESOLUTION = 'stream_target_resolution', - STREAM_PREFERRED_LOCALE = 'stream_preferred_locale', - STREAM_CODEC_PROFILE = 'stream_codec_profile', + STREAM_PREFERRED_LOCALE = 'stream.locale', + STREAM_RESOLUTION = 'stream.video.resolution', + STREAM_CODEC_PROFILE = 'stream.video.codecProfile', + STREAM_MAX_VIDEO_BITRATE = 'stream.video.maxBitrate', + STREAM_COMBINE_SOURCES = 'stream.video.combineAudio', - USER_AGENT_PROFILE = 'user_agent_profile', - STREAM_SIMPLIFY_MENU = 'stream_simplify_menu', + USER_AGENT_PROFILE = 'userAgent.profile', - STREAM_COMBINE_SOURCES = 'stream_combine_sources', + TOUCH_CONTROLLER_MODE = 'touchController.mode', + TOUCH_CONTROLLER_AUTO_OFF = 'touchController.autoOff', + TOUCH_CONTROLLER_DEFAULT_OPACITY = 'touchController.opacity.default', + TOUCH_CONTROLLER_STYLE_STANDARD = 'touchController.style.standard', + TOUCH_CONTROLLER_STYLE_CUSTOM = 'touchController.style.custom', - STREAM_TOUCH_CONTROLLER = 'stream_touch_controller', - STREAM_TOUCH_CONTROLLER_AUTO_OFF = 'stream_touch_controller_auto_off', - STREAM_TOUCH_CONTROLLER_DEFAULT_OPACITY = 'stream_touch_controller_default_opacity', - STREAM_TOUCH_CONTROLLER_STYLE_STANDARD = 'stream_touch_controller_style_standard', - STREAM_TOUCH_CONTROLLER_STYLE_CUSTOM = 'stream_touch_controller_style_custom', + GAME_BAR_POSITION = 'gameBar.position', - STREAM_DISABLE_FEEDBACK_DIALOG = 'stream_disable_feedback_dialog', + LOCAL_CO_OP_ENABLED = 'localCoOp.enabled', - BITRATE_VIDEO_MAX = 'bitrate_video_max', + DEVICE_VIBRATION_MODE = 'deviceVibration.mode', + DEVICE_VIBRATION_INTENSITY = 'deviceVibration.intensity', - GAME_BAR_POSITION = 'game_bar_position', + CONTROLLER_POLLING_RATE = 'controller.pollingRate', - LOCAL_CO_OP_ENABLED = 'local_co_op_enabled', - // LOCAL_CO_OP_SEPARATE_TOUCH_CONTROLLER = 'local_co_op_separate_touch_controller', + NATIVE_MKB_MODE = 'nativeMkb.mode', + FORCE_NATIVE_MKB_GAMES = 'nativeMkb.forcedGames', + NATIVE_MKB_SCROLL_HORIZONTAL_SENSITIVITY = 'nativeMkb.scroll.sensitivityX', + NATIVE_MKB_SCROLL_VERTICAL_SENSITIVITY = 'nativeMkb.scroll.sensitivityY', - CONTROLLER_ENABLE_VIBRATION = 'controller_enable_vibration', - CONTROLLER_DEVICE_VIBRATION = 'controller_device_vibration', - CONTROLLER_VIBRATION_INTENSITY = 'controller_vibration_intensity', - CONTROLLER_SHOW_CONNECTION_STATUS = 'controller_show_connection_status', - CONTROLLER_POLLING_RATE = 'controller_polling_rate', + MKB_ENABLED = 'mkb.enabled', + MKB_HIDE_IDLE_CURSOR = 'mkb.cursor.hideIdle', + MKB_P1_MAPPING_PRESET_ID = 'mkb.p1.preset.mappingId', + MKB_P1_SLOT = 'mkb.p1.slot', + MKB_P2_MAPPING_PRESET_ID = 'mkb.p2.preset.mappingId', + MKB_P2_SLOT = 'mkb.p2.slot', - NATIVE_MKB_ENABLED = 'native_mkb_enabled', - NATIVE_MKB_SCROLL_HORIZONTAL_SENSITIVITY = 'native_mkb_scroll_x_sensitivity', - NATIVE_MKB_SCROLL_VERTICAL_SENSITIVITY = 'native_mkb_scroll_y_sensitivity', + KEYBOARD_SHORTCUTS_IN_GAME_PRESET_ID = 'keyboardShortcuts.preset.inGameId', - MKB_ENABLED = 'mkb_enabled', - MKB_HIDE_IDLE_CURSOR = 'mkb_hide_idle_cursor', - MKB_ABSOLUTE_MOUSE = 'mkb_absolute_mouse', - MKB_DEFAULT_PRESET_ID = 'mkb_default_preset_id', + SCREENSHOT_APPLY_FILTERS = 'screenshot.applyFilters', - SCREENSHOT_APPLY_FILTERS = 'screenshot_apply_filters', + BLOCK_TRACKING = 'block.tracking', + BLOCK_SOCIAL_FEATURES = 'block.social', - BLOCK_TRACKING = 'block_tracking', - BLOCK_SOCIAL_FEATURES = 'block_social_features', - SKIP_SPLASH_VIDEO = 'skip_splash_video', - HIDE_DOTS_ICON = 'hide_dots_icon', - REDUCE_ANIMATIONS = 'reduce_animations', + LOADING_SCREEN_GAME_ART = 'loadingScreen.gameArt.show', + LOADING_SCREEN_SHOW_WAIT_TIME = 'loadingScreen.waitTime.show', + LOADING_SCREEN_ROCKET = 'loadingScreen.rocket', - UI_LOADING_SCREEN_GAME_ART = 'ui_loading_screen_game_art', - UI_LOADING_SCREEN_WAIT_TIME = 'ui_loading_screen_wait_time', - UI_LOADING_SCREEN_ROCKET = 'ui_loading_screen_rocket', + UI_CONTROLLER_FRIENDLY = 'ui.controllerFriendly', + UI_LAYOUT = 'ui.layout', + UI_SCROLLBAR_HIDE = 'ui.hideScrollbar', + UI_HIDE_SECTIONS = 'ui.hideSections', + BYOG_DISABLED = 'feature.byog.disabled', - UI_CONTROLLER_FRIENDLY = 'ui_controller_friendly', - UI_LAYOUT = 'ui_layout', - UI_SCROLLBAR_HIDE = 'ui_scrollbar_hide', - UI_HIDE_SECTIONS = 'ui_hide_sections', + UI_GAME_CARD_SHOW_WAIT_TIME = 'ui.gameCard.waitTime.show', + UI_SIMPLIFY_STREAM_MENU = 'ui.streamMenu.simplify', + UI_DISABLE_FEEDBACK_DIALOG = 'ui.feedbackDialog.disabled', + UI_CONTROLLER_SHOW_STATUS = 'ui.controllerStatus.show', - UI_GAME_CARD_SHOW_WAIT_TIME = 'ui_game_card_show_wait_time', + UI_SKIP_SPLASH_VIDEO = 'ui.splashVideo.skip', + UI_HIDE_SYSTEM_MENU_ICON = 'ui.systemMenu.hideHandle', + UI_REDUCE_ANIMATIONS = 'ui.reduceAnimations', - VIDEO_PLAYER_TYPE = 'video_player_type', - VIDEO_PROCESSING = 'video_processing', - VIDEO_POWER_PREFERENCE = 'video_power_preference', - VIDEO_MAX_FPS = 'video_max_fps', - VIDEO_SHARPNESS = 'video_sharpness', - VIDEO_RATIO = 'video_ratio', - VIDEO_BRIGHTNESS = 'video_brightness', - VIDEO_CONTRAST = 'video_contrast', - VIDEO_SATURATION = 'video_saturation', + VIDEO_PLAYER_TYPE = 'video.player.type', + VIDEO_POWER_PREFERENCE = 'video.player.powerPreference', + VIDEO_PROCESSING = 'video.processing', + VIDEO_SHARPNESS = 'video.processing.sharpness', + VIDEO_MAX_FPS = 'video.maxFps', + VIDEO_RATIO = 'video.ratio', + VIDEO_BRIGHTNESS = 'video.brightness', + VIDEO_CONTRAST = 'video.contrast', + VIDEO_SATURATION = 'video.saturation', - AUDIO_MIC_ON_PLAYING = 'audio_mic_on_playing', - AUDIO_ENABLE_VOLUME_CONTROL = 'audio_enable_volume_control', - AUDIO_VOLUME = 'audio_volume', + AUDIO_MIC_ON_PLAYING = 'audio.mic.onPlaying', + AUDIO_VOLUME_CONTROL_ENABLED = 'audio.volume.booster.enabled', + AUDIO_VOLUME = 'audio.volume', - STATS_ITEMS = 'stats_items', - STATS_SHOW_WHEN_PLAYING = 'stats_show_when_playing', - STATS_QUICK_GLANCE = 'stats_quick_glance', - STATS_POSITION = 'stats_position', - STATS_TEXT_SIZE = 'stats_text_size', - STATS_TRANSPARENT = 'stats_transparent', - STATS_OPACITY = 'stats_opacity', - STATS_CONDITIONAL_FORMATTING = 'stats_conditional_formatting', + STATS_ITEMS = 'stats.items', + STATS_SHOW_WHEN_PLAYING = 'stats.showWhenPlaying', + STATS_QUICK_GLANCE_ENABLED = 'stats.quickGlance.enabled', + STATS_POSITION = 'stats.position', + STATS_TEXT_SIZE = 'stats.textSize', + STATS_TRANSPARENT = 'stats.transparent', + STATS_OPACITY = 'stats.opacity', + STATS_CONDITIONAL_FORMATTING = 'stats.colors', - REMOTE_PLAY_ENABLED = 'xhome_enabled', - REMOTE_PLAY_RESOLUTION = 'xhome_resolution', + REMOTE_PLAY_ENABLED = 'xhome.enabled', + REMOTE_PLAY_STREAM_RESOLUTION = 'xhome.video.resolution', - GAME_FORTNITE_FORCE_CONSOLE = 'game_fortnite_force_console', - GAME_MSFS2020_FORCE_NATIVE_MKB = 'game_msfs2020_force_native_mkb', + GAME_FORTNITE_FORCE_CONSOLE = 'game.fortnite.forceConsole', } diff --git a/src/enums/pref-values.ts b/src/enums/pref-values.ts new file mode 100755 index 0000000..d52f5ac --- /dev/null +++ b/src/enums/pref-values.ts @@ -0,0 +1,104 @@ +export const enum UiSection { + ALL_GAMES = 'all-games', + FRIENDS = 'friends', + MOST_POPULAR = 'most-popular', + NATIVE_MKB = 'native-mkb', + NEWS = 'news', + TOUCH = 'touch', + BOYG = 'byog', +} + +export const enum GameBarPosition { + BOTTOM_LEFT = 'bottom-left', + BOTTOM_RIGHT = 'bottom-right', + OFF = 'off', +}; + +export const enum UiLayout { + TV = 'tv', + NORMAL = 'normal', + DEFAULT = 'default', +} + +export const enum LoadingScreenRocket { + SHOW = 'show', + HIDE = 'hide', + HIDE_QUEUE = 'hide-queue', +} + +export const enum StreamResolution { + DIM_720P = '720p', + DIM_1080P = '1080p', + DIM_1080P_HQ = '1080p-hq', + AUTO = 'auto', +} + +export const enum CodecProfile { + DEFAULT = 'default', + LOW = 'low', + NORMAL = 'normal', + HIGH = 'high', +}; + +export const enum TouchControllerMode { + DEFAULT = 'default', + ALL = 'all', + OFF = 'off', +} + +export const enum TouchControllerStyleStandard { + DEFAULT = 'default', + WHITE = 'white', + MUTED = 'muted', +} + +export const enum TouchControllerStyleCustom { + DEFAULT = 'default', + MUTED = 'muted', +} + +export const enum DeviceVibrationMode { + ON = 'on', + AUTO = 'auto', + OFF = 'off', +} + +export const enum NativeMkbMode { + DEFAULT = 'default', + ON = 'on', + OFF = 'off', +} + +export const enum StreamStat { + PING = 'ping', + JITTER = 'jit', + FPS = 'fps', + BITRATE = 'btr', + DECODE_TIME = 'dt', + PACKETS_LOST = 'pl', + FRAMES_LOST = 'fl', + DOWNLOAD = 'dl', + UPLOAD = 'ul', + PLAYTIME = 'play', + BATTERY = 'batt', + CLOCK = 'time', +}; + +export const enum VideoRatio { + '16:9' = '16:9', + '18:9' = '18:9', + '21:9' = '21:9', + '16:10' = '16:10', + '4:3' = '4:3', + FILL = 'fill', +} + +export const enum StreamPlayerType { + VIDEO = 'default', + WEBGL2 = 'webgl2', +} + +export const enum StreamVideoProcessing { + USM = 'usm', + CAS = 'cas', +} diff --git a/src/enums/prompt-font.ts b/src/enums/prompt-font.ts old mode 100644 new mode 100755 diff --git a/src/enums/shortcut-actions.ts b/src/enums/shortcut-actions.ts new file mode 100755 index 0000000..fde7f99 --- /dev/null +++ b/src/enums/shortcut-actions.ts @@ -0,0 +1,25 @@ +export const enum ShortcutAction { + BETTER_XCLOUD_SETTINGS_SHOW = 'bx.settings.show', + + STREAM_VIDEO_TOGGLE = 'stream.video.toggle', + STREAM_SCREENSHOT_CAPTURE = 'stream.screenshot.capture', + + STREAM_MENU_SHOW = 'stream.menu.show', + STREAM_STATS_TOGGLE = 'stream.stats.toggle', + STREAM_SOUND_TOGGLE = 'stream.sound.toggle', + STREAM_MICROPHONE_TOGGLE = 'stream.microphone.toggle', + + STREAM_VOLUME_INC = 'stream.volume.inc', + STREAM_VOLUME_DEC = 'stream.volume.dec', + + DEVICE_SOUND_TOGGLE = 'device.sound.toggle', + DEVICE_VOLUME_INC = 'device.volume.inc', + DEVICE_VOLUME_DEC = 'device.volume.dec', + + DEVICE_BRIGHTNESS_INC = 'device.brightness.inc', + DEVICE_BRIGHTNESS_DEC = 'device.brightness.dec', + + MKB_TOGGLE = 'mkb.toggle', + + TRUE_ACHIEVEMENTS_OPEN = 'ta.open', +}; diff --git a/src/enums/stream-player.ts b/src/enums/stream-player.ts deleted file mode 100644 index 4379e45..0000000 --- a/src/enums/stream-player.ts +++ /dev/null @@ -1,9 +0,0 @@ -export enum StreamPlayerType { - VIDEO = 'default', - WEBGL2 = 'webgl2', -} - -export enum StreamVideoProcessing { - USM = 'usm', - CAS = 'cas', -} diff --git a/src/enums/ui-sections.ts b/src/enums/ui-sections.ts deleted file mode 100644 index 1b6f706..0000000 --- a/src/enums/ui-sections.ts +++ /dev/null @@ -1,8 +0,0 @@ -export enum UiSection { - ALL_GAMES = 'all-games', - FRIENDS = 'friends', - MOST_POPULAR = 'most-popular', - NATIVE_MKB = 'native-mkb', - NEWS = 'news', - TOUCH = 'touch', -} diff --git a/src/enums/user-agent.ts b/src/enums/user-agent.ts old mode 100644 new mode 100755 diff --git a/src/index.ts b/src/index.ts old mode 100644 new mode 100755 index a957246..6592d76 --- a/src/index.ts +++ b/src/index.ts @@ -7,7 +7,7 @@ import { BxExposed } from "@utils/bx-exposed"; import { t } from "@utils/translation"; import { interceptHttpRequests } from "@utils/network"; import { CE } from "@utils/html"; -import { showGamepadToast, updatePollingRate } from "@utils/gamepad"; +import { showGamepadToast } from "@utils/gamepad"; import { EmulatedMkbHandler } from "@modules/mkb/mkb-handler"; import { StreamBadges } from "@modules/stream/stream-badges"; import { StreamStats } from "@modules/stream/stream-stats"; @@ -19,7 +19,6 @@ import { checkForUpdate, disablePwa, productTitleToSlug } from "@utils/utils"; import { Patcher } from "@modules/patcher"; import { RemotePlayManager } from "@/modules/remote-play-manager"; import { onHistoryChanged, patchHistoryMethod } from "@utils/history"; -import { VibrationManager } from "@modules/vibration-manager"; import { disableAdobeAudienceManager, patchAudioContext, patchCanvasContext, patchMeControl, patchPointerLockApi, patchRtcCodecs, patchRtcPeerConnection, patchVideoApi } from "@utils/monkey-patches"; import { AppInterface, STATES } from "@utils/global"; import { BxLogger } from "@utils/bx-logger"; @@ -28,19 +27,23 @@ import { ScreenshotManager } from "./utils/screenshot-manager"; import { NativeMkbHandler } from "./modules/mkb/native-mkb-handler"; import { GuideMenu } from "./modules/ui/guide-menu"; import { updateVideoPlayer } from "./modules/stream/stream-settings-utils"; -import { UiSection } from "./enums/ui-sections"; +import { NativeMkbMode, TouchControllerMode, UiSection } from "./enums/pref-values"; import { HeaderSection } from "./modules/ui/header"; import { GameTile } from "./modules/ui/game-tile"; import { ProductDetailsPage } from "./modules/ui/product-details"; import { NavigationDialogManager } from "./modules/ui/dialog/navigation-dialog"; import { PrefKey } from "./enums/pref-keys"; -import { getPref, StreamTouchController } from "./utils/settings-storages/global-settings-storage"; -import { SettingsNavigationDialog } from "./modules/ui/dialog/settings-dialog"; +import { getPref } from "./utils/settings-storages/global-settings-storage"; +import { SettingsDialog } from "./modules/ui/dialog/settings-dialog"; import { StreamUiHandler } from "./modules/stream/stream-ui"; import { UserAgent } from "./utils/user-agent"; import { XboxApi } from "./utils/xbox-api"; import { StreamStatsCollector } from "./utils/stream-stats-collector"; import { RootDialogObserver } from "./utils/root-dialog-observer"; +import { StreamSettings } from "./utils/stream-settings"; +import { KeyboardShortcutHandler } from "./modules/mkb/keyboard-shortcut-handler"; +import { GhPagesUtils } from "./utils/gh-pages"; +import { DeviceVibrationManager } from "./modules/device-vibration-manager"; // Handle login page if (window.location.pathname.includes('/auth/msa')) { @@ -160,14 +163,14 @@ document.addEventListener('readystatechange', e => { if (STATES.isSignedIn) { // Preload Remote Play - getPref(PrefKey.REMOTE_PLAY_ENABLED) && RemotePlayManager.getInstance().initialize(); + RemotePlayManager.getInstance()?.initialize(); } else { // Show Settings button in the header when not signed in window.setTimeout(HeaderSection.watchHeader, 2000); } // Hide "Play with Friends" skeleton section - if (getPref(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.FRIENDS)) { + if (getPref(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.FRIENDS)) { const $parent = document.querySelector('div[class*=PlayWithFriendsSkeleton]')?.closest('div[class*=HomePage-module]'); $parent && ($parent.style.display = 'none'); } @@ -194,7 +197,7 @@ window.addEventListener(BxEvent.XCLOUD_SERVERS_UNAVAILABLE, e => { // Open Settings dialog on Unsupported page const $unsupportedPage = document.querySelector('div[class^=UnsupportedMarketPage-module__container]'); if ($unsupportedPage) { - SettingsNavigationDialog.getInstance().show(); + SettingsDialog.getInstance().show(); } }, {once: true}); @@ -213,35 +216,49 @@ window.addEventListener(BxEvent.STREAM_LOADING, e => { }); // Setup loading screen -getPref(PrefKey.UI_LOADING_SCREEN_GAME_ART) && window.addEventListener(BxEvent.TITLE_INFO_READY, LoadingScreen.setup); +getPref(PrefKey.LOADING_SCREEN_GAME_ART) && window.addEventListener(BxEvent.TITLE_INFO_READY, LoadingScreen.setup); window.addEventListener(BxEvent.STREAM_STARTING, e => { // Hide loading screen LoadingScreen.hide(); - // Start hiding cursor - if (!getPref(PrefKey.MKB_ENABLED) && getPref(PrefKey.MKB_HIDE_IDLE_CURSOR)) { - MouseCursorHider.start(); - MouseCursorHider.hide(); + if (isFullVersion()) { + // Start hiding cursor + const cursorHider = MouseCursorHider.getInstance(); + if (cursorHider) { + cursorHider.start(); + cursorHider.hide(); + } } }); window.addEventListener(BxEvent.STREAM_PLAYING, e => { + window.BX_STREAM_SETTINGS = StreamSettings.settings; + StreamSettings.refreshAllSettings(); + STATES.isPlaying = true; StreamUiHandler.observe(); - if (isFullVersion() && getPref(PrefKey.GAME_BAR_POSITION) !== 'off') { - const gameBar = GameBar.getInstance(); - gameBar.reset(); - gameBar.enable(); - gameBar.showBar(); - } - if (isFullVersion()) { + const gameBar = GameBar.getInstance(); + if (gameBar) { + gameBar.reset(); + gameBar.enable(); + gameBar.showBar(); + } + + // Setup Keyboard shortcuts + KeyboardShortcutHandler.getInstance().start(); + + // Setup screenshot const $video = (e as any).$video as HTMLVideoElement; ScreenshotManager.getInstance().updateCanvasSize($video.videoWidth, $video.videoHeight); + + // Setup local co-op + getPref(PrefKey.LOCAL_CO_OP_ENABLED) && BxExposed.toggleLocalCoOp(getPref(PrefKey.LOCAL_CO_OP_ENABLED)); } + updateVideoPlayer(); }); @@ -294,9 +311,13 @@ function unload() { } if (isFullVersion()) { + KeyboardShortcutHandler.getInstance().stop(); + // Stop MKB listeners - EmulatedMkbHandler.getInstance().destroy(); - NativeMkbHandler.getInstance().destroy(); + EmulatedMkbHandler.getInstance()?.destroy(); + NativeMkbHandler.getInstance()?.destroy(); + + DeviceVibrationManager.getInstance()?.reset(); } // Destroy StreamPlayer @@ -312,9 +333,10 @@ function unload() { StreamBadges.getInstance().destroy(); if (isFullVersion()) { - MouseCursorHider.stop(); + MouseCursorHider.getInstance()?.stop(); TouchController.reset(); - (getPref(PrefKey.GAME_BAR_POSITION) !== 'off') && GameBar.getInstance().disable(); + + GameBar.getInstance()?.disable(); } } @@ -329,10 +351,15 @@ isFullVersion() && window.addEventListener(BxEvent.CAPTURE_SCREENSHOT, e => { function main() { - if (getPref(PrefKey.GAME_MSFS2020_FORCE_NATIVE_MKB)) { - BX_FLAGS.ForceNativeMkbTitles.push('9PMQDM08SNK9'); + GhPagesUtils.fetchLatestCommit(); + + if (getPref(PrefKey.NATIVE_MKB_MODE) === NativeMkbMode.ON) { + const customList = getPref(PrefKey.FORCE_NATIVE_MKB_GAMES); + BX_FLAGS.ForceNativeMkbTitles.push(...customList); } + StreamSettings.setup(); + // Monkey patches patchRtcPeerConnection(); patchRtcCodecs(); @@ -341,7 +368,7 @@ function main() { patchCanvasContext(); isFullVersion() && AppInterface && patchPointerLockApi(); - getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && patchAudioContext(); + getPref(PrefKey.AUDIO_VOLUME_CONTROL_ENABLED) && patchAudioContext(); if (getPref(PrefKey.BLOCK_TRACKING)) { patchMeControl(); @@ -359,10 +386,9 @@ function main() { StreamStats.setupEvents(); if (isFullVersion()) { - updatePollingRate(); STATES.userAgent.capabilities.touch && TouchController.updateCustomList(); - VibrationManager.initialSetup(); + DeviceVibrationManager.getInstance(); // Check for Update BX_FLAGS.CheckForUpdate && checkForUpdate(); @@ -375,7 +401,7 @@ function main() { RemotePlayManager.detect(); } - if (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === StreamTouchController.ALL) { + if (getPref(PrefKey.TOUCH_CONTROLLER_MODE) === TouchControllerMode.ALL) { TouchController.setup(); } @@ -392,7 +418,7 @@ function main() { } // Show a toast when connecting/disconecting controller - if (getPref(PrefKey.CONTROLLER_SHOW_CONNECTION_STATUS)) { + if (getPref(PrefKey.UI_CONTROLLER_SHOW_STATUS)) { window.addEventListener('gamepadconnected', e => showGamepadToast(e.gamepad)); window.addEventListener('gamepaddisconnected', e => showGamepadToast(e.gamepad)); } diff --git a/src/macros/build.ts b/src/macros/build.ts old mode 100644 new mode 100755 diff --git a/src/modules/controller-shortcut.ts b/src/modules/controller-shortcut.ts old mode 100644 new mode 100755 index 533a2c2..4fd854f --- a/src/modules/controller-shortcut.ts +++ b/src/modules/controller-shortcut.ts @@ -1,70 +1,29 @@ -import { ScreenshotManager } from "@/utils/screenshot-manager"; -import { GamepadKey } from "@enums/mkb"; -import { PrompFont } from "@enums/prompt-font"; -import { CE, removeChildElements } from "@utils/html"; -import { t } from "@utils/translation"; -import { StreamStats } from "./stream/stream-stats"; -import { MicrophoneShortcut } from "./shortcuts/shortcut-microphone"; -import { StreamUiShortcut } from "./shortcuts/shortcut-stream-ui"; -import { SoundShortcut } from "./shortcuts/shortcut-sound"; -import { BxEvent } from "@/utils/bx-event"; -import { AppInterface } from "@/utils/global"; -import { BxSelectElement } from "@/web-components/bx-select"; -import { setNearby } from "@/utils/navigation-utils"; -import { PrefKey } from "@/enums/pref-keys"; -import { getPref } from "@/utils/settings-storages/global-settings-storage"; -import { SettingsNavigationDialog } from "./ui/dialog/settings-dialog"; -import { VIRTUAL_GAMEPAD_ID } from "./mkb/mkb-handler"; +import { GamepadKey } from "@enums/gamepad"; +import { ShortcutHandler } from "@/utils/shortcut-handler"; -const enum ShortcutAction { - BETTER_XCLOUD_SETTINGS_SHOW = 'bx-settings-show', - - STREAM_SCREENSHOT_CAPTURE = 'stream-screenshot-capture', - - STREAM_MENU_SHOW = 'stream-menu-show', - STREAM_STATS_TOGGLE = 'stream-stats-toggle', - STREAM_SOUND_TOGGLE = 'stream-sound-toggle', - STREAM_MICROPHONE_TOGGLE = 'stream-microphone-toggle', - - STREAM_VOLUME_INC = 'stream-volume-inc', - STREAM_VOLUME_DEC = 'stream-volume-dec', - - DEVICE_SOUND_TOGGLE = 'device-sound-toggle', - DEVICE_VOLUME_INC = 'device-volume-inc', - DEVICE_VOLUME_DEC = 'device-volume-dec', - - DEVICE_BRIGHTNESS_INC = 'device-brightness-inc', - DEVICE_BRIGHTNESS_DEC = 'device-brightness-dec', -} export class ControllerShortcut { - private static readonly STORAGE_KEY = 'better_xcloud_controller_shortcuts'; - private static buttonsCache: {[key: string]: boolean[]} = {}; private static buttonsStatus: {[key: string]: boolean[]} = {}; - private static $selectProfile: HTMLSelectElement; - private static $selectActions: Partial<{[key in GamepadKey]: HTMLSelectElement}> = {}; - private static $container: HTMLElement; - - private static ACTIONS: {[key: string]: (ShortcutAction | null)[]} | null = null; - static reset(index: number) { ControllerShortcut.buttonsCache[index] = []; ControllerShortcut.buttonsStatus[index] = []; } static handle(gamepad: Gamepad): boolean { - if (!ControllerShortcut.ACTIONS) { - ControllerShortcut.ACTIONS = ControllerShortcut.getActionsFromStorage(); + const controllerSettings = window.BX_STREAM_SETTINGS.controllers[gamepad.id]; + if (!controllerSettings) { + return false; } - const gamepadIndex = gamepad.index; - const actions = ControllerShortcut.ACTIONS![gamepad.id]; + const actions = controllerSettings.shortcuts; if (!actions) { return false; } + const gamepadIndex = gamepad.index; + // Move the buttons status from the previous frame to the cache ControllerShortcut.buttonsCache[gamepadIndex] = ControllerShortcut.buttonsStatus[gamepadIndex].slice(0); // Clear the buttons status @@ -74,7 +33,9 @@ export class ControllerShortcut { let otherButtonPressed = false; const entries = gamepad.buttons.entries(); - for (const [index, button] of entries) { + let index: GamepadKey; + let button: GamepadButton; + for ([index, button] of entries) { // Only add the newly pressed button to the array (holding doesn't count) if (button.pressed && index !== GamepadKey.HOME) { otherButtonPressed = true; @@ -82,7 +43,8 @@ export class ControllerShortcut { // If this is newly pressed button -> run action if (actions[index] && !ControllerShortcut.buttonsCache[gamepadIndex][index]) { - setTimeout(() => ControllerShortcut.runAction(actions[index]!), 0); + const idx = index; + setTimeout(() => ShortcutHandler.runAction(actions[idx]!), 0); } } }; @@ -90,336 +52,4 @@ export class ControllerShortcut { ControllerShortcut.buttonsStatus[gamepadIndex] = pressed; return otherButtonPressed; } - - private static runAction(action: ShortcutAction) { - switch (action) { - case ShortcutAction.BETTER_XCLOUD_SETTINGS_SHOW: - SettingsNavigationDialog.getInstance().show(); - break; - - case ShortcutAction.STREAM_SCREENSHOT_CAPTURE: - ScreenshotManager.getInstance().takeScreenshot(); - break; - - case ShortcutAction.STREAM_STATS_TOGGLE: - StreamStats.getInstance().toggle(); - break; - - case ShortcutAction.STREAM_MICROPHONE_TOGGLE: - MicrophoneShortcut.toggle(); - break; - - case ShortcutAction.STREAM_MENU_SHOW: - StreamUiShortcut.showHideStreamMenu(); - break; - - case ShortcutAction.STREAM_SOUND_TOGGLE: - SoundShortcut.muteUnmute(); - break; - - case ShortcutAction.STREAM_VOLUME_INC: - SoundShortcut.adjustGainNodeVolume(10); - break; - - case ShortcutAction.STREAM_VOLUME_DEC: - SoundShortcut.adjustGainNodeVolume(-10); - break; - - case ShortcutAction.DEVICE_BRIGHTNESS_INC: - case ShortcutAction.DEVICE_BRIGHTNESS_DEC: - case ShortcutAction.DEVICE_SOUND_TOGGLE: - case ShortcutAction.DEVICE_VOLUME_INC: - case ShortcutAction.DEVICE_VOLUME_DEC: - AppInterface && AppInterface.runShortcut && AppInterface.runShortcut(action); - break; - } - } - - private static updateAction(profile: string, button: GamepadKey, action: ShortcutAction | null) { - const actions = ControllerShortcut.ACTIONS!; - if (!(profile in actions)) { - actions[profile] = []; - } - - if (!action) { - action = null; - } - - actions[profile][button] = action; - - // Remove empty profiles - for (const key in ControllerShortcut.ACTIONS) { - let empty = true; - for (const value of ControllerShortcut.ACTIONS[key]) { - if (!!value) { - empty = false; - break; - } - } - - if (empty) { - delete ControllerShortcut.ACTIONS[key]; - } - } - - // Save to storage - window.localStorage.setItem(ControllerShortcut.STORAGE_KEY, JSON.stringify(ControllerShortcut.ACTIONS)); - } - - private static updateProfileList(e?: GamepadEvent) { - const $select = ControllerShortcut.$selectProfile; - const $container = ControllerShortcut.$container; - - const $fragment = document.createDocumentFragment(); - - // Remove old profiles - removeChildElements($select); - - const gamepads = navigator.getGamepads(); - let hasGamepad = false; - - for (const gamepad of gamepads) { - if (!gamepad || !gamepad.connected) { - continue; - } - - // Ignore emulated gamepad - if (gamepad.id === VIRTUAL_GAMEPAD_ID) { - continue; - } - - hasGamepad = true; - - const $option = CE('option', {value: gamepad.id}, gamepad.id); - $fragment.appendChild($option); - } - - $container.dataset.hasGamepad = hasGamepad.toString(); - if (hasGamepad) { - $select.appendChild($fragment); - - $select.selectedIndex = 0; - $select.dispatchEvent(new Event('input')); - } - - } - - private static switchProfile(profile: string) { - let actions = ControllerShortcut.ACTIONS![profile]; - if (!actions) { - actions = []; - } - - // Reset selects' values - let button: any; - for (button in ControllerShortcut.$selectActions) { - const $select = ControllerShortcut.$selectActions[button as GamepadKey]!; - $select.value = actions[button] || ''; - - BxEvent.dispatch($select, 'input', { - ignoreOnChange: true, - manualTrigger: true, - }); - } - } - - private static getActionsFromStorage() { - return JSON.parse(window.localStorage.getItem(ControllerShortcut.STORAGE_KEY) || '{}'); - } - - static renderSettings() { - const PREF_CONTROLLER_FRIENDLY_UI = getPref(PrefKey.UI_CONTROLLER_FRIENDLY); - - // Read actions from localStorage - ControllerShortcut.ACTIONS = ControllerShortcut.getActionsFromStorage(); - - const buttons: Map = new Map(); - buttons.set(GamepadKey.Y, PrompFont.Y); - buttons.set(GamepadKey.A, PrompFont.A); - buttons.set(GamepadKey.B, PrompFont.B); - buttons.set(GamepadKey.X, PrompFont.X); - - buttons.set(GamepadKey.UP, PrompFont.UP); - buttons.set(GamepadKey.DOWN, PrompFont.DOWN); - buttons.set(GamepadKey.LEFT, PrompFont.LEFT); - buttons.set(GamepadKey.RIGHT, PrompFont.RIGHT); - - buttons.set(GamepadKey.SELECT, PrompFont.SELECT); - buttons.set(GamepadKey.START, PrompFont.START); - - buttons.set(GamepadKey.LB, PrompFont.LB); - buttons.set(GamepadKey.RB, PrompFont.RB); - - buttons.set(GamepadKey.LT, PrompFont.LT); - buttons.set(GamepadKey.RT, PrompFont.RT); - - buttons.set(GamepadKey.L3, PrompFont.L3); - buttons.set(GamepadKey.R3, PrompFont.R3); - - const actions: {[key: string]: Partial<{[key in ShortcutAction]: string | string[]}>} = { - [t('better-xcloud')]: { - [ShortcutAction.BETTER_XCLOUD_SETTINGS_SHOW]: [t('settings'), t('show')], - }, - - [t('device')]: AppInterface && { - [ShortcutAction.DEVICE_SOUND_TOGGLE]: [t('sound'), t('toggle')], - [ShortcutAction.DEVICE_VOLUME_INC]: [t('volume'), t('increase')], - [ShortcutAction.DEVICE_VOLUME_DEC]: [t('volume'), t('decrease')], - - [ShortcutAction.DEVICE_BRIGHTNESS_INC]: [t('brightness'), t('increase')], - [ShortcutAction.DEVICE_BRIGHTNESS_DEC]: [t('brightness'), t('decrease')], - }, - - [t('stream')]: { - [ShortcutAction.STREAM_SCREENSHOT_CAPTURE]: t('take-screenshot'), - - [ShortcutAction.STREAM_SOUND_TOGGLE]: [t('sound'), t('toggle')], - [ShortcutAction.STREAM_VOLUME_INC]: getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && [t('volume'), t('increase')], - [ShortcutAction.STREAM_VOLUME_DEC]: getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && [t('volume'), t('decrease')], - - [ShortcutAction.STREAM_MENU_SHOW]: [t('menu'), t('show')], - [ShortcutAction.STREAM_STATS_TOGGLE]: [t('stats'), t('show-hide')], - [ShortcutAction.STREAM_MICROPHONE_TOGGLE]: [t('microphone'), t('toggle')], - }, - }; - - const $baseSelect = CE('select', {autocomplete: 'off'}, CE('option', {value: ''}, '---')); - for (const groupLabel in actions) { - const items = actions[groupLabel]; - if (!items) { - continue; - } - - const $optGroup = CE('optgroup', {'label': groupLabel}); - - for (const action in items) { - let label = items[action as keyof typeof items]; - if (!label) { - continue; - } - - if (Array.isArray(label)) { - label = label.join(' ❯ '); - } - - const $option = CE('option', {value: action}, label); - $optGroup.appendChild($option); - } - - $baseSelect.appendChild($optGroup); - } - - let $remap: HTMLElement; - const $selectProfile = CE('select', {class: 'bx-shortcut-profile', autocomplete: 'off'}); - - const $profile = PREF_CONTROLLER_FRIENDLY_UI ? BxSelectElement.wrap($selectProfile) : $selectProfile; - $profile.classList.add('bx-full-width'); - - const $container = CE('div', { - 'data-has-gamepad': 'false', - _nearby: { - focus: $profile, - }, - }, - CE('div', {}, - CE('p', {class: 'bx-shortcut-note'}, t('controller-shortcuts-connect-note')), - ), - - $remap = CE('div', {}, - CE('div', { - _nearby: { - focus: $profile, - }, - }, $profile), - CE('p', {class: 'bx-shortcut-note'}, - CE('span', {class: 'bx-prompt'}, PrompFont.HOME), - ': ' + t('controller-shortcuts-xbox-note'), - ), - ), - ); - - $selectProfile.addEventListener('input', e => { - ControllerShortcut.switchProfile($selectProfile.value); - }); - - const onActionChanged = (e: Event) => { - const $target = e.target as HTMLSelectElement; - - const profile = $selectProfile.value; - const button: unknown = $target.dataset.button; - const action = $target.value as ShortcutAction; - - if (!PREF_CONTROLLER_FRIENDLY_UI) { - const $fakeSelect = $target.previousElementSibling! as HTMLSelectElement; - let fakeText = '---'; - if (action) { - const $selectedOption = $target.options[$target.selectedIndex]; - const $optGroup = $selectedOption.parentElement as HTMLOptGroupElement; - fakeText = $optGroup.label + ' ❯ ' + $selectedOption.text; - } - ($fakeSelect.firstElementChild as HTMLOptionElement).text = fakeText; - } - - !(e as any).ignoreOnChange && ControllerShortcut.updateAction(profile, button as GamepadKey, action); - }; - - - // @ts-ignore - for (const [button, prompt] of buttons) { - const $row = CE('div', { - class: 'bx-shortcut-row', - }); - - const $label = CE('label', {class: 'bx-prompt'}, `${PrompFont.HOME} + ${prompt}`); - - const $div = CE('div', {class: 'bx-shortcut-actions'}); - - if (!PREF_CONTROLLER_FRIENDLY_UI) { - const $fakeSelect = CE('select', {autocomplete: 'off'}, - CE('option', {}, '---'), - ); - - $div.appendChild($fakeSelect); - } - - const $select = $baseSelect.cloneNode(true) as HTMLSelectElement; - $select.dataset.button = button.toString(); - $select.addEventListener('input', onActionChanged); - - ControllerShortcut.$selectActions[button] = $select; - - if (PREF_CONTROLLER_FRIENDLY_UI) { - const $bxSelect = BxSelectElement.wrap($select); - $bxSelect.classList.add('bx-full-width'); - - $div.appendChild($bxSelect); - setNearby($row, { - focus: $bxSelect, - }); - } else { - $div.appendChild($select); - setNearby($row, { - focus: $select, - }); - } - - $row.appendChild($label); - $row.appendChild($div); - - $remap.appendChild($row); - } - - $container.appendChild($remap); - - ControllerShortcut.$selectProfile = $selectProfile; - ControllerShortcut.$container = $container; - - // Detect when gamepad connected/disconnect - window.addEventListener('gamepadconnected', ControllerShortcut.updateProfileList); - window.addEventListener('gamepaddisconnected', ControllerShortcut.updateProfileList); - - ControllerShortcut.updateProfileList(); - - return $container; - } } diff --git a/src/modules/device-vibration-manager.ts b/src/modules/device-vibration-manager.ts new file mode 100755 index 0000000..fd33984 --- /dev/null +++ b/src/modules/device-vibration-manager.ts @@ -0,0 +1,145 @@ +import { AppInterface, STATES } from "@utils/global"; +import { BxEvent } from "@utils/bx-event"; +import { StreamSettings } from "@/utils/stream-settings"; + +const VIBRATION_DATA_MAP = { + gamepadIndex: 8, + leftMotorPercent: 8, + rightMotorPercent: 8, + leftTriggerMotorPercent: 8, + rightTriggerMotorPercent: 8, + durationMs: 16, + // delayMs: 16, + // repeat: 8, +}; + +type VibrationData = { + [key in keyof typeof VIBRATION_DATA_MAP]?: number; +} + +export class DeviceVibrationManager { + private static instance: DeviceVibrationManager | null | undefined; + public static getInstance(): typeof DeviceVibrationManager['instance'] { + if (typeof DeviceVibrationManager.instance === 'undefined') { + if (STATES.browser.capabilities.deviceVibration) { + DeviceVibrationManager.instance = new DeviceVibrationManager(); + } else { + DeviceVibrationManager.instance = null; + } + } + + return DeviceVibrationManager.instance; + } + + private dataChannel: RTCDataChannel | null = null; + private boundOnMessage: (e: MessageEvent) => void; + + constructor() { + this.boundOnMessage = this.onMessage.bind(this); + + window.addEventListener(BxEvent.DATA_CHANNEL_CREATED, e => { + const dataChannel = (e as any).dataChannel as RTCDataChannel; + if (dataChannel?.label === 'input') { + this.reset(); + + this.dataChannel = dataChannel; + this.setupDataChannel(); + } + }); + + window.addEventListener(BxEvent.DEVICE_VIBRATION_CHANGED, e => { + this.setupDataChannel(); + }); + } + + private setupDataChannel() { + if (!this.dataChannel) { + return; + } + + this.removeEventListeners(); + + if (window.BX_STREAM_SETTINGS.deviceVibrationIntensity > 0) { + this.dataChannel.addEventListener('message', this.boundOnMessage); + } + } + + private playVibration(data: Required) { + const vibrationIntensity = StreamSettings.settings.deviceVibrationIntensity; + if (AppInterface) { + AppInterface.vibrate(JSON.stringify(data), vibrationIntensity); + return; + } + + const realIntensity = Math.min(100, data.leftMotorPercent + data.rightMotorPercent / 2) * vibrationIntensity; + if (realIntensity === 0 || realIntensity === 100) { + // Stop vibration + window.navigator.vibrate(realIntensity ? data.durationMs : 0); + return; + } + + const pulseDuration = 200; + const onDuration = Math.floor(pulseDuration * realIntensity / 100); + const offDuration = pulseDuration - onDuration; + + const repeats = Math.ceil(data.durationMs / pulseDuration); + const pulses = Array(repeats).fill([onDuration, offDuration]).flat(); + + window.navigator.vibrate(pulses); + } + + onMessage(e: MessageEvent) { + if (typeof e !== 'object' || !(e.data instanceof ArrayBuffer)) { + return; + } + + const dataView = new DataView(e.data); + let offset = 0; + + let messageType; + if (dataView.byteLength === 13) { // version >= 8 + messageType = dataView.getUint16(offset, true); + offset += Uint16Array.BYTES_PER_ELEMENT; + } else { + messageType = dataView.getUint8(offset); + offset += Uint8Array.BYTES_PER_ELEMENT; + } + + if (!(messageType & 128)) { // Vibration + return; + } + + const vibrationType = dataView.getUint8(offset); + offset += Uint8Array.BYTES_PER_ELEMENT; + + if (vibrationType !== 0) { // FourMotorRumble + return; + } + + const data: VibrationData = {}; + let key: keyof typeof VIBRATION_DATA_MAP; + for (key in VIBRATION_DATA_MAP) { + if (VIBRATION_DATA_MAP[key] === 16) { + data[key] = dataView.getUint16(offset, true); + offset += Uint16Array.BYTES_PER_ELEMENT; + } else { + data[key] = dataView.getUint8(offset); + offset += Uint8Array.BYTES_PER_ELEMENT; + } + } + + this.playVibration(data as Required); + } + + private removeEventListeners() { + // Clear event listeners in previous DataChannel + try { + this.dataChannel?.removeEventListener('message', this.boundOnMessage); + } catch (e) {} + } + + reset() { + this.removeEventListeners(); + this.dataChannel = null; + } +} diff --git a/src/modules/dialog.ts b/src/modules/dialog.ts deleted file mode 100644 index 96b8d14..0000000 --- a/src/modules/dialog.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { t } from "@utils/translation"; -import { CE, createButton, ButtonStyle } from "@utils/html"; -import { BxIcon } from "@utils/bx-icon"; - -type DialogOptions = Partial<{ - title: string; - className: string; - content: string | HTMLElement; - hideCloseButton: boolean; - onClose: string; - helpUrl: string; -}>; - -export class Dialog { - $dialog: HTMLElement; - $title: HTMLElement; - $content: HTMLElement; - $overlay: HTMLElement; - - onClose: any; - - constructor(options: DialogOptions) { - const { - title, - className, - content, - hideCloseButton, - onClose, - helpUrl, - } = options; - - // Create dialog overlay - const $overlay = document.querySelector('.bx-dialog-overlay'); - - if (!$overlay) { - this.$overlay = CE('div', {'class': 'bx-dialog-overlay bx-gone'}); - - // Disable right click - this.$overlay.addEventListener('contextmenu', e => e.preventDefault()); - - document.documentElement.appendChild(this.$overlay); - } else { - this.$overlay = $overlay; - } - - let $close; - this.onClose = onClose; - this.$dialog = CE('div', {'class': `bx-dialog ${className || ''} bx-gone`}, - this.$title = CE('h2', {}, CE('b', {}, title), - helpUrl && createButton({ - icon: BxIcon.QUESTION, - style: ButtonStyle.GHOST, - title: t('help'), - url: helpUrl, - }), - ), - this.$content = CE('div', {'class': 'bx-dialog-content'}, content), - !hideCloseButton && ($close = CE('button', {type: 'button'}, t('close'))), - ); - - $close && $close.addEventListener('click', e => { - this.hide(e); - }); - - !title && this.$title.classList.add('bx-gone'); - !content && this.$content.classList.add('bx-gone'); - - // Disable right click - this.$dialog.addEventListener('contextmenu', e => e.preventDefault()); - - document.documentElement.appendChild(this.$dialog); - } - - show(newOptions: DialogOptions) { - // Clear focus - document.activeElement && (document.activeElement as HTMLElement).blur(); - - if (newOptions && newOptions.title) { - this.$title.querySelector('b')!.textContent = newOptions.title; - this.$title.classList.remove('bx-gone'); - } - - this.$dialog.classList.remove('bx-gone'); - this.$overlay.classList.remove('bx-gone'); - - document.body.classList.add('bx-no-scroll'); - } - - hide(e?: any) { - this.$dialog.classList.add('bx-gone'); - this.$overlay.classList.add('bx-gone'); - - document.body.classList.remove('bx-no-scroll'); - - this.onClose && this.onClose(e); - } - - toggle() { - this.$dialog.classList.toggle('bx-gone'); - this.$overlay.classList.toggle('bx-gone'); - } -} diff --git a/src/modules/game-bar/action-base.ts b/src/modules/game-bar/base-action.ts old mode 100644 new mode 100755 similarity index 100% rename from src/modules/game-bar/action-base.ts rename to src/modules/game-bar/base-action.ts diff --git a/src/modules/game-bar/game-bar.ts b/src/modules/game-bar/game-bar.ts old mode 100644 new mode 100755 index c934424..14c85cb --- a/src/modules/game-bar/game-bar.ts +++ b/src/modules/game-bar/game-bar.ts @@ -1,22 +1,34 @@ import { CE, createSvgIcon } from "@utils/html"; -import { ScreenshotAction } from "./action-screenshot"; -import { TouchControlAction } from "./action-touch-control"; +import { ScreenshotAction } from "./screenshot-action"; +import { TouchControlAction } from "./touch-control-action"; import { BxEvent } from "@utils/bx-event"; import { BxIcon } from "@utils/bx-icon"; -import type { BaseGameBarAction } from "./action-base"; +import type { BaseGameBarAction } from "./base-action"; import { STATES } from "@utils/global"; -import { MicrophoneAction } from "./action-microphone"; +import { MicrophoneAction } from "./microphone-action"; import { PrefKey } from "@/enums/pref-keys"; -import { getPref, StreamTouchController, type GameBarPosition } from "@/utils/settings-storages/global-settings-storage"; -import { TrueAchievementsAction } from "./action-true-achievements"; -import { SpeakerAction } from "./action-speaker"; -import { RendererAction } from "./action-renderer"; +import { getPref } from "@/utils/settings-storages/global-settings-storage"; +import { TrueAchievementsAction } from "./true-achievements-action"; +import { SpeakerAction } from "./speaker-action"; +import { RendererAction } from "./renderer-action"; import { BxLogger } from "@/utils/bx-logger"; +import { GameBarPosition, TouchControllerMode } from "@/enums/pref-values"; export class GameBar { - private static instance: GameBar; - public static getInstance = () => GameBar.instance ?? (GameBar.instance = new 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) { + GameBar.instance = new GameBar(); + } else { + GameBar.instance = null; + } + } + + return GameBar.instance; + } + private readonly LOG_TAG = 'GameBar'; private static readonly VISIBLE_DURATION = 2000; @@ -33,7 +45,7 @@ export class GameBar { let $container; - const position = getPref(PrefKey.GAME_BAR_POSITION) as GameBarPosition; + 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'}), @@ -42,7 +54,7 @@ export class GameBar { this.actions = [ new ScreenshotAction(), - ...(STATES.userAgent.capabilities.touch && (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) !== StreamTouchController.OFF) ? [new TouchControlAction()] : []), + ...(STATES.userAgent.capabilities.touch && (getPref(PrefKey.TOUCH_CONTROLLER_MODE) !== TouchControllerMode.OFF) ? [new TouchControlAction()] : []), new SpeakerAction(), new RendererAction(), new MicrophoneAction(), @@ -69,10 +81,10 @@ export class GameBar { }); // Hide game bar after clicking on an action - window.addEventListener(BxEvent.GAME_BAR_ACTION_ACTIVATED, this.hideBar.bind(this)); + window.addEventListener(BxEvent.GAME_BAR_ACTION_ACTIVATED, this.hideBar); - $container.addEventListener('pointerover', this.clearHideTimeout.bind(this)); - $container.addEventListener('pointerout', this.beginHideTimeout.bind(this)); + $container.addEventListener('pointerover', this.clearHideTimeout); + $container.addEventListener('pointerout', this.beginHideTimeout); // Add animation when hiding game bar $container.addEventListener('transitionend', e => { @@ -84,16 +96,15 @@ export class GameBar { this.$container = $container; // Enable/disable Game Bar when playing/pausing - getPref(PrefKey.GAME_BAR_POSITION) !== 'off' && window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, ((e: Event) => { + position !== GameBarPosition.OFF && window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, ((e: Event) => { // Toggle Game bar if (STATES.isPlaying) { - const mode = (e as any).mode; - mode !== 'none' ? this.disable() : this.enable(); + window.BX_STREAM_SETTINGS.xCloudPollingMode !== 'none' ? this.disable() : this.enable(); } }).bind(this)); } - private beginHideTimeout() { + private beginHideTimeout = () => { this.clearHideTimeout(); this.timeoutId = window.setTimeout(() => { @@ -102,7 +113,7 @@ export class GameBar { }, GameBar.VISIBLE_DURATION); } - private clearHideTimeout() { + private clearHideTimeout = () => { this.timeoutId && clearTimeout(this.timeoutId); this.timeoutId = null; } @@ -123,7 +134,7 @@ export class GameBar { this.beginHideTimeout(); } - hideBar() { + hideBar = () => { this.clearHideTimeout(); this.$container.classList.replace('bx-show', 'bx-hide'); } diff --git a/src/modules/game-bar/action-microphone.ts b/src/modules/game-bar/microphone-action.ts old mode 100644 new mode 100755 similarity index 87% rename from src/modules/game-bar/action-microphone.ts rename to src/modules/game-bar/microphone-action.ts index 6a630d5..5cf92ea --- a/src/modules/game-bar/action-microphone.ts +++ b/src/modules/game-bar/microphone-action.ts @@ -1,8 +1,8 @@ import { BxEvent } from "@utils/bx-event"; import { BxIcon } from "@utils/bx-icon"; import { createButton, ButtonStyle, CE } from "@utils/html"; -import { BaseGameBarAction } from "./action-base"; -import { MicrophoneShortcut, MicrophoneState } from "../shortcuts/shortcut-microphone"; +import { BaseGameBarAction } from "./base-action"; +import { MicrophoneShortcut, MicrophoneState } from "../shortcuts/microphone-shortcut"; export class MicrophoneAction extends BaseGameBarAction { @@ -14,14 +14,14 @@ export class MicrophoneAction extends BaseGameBarAction { const $btnDefault = createButton({ style: ButtonStyle.GHOST, icon: BxIcon.MICROPHONE, - onClick: this.onClick.bind(this), + onClick: this.onClick, classes: ['bx-activated'], }); const $btnMuted = createButton({ style: ButtonStyle.GHOST, icon: BxIcon.MICROPHONE_MUTED, - onClick: this.onClick.bind(this), + onClick: this.onClick, }); this.$content = CE('div', {}, $btnMuted, $btnDefault); @@ -36,7 +36,7 @@ export class MicrophoneAction extends BaseGameBarAction { }); } - onClick(e: Event) { + onClick = (e: Event) => { super.onClick(e); const enabled = MicrophoneShortcut.toggle(false); this.$content.dataset.activated = enabled.toString(); diff --git a/src/modules/game-bar/action-renderer.ts b/src/modules/game-bar/renderer-action.ts old mode 100644 new mode 100755 similarity index 58% rename from src/modules/game-bar/action-renderer.ts rename to src/modules/game-bar/renderer-action.ts index 430d254..c21b064 --- a/src/modules/game-bar/action-renderer.ts +++ b/src/modules/game-bar/renderer-action.ts @@ -1,7 +1,8 @@ import { BxIcon } from "@utils/bx-icon"; import { createButton, ButtonStyle, CE } from "@utils/html"; -import { BaseGameBarAction } from "./action-base"; -import { RendererShortcut } from "../shortcuts/shortcut-renderer"; +import { BaseGameBarAction } from "./base-action"; +import { RendererShortcut } from "../shortcuts/renderer-shortcut"; +import { BxEvent } from "@/utils/bx-event"; export class RendererAction extends BaseGameBarAction { @@ -13,23 +14,27 @@ export class RendererAction extends BaseGameBarAction { const $btnDefault = createButton({ style: ButtonStyle.GHOST, icon: BxIcon.EYE, - onClick: this.onClick.bind(this), + onClick: this.onClick, }); const $btnActivated = createButton({ style: ButtonStyle.GHOST, icon: BxIcon.EYE_SLASH, - onClick: this.onClick.bind(this), + onClick: this.onClick, classes: ['bx-activated'], }); this.$content = CE('div', {}, $btnDefault, $btnActivated); + + window.addEventListener(BxEvent.VIDEO_VISIBILITY_CHANGED, e => { + const isShowing = (e as any).isShowing; + this.$content.dataset.activated = (!isShowing).toString(); + }); } - onClick(e: Event) { + onClick = (e: Event) => { super.onClick(e); - const isVisible = RendererShortcut.toggleVisibility(); - this.$content.dataset.activated = (!isVisible).toString(); + RendererShortcut.toggleVisibility(); } reset(): void { diff --git a/src/modules/game-bar/action-screenshot.ts b/src/modules/game-bar/screenshot-action.ts old mode 100644 new mode 100755 similarity index 82% rename from src/modules/game-bar/action-screenshot.ts rename to src/modules/game-bar/screenshot-action.ts index 073e66f..86709fa --- a/src/modules/game-bar/action-screenshot.ts +++ b/src/modules/game-bar/screenshot-action.ts @@ -1,6 +1,6 @@ import { BxIcon } from "@utils/bx-icon"; import { createButton, ButtonStyle } from "@utils/html"; -import { BaseGameBarAction } from "./action-base"; +import { BaseGameBarAction } from "./base-action"; import { t } from "@utils/translation"; import { ScreenshotManager } from "@/utils/screenshot-manager"; @@ -14,11 +14,11 @@ export class ScreenshotAction extends BaseGameBarAction { style: ButtonStyle.GHOST, icon: BxIcon.SCREENSHOT, title: t('take-screenshot'), - onClick: this.onClick.bind(this), + onClick: this.onClick, }); } - onClick(e: Event): void { + onClick = (e: Event) => { super.onClick(e); ScreenshotManager.getInstance().takeScreenshot(); } diff --git a/src/modules/game-bar/action-speaker.ts b/src/modules/game-bar/speaker-action.ts old mode 100644 new mode 100755 similarity index 81% rename from src/modules/game-bar/action-speaker.ts rename to src/modules/game-bar/speaker-action.ts index 2a6ac70..320ecce --- a/src/modules/game-bar/action-speaker.ts +++ b/src/modules/game-bar/speaker-action.ts @@ -1,8 +1,8 @@ import { BxEvent } from "@utils/bx-event"; import { BxIcon } from "@utils/bx-icon"; import { createButton, ButtonStyle, CE } from "@utils/html"; -import { BaseGameBarAction } from "./action-base"; -import { SoundShortcut, SpeakerState } from "../shortcuts/shortcut-sound"; +import { BaseGameBarAction } from "./base-action"; +import { SoundShortcut, SpeakerState } from "../shortcuts/sound-shortcut"; export class SpeakerAction extends BaseGameBarAction { @@ -14,13 +14,13 @@ export class SpeakerAction extends BaseGameBarAction { const $btnEnable = createButton({ style: ButtonStyle.GHOST, icon: BxIcon.AUDIO, - onClick: this.onClick.bind(this), + onClick: this.onClick, }); const $btnMuted = createButton({ style: ButtonStyle.GHOST, icon: BxIcon.SPEAKER_MUTED, - onClick: this.onClick.bind(this), + onClick: this.onClick, classes: ['bx-activated'], }); @@ -34,7 +34,7 @@ export class SpeakerAction extends BaseGameBarAction { }); } - onClick(e: Event) { + onClick = (e: Event) => { super.onClick(e); SoundShortcut.muteUnmute(); } diff --git a/src/modules/game-bar/action-touch-control.ts b/src/modules/game-bar/touch-control-action.ts old mode 100644 new mode 100755 similarity index 86% rename from src/modules/game-bar/action-touch-control.ts rename to src/modules/game-bar/touch-control-action.ts index c5225d2..b8eb1c1 --- a/src/modules/game-bar/action-touch-control.ts +++ b/src/modules/game-bar/touch-control-action.ts @@ -1,7 +1,7 @@ import { BxIcon } from "@utils/bx-icon"; import { createButton, ButtonStyle, CE } from "@utils/html"; import { TouchController } from "@modules/touch-controller"; -import { BaseGameBarAction } from "./action-base"; +import { BaseGameBarAction } from "./base-action"; import { t } from "@utils/translation"; export class TouchControlAction extends BaseGameBarAction { @@ -14,21 +14,21 @@ export class TouchControlAction extends BaseGameBarAction { style: ButtonStyle.GHOST, icon: BxIcon.TOUCH_CONTROL_ENABLE, title: t('show-touch-controller'), - onClick: this.onClick.bind(this), + onClick: this.onClick, }); const $btnDisable = createButton({ style: ButtonStyle.GHOST, icon: BxIcon.TOUCH_CONTROL_DISABLE, title: t('hide-touch-controller'), - onClick: this.onClick.bind(this), + onClick: this.onClick, classes: ['bx-activated'], }); this.$content = CE('div', {}, $btnEnable, $btnDisable); } - onClick(e: Event) { + onClick = (e: Event) => { super.onClick(e); const isVisible = TouchController.toggleVisibility(); this.$content.dataset.activated = (!isVisible).toString(); diff --git a/src/modules/game-bar/action-true-achievements.ts b/src/modules/game-bar/true-achievements-action.ts old mode 100644 new mode 100755 similarity index 81% rename from src/modules/game-bar/action-true-achievements.ts rename to src/modules/game-bar/true-achievements-action.ts index 690dc10..b3e6ff8 --- a/src/modules/game-bar/action-true-achievements.ts +++ b/src/modules/game-bar/true-achievements-action.ts @@ -1,6 +1,6 @@ import { BxIcon } from "@/utils/bx-icon"; import { createButton, ButtonStyle } from "@/utils/html"; -import { BaseGameBarAction } from "./action-base"; +import { BaseGameBarAction } from "./base-action"; import { TrueAchievements } from "@/utils/true-achievements"; export class TrueAchievementsAction extends BaseGameBarAction { @@ -12,11 +12,11 @@ export class TrueAchievementsAction extends BaseGameBarAction { this.$content = createButton({ style: ButtonStyle.GHOST, icon: BxIcon.TRUE_ACHIEVEMENTS, - onClick: this.onClick.bind(this), + onClick: this.onClick, }); } - onClick(e: Event) { + onClick = (e: Event) => { super.onClick(e); TrueAchievements.getInstance().open(false); } diff --git a/src/modules/loading-screen.ts b/src/modules/loading-screen.ts old mode 100644 new mode 100755 index 19847a9..9ac3597 --- a/src/modules/loading-screen.ts +++ b/src/modules/loading-screen.ts @@ -5,6 +5,7 @@ import { STATES } from "@utils/global"; import { PrefKey } from "@/enums/pref-keys"; import { getPref } from "@/utils/settings-storages/global-settings-storage"; import { compressCss } from "@macros/build" with {type: "macro"}; +import { LoadingScreenRocket } from "@/enums/pref-values"; export class LoadingScreen { private static $bgStyle: HTMLElement; @@ -36,7 +37,7 @@ export class LoadingScreen { LoadingScreen.setBackground(titleInfo.product.heroImageUrl || titleInfo.product.titledHeroImageUrl || titleInfo.product.tileImageUrl); - if (getPref(PrefKey.UI_LOADING_SCREEN_ROCKET) === 'hide') { + if (getPref(PrefKey.LOADING_SCREEN_ROCKET) === LoadingScreenRocket.HIDE) { LoadingScreen.hideRocket(); } } @@ -88,7 +89,7 @@ export class LoadingScreen { static setupWaitTime(waitTime: number) { // Hide rocket when queing - if (getPref(PrefKey.UI_LOADING_SCREEN_ROCKET) === 'hide-queue') { + if (getPref(PrefKey.LOADING_SCREEN_ROCKET) === LoadingScreenRocket.HIDE_QUEUE) { LoadingScreen.hideRocket(); } @@ -108,14 +109,14 @@ export class LoadingScreen { let $waitTimeBox = LoadingScreen.$waitTimeBox; if (!$waitTimeBox) { - $waitTimeBox = CE('div', {'class': 'bx-wait-time-box'}, - CE('label', {}, t('server')), - CE('span', {}, getPreferredServerRegion()), - CE('label', {}, t('wait-time-estimated')), - $estimated = CE('span', {}), - CE('label', {}, t('wait-time-countdown')), - $countDown = CE('span', {}), - ); + $waitTimeBox = CE('div', { class: 'bx-wait-time-box' }, + CE('label', {}, t('server')), + CE('span', {}, getPreferredServerRegion()), + CE('label', {}, t('wait-time-estimated')), + $estimated = CE('span', {}), + CE('label', {}, t('wait-time-countdown')), + $countDown = CE('span', {}), + ); document.documentElement.appendChild($waitTimeBox); LoadingScreen.$waitTimeBox = $waitTimeBox; @@ -145,7 +146,7 @@ export class LoadingScreen { LoadingScreen.orgWebTitle && (document.title = LoadingScreen.orgWebTitle); LoadingScreen.$waitTimeBox && LoadingScreen.$waitTimeBox.classList.add('bx-gone'); - if (getPref(PrefKey.UI_LOADING_SCREEN_GAME_ART) && LoadingScreen.$bgStyle) { + if (getPref(PrefKey.LOADING_SCREEN_GAME_ART) && LoadingScreen.$bgStyle) { const $rocketBg = document.querySelector('#game-stream rect[width="800"]'); $rocketBg && $rocketBg.addEventListener('transitionend', e => { LoadingScreen.$bgStyle.textContent += compressCss(` diff --git a/src/modules/mkb/base-mkb-handler.ts b/src/modules/mkb/base-mkb-handler.ts old mode 100644 new mode 100755 index 382a9ce..644fad1 --- a/src/modules/mkb/base-mkb-handler.ts +++ b/src/modules/mkb/base-mkb-handler.ts @@ -4,10 +4,11 @@ export abstract class MouseDataProvider { this.mkbHandler = handler; } - abstract init(): void; + init() {}; + destroy() {}; + abstract start(): void; abstract stop(): void; - abstract destroy(): void; } export abstract class MkbHandler { @@ -15,6 +16,7 @@ export abstract class MkbHandler { abstract start(): void; abstract stop(): void; abstract destroy(): void; + abstract toggle(force: boolean): void; abstract handleMouseMove(data: MkbMouseMove): void; abstract handleMouseClick(data: MkbMouseClick): void; abstract handleMouseWheel(data: MkbMouseWheel): boolean; diff --git a/src/modules/mkb/key-helper.ts b/src/modules/mkb/key-helper.ts old mode 100644 new mode 100755 index fbde4b6..1f68c2c --- a/src/modules/mkb/key-helper.ts +++ b/src/modules/mkb/key-helper.ts @@ -1,8 +1,36 @@ -import { MouseButtonCode, WheelCode } from "@enums/mkb"; +import { MouseButtonCode, WheelCode, type KeyCode } from "@/enums/mkb"; + +export const enum KeyModifier { + CTRL = 1, + SHIFT = 2, + ALT = 4, +}; + +export type KeyEventInfo = { + code: KeyCode | MouseButtonCode | WheelCode; + modifiers?: number; +}; export class KeyHelper { - static #NON_PRINTABLE_KEYS = { - 'Backquote': '`', + private static readonly NON_PRINTABLE_KEYS = { + Backquote: '`', + Minus: '-', + Equal: '=', + BracketLeft: '[', + BracketRight: ']', + Backslash: '\\', + Semicolon: ';', + Quote: '\'', + Comma: ',', + Period: '.', + Slash: '/', + + NumpadMultiply: 'Numpad *', + NumpadAdd: 'Numpad +', + NumpadSubtract: 'Numpad -', + NumpadDecimal: 'Numpad .', + NumpadDivide: 'Numpad /', + NumpadEqual: 'Numpad =', // Mouse buttons [MouseButtonCode.LEFT_CLICK]: 'Left Click', @@ -15,12 +43,19 @@ export class KeyHelper { [WheelCode.SCROLL_RIGHT]: 'Scroll Right', }; - static getKeyFromEvent(e: Event) { - let code; - let name; + static getKeyFromEvent(e: Event): KeyEventInfo | null { + let code: KeyEventInfo['code'] | null = null; + let modifiers; if (e instanceof KeyboardEvent) { - code = e.code || e.key; + code = (e.code || e.key) as KeyCode; + + // Modifiers + modifiers = 0; + modifiers ^= e.ctrlKey ? KeyModifier.CTRL : 0; + modifiers ^= e.shiftKey ? KeyModifier.SHIFT : 0; + modifiers ^= e.altKey ? KeyModifier.ALT : 0; + } else if (e instanceof WheelEvent) { if (e.deltaY < 0) { code = WheelCode.SCROLL_UP; @@ -32,20 +67,47 @@ export class KeyHelper { code = WheelCode.SCROLL_RIGHT; } } else if (e instanceof MouseEvent) { - code = 'Mouse' + e.button; + code = 'Mouse' + e.button as MouseButtonCode; } if (code) { - name = KeyHelper.codeToKeyName(code); + const results: KeyEventInfo = { code }; + if (modifiers) { + results.modifiers = modifiers; + } + + return results; } - return code ? {code, name} : null; + return null; } - static codeToKeyName(code: string) { - return ( - // @ts-ignore - KeyHelper.#NON_PRINTABLE_KEYS[code] + static getFullKeyCodeFromEvent(e: KeyboardEvent): string { + const key = KeyHelper.getKeyFromEvent(e); + return key ? `${key.code}:${key.modifiers || 0}` : ''; + } + + static parseFullKeyCode(str: string | undefined | null): KeyEventInfo | null { + if (!str) { + return null; + } + + const tmp = str.split(':'); + + const code = tmp[0] as KeyEventInfo['code']; + const modifiers = parseInt(tmp[1]); + + return { + code, + modifiers, + } as KeyEventInfo; + } + + static codeToKeyName(key: KeyEventInfo): string { + const { code, modifiers } = key; + + const text = [( + KeyHelper.NON_PRINTABLE_KEYS[code as keyof typeof KeyHelper.NON_PRINTABLE_KEYS] || (code.startsWith('Key') && code.substring(3)) || @@ -62,6 +124,27 @@ export class KeyHelper { (code.endsWith('Right') && ('Right ' + code.replace('Right', ''))) || code - ); + )]; + + if (modifiers && modifiers !== 0) { + if (!code.startsWith('Control') && !code.startsWith('Shift') && !code.startsWith('Alt')) { + // Shift + if (modifiers & KeyModifier.SHIFT) { + text.unshift('Shift'); + } + + // Alt + if (modifiers & KeyModifier.ALT) { + text.unshift('Alt'); + } + + // Ctrl + if (modifiers & KeyModifier.CTRL) { + text.unshift('Ctrl'); + } + } + } + + return text.join(' + '); } } diff --git a/src/modules/mkb/keyboard-shortcut-handler.ts b/src/modules/mkb/keyboard-shortcut-handler.ts new file mode 100755 index 0000000..88eba70 --- /dev/null +++ b/src/modules/mkb/keyboard-shortcut-handler.ts @@ -0,0 +1,40 @@ +import { ShortcutHandler } from "@/utils/shortcut-handler"; +import { KeyHelper } from "./key-helper"; + +export class KeyboardShortcutHandler { + private static instance: KeyboardShortcutHandler; + public static getInstance = () => KeyboardShortcutHandler.instance ?? (KeyboardShortcutHandler.instance = new KeyboardShortcutHandler()); + + start() { + window.addEventListener('keydown', this.onKeyDown); + } + + stop() { + window.removeEventListener('keydown', this.onKeyDown); + } + + onKeyDown = (e: KeyboardEvent) => { + // Don't run when the stream is not being focused + if (window.BX_STREAM_SETTINGS.xCloudPollingMode !== 'none') { + return; + } + + // Don't activate repeated key + if (e.repeat) { + return; + } + + // Check unknown key + const fullKeyCode = KeyHelper.getFullKeyCodeFromEvent(e); + if (!fullKeyCode) { + return; + } + + const action = window.BX_STREAM_SETTINGS.keyboardShortcuts?.[fullKeyCode]; + if (action) { + e.preventDefault(); + e.stopPropagation(); + ShortcutHandler.runAction(action); + } + } +} diff --git a/src/modules/mkb/mkb-handler.ts b/src/modules/mkb/mkb-handler.ts old mode 100644 new mode 100755 index 6a36984..5a82904 --- a/src/modules/mkb/mkb-handler.ts +++ b/src/modules/mkb/mkb-handler.ts @@ -1,24 +1,21 @@ import { isFullVersion } from "@macros/build" with {type: "macro"}; -import { MkbPreset } from "./mkb-preset"; -import { GamepadKey, MkbPresetKey, GamepadStick, MouseMapTo, WheelCode } from "@enums/mkb"; -import { createButton, ButtonStyle, CE } from "@utils/html"; +import { MkbPresetKey, MouseConstant, MouseMapTo, WheelCode } from "@/enums/mkb"; import { BxEvent } from "@utils/bx-event"; import { Toast } from "@utils/toast"; import { t } from "@utils/translation"; import { KeyHelper } from "./key-helper"; -import type { MkbStoredPreset } from "@/types/mkb"; import { AppInterface, STATES } from "@utils/global"; import { UserAgent } from "@utils/user-agent"; import { BxLogger } from "@utils/bx-logger"; import { PointerClient } from "./pointer-client"; import { NativeMkbHandler } from "./native-mkb-handler"; import { MkbHandler, MouseDataProvider } from "./base-mkb-handler"; -import { SettingsNavigationDialog } from "../ui/dialog/settings-dialog"; -import { NavigationDialogManager } from "../ui/dialog/navigation-dialog"; import { PrefKey } from "@/enums/pref-keys"; import { getPref } from "@/utils/settings-storages/global-settings-storage"; -import { MkbPresetsDb } from "@/utils/local-db/mkb-presets-db"; +import { GamepadKey, GamepadStick } from "@/enums/gamepad"; +import { MkbPopup } from "./mkb-popup"; +import type { MkbConvertedPresetData } from "@/types/presets"; const PointerToMouseButton = { 1: 0, @@ -26,79 +23,74 @@ const PointerToMouseButton = { 4: 1, } -export const VIRTUAL_GAMEPAD_ID = 'Xbox 360 Controller'; +export const VIRTUAL_GAMEPAD_ID = 'Better xCloud Virtual Controller'; class WebSocketMouseDataProvider extends MouseDataProvider { - #pointerClient: PointerClient | undefined - #connected = false + private pointerClient: PointerClient | undefined + private isConnected = false init(): void { - this.#pointerClient = PointerClient.getInstance(); - this.#connected = false; + this.pointerClient = PointerClient.getInstance(); + this.isConnected = false; try { - this.#pointerClient.start(STATES.pointerServerPort, this.mkbHandler); - this.#connected = true; + this.pointerClient.start(STATES.pointerServerPort, this.mkbHandler); + this.isConnected = true; } catch (e) { Toast.show('Cannot enable Mouse & Keyboard feature'); } } start(): void { - this.#connected && AppInterface.requestPointerCapture(); + this.isConnected && AppInterface.requestPointerCapture(); } stop(): void { - this.#connected && AppInterface.releasePointerCapture(); + this.isConnected && AppInterface.releasePointerCapture(); } destroy(): void { - this.#connected && this.#pointerClient?.stop(); + this.isConnected && this.pointerClient?.stop(); } } class PointerLockMouseDataProvider extends MouseDataProvider { - init(): void {} - - start(): void { - window.addEventListener('mousemove', this.#onMouseMoveEvent); - window.addEventListener('mousedown', this.#onMouseEvent); - window.addEventListener('mouseup', this.#onMouseEvent); - window.addEventListener('wheel', this.#onWheelEvent, {passive: false}); - window.addEventListener('contextmenu', this.#disableContextMenu); + start() { + window.addEventListener('mousemove', this.onMouseMoveEvent); + window.addEventListener('mousedown', this.onMouseEvent); + window.addEventListener('mouseup', this.onMouseEvent); + window.addEventListener('wheel', this.onWheelEvent, { passive: false }); + window.addEventListener('contextmenu', this.disableContextMenu); } - stop(): void { + stop() { document.pointerLockElement && document.exitPointerLock(); - window.removeEventListener('mousemove', this.#onMouseMoveEvent); - window.removeEventListener('mousedown', this.#onMouseEvent); - window.removeEventListener('mouseup', this.#onMouseEvent); - window.removeEventListener('wheel', this.#onWheelEvent); - window.removeEventListener('contextmenu', this.#disableContextMenu); + window.removeEventListener('mousemove', this.onMouseMoveEvent); + window.removeEventListener('mousedown', this.onMouseEvent); + window.removeEventListener('mouseup', this.onMouseEvent); + window.removeEventListener('wheel', this.onWheelEvent); + window.removeEventListener('contextmenu', this.disableContextMenu); } - destroy(): void {} - - #onMouseMoveEvent = (e: MouseEvent) => { + private onMouseMoveEvent = (e: MouseEvent) => { this.mkbHandler.handleMouseMove({ movementX: e.movementX, movementY: e.movementY, }); } - #onMouseEvent = (e: MouseEvent) => { + private onMouseEvent = (e: MouseEvent) => { e.preventDefault(); - const isMouseDown = e.type === 'mousedown'; const data: MkbMouseClick = { mouseButton: e.button, - pressed: isMouseDown, + pressed: e.type === 'mousedown', }; this.mkbHandler.handleMouseClick(data); } - #onWheelEvent = (e: WheelEvent) => { + private onWheelEvent = (e: WheelEvent) => { const key = KeyHelper.getKeyFromEvent(e); if (!key) { return; @@ -114,7 +106,7 @@ class PointerLockMouseDataProvider extends MouseDataProvider { } } - #disableContextMenu = (e: Event) => e.preventDefault(); + private disableContextMenu = (e: Event) => e.preventDefault(); } /* @@ -122,80 +114,95 @@ This class uses some code from Yuzu emulator to handle mouse's movements Source: https://github.com/yuzu-emu/yuzu-mainline/blob/master/src/input_common/drivers/mouse.cpp */ export class EmulatedMkbHandler extends MkbHandler { - private static instance: EmulatedMkbHandler; - public static getInstance = () => EmulatedMkbHandler.instance ?? (EmulatedMkbHandler.instance = new EmulatedMkbHandler()); + private static instance: EmulatedMkbHandler | null | undefined; + public static getInstance(): typeof EmulatedMkbHandler['instance'] { + if (typeof EmulatedMkbHandler.instance === 'undefined') { + if (EmulatedMkbHandler.isAllowed()) { + EmulatedMkbHandler.instance = new EmulatedMkbHandler(); + } else { + EmulatedMkbHandler.instance = null; + } + } + + return EmulatedMkbHandler.instance; + } + private static readonly LOG_TAG = 'EmulatedMkbHandler'; - #CURRENT_PRESET_DATA = MkbPreset.convert(MkbPreset.DEFAULT_PRESET); + static isAllowed() { + return getPref(PrefKey.MKB_ENABLED) && (AppInterface || !UserAgent.isMobile()); + } - static readonly DEFAULT_PANNING_SENSITIVITY = 0.0010; - static readonly DEFAULT_DEADZONE_COUNTERWEIGHT = 0.01; - static readonly MAXIMUM_STICK_RANGE = 1.1; + private PRESET!: MkbConvertedPresetData | null; + private VIRTUAL_GAMEPAD = { + id: VIRTUAL_GAMEPAD_ID, + index: 0, + connected: false, + hapticActuators: null, + mapping: 'standard', - #VIRTUAL_GAMEPAD = { - id: VIRTUAL_GAMEPAD_ID, - index: 3, - connected: false, - hapticActuators: null, - mapping: 'standard', + axes: [0, 0, 0, 0], + buttons: new Array(17).fill(null).map(() => ({pressed: false, value: 0})), + timestamp: performance.now(), - axes: [0, 0, 0, 0], - buttons: new Array(17).fill(null).map(() => ({pressed: false, value: 0})), - timestamp: performance.now(), + vibrationActuator: null, + }; + private nativeGetGamepads: Navigator['getGamepads']; - vibrationActuator: null, - }; - #nativeGetGamepads = window.navigator.getGamepads.bind(window.navigator); + private initialized = false; + private enabled = false; + private mouseDataProvider: MouseDataProvider | undefined; + private isPolling = false; - #enabled = false; - #mouseDataProvider: MouseDataProvider | undefined; - #isPolling = false; + private prevWheelCode = null; + private wheelStoppedTimeoutId: number | null = null; - #prevWheelCode = null; - #wheelStoppedTimeout?: number | null; + private detectMouseStoppedTimeoutId: number | null = null; - #detectMouseStoppedTimeout?: number | null; + private escKeyDownTime: number = -1; - #$message?: HTMLElement; + private LEFT_STICK_X: GamepadKey[] = []; + private LEFT_STICK_Y: GamepadKey[] = []; + private RIGHT_STICK_X: GamepadKey[] = []; + private RIGHT_STICK_Y: GamepadKey[] = []; - #escKeyDownTime: number = -1; + private popup: MkbPopup; - #STICK_MAP: {[key in GamepadKey]?: [GamepadKey[], number, number]}; - #LEFT_STICK_X: GamepadKey[] = []; - #LEFT_STICK_Y: GamepadKey[] = []; - #RIGHT_STICK_X: GamepadKey[] = []; - #RIGHT_STICK_Y: GamepadKey[] = []; + private STICK_MAP: {[key in GamepadKey]?: [GamepadKey[], number, number]} = { + [GamepadKey.LS_LEFT]: [this.LEFT_STICK_X, 0, -1], + [GamepadKey.LS_RIGHT]: [this.LEFT_STICK_X, 0, 1], + [GamepadKey.LS_UP]: [this.LEFT_STICK_Y, 1, -1], + [GamepadKey.LS_DOWN]: [this.LEFT_STICK_Y, 1, 1], + + [GamepadKey.RS_LEFT]: [this.RIGHT_STICK_X, 2, -1], + [GamepadKey.RS_RIGHT]: [this.RIGHT_STICK_X, 2, 1], + [GamepadKey.RS_UP]: [this.RIGHT_STICK_Y, 3, -1], + [GamepadKey.RS_DOWN]: [this.RIGHT_STICK_Y, 3, 1], + }; private constructor() { super(); BxLogger.info(EmulatedMkbHandler.LOG_TAG, 'constructor()'); - this.#STICK_MAP = { - [GamepadKey.LS_LEFT]: [this.#LEFT_STICK_X, 0, -1], - [GamepadKey.LS_RIGHT]: [this.#LEFT_STICK_X, 0, 1], - [GamepadKey.LS_UP]: [this.#LEFT_STICK_Y, 1, -1], - [GamepadKey.LS_DOWN]: [this.#LEFT_STICK_Y, 1, 1], + this.nativeGetGamepads = window.navigator.getGamepads.bind(window.navigator); - [GamepadKey.RS_LEFT]: [this.#RIGHT_STICK_X, 2, -1], - [GamepadKey.RS_RIGHT]: [this.#RIGHT_STICK_X, 2, 1], - [GamepadKey.RS_UP]: [this.#RIGHT_STICK_Y, 3, -1], - [GamepadKey.RS_DOWN]: [this.#RIGHT_STICK_Y, 3, 1], - }; + this.popup = MkbPopup.getInstance(); + this.popup.attachMkbHandler(this); } - isEnabled = () => this.#enabled; + isEnabled = () => this.enabled; - #patchedGetGamepads = () => { - const gamepads = this.#nativeGetGamepads() || []; - (gamepads as any)[this.#VIRTUAL_GAMEPAD.index] = this.#VIRTUAL_GAMEPAD; + private patchedGetGamepads = () => { + const gamepads = (this.nativeGetGamepads() || []) as any; + gamepads[this.VIRTUAL_GAMEPAD.index] = this.VIRTUAL_GAMEPAD; return gamepads; } - #getVirtualGamepad = () => this.#VIRTUAL_GAMEPAD; + private getVirtualGamepad = () => this.VIRTUAL_GAMEPAD; - #updateStick(stick: GamepadStick, x: number, y: number) { - const virtualGamepad = this.#getVirtualGamepad(); + private updateStick(stick: GamepadStick, x: number, y: number) { + const virtualGamepad = this.getVirtualGamepad(); virtualGamepad.axes[stick * 2] = x; virtualGamepad.axes[stick * 2 + 1] = y; @@ -212,10 +219,10 @@ export class EmulatedMkbHandler extends MkbHandler { } */ - #vectorLength = (x: number, y: number): number => Math.sqrt(x ** 2 + y ** 2); + private vectorLength = (x: number, y: number): number => Math.sqrt(x ** 2 + y ** 2); - #resetGamepad = () => { - const gamepad = this.#getVirtualGamepad(); + private resetGamepad() { + const gamepad = this.getVirtualGamepad(); // Reset axes gamepad.axes = [0, 0, 0, 0]; @@ -229,11 +236,11 @@ export class EmulatedMkbHandler extends MkbHandler { gamepad.timestamp = performance.now(); } - #pressButton = (buttonIndex: GamepadKey, pressed: boolean) => { - const virtualGamepad = this.#getVirtualGamepad(); + private pressButton(buttonIndex: GamepadKey, pressed: boolean) { + const virtualGamepad = this.getVirtualGamepad(); if (buttonIndex >= 100) { - let [valueArr, axisIndex] = this.#STICK_MAP[buttonIndex]!; + let [valueArr, axisIndex] = this.STICK_MAP[buttonIndex]!; valueArr = valueArr as number[]; axisIndex = axisIndex as number; @@ -249,7 +256,7 @@ export class EmulatedMkbHandler extends MkbHandler { let value; if (valueArr.length) { // Get value of the last key of the axis - value = this.#STICK_MAP[valueArr[valueArr.length - 1]]![2] as number; + value = this.STICK_MAP[valueArr[valueArr.length - 1]]![2] as number; } else { value = 0; } @@ -263,41 +270,35 @@ export class EmulatedMkbHandler extends MkbHandler { virtualGamepad.timestamp = performance.now(); } - #onKeyboardEvent = (e: KeyboardEvent) => { + private onKeyboardEvent = (e: KeyboardEvent) => { const isKeyDown = e.type === 'keydown'; - // Toggle MKB feature - if (e.code === 'F8') { - if (!isKeyDown) { - e.preventDefault(); - this.toggle(); - } - - return; - } - // Hijack the Esc button if (e.code === 'Escape') { e.preventDefault(); // Hold the Esc for 1 second to disable MKB - if (this.#enabled && isKeyDown) { - if (this.#escKeyDownTime === -1) { - this.#escKeyDownTime = performance.now(); - } else if (performance.now() - this.#escKeyDownTime >= 1000) { + if (this.enabled && isKeyDown) { + if (this.escKeyDownTime === -1) { + this.escKeyDownTime = performance.now(); + } else if (performance.now() - this.escKeyDownTime >= 1000) { this.stop(); } } else { - this.#escKeyDownTime = -1; + this.escKeyDownTime = -1; } return; } - if (!this.#isPolling) { + if (!this.isPolling || !this.PRESET) { return; } - const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[e.code || e.key]!; + if (window.BX_STREAM_SETTINGS.xCloudPollingMode !== 'none') { + return; + } + + const buttonIndex = this.PRESET.mapping[e.code || e.key]!; if (typeof buttonIndex === 'undefined') { return; } @@ -308,19 +309,23 @@ export class EmulatedMkbHandler extends MkbHandler { } e.preventDefault(); - this.#pressButton(buttonIndex, isKeyDown); + this.pressButton(buttonIndex, isKeyDown); } - #onMouseStopped = () => { + private onMouseStopped = () => { // Reset stick position - this.#detectMouseStoppedTimeout = null; + this.detectMouseStoppedTimeoutId = null; - const mouseMapTo = this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_MAP_TO]; + if (!this.PRESET) { + return; + } + + const mouseMapTo = this.PRESET.mouse[MkbPresetKey.MOUSE_MAP_TO]; const analog = mouseMapTo === MouseMapTo.LS ? GamepadStick.LEFT : GamepadStick.RIGHT; - this.#updateStick(analog, 0, 0); + this.updateStick(analog, 0, 0); } - handleMouseClick = (data: MkbMouseClick) => { + handleMouseClick(data: MkbMouseClick) { let mouseButton; if (typeof data.mouseButton !== 'undefined') { mouseButton = data.mouseButton; @@ -331,51 +336,54 @@ export class EmulatedMkbHandler extends MkbHandler { const keyCode = 'Mouse' + mouseButton; const key = { code: keyCode, - name: KeyHelper.codeToKeyName(keyCode), }; - if (!key.name) { + if (!this.PRESET) { return; } - - const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[key.code]!; + const buttonIndex = this.PRESET.mapping[key.code]!; if (typeof buttonIndex === 'undefined') { return; } - this.#pressButton(buttonIndex, data.pressed); + this.pressButton(buttonIndex, data.pressed); } - handleMouseMove = (data: MkbMouseMove) => { + handleMouseMove(data: MkbMouseMove) { + const preset = this.PRESET; + if (!preset) { + return; + } + // TODO: optimize this - const mouseMapTo = this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_MAP_TO]; + const mouseMapTo = preset.mouse[MkbPresetKey.MOUSE_MAP_TO]; if (mouseMapTo === MouseMapTo.OFF) { // Ignore mouse movements return; } - this.#detectMouseStoppedTimeout && clearTimeout(this.#detectMouseStoppedTimeout); - this.#detectMouseStoppedTimeout = window.setTimeout(this.#onMouseStopped.bind(this), 50); + this.detectMouseStoppedTimeoutId && clearTimeout(this.detectMouseStoppedTimeoutId); + this.detectMouseStoppedTimeoutId = window.setTimeout(this.onMouseStopped, 50); - const deadzoneCounterweight = this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT]; + const deadzoneCounterweight = preset.mouse[MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT]; - let x = data.movementX * this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_SENSITIVITY_X]; - let y = data.movementY * this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_SENSITIVITY_Y]; + let x = data.movementX * preset.mouse[MkbPresetKey.MOUSE_SENSITIVITY_X]; + let y = data.movementY * preset.mouse[MkbPresetKey.MOUSE_SENSITIVITY_Y]; - let length = this.#vectorLength(x, y); + let length = this.vectorLength(x, y); if (length !== 0 && length < deadzoneCounterweight) { x *= deadzoneCounterweight / length; y *= deadzoneCounterweight / length; - } else if (length > EmulatedMkbHandler.MAXIMUM_STICK_RANGE) { - x *= EmulatedMkbHandler.MAXIMUM_STICK_RANGE / length; - y *= EmulatedMkbHandler.MAXIMUM_STICK_RANGE / length; + } else if (length > MouseConstant.MAXIMUM_STICK_RANGE) { + x *= MouseConstant.MAXIMUM_STICK_RANGE / length; + y *= MouseConstant.MAXIMUM_STICK_RANGE / length; } const analog = mouseMapTo === MouseMapTo.LS ? GamepadStick.LEFT : GamepadStick.RIGHT; - this.#updateStick(analog, x, y); + this.updateStick(analog, x, y); } - handleMouseWheel = (data: MkbMouseWheel): boolean => { + handleMouseWheel(data: MkbMouseWheel): boolean { let code = ''; if (data.vertical < 0) { code = WheelCode.SCROLL_UP; @@ -391,136 +399,69 @@ export class EmulatedMkbHandler extends MkbHandler { return false; } + if (!this.PRESET) { + return false; + } + const key = { code: code, - name: KeyHelper.codeToKeyName(code), }; - const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[key.code]!; + const buttonIndex = this.PRESET.mapping[key.code]!; if (typeof buttonIndex === 'undefined') { return false; } - if (this.#prevWheelCode === null || this.#prevWheelCode === key.code) { - this.#wheelStoppedTimeout && clearTimeout(this.#wheelStoppedTimeout); - this.#pressButton(buttonIndex, true); + if (this.prevWheelCode === null || this.prevWheelCode === key.code) { + this.wheelStoppedTimeoutId && clearTimeout(this.wheelStoppedTimeoutId); + this.pressButton(buttonIndex, true); } - this.#wheelStoppedTimeout = window.setTimeout(() => { - this.#prevWheelCode = null; - this.#pressButton(buttonIndex, false); + this.wheelStoppedTimeoutId = window.setTimeout(() => { + this.prevWheelCode = null; + this.pressButton(buttonIndex, false); }, 20); return true; } - toggle = (force?: boolean) => { - if (typeof force !== 'undefined') { - this.#enabled = force; - } else { - this.#enabled = !this.#enabled; + toggle(force?: boolean) { + if (!this.initialized) { + return; } - if (this.#enabled) { + if (typeof force !== 'undefined') { + this.enabled = force; + } else { + this.enabled = !this.enabled; + } + + if (this.enabled) { document.body.requestPointerLock(); } else { document.pointerLockElement && document.exitPointerLock(); } } - #getCurrentPreset = (): Promise => { - return new Promise(resolve => { - const presetId = getPref(PrefKey.MKB_DEFAULT_PRESET_ID); - MkbPresetsDb.getInstance().getPreset(presetId).then((preset: MkbStoredPreset) => { - resolve(preset); - }); - }); + refreshPresetData() { + this.PRESET = window.BX_STREAM_SETTINGS.mkbPreset; + this.resetGamepad(); } - refreshPresetData = () => { - this.#getCurrentPreset().then((preset: MkbStoredPreset) => { - this.#CURRENT_PRESET_DATA = MkbPreset.convert(preset ? preset.data : MkbPreset.DEFAULT_PRESET); - this.#resetGamepad(); - }); + waitForMouseData(showPopup: boolean) { + this.popup.toggleVisibility(showPopup); } - waitForMouseData = (wait: boolean) => { - this.#$message && this.#$message.classList.toggle('bx-gone', !wait); + private onPollingModeChanged = (e: Event) => { + const move = window.BX_STREAM_SETTINGS.xCloudPollingMode !== 'none'; + this.popup.moveOffscreen(move); } - #onPollingModeChanged = (e: Event) => { - if (!this.#$message) { - return; - } - - const mode = (e as any).mode; - if (mode === 'none') { - this.#$message.classList.remove('bx-offscreen'); - } else { - this.#$message.classList.add('bx-offscreen'); - } - } - - #onDialogShown = () => { + private onDialogShown = () => { document.pointerLockElement && document.exitPointerLock(); } - #initMessage = () => { - if (!this.#$message) { - this.#$message = CE('div', {'class': 'bx-mkb-pointer-lock-msg bx-gone'}, - CE('div', {}, - CE('p', {}, t('virtual-controller')), - CE('p', {}, t('press-key-to-toggle-mkb', {key: 'F8'})), - ), - - CE('div', {'data-type': 'virtual'}, - createButton({ - style: ButtonStyle.PRIMARY | ButtonStyle.TALL | ButtonStyle.FULL_WIDTH, - label: t('activate'), - onClick: ((e: Event) => { - e.preventDefault(); - e.stopPropagation(); - - this.toggle(true); - }).bind(this), - }), - - CE('div', {}, - createButton({ - label: t('ignore'), - style: ButtonStyle.GHOST, - onClick: e => { - e.preventDefault(); - e.stopPropagation(); - - this.toggle(false); - this.waitForMouseData(false); - }, - }), - - createButton({ - label: t('edit'), - onClick: e => { - e.preventDefault(); - e.stopPropagation(); - - // Show Settings dialog & focus the MKB tab - const dialog = SettingsNavigationDialog.getInstance(); - dialog.focusTab('mkb'); - NavigationDialogManager.getInstance().show(dialog); - }, - }), - ), - ), - ); - } - - if (!this.#$message.isConnected) { - document.documentElement.appendChild(this.#$message); - } - } - - #onPointerLockChange = () => { + private onPointerLockChange = () => { if (document.pointerLockElement) { this.start(); } else { @@ -528,58 +469,64 @@ export class EmulatedMkbHandler extends MkbHandler { } } - #onPointerLockError = (e: Event) => { + private onPointerLockError = (e: Event) => { console.log(e); this.stop(); } - #onPointerLockRequested = () => { + private onPointerLockRequested = () => { this.start(); } - #onPointerLockExited = () => { - this.#mouseDataProvider?.stop(); + private onPointerLockExited = () => { + this.mouseDataProvider?.stop(); } handleEvent(event: Event) { switch (event.type) { case BxEvent.POINTER_LOCK_REQUESTED: - this.#onPointerLockRequested(); + this.onPointerLockRequested(); break; case BxEvent.POINTER_LOCK_EXITED: - this.#onPointerLockExited(); + this.onPointerLockExited(); break; } } - init = () => { + init() { + if (!STATES.browser.capabilities.mkb) { + this.initialized = false; + return; + } + + this.initialized = true; + this.refreshPresetData(); - this.#enabled = false; + this.enabled = false; if (AppInterface) { - this.#mouseDataProvider = new WebSocketMouseDataProvider(this); + this.mouseDataProvider = new WebSocketMouseDataProvider(this); } else { - this.#mouseDataProvider = new PointerLockMouseDataProvider(this); + this.mouseDataProvider = new PointerLockMouseDataProvider(this); } - this.#mouseDataProvider.init(); + this.mouseDataProvider.init(); - window.addEventListener('keydown', this.#onKeyboardEvent); - window.addEventListener('keyup', this.#onKeyboardEvent); + window.addEventListener('keydown', this.onKeyboardEvent); + window.addEventListener('keyup', this.onKeyboardEvent); - window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.#onPollingModeChanged); - window.addEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this.#onDialogShown); + window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.onPollingModeChanged); + window.addEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this.onDialogShown); if (AppInterface) { // Android app doesn't support PointerLock API so we need to use a different method window.addEventListener(BxEvent.POINTER_LOCK_REQUESTED, this); window.addEventListener(BxEvent.POINTER_LOCK_EXITED, this); } else { - document.addEventListener('pointerlockchange', this.#onPointerLockChange); - document.addEventListener('pointerlockerror', this.#onPointerLockError); + document.addEventListener('pointerlockchange', this.onPointerLockChange); + document.addEventListener('pointerlockerror', this.onPointerLockError); } - this.#initMessage(); - this.#$message?.classList.add('bx-gone'); + MkbPopup.getInstance().reset(); if (AppInterface) { Toast.show(t('press-key-to-toggle-mkb', {key: `F8`}), t('virtual-controller'), {html: true}); @@ -589,51 +536,62 @@ export class EmulatedMkbHandler extends MkbHandler { } } - destroy = () => { - this.#isPolling = false; - this.#enabled = false; + destroy() { + if (!this.initialized) { + return; + } + + this.initialized = false; + this.isPolling = false; + this.enabled = false; this.stop(); this.waitForMouseData(false); document.pointerLockElement && document.exitPointerLock(); - window.removeEventListener('keydown', this.#onKeyboardEvent); - window.removeEventListener('keyup', this.#onKeyboardEvent); + window.removeEventListener('keydown', this.onKeyboardEvent); + window.removeEventListener('keyup', this.onKeyboardEvent); if (AppInterface) { window.removeEventListener(BxEvent.POINTER_LOCK_REQUESTED, this); window.removeEventListener(BxEvent.POINTER_LOCK_EXITED, this); } else { - document.removeEventListener('pointerlockchange', this.#onPointerLockChange); - document.removeEventListener('pointerlockerror', this.#onPointerLockError); + document.removeEventListener('pointerlockchange', this.onPointerLockChange); + document.removeEventListener('pointerlockerror', this.onPointerLockError); } - window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.#onPollingModeChanged); - window.removeEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this.#onDialogShown); + window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.onPollingModeChanged); + window.removeEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this.onDialogShown); - this.#mouseDataProvider?.destroy(); + this.mouseDataProvider?.destroy(); - window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.#onPollingModeChanged); + window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.onPollingModeChanged); } - start = () => { - if (!this.#enabled) { - this.#enabled = true; - Toast.show(t('virtual-controller'), t('enabled'), {instant: true}); + updateGamepadSlots() { + // Set gamepad slot + this.VIRTUAL_GAMEPAD.index = getPref(PrefKey.MKB_P1_SLOT) - 1; + } + + start() { + if (!this.enabled) { + this.enabled = true; + Toast.show(t('virtual-controller'), t('enabled'), { instant: true }); } - this.#isPolling = true; - this.#escKeyDownTime = -1; + this.isPolling = true; + this.escKeyDownTime = -1; - this.#resetGamepad(); - window.navigator.getGamepads = this.#patchedGetGamepads; + this.resetGamepad(); + this.updateGamepadSlots(); + window.navigator.getGamepads = this.patchedGetGamepads; this.waitForMouseData(false); - this.#mouseDataProvider?.start(); + this.mouseDataProvider?.start(); // Dispatch "gamepadconnected" event - const virtualGamepad = this.#getVirtualGamepad(); + const virtualGamepad = this.getVirtualGamepad(); virtualGamepad.connected = true; virtualGamepad.timestamp = performance.now(); @@ -643,46 +601,51 @@ export class EmulatedMkbHandler extends MkbHandler { window.BX_EXPOSED.stopTakRendering = true; - Toast.show(t('virtual-controller'), t('enabled'), {instant: true}); + Toast.show(t('virtual-controller'), t('enabled'), { instant: true }); } - stop = () => { - this.#enabled = false; - this.#isPolling = false; - this.#escKeyDownTime = -1; + stop() { + this.enabled = false; + this.isPolling = false; + this.escKeyDownTime = -1; - const virtualGamepad = this.#getVirtualGamepad(); + const virtualGamepad = this.getVirtualGamepad(); if (virtualGamepad.connected) { // Dispatch "gamepaddisconnected" event - this.#resetGamepad(); + this.resetGamepad(); virtualGamepad.connected = false; virtualGamepad.timestamp = performance.now(); BxEvent.dispatch(window, 'gamepaddisconnected', { - gamepad: virtualGamepad, - }); + gamepad: virtualGamepad, + }); - window.navigator.getGamepads = this.#nativeGetGamepads; + window.navigator.getGamepads = this.nativeGetGamepads; } this.waitForMouseData(true); - this.#mouseDataProvider?.stop(); + this.mouseDataProvider?.stop(); // Toast.show(t('virtual-controller'), t('disabled'), {instant: true}); } static setupEvents() { - isFullVersion() && window.addEventListener(BxEvent.STREAM_PLAYING, () => { - if (STATES.currentStream.titleInfo?.details.hasMkbSupport) { - // Enable native MKB in Android app - if (AppInterface && getPref(PrefKey.NATIVE_MKB_ENABLED) === 'on') { - AppInterface && NativeMkbHandler.getInstance().init(); + if (isFullVersion()) { + window.addEventListener(BxEvent.STREAM_PLAYING, () => { + if (STATES.currentStream.titleInfo?.details.hasMkbSupport) { + // Enable native MKB in Android app + NativeMkbHandler.getInstance()?.init(); + } else { + EmulatedMkbHandler.getInstance()?.init(); } - } else if (getPref(PrefKey.MKB_ENABLED) && (AppInterface || !UserAgent.isMobile())) { - BxLogger.info(EmulatedMkbHandler.LOG_TAG, 'Emulate MKB'); - EmulatedMkbHandler.getInstance().init(); + }); + + if (EmulatedMkbHandler.isAllowed()) { + window.addEventListener(BxEvent.MKB_UPDATED, () => { + EmulatedMkbHandler.getInstance()?.refreshPresetData(); + }); } - }); + } } } diff --git a/src/modules/mkb/mkb-popup.ts b/src/modules/mkb/mkb-popup.ts new file mode 100755 index 0000000..9808f08 --- /dev/null +++ b/src/modules/mkb/mkb-popup.ts @@ -0,0 +1,110 @@ +import { CE, createButton, ButtonStyle, type BxButtonOptions } from "@/utils/html"; +import { t } from "@/utils/translation"; +import { BxEvent } from "@/utils/bx-event"; +import { ShortcutAction } from "@/enums/shortcut-actions"; +import { SettingsDialog } from "../ui/dialog/settings-dialog"; +import type { MkbHandler } from "./base-mkb-handler"; +import { NativeMkbHandler } from "./native-mkb-handler"; +import { StreamSettings } from "@/utils/stream-settings"; +import { KeyHelper } from "./key-helper"; + +type MkbPopupType = 'virtual' | 'native'; + +export class MkbPopup { + private static instance: MkbPopup; + public static getInstance = () => MkbPopup.instance ?? (MkbPopup.instance = new MkbPopup()); + + private popupType!: MkbPopupType; + private $popup!: HTMLElement; + private $title!: HTMLElement; + private $btnActivate!: HTMLButtonElement; + + private mkbHandler!: MkbHandler; + + constructor() { + this.render(); + + window.addEventListener(BxEvent.KEYBOARD_SHORTCUTS_UPDATED, e => { + const $newButton = this.createActivateButton(); + this.$btnActivate.replaceWith($newButton); + this.$btnActivate = $newButton; + }); + } + + attachMkbHandler(handler: MkbHandler) { + this.mkbHandler = handler; + + // Set popupType + this.popupType = (handler instanceof NativeMkbHandler) ? 'native' : 'virtual'; + this.$popup.dataset.type = this.popupType; + + // Update popup title + this.$title.innerText = t(this.popupType === 'native' ? 'native-mkb' : 'virtual-controller'); + } + + toggleVisibility(show: boolean) { + this.$popup.classList.toggle('bx-gone', !show); + show && this.moveOffscreen(false); + } + + moveOffscreen(doMove: boolean) { + this.$popup.classList.toggle('bx-offscreen', doMove); + } + + private createActivateButton() { + const options: BxButtonOptions = { + style: ButtonStyle.PRIMARY | ButtonStyle.TALL | ButtonStyle.FULL_WIDTH, + label: t('activate'), + onClick: this.onActivate, + }; + + // Find shortcut key + const shortcutKey = StreamSettings.findKeyboardShortcut(ShortcutAction.MKB_TOGGLE); + if (shortcutKey) { + options.secondaryText = t('press-key-to-toggle-mkb', { key: KeyHelper.codeToKeyName(shortcutKey) }); + } + + return createButton(options); + } + + private onActivate = (e: Event) => { + e.preventDefault(); + this.mkbHandler.toggle(true); + } + + private render() { + this.$popup = CE('div', { class: 'bx-mkb-pointer-lock-msg bx-gone' }, + this.$title = CE('p'), + this.$btnActivate = this.createActivateButton(), + + CE('div', {}, + createButton({ + label: t('ignore'), + style: ButtonStyle.GHOST, + onClick: e => { + e.preventDefault(); + this.mkbHandler.toggle(false); + this.mkbHandler.waitForMouseData(false); + }, + }), + + createButton({ + label: t('manage'), + style: ButtonStyle.FOCUSABLE, + onClick: () => { + const dialog = SettingsDialog.getInstance(); + dialog.focusTab('mkb'); + dialog.show(); + }, + }), + ), + ); + + document.documentElement.appendChild(this.$popup); + } + + reset() { + this.toggleVisibility(true); + this.moveOffscreen(false); + } +} diff --git a/src/modules/mkb/mkb-preset.ts b/src/modules/mkb/mkb-preset.ts deleted file mode 100644 index 778dd1f..0000000 --- a/src/modules/mkb/mkb-preset.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { t } from "@utils/translation"; -import { GamepadKey, MouseButtonCode, MouseMapTo, MkbPresetKey } from "@enums/mkb"; -import { EmulatedMkbHandler } from "./mkb-handler"; -import type { MkbPresetData, MkbConvertedPresetData } from "@/types/mkb"; -import type { PreferenceSettings } from "@/types/preferences"; -import { SettingElementType } from "@/utils/setting-element"; - - -export class MkbPreset { - static MOUSE_SETTINGS: PreferenceSettings = { - [MkbPresetKey.MOUSE_MAP_TO]: { - label: t('map-mouse-to'), - type: SettingElementType.OPTIONS, - default: MouseMapTo[MouseMapTo.RS], - options: { - [MouseMapTo[MouseMapTo.RS]]: t('right-stick'), - [MouseMapTo[MouseMapTo.LS]]: t('left-stick'), - [MouseMapTo[MouseMapTo.OFF]]: t('off'), - }, - }, - - [MkbPresetKey.MOUSE_SENSITIVITY_Y]: { - label: t('horizontal-sensitivity'), - type: SettingElementType.NUMBER_STEPPER, - default: 50, - min: 1, - max: 300, - - params: { - suffix: '%', - exactTicks: 50, - }, - }, - - [MkbPresetKey.MOUSE_SENSITIVITY_X]: { - label: t('vertical-sensitivity'), - type: SettingElementType.NUMBER_STEPPER, - default: 50, - min: 1, - max: 300, - - params: { - suffix: '%', - exactTicks: 50, - }, - }, - - [MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT]: { - label: t('deadzone-counterweight'), - type: SettingElementType.NUMBER_STEPPER, - default: 20, - min: 1, - max: 50, - - params: { - suffix: '%', - exactTicks: 10, - }, - }, - }; - - static DEFAULT_PRESET: MkbPresetData = { - 'mapping': { - // Use "e.code" value from https://keyjs.dev - [GamepadKey.UP]: ['ArrowUp'], - [GamepadKey.DOWN]: ['ArrowDown'], - [GamepadKey.LEFT]: ['ArrowLeft'], - [GamepadKey.RIGHT]: ['ArrowRight'], - - [GamepadKey.LS_UP]: ['KeyW'], - [GamepadKey.LS_DOWN]: ['KeyS'], - [GamepadKey.LS_LEFT]: ['KeyA'], - [GamepadKey.LS_RIGHT]: ['KeyD'], - - [GamepadKey.RS_UP]: ['KeyI'], - [GamepadKey.RS_DOWN]: ['KeyK'], - [GamepadKey.RS_LEFT]: ['KeyJ'], - [GamepadKey.RS_RIGHT]: ['KeyL'], - - [GamepadKey.A]: ['Space', 'KeyE'], - [GamepadKey.X]: ['KeyR'], - [GamepadKey.B]: ['ControlLeft', 'Backspace'], - [GamepadKey.Y]: ['KeyV'], - - [GamepadKey.START]: ['Enter'], - [GamepadKey.SELECT]: ['Tab'], - - [GamepadKey.LB]: ['KeyC', 'KeyG'], - [GamepadKey.RB]: ['KeyQ'], - - [GamepadKey.HOME]: ['Backquote'], - - [GamepadKey.RT]: [MouseButtonCode.LEFT_CLICK], - [GamepadKey.LT]: [MouseButtonCode.RIGHT_CLICK], - - [GamepadKey.L3]: ['ShiftLeft'], - [GamepadKey.R3]: ['KeyF'], - }, - - 'mouse': { - [MkbPresetKey.MOUSE_MAP_TO]: MouseMapTo[MouseMapTo.RS], - [MkbPresetKey.MOUSE_SENSITIVITY_X]: 100, - [MkbPresetKey.MOUSE_SENSITIVITY_Y]: 100, - [MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT]: 20, - }, - }; - - static convert(preset: MkbPresetData): MkbConvertedPresetData { - const obj: MkbConvertedPresetData = { - mapping: {}, - mouse: Object.assign({}, preset.mouse), - }; - - for (const buttonIndex in preset.mapping) { - for (const keyName of preset.mapping[parseInt(buttonIndex)]) { - obj.mapping[keyName!] = parseInt(buttonIndex); - } - } - - // Pre-calculate mouse's sensitivities - const mouse = obj.mouse; - mouse[MkbPresetKey.MOUSE_SENSITIVITY_X] *= EmulatedMkbHandler.DEFAULT_PANNING_SENSITIVITY; - mouse[MkbPresetKey.MOUSE_SENSITIVITY_Y] *= EmulatedMkbHandler.DEFAULT_PANNING_SENSITIVITY; - mouse[MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT] *= EmulatedMkbHandler.DEFAULT_DEADZONE_COUNTERWEIGHT; - - const mouseMapTo = MouseMapTo[mouse[MkbPresetKey.MOUSE_MAP_TO]!]; - if (typeof mouseMapTo !== 'undefined') { - mouse[MkbPresetKey.MOUSE_MAP_TO] = mouseMapTo; - } else { - mouse[MkbPresetKey.MOUSE_MAP_TO] = MkbPreset.MOUSE_SETTINGS[MkbPresetKey.MOUSE_MAP_TO].default; - } - - return obj; - } -} diff --git a/src/modules/mkb/mkb-remapper.ts b/src/modules/mkb/mkb-remapper.ts deleted file mode 100644 index 20f65ba..0000000 --- a/src/modules/mkb/mkb-remapper.ts +++ /dev/null @@ -1,541 +0,0 @@ -import { CE, createButton, ButtonStyle, removeChildElements } from "@utils/html"; -import { t } from "@utils/translation"; -import { Dialog } from "@modules/dialog"; -import { KeyHelper } from "./key-helper"; -import { MkbPreset } from "./mkb-preset"; -import { EmulatedMkbHandler } from "./mkb-handler"; -import { BxIcon } from "@utils/bx-icon"; -import type { MkbPresetData, MkbStoredPresets } from "@/types/mkb"; -import { MkbPresetKey, GamepadKey, GamepadKeyName } from "@enums/mkb"; -import { deepClone } from "@utils/global"; -import { SettingElement } from "@/utils/setting-element"; -import { PrefKey } from "@/enums/pref-keys"; -import { getPref, setPref } from "@/utils/settings-storages/global-settings-storage"; -import { MkbPresetsDb } from "@/utils/local-db/mkb-presets-db"; -import { BxLogger } from "@/utils/bx-logger"; - - -type MkbRemapperStates = { - currentPresetId: number; - presets: MkbStoredPresets; - - editingPresetData?: MkbPresetData | null; - isEditing: boolean; -}; - -export class MkbRemapper { - private readonly BUTTON_ORDERS = [ - GamepadKey.UP, - GamepadKey.DOWN, - GamepadKey.LEFT, - GamepadKey.RIGHT, - - GamepadKey.A, - GamepadKey.B, - GamepadKey.X, - GamepadKey.Y, - - GamepadKey.LB, - GamepadKey.RB, - GamepadKey.LT, - GamepadKey.RT, - - GamepadKey.SELECT, - GamepadKey.START, - GamepadKey.HOME, - - GamepadKey.L3, - GamepadKey.LS_UP, - GamepadKey.LS_DOWN, - GamepadKey.LS_LEFT, - GamepadKey.LS_RIGHT, - - GamepadKey.R3, - GamepadKey.RS_UP, - GamepadKey.RS_DOWN, - GamepadKey.RS_LEFT, - GamepadKey.RS_RIGHT, - ]; - - private static instance: MkbRemapper; - public static getInstance = () => MkbRemapper.instance ?? (MkbRemapper.instance = new MkbRemapper()); - private readonly LOG_TAG = 'MkbRemapper'; - - private states: MkbRemapperStates = { - currentPresetId: 0, - presets: {}, - editingPresetData: null, - isEditing: false, - }; - - private $wrapper!: HTMLElement; - private $presetsSelect!: HTMLSelectElement; - private $activateButton!: HTMLButtonElement; - - private $currentBindingKey!: HTMLElement; - - private allKeyElements: HTMLElement[] = []; - private allMouseElements: {[key in MkbPresetKey]?: HTMLElement} = {}; - - bindingDialog: Dialog; - - private constructor() { - BxLogger.info(this.LOG_TAG, 'constructor()'); - this.states.currentPresetId = getPref(PrefKey.MKB_DEFAULT_PRESET_ID); - - this.bindingDialog = new Dialog({ - className: 'bx-binding-dialog', - content: CE('div', {}, - CE('p', {}, t('press-to-bind')), - CE('i', {}, t('press-esc-to-cancel')), - ), - hideCloseButton: true, - }); - } - - private clearEventListeners = () => { - window.removeEventListener('keydown', this.onKeyDown); - window.removeEventListener('mousedown', this.onMouseDown); - window.removeEventListener('wheel', this.onWheel); - }; - - private bindKey = ($elm: HTMLElement, key: any) => { - const buttonIndex = parseInt($elm.dataset.buttonIndex!); - const keySlot = parseInt($elm.dataset.keySlot!); - - // Ignore if bind the save key to the same element - if ($elm.dataset.keyCode! === key.code) { - return; - } - - // Unbind duplicated keys - for (const $otherElm of this.allKeyElements) { - if ($otherElm.dataset.keyCode === key.code) { - this.unbindKey($otherElm); - } - } - - this.states.editingPresetData!.mapping[buttonIndex][keySlot] = key.code; - $elm.textContent = key.name; - $elm.dataset.keyCode = key.code; - } - - private unbindKey = ($elm: HTMLElement) => { - const buttonIndex = parseInt($elm.dataset.buttonIndex!); - const keySlot = parseInt($elm.dataset.keySlot!); - - // Remove key from preset - this.states.editingPresetData!.mapping[buttonIndex][keySlot] = null; - $elm.textContent = ''; - delete $elm.dataset.keyCode; - } - - private onWheel = (e: WheelEvent) => { - e.preventDefault(); - this.clearEventListeners(); - - this.bindKey(this.$currentBindingKey!, KeyHelper.getKeyFromEvent(e)); - window.setTimeout(() => this.bindingDialog.hide(), 200); - }; - - private onMouseDown = (e: MouseEvent) => { - e.preventDefault(); - this.clearEventListeners(); - - this.bindKey(this.$currentBindingKey!, KeyHelper.getKeyFromEvent(e)); - window.setTimeout(() => this.bindingDialog.hide(), 200); - }; - - private onKeyDown = (e: KeyboardEvent) => { - e.preventDefault(); - e.stopPropagation(); - this.clearEventListeners(); - - if (e.code !== 'Escape') { - this.bindKey(this.$currentBindingKey!, KeyHelper.getKeyFromEvent(e)); - } - - window.setTimeout(() => this.bindingDialog.hide(), 200); - }; - - private onBindingKey = (e: MouseEvent) => { - if (!this.states.isEditing || e.button !== 0) { - return; - } - - console.log(e); - - this.$currentBindingKey = e.target as HTMLElement; - - window.addEventListener('keydown', this.onKeyDown); - window.addEventListener('mousedown', this.onMouseDown); - window.addEventListener('wheel', this.onWheel); - - this.bindingDialog.show({title: this.$currentBindingKey.dataset.prompt!}); - }; - - private onContextMenu = (e: Event) => { - e.preventDefault(); - if (!this.states.isEditing) { - return; - } - - this.unbindKey(e.target as HTMLElement); - }; - - private getPreset = (presetId: number) => { - return this.states.presets[presetId]; - } - - private getCurrentPreset = () => { - let preset = this.getPreset(this.states.currentPresetId); - if (!preset) { - // Get the first preset in the list - const firstPresetId = parseInt(Object.keys(this.states.presets)[0]); - preset = this.states.presets[firstPresetId]; - this.states.currentPresetId = firstPresetId; - setPref(PrefKey.MKB_DEFAULT_PRESET_ID, firstPresetId); - } - - return preset; - } - - private switchPreset = (presetId: number) => { - this.states.currentPresetId = presetId; - const presetData = this.getCurrentPreset().data; - - for (const $elm of this.allKeyElements) { - const buttonIndex = parseInt($elm.dataset.buttonIndex!); - const keySlot = parseInt($elm.dataset.keySlot!); - - const buttonKeys = presetData.mapping[buttonIndex]; - if (buttonKeys && buttonKeys[keySlot]) { - $elm.textContent = KeyHelper.codeToKeyName(buttonKeys[keySlot]!); - $elm.dataset.keyCode = buttonKeys[keySlot]!; - } else { - $elm.textContent = ''; - delete $elm.dataset.keyCode; - } - } - - let key: MkbPresetKey; - for (key in this.allMouseElements) { - const $elm = this.allMouseElements[key]!; - let value = presetData.mouse[key]; - if (typeof value === 'undefined') { - value = MkbPreset.MOUSE_SETTINGS[key].default; - } - - 'setValue' in $elm && ($elm as any).setValue(value); - } - - // Update state of Activate button - const activated = getPref(PrefKey.MKB_DEFAULT_PRESET_ID) === this.states.currentPresetId; - this.$activateButton.disabled = activated; - this.$activateButton.querySelector('span')!.textContent = activated ? t('activated') : t('activate'); - } - - private async refresh() { - // Clear presets select - removeChildElements(this.$presetsSelect); - - const presets = await MkbPresetsDb.getInstance().getPresets(); - - this.states.presets = presets; - const fragment = document.createDocumentFragment(); - - let defaultPresetId; - if (this.states.currentPresetId === 0) { - this.states.currentPresetId = parseInt(Object.keys(presets)[0]); - - defaultPresetId = this.states.currentPresetId; - setPref(PrefKey.MKB_DEFAULT_PRESET_ID, defaultPresetId); - EmulatedMkbHandler.getInstance().refreshPresetData(); - } else { - defaultPresetId = getPref(PrefKey.MKB_DEFAULT_PRESET_ID); - } - - for (let id in presets) { - const preset = presets[id]; - let name = preset.name; - if (id === defaultPresetId) { - name = `🎮 ` + name; - } - - const $options = CE('option', {value: id}, name); - $options.selected = parseInt(id) === this.states.currentPresetId; - - fragment.appendChild($options); - }; - - this.$presetsSelect.appendChild(fragment); - - // Update state of Activate button - const activated = defaultPresetId === this.states.currentPresetId; - this.$activateButton.disabled = activated; - this.$activateButton.querySelector('span')!.textContent = activated ? t('activated') : t('activate'); - - !this.states.isEditing && this.switchPreset(this.states.currentPresetId); - } - - private toggleEditing = (force?: boolean) => { - this.states.isEditing = typeof force !== 'undefined' ? force : !this.states.isEditing; - this.$wrapper.classList.toggle('bx-editing', this.states.isEditing); - - if (this.states.isEditing) { - this.states.editingPresetData = deepClone(this.getCurrentPreset().data); - } else { - this.states.editingPresetData = null; - } - - - const childElements = this.$wrapper.querySelectorAll('select, button, input'); - for (const $elm of Array.from(childElements)) { - if ($elm.parentElement!.parentElement!.classList.contains('bx-mkb-action-buttons')) { - continue; - } - - let disable = !this.states.isEditing; - - if ($elm.parentElement!.classList.contains('bx-mkb-preset-tools')) { - disable = !disable; - } - - ($elm as HTMLButtonElement).disabled = disable; - } - } - - render() { - this.$wrapper = CE('div', {class: 'bx-mkb-settings'}); - - this.$presetsSelect = CE('select', {tabindex: -1}); - this.$presetsSelect.addEventListener('change', e => { - this.switchPreset(parseInt((e.target as HTMLSelectElement).value)); - }); - - const promptNewName = (value: string) => { - let newName: string | null = ''; - while (!newName) { - newName = prompt(t('prompt-preset-name'), value); - if (newName === null) { - return false; - } - newName = newName.trim(); - } - - return newName ? newName : false; - }; - - const $header = CE('div', {class: 'bx-mkb-preset-tools'}, - this.$presetsSelect, - // Rename button - createButton({ - title: t('rename'), - icon: BxIcon.CURSOR_TEXT, - tabIndex: -1, - onClick: async () => { - const preset = this.getCurrentPreset(); - - let newName = promptNewName(preset.name); - if (!newName || newName === preset.name) { - return; - } - - // Update preset with new name - preset.name = newName; - - await MkbPresetsDb.getInstance().updatePreset(preset); - await this.refresh(); - }, - }), - - // New button - createButton({ - icon: BxIcon.NEW, - title: t('new'), - tabIndex: -1, - onClick: e => { - let newName = promptNewName(''); - if (!newName) { - return; - } - - // Create new preset selected name - MkbPresetsDb.getInstance().newPreset(newName, MkbPreset.DEFAULT_PRESET).then(id => { - this.states.currentPresetId = id; - this.refresh(); - }); - }, - }), - - // Copy button - createButton({ - icon: BxIcon.COPY, - title: t('copy'), - tabIndex: -1, - onClick: e => { - const preset = this.getCurrentPreset(); - - let newName = promptNewName(`${preset.name} (2)`); - if (!newName) { - return; - } - - // Create new preset selected name - MkbPresetsDb.getInstance().newPreset(newName, preset.data).then(id => { - this.states.currentPresetId = id; - this.refresh(); - }); - }, - }), - - // Delete button - createButton({ - icon: BxIcon.TRASH, - style: ButtonStyle.DANGER, - title: t('delete'), - tabIndex: -1, - onClick: e => { - if (!confirm(t('confirm-delete-preset'))) { - return; - } - - MkbPresetsDb.getInstance().deletePreset(this.states.currentPresetId).then(id => { - this.states.currentPresetId = 0; - this.refresh(); - }); - }, - }), - ); - - this.$wrapper.appendChild($header); - - const $rows = CE('div', {class: 'bx-mkb-settings-rows'}, - CE('i', {class: 'bx-mkb-note'}, t('right-click-to-unbind')), - ); - - // Render keys - const keysPerButton = 2; - for (const buttonIndex of this.BUTTON_ORDERS) { - const [buttonName, buttonPrompt] = GamepadKeyName[buttonIndex]; - - let $elm; - const $fragment = document.createDocumentFragment(); - for (let i = 0; i < keysPerButton; i++) { - $elm = CE('button', { - type: 'button', - 'data-prompt': buttonPrompt, - 'data-button-index': buttonIndex, - 'data-key-slot': i, - }, ' '); - - $elm.addEventListener('mouseup', this.onBindingKey); - $elm.addEventListener('contextmenu', this.onContextMenu); - - $fragment.appendChild($elm); - this.allKeyElements.push($elm); - } - - const $keyRow = CE('div', {class: 'bx-mkb-key-row'}, - CE('label', {title: buttonName}, buttonPrompt), - $fragment, - ); - - $rows.appendChild($keyRow); - } - - $rows.appendChild(CE('i', {class: 'bx-mkb-note'}, t('mkb-adjust-ingame-settings')),); - - // Render mouse settings - const $mouseSettings = document.createDocumentFragment(); - - for (const key in MkbPreset.MOUSE_SETTINGS) { - const setting = MkbPreset.MOUSE_SETTINGS[key]; - const value = setting.default; - - let $elm; - const onChange = (e: Event, value: any) => { - (this.states.editingPresetData!.mouse as any)[key] = value; - }; - const $row = CE('label', { - class: 'bx-settings-row', - for: `bx_setting_${key}` - }, - CE('span', {class: 'bx-settings-label'}, setting.label), - $elm = SettingElement.render(setting.type, key, setting, value, onChange, setting.params), - ); - - $mouseSettings.appendChild($row); - this.allMouseElements[key as MkbPresetKey] = $elm; - } - - $rows.appendChild($mouseSettings); - this.$wrapper.appendChild($rows); - - // Render action buttons - const $actionButtons = CE('div', {class: 'bx-mkb-action-buttons'}, - CE('div', {}, - // Edit button - createButton({ - label: t('edit'), - tabIndex: -1, - onClick: e => this.toggleEditing(true), - }), - - // Activate button - this.$activateButton = createButton({ - label: t('activate'), - style: ButtonStyle.PRIMARY, - tabIndex: -1, - onClick: e => { - setPref(PrefKey.MKB_DEFAULT_PRESET_ID, this.states.currentPresetId); - EmulatedMkbHandler.getInstance().refreshPresetData(); - - this.refresh(); - }, - }), - ), - - CE('div', {}, - // Cancel button - createButton({ - label: t('cancel'), - style: ButtonStyle.GHOST, - tabIndex: -1, - onClick: e => { - // Restore preset - this.switchPreset(this.states.currentPresetId); - this.toggleEditing(false); - }, - }), - - // Save button - createButton({ - label: t('save'), - style: ButtonStyle.PRIMARY, - tabIndex: -1, - onClick: e => { - const updatedPreset = deepClone(this.getCurrentPreset()); - updatedPreset.data = this.states.editingPresetData as MkbPresetData; - - MkbPresetsDb.getInstance().updatePreset(updatedPreset).then(id => { - // If this is the default preset => refresh preset data - if (id === getPref(PrefKey.MKB_DEFAULT_PRESET_ID)) { - EmulatedMkbHandler.getInstance().refreshPresetData(); - } - - this.toggleEditing(false); - this.refresh(); - }); - }, - }), - ), - ); - - this.$wrapper.appendChild($actionButtons); - - this.toggleEditing(false); - this.refresh(); - return this.$wrapper; - } -} diff --git a/src/modules/mkb/mouse-cursor-hider.ts b/src/modules/mkb/mouse-cursor-hider.ts old mode 100644 new mode 100755 index 596d6db..1bbf475 --- a/src/modules/mkb/mouse-cursor-hider.ts +++ b/src/modules/mkb/mouse-cursor-hider.ts @@ -1,34 +1,52 @@ +import { PrefKey } from "@/enums/pref-keys"; +import { getPref } from "@/utils/settings-storages/global-settings-storage"; + export class MouseCursorHider { - static #timeout: number | null; - static #cursorVisible = true; + private static instance: MouseCursorHider | null | undefined; + public static getInstance(): typeof MouseCursorHider['instance'] { + if (typeof MouseCursorHider.instance === 'undefined') { + if (!getPref(PrefKey.MKB_ENABLED) && getPref(PrefKey.MKB_HIDE_IDLE_CURSOR)) { + MouseCursorHider.instance = new MouseCursorHider(); + } else { + MouseCursorHider.instance = null; + } + } - static show() { + return MouseCursorHider.instance; + } + + private timeoutId!: number | null; + private isCursorVisible = true; + + show() { document.body && (document.body.style.cursor = 'unset'); - MouseCursorHider.#cursorVisible = true; + this.isCursorVisible = true; } - static hide() { + hide() { document.body && (document.body.style.cursor = 'none'); - MouseCursorHider.#timeout = null; - MouseCursorHider.#cursorVisible = false; + this.timeoutId = null; + this.isCursorVisible = false; } - static onMouseMove(e: MouseEvent) { + onMouseMove = (e: MouseEvent) => { // Toggle cursor - !MouseCursorHider.#cursorVisible && MouseCursorHider.show(); + !this.isCursorVisible && this.show(); // Setup timeout - MouseCursorHider.#timeout && clearTimeout(MouseCursorHider.#timeout); - MouseCursorHider.#timeout = window.setTimeout(MouseCursorHider.hide, 3000); + this.timeoutId && clearTimeout(this.timeoutId); + this.timeoutId = window.setTimeout(this.hide, 3000); } - static start() { - MouseCursorHider.show(); - document.addEventListener('mousemove', MouseCursorHider.onMouseMove); + start() { + this.show(); + document.addEventListener('mousemove', this.onMouseMove); } - static stop() { - MouseCursorHider.#timeout && clearTimeout(MouseCursorHider.#timeout); - document.removeEventListener('mousemove', MouseCursorHider.onMouseMove); - MouseCursorHider.show(); + stop() { + this.timeoutId && clearTimeout(this.timeoutId); + this.timeoutId = null; + + document.removeEventListener('mousemove', this.onMouseMove); + this.show(); } } diff --git a/src/modules/mkb/native-mkb-handler.ts b/src/modules/mkb/native-mkb-handler.ts old mode 100644 new mode 100755 index 2a2d6c4..987a25d --- a/src/modules/mkb/native-mkb-handler.ts +++ b/src/modules/mkb/native-mkb-handler.ts @@ -4,10 +4,14 @@ import { AppInterface, STATES } from "@/utils/global"; import { MkbHandler } from "./base-mkb-handler"; import { t } from "@/utils/translation"; import { BxEvent } from "@/utils/bx-event"; -import { ButtonStyle, CE, createButton } from "@/utils/html"; import { PrefKey } from "@/enums/pref-keys"; import { getPref } from "@/utils/settings-storages/global-settings-storage"; import { BxLogger } from "@/utils/bx-logger"; +import { MkbPopup } from "./mkb-popup"; +import { KeyHelper } from "./key-helper"; +import { StreamSettings } from "@/utils/stream-settings"; +import { ShortcutAction } from "@/enums/shortcut-actions"; +import { NativeMkbMode } from "@/enums/pref-values"; type NativeMouseData = { X: number, @@ -15,7 +19,7 @@ type NativeMouseData = { Buttons: number, WheelX: number, WheelY: number, - Type? : 0, // 0: Relative, 1: Absolute + Type?: 0, // 0: Relative, 1: Absolute } type XcloudInputSink = { @@ -23,30 +27,47 @@ type XcloudInputSink = { } export class NativeMkbHandler extends MkbHandler { - private static instance: NativeMkbHandler; - public static getInstance = () => NativeMkbHandler.instance ?? (NativeMkbHandler.instance = new NativeMkbHandler()); + private static instance: NativeMkbHandler | null | undefined; + public static getInstance(): typeof NativeMkbHandler['instance'] { + if (typeof NativeMkbHandler.instance === 'undefined') { + if (NativeMkbHandler.isAllowed()) { + NativeMkbHandler.instance = new NativeMkbHandler(); + } else { + NativeMkbHandler.instance = null; + } + } + + return NativeMkbHandler.instance; + } private readonly LOG_TAG = 'NativeMkbHandler'; - #pointerClient: PointerClient | undefined; - #enabled: boolean = false; + static isAllowed = () => { + return STATES.browser.capabilities.emulatedNativeMkb && getPref(PrefKey.NATIVE_MKB_MODE) === NativeMkbMode.ON; + } - #mouseButtonsPressed = 0; - #mouseWheelX = 0; - #mouseWheelY = 0; + private pointerClient: PointerClient | undefined; + private enabled = false; - #mouseVerticalMultiply = 0; - #mouseHorizontalMultiply = 0; + private mouseButtonsPressed = 0; + private mouseWheelX = 0; + private mouseWheelY = 0; - #inputSink: XcloudInputSink | undefined; + private mouseVerticalMultiply = 0; + private mouseHorizontalMultiply = 0; - #$message?: HTMLElement; + private inputSink: XcloudInputSink | undefined; + + private popup!: MkbPopup; private constructor() { super(); BxLogger.info(this.LOG_TAG, 'constructor()'); + + this.popup = MkbPopup.getInstance(); + this.popup.attachMkbHandler(this); } - #onKeyboardEvent(e: KeyboardEvent) { + private onKeyboardEvent(e: KeyboardEvent) { if (e.type === 'keyup' && e.code === 'F8') { e.preventDefault(); this.toggle(); @@ -54,110 +75,63 @@ export class NativeMkbHandler extends MkbHandler { } } - #onPointerLockRequested(e: Event) { + private onPointerLockRequested(e: Event) { AppInterface.requestPointerCapture(); this.start(); } - #onPointerLockExited(e: Event) { + private onPointerLockExited(e: Event) { AppInterface.releasePointerCapture(); this.stop(); } - #onPollingModeChanged = (e: Event) => { - if (!this.#$message) { - return; - } - - const mode = (e as any).mode; - if (mode === 'none') { - this.#$message.classList.remove('bx-offscreen'); - } else { - this.#$message.classList.add('bx-offscreen'); - } + private onPollingModeChanged = (e: Event) => { + const move = window.BX_STREAM_SETTINGS.xCloudPollingMode !== 'none'; + this.popup.moveOffscreen(move); } - #onDialogShown = () => { + private onDialogShown = () => { document.pointerLockElement && document.exitPointerLock(); } - #initMessage() { - if (!this.#$message) { - this.#$message = CE('div', {'class': 'bx-mkb-pointer-lock-msg'}, - CE('div', {}, - CE('p', {}, t('native-mkb')), - CE('p', {}, t('press-key-to-toggle-mkb', {key: 'F8'})), - ), - - CE('div', {'data-type': 'native'}, - createButton({ - style: ButtonStyle.PRIMARY | ButtonStyle.FULL_WIDTH | ButtonStyle.TALL, - label: t('activate'), - onClick: ((e: Event) => { - e.preventDefault(); - e.stopPropagation(); - - this.toggle(true); - }).bind(this), - }), - - createButton({ - style: ButtonStyle.GHOST | ButtonStyle.FULL_WIDTH, - label: t('ignore'), - onClick: e => { - e.preventDefault(); - e.stopPropagation(); - - this.#$message?.classList.add('bx-gone'); - }, - }), - ), - ); - } - - if (!this.#$message.isConnected) { - document.documentElement.appendChild(this.#$message); - } - } - handleEvent(event: Event) { switch (event.type) { case 'keyup': - this.#onKeyboardEvent(event as KeyboardEvent); + this.onKeyboardEvent(event as KeyboardEvent); break; case BxEvent.XCLOUD_DIALOG_SHOWN: - this.#onDialogShown(); + this.onDialogShown(); break; case BxEvent.POINTER_LOCK_REQUESTED: - this.#onPointerLockRequested(event); + this.onPointerLockRequested(event); break; case BxEvent.POINTER_LOCK_EXITED: - this.#onPointerLockExited(event); + this.onPointerLockExited(event); break; case BxEvent.XCLOUD_POLLING_MODE_CHANGED: - this.#onPollingModeChanged(event); + this.onPollingModeChanged(event); break; } } init() { - this.#pointerClient = PointerClient.getInstance(); - this.#inputSink = window.BX_EXPOSED.inputSink; + this.pointerClient = PointerClient.getInstance(); + this.inputSink = window.BX_EXPOSED.inputSink; // Stop keyboard input at startup - this.#updateInputConfigurationAsync(false); + this.updateInputConfigurationAsync(false); try { - this.#pointerClient.start(STATES.pointerServerPort, this); + this.pointerClient.start(STATES.pointerServerPort, this); } catch (e) { Toast.show('Cannot enable Mouse & Keyboard feature'); } - this.#mouseVerticalMultiply = getPref(PrefKey.NATIVE_MKB_SCROLL_VERTICAL_SENSITIVITY); - this.#mouseHorizontalMultiply = getPref(PrefKey.NATIVE_MKB_SCROLL_HORIZONTAL_SENSITIVITY); + this.mouseVerticalMultiply = getPref(PrefKey.NATIVE_MKB_SCROLL_VERTICAL_SENSITIVITY); + this.mouseHorizontalMultiply = getPref(PrefKey.NATIVE_MKB_SCROLL_HORIZONTAL_SENSITIVITY); window.addEventListener('keyup', this); @@ -166,14 +140,13 @@ export class NativeMkbHandler extends MkbHandler { window.addEventListener(BxEvent.POINTER_LOCK_EXITED, this); window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this); - this.#initMessage(); - - if (AppInterface) { - Toast.show(t('press-key-to-toggle-mkb', {key: `F8`}), t('native-mkb'), {html: true}); - this.#$message?.classList.add('bx-gone'); - } else { - this.#$message?.classList.remove('bx-gone'); + const shortcutKey = StreamSettings.findKeyboardShortcut(ShortcutAction.MKB_TOGGLE); + if (shortcutKey) { + const msg = t('press-key-to-toggle-mkb', { key: `${KeyHelper.codeToKeyName(shortcutKey)}` }); + Toast.show(msg, t('native-mkb'), { html: true }); } + + this.waitForMouseData(false); } toggle(force?: boolean) { @@ -181,7 +154,7 @@ export class NativeMkbHandler extends MkbHandler { if (typeof force !== 'undefined') { setEnable = force; } else { - setEnable = !this.#enabled; + setEnable = !this.enabled; } if (setEnable) { @@ -191,7 +164,7 @@ export class NativeMkbHandler extends MkbHandler { } } - #updateInputConfigurationAsync(enabled: boolean) { + private updateInputConfigurationAsync(enabled: boolean) { window.BX_EXPOSED.streamSession.updateInputConfigurationAsync({ enableKeyboardInput: enabled, enableMouseInput: enabled, @@ -201,27 +174,27 @@ export class NativeMkbHandler extends MkbHandler { } start() { - this.#resetMouseInput(); - this.#enabled = true; + this.resetMouseInput(); + this.enabled = true; - this.#updateInputConfigurationAsync(true); + this.updateInputConfigurationAsync(true); window.BX_EXPOSED.stopTakRendering = true; - this.#$message?.classList.add('bx-gone'); + this.waitForMouseData(false); Toast.show(t('native-mkb'), t('enabled'), {instant: true}); } stop() { - this.#resetMouseInput(); - this.#enabled = false; - this.#updateInputConfigurationAsync(false); + this.resetMouseInput(); + this.enabled = false; + this.updateInputConfigurationAsync(false); - this.#$message?.classList.remove('bx-gone'); + this.waitForMouseData(true); } destroy(): void { - this.#pointerClient?.stop(); + this.pointerClient?.stop(); window.removeEventListener('keyup', this); window.removeEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this); @@ -229,16 +202,16 @@ export class NativeMkbHandler extends MkbHandler { window.removeEventListener(BxEvent.POINTER_LOCK_EXITED, this); window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this); - this.#$message?.classList.add('bx-gone'); + this.waitForMouseData(false); } handleMouseMove(data: MkbMouseMove): void { - this.#sendMouseInput({ + this.sendMouseInput({ X: data.movementX, Y: data.movementY, - Buttons: this.#mouseButtonsPressed, - WheelX: this.#mouseWheelX, - WheelY: this.#mouseWheelY, + Buttons: this.mouseButtonsPressed, + WheelX: this.mouseWheelX, + WheelY: this.mouseWheelY, }); } @@ -246,71 +219,72 @@ export class NativeMkbHandler extends MkbHandler { const { pointerButton, pressed } = data; if (pressed) { - this.#mouseButtonsPressed |= pointerButton!; + this.mouseButtonsPressed |= pointerButton!; } else { - this.#mouseButtonsPressed ^= pointerButton!; + this.mouseButtonsPressed ^= pointerButton!; } - this.#mouseButtonsPressed = Math.max(0, this.#mouseButtonsPressed); + this.mouseButtonsPressed = Math.max(0, this.mouseButtonsPressed); - this.#sendMouseInput({ + this.sendMouseInput({ X: 0, Y: 0, - Buttons: this.#mouseButtonsPressed, - WheelX: this.#mouseWheelX, - WheelY: this.#mouseWheelY, + Buttons: this.mouseButtonsPressed, + WheelX: this.mouseWheelX, + WheelY: this.mouseWheelY, }); } handleMouseWheel(data: MkbMouseWheel): boolean { const { vertical, horizontal } = data; - this.#mouseWheelX = horizontal; - if (this.#mouseHorizontalMultiply && this.#mouseHorizontalMultiply !== 1) { - this.#mouseWheelX *= this.#mouseHorizontalMultiply; + this.mouseWheelX = horizontal; + if (this.mouseHorizontalMultiply && this.mouseHorizontalMultiply !== 1) { + this.mouseWheelX *= this.mouseHorizontalMultiply; } - this.#mouseWheelY = vertical; - if (this.#mouseVerticalMultiply && this.#mouseVerticalMultiply !== 1) { - this.#mouseWheelY *= this.#mouseVerticalMultiply; + this.mouseWheelY = vertical; + if (this.mouseVerticalMultiply && this.mouseVerticalMultiply !== 1) { + this.mouseWheelY *= this.mouseVerticalMultiply; } - this.#sendMouseInput({ + this.sendMouseInput({ X: 0, Y: 0, - Buttons: this.#mouseButtonsPressed, - WheelX: this.#mouseWheelX, - WheelY: this.#mouseWheelY, + Buttons: this.mouseButtonsPressed, + WheelX: this.mouseWheelX, + WheelY: this.mouseWheelY, }); return true; } setVerticalScrollMultiplier(vertical: number) { - this.#mouseVerticalMultiply = vertical; + this.mouseVerticalMultiply = vertical; } setHorizontalScrollMultiplier(horizontal: number) { - this.#mouseHorizontalMultiply = horizontal; + this.mouseHorizontalMultiply = horizontal; } - waitForMouseData(enabled: boolean): void { + waitForMouseData(showPopup: boolean) { + this.popup.toggleVisibility(showPopup); } isEnabled(): boolean { - return this.#enabled; + return this.enabled; } - #sendMouseInput(data: NativeMouseData) { + private sendMouseInput(data: NativeMouseData) { data.Type = 0; // Relative - this.#inputSink?.onMouseInput(data); + this.inputSink?.onMouseInput(data); } - #resetMouseInput() { - this.#mouseButtonsPressed = 0; - this.#mouseWheelX = 0; - this.#mouseWheelY = 0; + private resetMouseInput() { + this.mouseButtonsPressed = 0; + this.mouseWheelX = 0; + this.mouseWheelY = 0; - this.#sendMouseInput({ + this.sendMouseInput({ X: 0, Y: 0, Buttons: 0, diff --git a/src/modules/mkb/pointer-client.ts b/src/modules/mkb/pointer-client.ts old mode 100644 new mode 100755 diff --git a/src/modules/patcher.ts b/src/modules/patcher.ts old mode 100644 new mode 100755 index e382711..ea740cd --- a/src/modules/patcher.ts +++ b/src/modules/patcher.ts @@ -1,6 +1,5 @@ -import { AppInterface, SCRIPT_VERSION, STATES } from "@utils/global"; +import { SCRIPT_VERSION, STATES } from "@utils/global"; import { BX_FLAGS } from "@utils/bx-flags"; -import { VibrationManager } from "@modules/vibration-manager"; import { BxLogger } from "@utils/bx-logger"; import { hashCode, renderString } from "@utils/utils"; import { BxEvent } from "@/utils/bx-event"; @@ -13,13 +12,14 @@ import codeRemotePlayEnable from "./patches/remote-play-enable.js" with { type: import codeRemotePlayKeepAlive from "./patches/remote-play-keep-alive.js" with { type: "text" }; import codeVibrationAdjust from "./patches/vibration-adjust.js" with { type: "text" }; import { FeatureGates } from "@/utils/feature-gates.js"; -import { UiSection } from "@/enums/ui-sections.js"; -import { PrefKey } from "@/enums/pref-keys.js"; -import { getPref, StreamTouchController } from "@/utils/settings-storages/global-settings-storage"; -import { GamePassCloudGallery } from "@/enums/game-pass-gallery.js"; -import { t } from "@/utils/translation.js"; +import { PrefKey, StorageKey } from "@/enums/pref-keys.js"; +import { getPref } from "@/utils/settings-storages/global-settings-storage"; +import { GamePassCloudGallery } from "@/enums/game-pass-gallery"; +import { t } from "@/utils/translation"; +import { NativeMkbMode, TouchControllerMode, UiLayout, UiSection } from "@/enums/pref-values"; -type PatchArray = (keyof typeof PATCHES)[]; +type PathName = keyof typeof PATCHES; +type PatchArray = PathName[]; class PatcherUtils { static indexOf(txt: string, searchString: string, startIndex: number, maxRange: number): number { @@ -117,7 +117,7 @@ const PATCHES = { return false; } - const layout = getPref(PrefKey.UI_LAYOUT) === 'tv' ? 'tv' : 'default'; + const layout = getPref(PrefKey.UI_LAYOUT) === UiLayout.TV ? UiLayout.TV : UiLayout.DEFAULT; return str.replace(text, `?"${layout}":"${layout}"`); }, @@ -211,7 +211,7 @@ const PATCHES = { // Patch polling rate const tmp = str.substring(setTimeoutIndex, setTimeoutIndex + 150); - const tmpPatched = tmp.replaceAll('Math.max(0,4-', 'Math.max(0,window.BX_CONTROLLER_POLLING_RATE-'); + const tmpPatched = tmp.replaceAll('Math.max(0,4-', 'Math.max(0,window.BX_STREAM_SETTINGS.controllerPollingRate - '); str = PatcherUtils.replaceWith(str, setTimeoutIndex, tmp, tmpPatched); // Block gamepad stats collecting @@ -268,7 +268,6 @@ logFunc(logTag, '//', logMessage); return false; } - VibrationManager.updateGlobalVars(); str = str.replaceAll(text, text + codeVibrationAdjust); return str; }, @@ -419,9 +418,9 @@ if (window.BX_EXPOSED.stopTakRendering) { } let autoOffCode = ''; - if (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === StreamTouchController.OFF) { + if (getPref(PrefKey.TOUCH_CONTROLLER_MODE) === TouchControllerMode.OFF) { autoOffCode = 'return;'; - } else if (getPref(PrefKey.STREAM_TOUCH_CONTROLLER_AUTO_OFF)) { + } else if (getPref(PrefKey.TOUCH_CONTROLLER_AUTO_OFF)) { autoOffCode = ` const gamepads = window.navigator.getGamepads(); let gamepadFound = false; @@ -476,7 +475,7 @@ e.guideUI = null; `; // Remove the TAK Edit button when the touch controller is disabled - if (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === StreamTouchController.OFF) { + if (getPref(PrefKey.TOUCH_CONTROLLER_MODE) === TouchControllerMode.OFF) { newCode += 'e.canShowTakHUD = false;'; } @@ -491,7 +490,8 @@ e.guideUI = null; } const newCode = ` -BxEvent.dispatch(window, BxEvent.XCLOUD_POLLING_MODE_CHANGED, {mode: e.toLowerCase()}); +window.BX_STREAM_SETTINGS.xCloudPollingMode = e.toLowerCase(); +BxEvent.dispatch(window, BxEvent.XCLOUD_POLLING_MODE_CHANGED); `; str = str.replace(text, text + newCode); return str; @@ -587,7 +587,7 @@ BxLogger.info('patchRemotePlayMkb', ${configsVar}); return false; } - const opacity = (getPref(PrefKey.STREAM_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; @@ -648,7 +648,16 @@ true` + text; }, enableNativeMkb(str: string) { - let text = 'e.mouseSupported&&e.keyboardSupported&&e.fullscreenSupported;'; + // l = t.mouseSupported && t.keyboardSupported && t.fullscreenSupported; + let index = str.indexOf('.mouseSupported&&'); + if (index < 0) { + return false; + } + + // Get the variable name "t" + const varName = str.charAt(index - 1); + // Find the full text + let text = `${varName}.mouseSupported&&${varName}.keyboardSupported&&${varName}.fullscreenSupported;`; if ((!str.includes(text))) { return false; } @@ -827,7 +836,7 @@ true` + text; return false; } - const PREF_HIDE_SECTIONS = getPref(PrefKey.UI_HIDE_SECTIONS) as UiSection[]; + const PREF_HIDE_SECTIONS = getPref(PrefKey.UI_HIDE_SECTIONS); const siglIds: GamePassCloudGallery[] = []; const sections: PartialRecord = { @@ -906,31 +915,19 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) { // product-details-page.js#2388, 24.17.20 detectProductDetailsPage(str: string) { let index = str.indexOf('{location:"ProductDetailPage",'); + index >= 0 && (index = PatcherUtils.lastIndexOf('return', str, index, 200)); + if (index < 0) { return false; } - index = str.indexOf('return', index - 40); - if (index < 0) { - return false; - } - - str = str.substring(0, index) + 'BxEvent.dispatch(window, BxEvent.XCLOUD_RENDERING_COMPONENT, {component: "product-details"});' + str.substring(index); + str = str.substring(0, index) + 'BxEvent.dispatch(window, BxEvent.XCLOUD_RENDERING_COMPONENT, { component: "product-details" });' + str.substring(index); return str; }, detectBrowserRouterReady(str: string) { - let text = 'BrowserRouter:()=>'; - if (!str.includes(text)) { - return false; - } - let index = str.indexOf('{history:this.history,'); - if (index < 0) { - return false; - } - - index = PatcherUtils.lastIndexOf(str, 'return', index, 100); + index >= 0 && (index = PatcherUtils.lastIndexOf(str, 'return', index, 100)); if (index < 0) { return false; } @@ -998,10 +995,8 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) { }; let PATCH_ORDERS: PatchArray = [ - ...(getPref(PrefKey.NATIVE_MKB_ENABLED) === 'on' ? [ + ...(getPref(PrefKey.NATIVE_MKB_MODE) === NativeMkbMode.ON ? [ 'enableNativeMkb', - 'patchMouseAndKeyboardEnabled', - 'disableNativeRequestPointerLock', 'exposeInputSink', ] : []), @@ -1023,19 +1018,19 @@ let PATCH_ORDERS: PatchArray = [ 'guideAchievementsDefaultLocked', 'enableTvRoutes', - AppInterface && 'detectProductDetailsPage', + // AppInterface && 'detectProductDetailsPage', + 'supportLocalCoOp', 'overrideStorageGetSettings', getPref(PrefKey.UI_GAME_CARD_SHOW_WAIT_TIME) && 'patchSetCurrentlyFocusedInteractable', - getPref(PrefKey.UI_LAYOUT) !== 'default' && 'websiteLayout', - getPref(PrefKey.LOCAL_CO_OP_ENABLED) && 'supportLocalCoOp', + getPref(PrefKey.UI_LAYOUT) !== UiLayout.DEFAULT && 'websiteLayout', getPref(PrefKey.GAME_FORTNITE_FORCE_CONSOLE) && 'forceFortniteConsole', - getPref(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.FRIENDS) && 'ignorePlayWithFriendsSection', - getPref(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.ALL_GAMES) && 'ignoreAllGamesSection', - getPref(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.TOUCH) && 'ignorePlayWithTouchSection', - (getPref(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.NATIVE_MKB) || getPref(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.MOST_POPULAR)) && 'ignoreSiglSections', + getPref(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.FRIENDS) && 'ignorePlayWithFriendsSection', + getPref(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.ALL_GAMES) && 'ignoreAllGamesSection', + getPref(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.TOUCH) && 'ignorePlayWithTouchSection', + (getPref(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.NATIVE_MKB) || getPref(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.MOST_POPULAR)) && 'ignoreSiglSections', ...(STATES.userAgent.capabilities.touch ? [ 'disableTouchContextMenu', @@ -1064,9 +1059,11 @@ let PATCH_ORDERS: PatchArray = [ 'enableConsoleLogging', 'enableXcloudLogger', ] : []), -].filter(item => !!item); +].filter((item): item is string => !!item) as PatchArray; // Only when playing +// TODO: check this +// @ts-ignore let PLAYING_PATCH_ORDERS: PatchArray = [ 'patchXcloudTitleInfo', 'disableGamepadDisconnectedScreen', @@ -1078,18 +1075,18 @@ let PLAYING_PATCH_ORDERS: PatchArray = [ // 'exposeEventTarget', // Patch volume control for normal stream - getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && !getPref(PrefKey.STREAM_COMBINE_SOURCES) && 'patchAudioMediaStream', + getPref(PrefKey.AUDIO_VOLUME_CONTROL_ENABLED) && !getPref(PrefKey.STREAM_COMBINE_SOURCES) && 'patchAudioMediaStream', // Patch volume control for combined audio+video stream - getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && getPref(PrefKey.STREAM_COMBINE_SOURCES) && 'patchCombinedAudioVideoMediaStream', + getPref(PrefKey.AUDIO_VOLUME_CONTROL_ENABLED) && getPref(PrefKey.STREAM_COMBINE_SOURCES) && 'patchCombinedAudioVideoMediaStream', // Skip feedback dialog - getPref(PrefKey.STREAM_DISABLE_FEEDBACK_DIALOG) && 'skipFeedbackDialog', + getPref(PrefKey.UI_DISABLE_FEEDBACK_DIALOG) && 'skipFeedbackDialog', ...(STATES.userAgent.capabilities.touch ? [ - getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === StreamTouchController.ALL && 'patchShowSensorControls', - getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === StreamTouchController.ALL && 'exposeTouchLayoutManager', - (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === StreamTouchController.OFF || getPref(PrefKey.STREAM_TOUCH_CONTROLLER_AUTO_OFF) || !STATES.userAgent.capabilities.touch) && 'disableTakRenderer', - getPref(PrefKey.STREAM_TOUCH_CONTROLLER_DEFAULT_OPACITY) !== 100 && 'patchTouchControlDefaultOpacity', + 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) || !STATES.userAgent.capabilities.touch) && 'disableTakRenderer', + getPref(PrefKey.TOUCH_CONTROLLER_DEFAULT_OPACITY) !== 100 && 'patchTouchControlDefaultOpacity', 'patchBabylonRendererClass', ] : []), @@ -1103,7 +1100,13 @@ let PLAYING_PATCH_ORDERS: PatchArray = [ 'patchRemotePlayMkb', 'remotePlayConnectMode', ] : []), -].filter(item => !!item); + + // Native MKB + ...(getPref(PrefKey.NATIVE_MKB_MODE) === NativeMkbMode.ON ? [ + 'patchMouseAndKeyboardEnabled', + 'disableNativeRequestPointerLock', + ] : []), +].filter((item): item is string => !!item); const ALL_PATCHES = [...PATCH_ORDERS, ...PLAYING_PATCH_ORDERS]; @@ -1138,7 +1141,7 @@ export class Patcher { const orgFunc = this; const newFunc = (a: any, item: any) => { - Patcher.patch(item); + Patcher.checkChunks(item); orgFunc(a, item); } @@ -1147,20 +1150,22 @@ export class Patcher { }; } - static patch(item: [[number], { [key: string]: () => {} }]) { + static checkChunks(item: [[number], { [key: string]: () => {} }]) { // !!! Use "caches" as variable name will break touch controller??? // console.log('patch', '-----'); let patchesToCheck: PatchArray; let appliedPatches: PatchArray; + const chunkData = item[1]; const patchesMap: Record = {}; const patcherCache = PatcherCache.getInstance(); - for (let id in item[1]) { + for (const chunkId in chunkData) { appliedPatches = []; - const cachedPatches = patcherCache.getPatches(id); + const cachedPatches = patcherCache.getPatches(chunkId); if (cachedPatches) { + // clone cachedPatches patchesToCheck = cachedPatches.slice(0); patchesToCheck.push(...PATCH_ORDERS); } else { @@ -1172,7 +1177,7 @@ export class Patcher { continue; } - const func = item[1][id]; + const func = chunkData[chunkId]; const funcStr = func.toString(); let patchedFuncStr = funcStr; @@ -1211,7 +1216,7 @@ export class Patcher { // Apply patched functions if (modified) { try { - item[1][id] = eval(patchedFuncStr); + chunkData[chunkId] = eval(patchedFuncStr); } catch (e: unknown) { if (e instanceof Error) { BxLogger.error(LOG_TAG, 'Error', appliedPatches, e.message, patchedFuncStr); @@ -1221,7 +1226,7 @@ export class Patcher { // Save to cache if (appliedPatches.length) { - patchesMap[id] = appliedPatches; + patchesMap[chunkId] = appliedPatches; } } @@ -1239,10 +1244,10 @@ export class PatcherCache { private static instance: PatcherCache; public static getInstance = () => PatcherCache.instance ?? (PatcherCache.instance = new PatcherCache()); - private readonly KEY_CACHE = 'better_xcloud_patches_cache'; - private readonly KEY_SIGNATURE = 'better_xcloud_patches_cache_signature'; + private readonly KEY_CACHE = StorageKey.PATCHES_CACHE; + private readonly KEY_SIGNATURE = StorageKey.PATCHES_SIGNATURE; - private CACHE: any; + private CACHE!: { [key: string]: PatchArray }; private isInitialized = false; diff --git a/src/modules/patches/controller-shortcuts.js b/src/modules/patches/controller-shortcuts.js old mode 100644 new mode 100755 index 254aa95..d3a1376 --- a/src/modules/patches/controller-shortcuts.js +++ b/src/modules/patches/controller-shortcuts.js @@ -85,7 +85,7 @@ if (btnHome) { this.inputSink.onGamepadInput(performance.now() - intervalMs, fakeGamepadMappings); } else { - intervalMs = window.BX_CONTROLLER_POLLING_RATE; + intervalMs = window.BX_STREAM_SETTINGS.controllerPollingRate; } } diff --git a/src/modules/patches/expose-stream-session.js b/src/modules/patches/expose-stream-session.js old mode 100644 new mode 100755 diff --git a/src/modules/patches/local-co-op-enable.js b/src/modules/patches/local-co-op-enable.js old mode 100644 new mode 100755 index e1cd75e..f3ed93c --- a/src/modules/patches/local-co-op-enable.js +++ b/src/modules/patches/local-co-op-enable.js @@ -1,21 +1,57 @@ +// Save the original onGamepadChanged() and onGamepadInput() +this.orgOnGamepadChanged = this.onGamepadChanged; +this.orgOnGamepadInput = this.onGamepadInput; + let match; let onGamepadChangedStr = this.onGamepadChanged.toString(); +// Fix problem with Safari if (onGamepadChangedStr.startsWith('function ')) { onGamepadChangedStr = onGamepadChangedStr.substring(9); } onGamepadChangedStr = onGamepadChangedStr.replaceAll('0', 'arguments[1]'); -eval(`this.onGamepadChanged = function ${onGamepadChangedStr}`); +eval(`this.patchedOnGamepadChanged = function ${onGamepadChangedStr}`); let onGamepadInputStr = this.onGamepadInput.toString(); +// Fix problem with Safari +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.onGamepadInput = function ${onGamepadInputStr}`); + 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 => { + BxLogger.info('toggleLocalCoOp', enable ? 'Enabled' : 'Disabled'); + + this.onGamepadChanged = enable ? this.patchedOnGamepadChanged : this.orgOnGamepadChanged; + this.onGamepadInput = enable ? this.patchedOnGamepadInput : this.orgOnGamepadInput; + + // Reconnect all gamepads + const gamepads = window.navigator.getGamepads(); + for (const gamepad of gamepads) { + if (!gamepad?.connected) { + continue; + } + + // Ignore virtual controller + if (gamepad.id.includes('Better xCloud')) { + continue; + } + + window.dispatchEvent(new GamepadEvent('gamepaddisconnected', { gamepad })); + window.dispatchEvent(new GamepadEvent('gamepadconnected', { gamepad })); + } +}; + +// Expose this method +window.BX_EXPOSED.toggleLocalCoOp = this.toggleLocalCoOp.bind(this); diff --git a/src/modules/patches/remote-play-enable.js b/src/modules/patches/remote-play-enable.js old mode 100644 new mode 100755 diff --git a/src/modules/patches/remote-play-keep-alive.js b/src/modules/patches/remote-play-keep-alive.js old mode 100644 new mode 100755 diff --git a/src/modules/patches/set-currently-focused-interactable.js b/src/modules/patches/set-currently-focused-interactable.js old mode 100644 new mode 100755 diff --git a/src/modules/patches/vibration-adjust.js b/src/modules/patches/vibration-adjust.js old mode 100644 new mode 100755 index 8b015e0..f75356d --- a/src/modules/patches/vibration-adjust.js +++ b/src/modules/patches/vibration-adjust.js @@ -1,15 +1,16 @@ -if (!window.BX_ENABLE_CONTROLLER_VIBRATION) { - return void(0); -} +const gamepad = e.gamepad; +if (gamepad?.connected) { + const gamepadSettings = window.BX_STREAM_SETTINGS.controllers[gamepad.id]; + if (gamepadSettings) { + const intensity = gamepadSettings.vibrationIntensity; -const intensity = window.BX_VIBRATION_INTENSITY; -if (intensity === 0) { - return void(0); -} - -if (intensity < 1) { - e.leftMotorPercent *= intensity; - e.rightMotorPercent *= intensity; - e.leftTriggerMotorPercent *= intensity; - e.rightTriggerMotorPercent *= intensity; + 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/player/shaders/clarity_boost.fs b/src/modules/player/shaders/clarity_boost.fs old mode 100644 new mode 100755 diff --git a/src/modules/player/shaders/clarity_boost.vert b/src/modules/player/shaders/clarity_boost.vert old mode 100644 new mode 100755 diff --git a/src/modules/player/webgl2-player.ts b/src/modules/player/webgl2-player.ts old mode 100644 new mode 100755 index ae3cd17..a872f05 --- a/src/modules/player/webgl2-player.ts +++ b/src/modules/player/webgl2-player.ts @@ -115,6 +115,7 @@ export class WebGL2Player { } this.animFrameId = frameCallback(animate); + let draw = true; // Don't draw when FPS is 0 diff --git a/src/modules/remote-play-manager.ts b/src/modules/remote-play-manager.ts old mode 100644 new mode 100755 index 163f2b3..a695f37 --- a/src/modules/remote-play-manager.ts +++ b/src/modules/remote-play-manager.ts @@ -1,4 +1,4 @@ -import { STATES, AppInterface } from "@utils/global"; +import { STATES } from "@utils/global"; import { Toast } from "@utils/toast"; import { BxEvent } from "@utils/bx-event"; import { t } from "@utils/translation"; @@ -7,7 +7,7 @@ import { BxLogger } from "@utils/bx-logger"; import { HeaderSection } from "./ui/header"; import { PrefKey } from "@/enums/pref-keys"; import { getPref, setPref } from "@/utils/settings-storages/global-settings-storage"; -import { RemotePlayNavigationDialog } from "./ui/dialog/remote-play-dialog"; +import { RemotePlayDialog } from "./ui/dialog/remote-play-dialog"; export const enum RemotePlayConsoleState { ON = 'On', @@ -34,8 +34,18 @@ type RemotePlayConsole = { }; export class RemotePlayManager { - private static instance: RemotePlayManager; - public static getInstance = () => RemotePlayManager.instance ?? (RemotePlayManager.instance = new RemotePlayManager()); + private static instance: RemotePlayManager | null | undefined; + public static getInstance(): typeof RemotePlayManager['instance'] { + if (typeof RemotePlayManager.instance === 'undefined') { + if (getPref(PrefKey.REMOTE_PLAY_ENABLED)) { + RemotePlayManager.instance = new RemotePlayManager(); + } else { + RemotePlayManager.instance = null; + } + } + + return RemotePlayManager.instance; + } private readonly LOG_TAG = 'RemotePlayManager'; private isInitialized = false; @@ -57,7 +67,7 @@ export class RemotePlayManager { this.isInitialized = true; - this.getXhomeToken(() => { + this.requestXhomeToken(() => { this.getConsolesList(() => { BxLogger.info(this.LOG_TAG, 'Consoles', this.consoles); @@ -67,15 +77,15 @@ export class RemotePlayManager { }); } - get xcloudToken() { + getXcloudToken() { return this.XCLOUD_TOKEN; } - set xcloudToken(token: string) { + setXcloudToken(token: string) { this.XCLOUD_TOKEN = token; } - get xhomeToken() { + getXhomeToken() { return this.XHOME_TOKEN; } @@ -84,7 +94,7 @@ export class RemotePlayManager { } - private getXhomeToken(callback: any) { + private requestXhomeToken(callback: any) { if (this.XHOME_TOKEN) { callback(); return; @@ -142,7 +152,7 @@ export class RemotePlayManager { const options = { method: 'GET', headers: { - 'Authorization': `Bearer ${this.XHOME_TOKEN}`, + Authorization: `Bearer ${this.XHOME_TOKEN}`, }, }; @@ -176,7 +186,7 @@ export class RemotePlayManager { play(serverId: string, resolution?: string) { if (resolution) { - setPref(PrefKey.REMOTE_PLAY_RESOLUTION, resolution); + setPref(PrefKey.REMOTE_PLAY_STREAM_RESOLUTION, resolution); } STATES.remotePlay.config = { @@ -198,14 +208,16 @@ export class RemotePlayManager { return; } + /* // Show native dialog in Android app if (AppInterface && AppInterface.showRemotePlayDialog) { AppInterface.showRemotePlayDialog(JSON.stringify(this.consoles)); (document.activeElement as HTMLElement).blur(); return; } + */ - RemotePlayNavigationDialog.getInstance().show(); + RemotePlayDialog.getInstance().show(); } static detect() { diff --git a/src/modules/shortcuts/shortcut-microphone.ts b/src/modules/shortcuts/microphone-shortcut.ts old mode 100644 new mode 100755 similarity index 100% rename from src/modules/shortcuts/shortcut-microphone.ts rename to src/modules/shortcuts/microphone-shortcut.ts diff --git a/src/modules/shortcuts/shortcut-renderer.ts b/src/modules/shortcuts/renderer-shortcut.ts old mode 100644 new mode 100755 similarity index 69% rename from src/modules/shortcuts/shortcut-renderer.ts rename to src/modules/shortcuts/renderer-shortcut.ts index ff36687..95f6552 --- a/src/modules/shortcuts/shortcut-renderer.ts +++ b/src/modules/shortcuts/renderer-shortcut.ts @@ -1,18 +1,21 @@ import { PrefKey } from "@/enums/pref-keys"; import { getPref } from "@/utils/settings-storages/global-settings-storage"; import { limitVideoPlayerFps } from "../stream/stream-settings-utils"; +import { BxEvent } from "@/utils/bx-event"; export class RendererShortcut { - static toggleVisibility(): boolean { + static toggleVisibility() { const $mediaContainer = document.querySelector('#game-stream div[data-testid="media-container"]'); if (!$mediaContainer) { - return true; + BxEvent.dispatch(window, BxEvent.VIDEO_VISIBILITY_CHANGED, { isShowing: true }); + return; } $mediaContainer.classList.toggle('bx-gone'); const isShowing = !$mediaContainer.classList.contains('bx-gone'); + // Switch FPS limitVideoPlayerFps(isShowing ? getPref(PrefKey.VIDEO_MAX_FPS) : 0); - return isShowing; + BxEvent.dispatch(window, BxEvent.VIDEO_VISIBILITY_CHANGED, { isShowing }); } } diff --git a/src/modules/shortcuts/shortcut-actions.ts b/src/modules/shortcuts/shortcut-actions.ts new file mode 100755 index 0000000..ea0d8d4 --- /dev/null +++ b/src/modules/shortcuts/shortcut-actions.ts @@ -0,0 +1,59 @@ +import { PrefKey } from "@/enums/pref-keys"; +import { ShortcutAction } from "@/enums/shortcut-actions"; +import { AppInterface, STATES } from "@/utils/global"; +import { getPref } from "@/utils/settings-storages/global-settings-storage"; +import { t } from "@/utils/translation"; + +type ShortcutActions = { + [key: string]: { + [key in ShortcutAction]?: string[]; + }; +}; + +export const SHORTCUT_ACTIONS: ShortcutActions = { + // Script + [t('better-xcloud')]: { + [ShortcutAction.BETTER_XCLOUD_SETTINGS_SHOW]: [t('settings'), t('show')], + }, + + // Device + ...(!!AppInterface ? { + [t('device')]: { + [ShortcutAction.DEVICE_SOUND_TOGGLE]: [t('sound'), t('toggle')], + [ShortcutAction.DEVICE_VOLUME_INC]: [t('volume'), t('increase')], + [ShortcutAction.DEVICE_VOLUME_DEC]: [t('volume'), t('decrease')], + + [ShortcutAction.DEVICE_BRIGHTNESS_INC]: [t('brightness'), t('increase')], + [ShortcutAction.DEVICE_BRIGHTNESS_DEC]: [t('brightness'), t('decrease')], + }, + } : {}), + + // Stream + [t('stream')]: { + [ShortcutAction.STREAM_SCREENSHOT_CAPTURE]: [t('take-screenshot')], + [ShortcutAction.STREAM_VIDEO_TOGGLE]: [t('video'), t('toggle')], + + [ShortcutAction.STREAM_SOUND_TOGGLE]: [t('sound'), t('toggle')], + + ...(getPref(PrefKey.AUDIO_VOLUME_CONTROL_ENABLED) ? { + [ShortcutAction.STREAM_VOLUME_INC]: [t('volume'), t('increase')], + [ShortcutAction.STREAM_VOLUME_DEC]: [t('volume'), t('decrease')], + } : {}), + + [ShortcutAction.STREAM_MENU_SHOW]: [t('menu'), t('show')], + [ShortcutAction.STREAM_STATS_TOGGLE]: [t('stats'), t('show-hide')], + [ShortcutAction.STREAM_MICROPHONE_TOGGLE]: [t('microphone'), t('toggle')], + }, + + // MKB + ...(STATES.browser.capabilities.mkb ? { + [t('mouse-and-keyboard')]: { + [ShortcutAction.MKB_TOGGLE]: [t('toggle')], + }, + } : {}), + + // Other + [t('other')]: { + [ShortcutAction.TRUE_ACHIEVEMENTS_OPEN]: [t('true-achievements'), t('show')], + }, +} as const; diff --git a/src/modules/shortcuts/shortcut-sound.ts b/src/modules/shortcuts/sound-shortcut.ts old mode 100644 new mode 100755 similarity index 90% rename from src/modules/shortcuts/shortcut-sound.ts rename to src/modules/shortcuts/sound-shortcut.ts index 1a908c9..5f1b9f8 --- a/src/modules/shortcuts/shortcut-sound.ts +++ b/src/modules/shortcuts/sound-shortcut.ts @@ -13,11 +13,11 @@ export enum SpeakerState { export class SoundShortcut { static adjustGainNodeVolume(amount: number): number { - if (!getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL)) { + if (!getPref(PrefKey.AUDIO_VOLUME_CONTROL_ENABLED)) { return 0; } - const currentValue = getPref(PrefKey.AUDIO_VOLUME); + const currentValue = getPref(PrefKey.AUDIO_VOLUME); let nearestValue: number; if (amount > 0) { // Increase @@ -47,9 +47,9 @@ export class SoundShortcut { } static muteUnmute() { - if (getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && STATES.currentStream.audioGainNode) { + 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 @@ -73,7 +73,7 @@ export class SoundShortcut { BxEvent.dispatch(window, BxEvent.SPEAKER_STATE_CHANGED, { speakerState: targetValue === 0 ? SpeakerState.MUTED : SpeakerState.ENABLED, - }) + }); return; } diff --git a/src/modules/shortcuts/shortcut-stream-ui.ts b/src/modules/shortcuts/stream-ui-shortcut.ts old mode 100644 new mode 100755 similarity index 100% rename from src/modules/shortcuts/shortcut-stream-ui.ts rename to src/modules/shortcuts/stream-ui-shortcut.ts diff --git a/src/modules/stream-player.ts b/src/modules/stream-player.ts old mode 100644 new mode 100755 index 8369c77..5db8054 --- a/src/modules/stream-player.ts +++ b/src/modules/stream-player.ts @@ -3,11 +3,11 @@ import { isFullVersion } from "@macros/build" with {type: "macro"}; import { CE } from "@/utils/html"; import { WebGL2Player } from "./player/webgl2-player"; import { ScreenshotManager } from "@/utils/screenshot-manager"; -import { StreamPlayerType, StreamVideoProcessing } from "@enums/stream-player"; 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, VideoRatio } from "@/enums/pref-values"; export type StreamPlayerOptions = Partial<{ processing: string, @@ -99,7 +99,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; diff --git a/src/modules/stream/stream-badges.ts b/src/modules/stream/stream-badges.ts old mode 100644 new mode 100755 index f9ee7ab..8c61c47 --- a/src/modules/stream/stream-badges.ts +++ b/src/modules/stream/stream-badges.ts @@ -7,7 +7,8 @@ import { STATES } from "@utils/global"; import { BxLogger } from "@/utils/bx-logger"; import { BxIcon } from "@/utils/bx-icon"; import { GuideMenuTab } from "../ui/guide-menu"; -import { StreamStat, StreamStatsCollector } from "@/utils/stream-stats-collector"; +import { StreamStatsCollector } from "@/utils/stream-stats-collector"; +import { StreamStat } from "@/enums/pref-values"; type StreamBadgeInfo = { @@ -130,7 +131,7 @@ export class StreamBadges { return $badge; } - private async updateBadges(forceUpdate = false) { + private updateBadges = async (forceUpdate = false) => { if (!this.$container || (!forceUpdate && !this.$container.isConnected)) { this.stop(); return; @@ -181,7 +182,7 @@ export class StreamBadges { private async start() { await this.updateBadges(true); this.stop(); - this.intervalId = window.setInterval(this.updateBadges.bind(this), this.REFRESH_INTERVAL); + this.intervalId = window.setInterval(this.updateBadges, this.REFRESH_INTERVAL); } private stop() { diff --git a/src/modules/stream/stream-settings-utils.ts b/src/modules/stream/stream-settings-utils.ts old mode 100644 new mode 100755 index 840ba97..9c5c603 --- a/src/modules/stream/stream-settings-utils.ts +++ b/src/modules/stream/stream-settings-utils.ts @@ -1,16 +1,17 @@ -import { StreamPlayerType, StreamVideoProcessing } from "@enums/stream-player"; import { STATES } from "@utils/global"; import { UserAgent } from "@utils/user-agent"; import type { StreamPlayerOptions } from "../stream-player"; import { PrefKey } from "@/enums/pref-keys"; import { getPref, setPref } from "@/utils/settings-storages/global-settings-storage"; +import { StreamVideoProcessing, StreamPlayerType } from "@/enums/pref-values"; +import { escapeCssSelector } from "@/utils/html"; export function onChangeVideoPlayerType() { - const playerType = getPref(PrefKey.VIDEO_PLAYER_TYPE); - const $videoProcessing = document.getElementById(`bx_setting_${PrefKey.VIDEO_PROCESSING}`) as HTMLSelectElement; - const $videoSharpness = document.getElementById(`bx_setting_${PrefKey.VIDEO_SHARPNESS}`) as HTMLElement; - const $videoPowerPreference = document.getElementById(`bx_setting_${PrefKey.VIDEO_POWER_PREFERENCE}`) as HTMLElement; - const $videoMaxFps = document.getElementById(`bx_setting_${PrefKey.VIDEO_MAX_FPS}`) as HTMLElement; + 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; + const $videoMaxFps = document.getElementById(`bx_setting_${escapeCssSelector(PrefKey.VIDEO_MAX_FPS)}`) as HTMLElement; if (!$videoProcessing) { return; diff --git a/src/modules/stream/stream-stats.ts b/src/modules/stream/stream-stats.ts old mode 100644 new mode 100755 index bf2bc67..0569cf4 --- a/src/modules/stream/stream-stats.ts +++ b/src/modules/stream/stream-stats.ts @@ -4,8 +4,9 @@ import { t } from "@utils/translation" import { STATES } from "@utils/global" import { PrefKey } from "@/enums/pref-keys" import { getPref } from "@/utils/settings-storages/global-settings-storage" -import { StreamStat, StreamStatsCollector, type StreamStatGrade } from "@/utils/stream-stats-collector" +import { StreamStatsCollector, type StreamStatGrade } from "@/utils/stream-stats-collector" import { BxLogger } from "@/utils/bx-logger" +import { StreamStat } from "@/enums/pref-values" export class StreamStats { @@ -87,7 +88,7 @@ export class StreamStats { this.$container.classList.remove('bx-gone'); this.$container.dataset.display = glancing ? 'glancing' : 'fixed'; - this.intervalId = window.setInterval(this.update.bind(this), this.REFRESH_INTERVAL); + this.intervalId = window.setInterval(this.update, this.REFRESH_INTERVAL); } async stop(glancing=false) { @@ -157,7 +158,7 @@ export class StreamStats { this.quickGlanceObserver = null; } - private async update(forceUpdate=false) { + private update = async (forceUpdate=false) => { if ((!forceUpdate && this.isHidden()) || !STATES.currentStream.peerConnection) { this.destroy(); return; @@ -191,7 +192,7 @@ export class StreamStats { } refreshStyles() { - const PREF_ITEMS = getPref(PrefKey.STATS_ITEMS); + const PREF_ITEMS = getPref(PrefKey.STATS_ITEMS); const $container = this.$container; $container.dataset.stats = '[' + PREF_ITEMS.join('][') + ']'; @@ -202,7 +203,7 @@ export class StreamStats { } hideSettingsUi() { - if (this.isGlancing() && !getPref(PrefKey.STATS_QUICK_GLANCE)) { + if (this.isGlancing() && !getPref(PrefKey.STATS_QUICK_GLANCE_ENABLED)) { this.stop(); } } @@ -230,7 +231,7 @@ export class StreamStats { static setupEvents() { window.addEventListener(BxEvent.STREAM_PLAYING, e => { - const PREF_STATS_QUICK_GLANCE = getPref(PrefKey.STATS_QUICK_GLANCE); + const PREF_STATS_QUICK_GLANCE = getPref(PrefKey.STATS_QUICK_GLANCE_ENABLED); const PREF_STATS_SHOW_WHEN_PLAYING = getPref(PrefKey.STATS_SHOW_WHEN_PLAYING); const streamStats = StreamStats.getInstance(); diff --git a/src/modules/stream/stream-ui.ts b/src/modules/stream/stream-ui.ts old mode 100644 new mode 100755 index 376ef8a..ae3a6c4 --- a/src/modules/stream/stream-ui.ts +++ b/src/modules/stream/stream-ui.ts @@ -5,7 +5,7 @@ import { BxEvent } from "@utils/bx-event.ts"; import { t } from "@utils/translation.ts"; import { StreamBadges } from "./stream-badges.ts"; import { StreamStats } from "./stream-stats.ts"; -import { SettingsNavigationDialog } from "../ui/dialog/settings-dialog.ts"; +import { SettingsDialog } from "../ui/dialog/settings-dialog.ts"; export class StreamUiHandler { @@ -161,7 +161,7 @@ export class StreamUiHandler { e.preventDefault(); // Show Stream Settings dialog - SettingsNavigationDialog.getInstance().show(); + SettingsDialog.getInstance().show(); }); StreamUiHandler.$btnStreamSettings = $btnStreamSettings; diff --git a/src/modules/touch-controller.ts b/src/modules/touch-controller.ts old mode 100644 new mode 100755 index f39069b..53aecc0 --- a/src/modules/touch-controller.ts +++ b/src/modules/touch-controller.ts @@ -6,6 +6,8 @@ import { t } from "@utils/translation"; import { BxLogger } from "@utils/bx-logger"; import { PrefKey } from "@/enums/pref-keys"; import { getPref } from "@/utils/settings-storages/global-settings-storage"; +import { TouchControllerStyleCustom, TouchControllerStyleStandard } from "@/enums/pref-values"; +import { GhPagesUtils } from "@/utils/gh-pages"; const LOG_TAG = 'TouchController'; @@ -145,12 +147,9 @@ export class TouchController { return; } - const baseUrl = 'https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/touch-layouts'; - const url = `${baseUrl}/${xboxTitleId}.json`; - // Get layout info try { - const resp = await NATIVE_FETCH(url); + const resp = await NATIVE_FETCH(GhPagesUtils.getUrl(`touch-layouts/${xboxTitleId}.json`)); const json = await resp.json(); const layouts = {}; @@ -161,7 +160,7 @@ export class TouchController { baseLayouts = TouchController.#baseCustomLayouts[layoutName]; } else { try { - const layoutUrl = `${baseUrl}/layouts/${layoutName}.json`; + const layoutUrl = GhPagesUtils.getUrl(`touch-layouts/layouts/${layoutName}.json`); const resp = await NATIVE_FETCH(layoutUrl); const json = await resp.json(); @@ -188,12 +187,11 @@ export class TouchController { // TODO: fix this if (!window.BX_EXPOSED.touchLayoutManager) { const listener = (e: Event) => { - window.removeEventListener(BxEvent.TOUCH_LAYOUT_MANAGER_READY, listener); if (TouchController.#enabled) { TouchController.applyCustomLayout(layoutId, 0); } }; - window.addEventListener(BxEvent.TOUCH_LAYOUT_MANAGER_READY, listener); + window.addEventListener(BxEvent.TOUCH_LAYOUT_MANAGER_READY, listener, { once: true }); return; } @@ -261,15 +259,7 @@ export class TouchController { } static updateCustomList() { - const key = 'better_xcloud_custom_touch_layouts'; - TouchController.#customList = JSON.parse(window.localStorage.getItem(key) || '[]'); - - NATIVE_FETCH('https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/touch-layouts/ids.json') - .then(response => response.json()) - .then(json => { - TouchController.#customList = json; - window.localStorage.setItem(key, JSON.stringify(json)); - }); + TouchController.#customList = GhPagesUtils.getTouchControlCustomList(); } static getCustomList(): string[] { @@ -298,8 +288,8 @@ export class TouchController { TouchController.#$style = $style; - const PREF_STYLE_STANDARD = getPref(PrefKey.STREAM_TOUCH_CONTROLLER_STYLE_STANDARD); - const PREF_STYLE_CUSTOM = getPref(PrefKey.STREAM_TOUCH_CONTROLLER_STYLE_CUSTOM); + const PREF_STYLE_STANDARD = getPref(PrefKey.TOUCH_CONTROLLER_STYLE_STANDARD); + const PREF_STYLE_CUSTOM = getPref(PrefKey.TOUCH_CONTROLLER_STYLE_CUSTOM); window.addEventListener(BxEvent.DATA_CHANNEL_CREATED, e => { const dataChannel = (e as any).dataChannel; @@ -310,12 +300,12 @@ export class TouchController { // Apply touch controller's style let filter = ''; if (TouchController.#enabled) { - if (PREF_STYLE_STANDARD === 'white') { + if (PREF_STYLE_STANDARD === TouchControllerStyleStandard.WHITE) { filter = 'grayscale(1) brightness(2)'; - } else if (PREF_STYLE_STANDARD === 'muted') { + } else if (PREF_STYLE_STANDARD === TouchControllerStyleStandard.MUTED) { filter = 'sepia(0.5)'; } - } else if (PREF_STYLE_CUSTOM === 'muted') { + } else if (PREF_STYLE_CUSTOM === TouchControllerStyleCustom.MUTED) { filter = 'sepia(0.5)'; } diff --git a/src/modules/ui/dialog/navigation-dialog.ts b/src/modules/ui/dialog/navigation-dialog.ts old mode 100644 new mode 100755 index bfcd1a3..b30dd9b --- a/src/modules/ui/dialog/navigation-dialog.ts +++ b/src/modules/ui/dialog/navigation-dialog.ts @@ -1,9 +1,8 @@ -import { GamepadKey } from "@/enums/mkb"; +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 { BxLogger } from "@/utils/bx-logger"; -import { STATES } from "@/utils/global"; import { CE, isElementVisible } from "@/utils/html"; import { setNearby } from "@/utils/navigation-utils"; import { getPref } from "@/utils/settings-storages/global-settings-storage"; @@ -40,13 +39,22 @@ export abstract class NavigationDialog { abstract $container: HTMLElement; dialogManager: NavigationDialogManager; + onMountedCallbacks: Array<() => void> = []; constructor() { this.dialogManager = NavigationDialogManager.getInstance(); } - show() { - NavigationDialogManager.getInstance().show(this); + isCancellable(): boolean { + return true; + } + + isOverlayVisible(): boolean { + return true; + } + + show(configs={}, clearStack=false) { + NavigationDialogManager.getInstance().show(this, configs, clearStack); const $currentFocus = this.getFocusedElement(); // If not focusing on any element @@ -73,8 +81,12 @@ export abstract class NavigationDialog { return null; } - onBeforeMount(): void {} - onMounted(): void {} + onBeforeMount(configs={}): void {} + onMounted(configs={}): void { + for (const callback of this.onMountedCallbacks) { + callback.call(this); + } + } onBeforeUnmount(): void {} onUnmounted(): void {} @@ -119,12 +131,12 @@ export class NavigationDialogManager { }; private static readonly SIBLING_PROPERTY_MAP = { - 'horizontal': { + horizontal: { [NavigationDirection.LEFT]: 'previousElementSibling', [NavigationDirection.RIGHT]: 'nextElementSibling', }, - 'vertical': { + vertical: { [NavigationDirection.UP]: 'previousElementSibling', [NavigationDirection.DOWN]: 'nextElementSibling', }, @@ -137,6 +149,7 @@ export class NavigationDialogManager { private $overlay: HTMLElement; private $container: HTMLElement; private dialog: NavigationDialog | null = null; + private dialogsStack: Array = []; private constructor() { BxLogger.info(this.LOG_TAG, 'constructor()'); @@ -145,7 +158,8 @@ export class NavigationDialogManager { this.$overlay.addEventListener('click', e => { e.preventDefault(); e.stopPropagation(); - this.hide(); + + this.dialog?.isCancellable() && this.hide(); }); document.documentElement.appendChild(this.$overlay); @@ -214,9 +228,16 @@ export class NavigationDialogManager { }; } + private updateActiveInput(input: 'keyboard' | 'gamepad' | 'mouse') { + // Set 's activeInput + document.documentElement.dataset.activeInput = input; + } + handleEvent(event: Event) { switch (event.type) { case 'keydown': + this.updateActiveInput('keyboard'); + const $target = event.target as HTMLElement; const keyboardEvent = event as KeyboardEvent; const keyCode = keyboardEvent.code || keyboardEvent.key; @@ -259,7 +280,7 @@ export class NavigationDialogManager { return this.$container && !this.$container.classList.contains('bx-gone'); } - private pollGamepad() { + private pollGamepad = () => { const gamepads = window.navigator.getGamepads(); for (const gamepad of gamepads) { @@ -365,6 +386,12 @@ export class NavigationDialogManager { return; } + this.updateActiveInput('gamepad'); + + if (this.handleGamepad(gamepad, releasedButton)) { + return; + } + if (releasedButton === GamepadKey.A) { document.activeElement && document.activeElement.dispatchEvent(new MouseEvent('click', {bubbles: true})); return; @@ -372,10 +399,6 @@ export class NavigationDialogManager { this.hide(); return; } - - if (this.handleGamepad(gamepad, releasedButton)) { - return; - } } } @@ -413,7 +436,7 @@ export class NavigationDialogManager { this.gamepadHoldingIntervalId = null; } - show(dialog: NavigationDialog) { + show(dialog: NavigationDialog, configs={}, clearStack=false) { this.clearGamepadHoldingInterval(); BxEvent.dispatch(window, BxEvent.XCLOUD_DIALOG_SHOWN); @@ -424,20 +447,21 @@ export class NavigationDialogManager { // Lock scroll bar document.body.classList.add('bx-no-scroll'); - // Show overlay - this.$overlay.classList.remove('bx-gone'); - if (STATES.isPlaying) { - this.$overlay.classList.add('bx-invisible'); - } - // Unmount current dialog this.unmountCurrentDialog(); + // Add to dialogs stack + this.dialogsStack.push(dialog); + // Setup new dialog this.dialog = dialog; - dialog.onBeforeMount(); + dialog.onBeforeMount(configs); this.$container.appendChild(dialog.getContent()); - dialog.onMounted(); + dialog.onMounted(configs); + + // Show overlay + this.$overlay.classList.remove('bx-gone'); + this.$overlay.classList.toggle('bx-invisible', !dialog.isOverlayVisible()); // Show content this.$container.classList.remove('bx-gone'); @@ -468,11 +492,24 @@ export class NavigationDialogManager { // Stop gamepad polling this.stopGamepadPolling(); + // Remove current dialog and everything after it from dialogs stack + if (this.dialog) { + const dialogIndex = this.dialogsStack.indexOf(this.dialog); + if (dialogIndex > -1) { + this.dialogsStack = this.dialogsStack.slice(0, dialogIndex); + } + } + // Unmount dialog this.unmountCurrentDialog(); // Enable xCloud's navigation polling (window as any).BX_EXPOSED.disableGamepadPolling = false; + + // Show the last dialog in dialogs stack + if (this.dialogsStack.length) { + this.dialogsStack[this.dialogsStack.length - 1].show(); + } } focus($elm: NavigationElement | null): boolean { @@ -624,7 +661,7 @@ export class NavigationDialogManager { private startGamepadPolling() { this.stopGamepadPolling(); - this.gamepadPollingIntervalId = window.setInterval(this.pollGamepad.bind(this), NavigationDialogManager.GAMEPAD_POLLING_INTERVAL); + this.gamepadPollingIntervalId = window.setInterval(this.pollGamepad, NavigationDialogManager.GAMEPAD_POLLING_INTERVAL); } private stopGamepadPolling() { diff --git a/src/modules/ui/dialog/profile-manger/base-profile-manager-dialog.ts b/src/modules/ui/dialog/profile-manger/base-profile-manager-dialog.ts new file mode 100755 index 0000000..4cfc725 --- /dev/null +++ b/src/modules/ui/dialog/profile-manger/base-profile-manager-dialog.ts @@ -0,0 +1,205 @@ +import { ButtonStyle, CE, createButton, renderPresetsList } from "@/utils/html"; +import { NavigationDialog } from "../navigation-dialog"; +import { BxIcon } from "@/utils/bx-icon"; +import { t } from "@/utils/translation"; +import type { AllPresets, PresetRecord } from "@/types/presets"; +import type { BasePresetsTable } from "@/utils/local-db/base-presets-table"; +import { BxSelectElement } from "@/web-components/bx-select"; + +export abstract class BaseProfileManagerDialog extends NavigationDialog { + $container!: HTMLElement; + + private title: string; + protected presetsDb: BasePresetsTable; + protected allPresets!: AllPresets; + protected currentPresetId: number = 0; + + private $presets!: HTMLSelectElement; + private $header!: HTMLElement; + protected $content!: HTMLElement; + + private $btnRename!: HTMLButtonElement; + private $btnDelete!: HTMLButtonElement; + + protected abstract readonly BLANK_PRESET_DATA: T['data']; + + constructor(title: string, presetsDb: BasePresetsTable) { + super(); + + this.title = title; + this.presetsDb = presetsDb; + } + + protected abstract switchPreset(id: number): void; + + protected updateButtonStates() { + const isDefaultPreset = this.currentPresetId <= 0; + this.$btnRename.disabled = isDefaultPreset; + this.$btnDelete.disabled = isDefaultPreset; + } + + private async renderPresetsList() { + this.allPresets = await this.presetsDb.getPresets(); + if (!this.currentPresetId) { + this.currentPresetId = this.allPresets.default[0]; + } + + renderPresetsList(this.$presets, this.allPresets, this.currentPresetId); + } + + private promptNewName(action: string,value='') { + let newName: string | null = ''; + while (!newName) { + newName = prompt(`[${action}] ${t('prompt-preset-name')}`, value); + if (newName === null) { + return false; + } + newName = newName.trim(); + } + + return newName ? newName : false; + }; + + private async renderDialog() { + this.$presets = CE('select', { tabindex: -1 }); + + const $select = BxSelectElement.create(this.$presets); + $select.classList.add('bx-full-width'); + $select.addEventListener('input', e => { + this.switchPreset(parseInt(($select as HTMLSelectElement).value)); + }); + + const $header = CE('div', { + class: 'bx-dialog-preset-tools', + _nearby: { + orientation: 'horizontal', + focus: $select, + }, + }, + $select, + + // Rename button + this.$btnRename = createButton({ + title: t('rename'), + icon: BxIcon.CURSOR_TEXT, + style: ButtonStyle.FOCUSABLE, + onClick: async () => { + const preset = this.allPresets.data[this.currentPresetId]; + + const newName = this.promptNewName(t('rename'), preset.name); + if (!newName) { + return; + } + + // Update preset with new name + preset.name = newName; + + await this.presetsDb.updatePreset(preset); + await this.refresh(); + }, + }), + + // Delete button + this.$btnDelete = createButton({ + icon: BxIcon.TRASH, + title: t('delete'), + style: ButtonStyle.DANGER | ButtonStyle.FOCUSABLE, + onClick: async (e) => { + if (!confirm(t('confirm-delete-preset'))) { + return; + } + + await this.presetsDb.deletePreset(this.currentPresetId); + delete this.allPresets.data[this.currentPresetId]; + this.currentPresetId = parseInt(Object.keys(this.allPresets.data)[0]); + + await this.refresh(); + }, + }), + + // New button + createButton({ + icon: BxIcon.NEW, + title: t('new'), + style: ButtonStyle.FOCUSABLE, + onClick: async (e) => { + const newName = this.promptNewName(t('new')); + if (!newName) { + return; + } + + // Create new preset selected name + const newId = await this.presetsDb.newPreset(newName, this.BLANK_PRESET_DATA); + this.currentPresetId = newId; + + await this.refresh(); + }, + }), + + // Copy button + createButton({ + icon: BxIcon.COPY, + title: t('copy'), + style: ButtonStyle.FOCUSABLE, + onClick: async (e) => { + const preset = this.allPresets.data[this.currentPresetId]; + + const newName = this.promptNewName(t('copy'), `${preset.name} (2)`); + if (!newName) { + return; + } + + // Create new preset with selected name + const newId = await this.presetsDb.newPreset(newName, preset.data); + this.currentPresetId = newId; + + await this.refresh(); + }, + }), + ); + 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: ButtonStyle.FOCUSABLE | ButtonStyle.CIRCULAR | ButtonStyle.GHOST, + onClick: e => this.hide(), + }), + ), + $header, + CE('div', { class: 'bx-dialog-content bx-hide-scroll-bar' }, this.$content), + ); + } + + async refresh() { + await this.renderPresetsList(); + this.switchPreset(this.currentPresetId); + } + + async onBeforeMount(configs:{ id?: number }={}) { + if (configs?.id) { + this.currentPresetId = configs.id; + } + + // Select first preset + this.refresh(); + } + + getDialog(): NavigationDialog { + return this; + } + + getContent(): HTMLElement { + if (!this.$container) { + this.renderDialog(); + } + + return this.$container; + } + + focusIfNeeded(): void { + this.dialogManager.focus(this.$header); + } +} diff --git a/src/modules/ui/dialog/profile-manger/controller-shortcuts-manager-dialog.ts b/src/modules/ui/dialog/profile-manger/controller-shortcuts-manager-dialog.ts new file mode 100755 index 0000000..ac81f1b --- /dev/null +++ b/src/modules/ui/dialog/profile-manger/controller-shortcuts-manager-dialog.ts @@ -0,0 +1,194 @@ +import { t } from "@/utils/translation"; +import { BaseProfileManagerDialog } from "./base-profile-manager-dialog"; +import { CE } from "@/utils/html"; +import { GamepadKey, GamepadKeyName } from "@/enums/gamepad"; +import { PrefKey } from "@/enums/pref-keys"; +import { PrompFont } from "@/enums/prompt-font"; +import { ShortcutAction } from "@/enums/shortcut-actions"; +import { deepClone } from "@/utils/global"; +import { setNearby } from "@/utils/navigation-utils"; +import { getPref } from "@/utils/settings-storages/global-settings-storage"; +import { BxSelectElement } from "@/web-components/bx-select"; +import type { ControllerShortcutPresetData, ControllerShortcutPresetRecord } from "@/types/presets"; +import { ControllerShortcutsTable } from "@/utils/local-db/controller-shortcuts-table"; +import { BxEvent } from "@/utils/bx-event"; +import { StreamSettings } from "@/utils/stream-settings"; +import { SHORTCUT_ACTIONS } from "@/modules/shortcuts/shortcut-actions"; + +export class ControllerShortcutsManagerDialog extends BaseProfileManagerDialog { + private static instance: ControllerShortcutsManagerDialog; + public static getInstance = () => ControllerShortcutsManagerDialog.instance ?? (ControllerShortcutsManagerDialog.instance = new ControllerShortcutsManagerDialog(t('controller-shortcuts'))); + // private readonly LOG_TAG = 'ControllerShortcutsManagerDialog'; + + protected $content: HTMLElement; + private selectActions: Partial> = {}; + + protected readonly BLANK_PRESET_DATA = { + mapping: {}, + }; + + private readonly BUTTONS_ORDER = [ + GamepadKey.Y, GamepadKey.A, GamepadKey.X, GamepadKey.B, + GamepadKey.UP, GamepadKey.DOWN, GamepadKey.LEFT, GamepadKey.RIGHT, + GamepadKey.SELECT, GamepadKey.START, + GamepadKey.LB, GamepadKey.RB, + GamepadKey.LT, GamepadKey.RT, + GamepadKey.L3, GamepadKey.R3, + ]; + + constructor(title: string) { + super(title, ControllerShortcutsTable.getInstance()); + + const PREF_CONTROLLER_FRIENDLY_UI = getPref(PrefKey.UI_CONTROLLER_FRIENDLY); + + // Read actions from localStorage + // ControllerShortcut.ACTIONS = ControllerShortcut.getActionsFromStorage(); + + const $baseSelect = CE('select', { autocomplete: 'off' }, CE('option', { value: '' }, '---')); + for (const groupLabel in SHORTCUT_ACTIONS) { + const items = SHORTCUT_ACTIONS[groupLabel]; + if (!items) { + continue; + } + + const $optGroup = CE('optgroup', { label: groupLabel }); + for (const action in items) { + const crumbs = items[action as keyof typeof items]; + if (!crumbs) { + continue; + } + + const label = crumbs.join(' ❯ '); + const $option = CE('option', { value: action }, label); + $optGroup.appendChild($option); + } + + $baseSelect.appendChild($optGroup); + } + + const $content = CE('div', { + class: 'bx-controller-shortcuts-manager-container', + }); + + const onActionChanged = (e: Event) => { + const $target = e.target as HTMLSelectElement; + + // const profile = $selectProfile.value; + // const button: unknown = $target.dataset.button; + const action = $target.value as ShortcutAction; + + if (!PREF_CONTROLLER_FRIENDLY_UI) { + const $fakeSelect = $target.previousElementSibling! as HTMLSelectElement; + let fakeText = '---'; + if (action) { + const $selectedOption = $target.options[$target.selectedIndex]; + const $optGroup = $selectedOption.parentElement as HTMLOptGroupElement; + fakeText = $optGroup.label + ' ❯ ' + $selectedOption.text; + } + ($fakeSelect.firstElementChild as HTMLOptionElement).text = fakeText; + } + + // Update preset + if (!(e as any).ignoreOnChange) { + this.updatePreset(); + } + }; + + const fragment = document.createDocumentFragment(); + fragment.appendChild(CE('p', {class: 'bx-shortcut-note'}, + CE('span', {class: 'bx-prompt'}, PrompFont.HOME), + ': ' + t('controller-shortcuts-xbox-note'), + )); + + for (const button of this.BUTTONS_ORDER) { + const prompt = GamepadKeyName[button][1]; + + const $row = CE('div', { + class: 'bx-shortcut-row', + _nearby: { + orientation: 'horizontal', + }, + }); + const $label = CE('label', {class: 'bx-prompt'}, `${PrompFont.HOME}${prompt}`); + const $div = CE('div', {class: 'bx-shortcut-actions'}); + + let $fakeSelect: HTMLSelectElement | null = null; + if (!PREF_CONTROLLER_FRIENDLY_UI) { + $fakeSelect = CE('select', { autocomplete: 'off' }, + CE('option', {}, '---'), + ); + + $div.appendChild($fakeSelect); + } + + const $select = BxSelectElement.create($baseSelect.cloneNode(true) as HTMLSelectElement); + $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, { + focus: $select, + }); + + $row.append($label, $div); + fragment.appendChild($row); + } + + $content.appendChild(fragment); + + this.$content = $content; + } + + protected switchPreset(id: number): void { + const preset = this.allPresets.data[id]; + if (!preset) { + this.currentPresetId = 0; + return; + } + + this.currentPresetId = id; + const isDefaultPreset = id <= 0; + const actions = preset.data; + + // Reset selects' values + let button: unknown; + for (button in this.selectActions) { + const [$select, $fakeSelect] = this.selectActions[button as GamepadKey]!; + $select.value = actions.mapping[button as GamepadKey] || ''; + $select.disabled = isDefaultPreset; + $fakeSelect && ($fakeSelect.disabled = isDefaultPreset); + + BxEvent.dispatch($select, 'input', { + ignoreOnChange: true, + manualTrigger: true, + }); + } + + super.updateButtonStates(); + } + + private updatePreset() { + const newData: ControllerShortcutPresetData = deepClone(this.BLANK_PRESET_DATA); + + let button: unknown; + for (button in this.selectActions) { + const [$select, _] = this.selectActions[button as GamepadKey]!; + + const action = $select.value; + if (!action) { + continue; + } + + newData.mapping[button as GamepadKey] = action as ShortcutAction; + } + + const preset = this.allPresets.data[this.currentPresetId]; + preset.data = newData; + this.presetsDb.updatePreset(preset); + + StreamSettings.refreshControllerSettings(); + } +} diff --git a/src/modules/ui/dialog/profile-manger/keyboard-shortcuts-manager-dialog.ts b/src/modules/ui/dialog/profile-manger/keyboard-shortcuts-manager-dialog.ts new file mode 100755 index 0000000..34c62c5 --- /dev/null +++ b/src/modules/ui/dialog/profile-manger/keyboard-shortcuts-manager-dialog.ts @@ -0,0 +1,151 @@ +import { t } from "@/utils/translation"; +import { BaseProfileManagerDialog } from "./base-profile-manager-dialog"; +import type { KeyboardShortcutPresetData, KeyboardShortcutPresetRecord } from "@/types/presets"; +import { CE, createSettingRow } from "@/utils/html"; +import { KeyboardShortcutDefaultId, KeyboardShortcutsTable } from "@/utils/local-db/keyboard-shortcuts-table"; +import { SHORTCUT_ACTIONS } from "@/modules/shortcuts/shortcut-actions"; +import { BxKeyBindingButton, BxKeyBindingButtonFlag } from "@/web-components/bx-key-binding-button"; +import type { ShortcutAction } from "@/enums/shortcut-actions"; +import { deepClone } from "@/utils/global"; +import { StreamSettings } from "@/utils/stream-settings"; + +type KeyboardShortcutButtonDataset = { + action: ShortcutAction, +} + +export class KeyboardShortcutsManagerDialog extends BaseProfileManagerDialog { + private static instance: KeyboardShortcutsManagerDialog; + public static getInstance = () => KeyboardShortcutsManagerDialog.instance ?? (KeyboardShortcutsManagerDialog.instance = new KeyboardShortcutsManagerDialog(t('keyboard-shortcuts'))); + // private readonly LOG_TAG = 'KeyboardShortcutsManagerDialog'; + + protected $content: HTMLElement; + private readonly allKeyElements: BxKeyBindingButton[] = []; + + protected readonly BLANK_PRESET_DATA: KeyboardShortcutPresetData = { + mapping: {}, + }; + + constructor(title: string) { + super(title, KeyboardShortcutsTable.getInstance()); + + const $rows = CE('div', { class: 'bx-keyboard-shortcuts-manager-container' }); + + for (const groupLabel in SHORTCUT_ACTIONS) { + const items = SHORTCUT_ACTIONS[groupLabel]; + if (!items) { + continue; + } + + const $fieldSet = CE('fieldset', {}, CE('legend', {}, groupLabel)); + for (const action in items) { + const crumbs = items[action as keyof typeof items]; + if (!crumbs) { + continue; + } + + const label = crumbs.join(' ❯ '); + const $btn = BxKeyBindingButton.create({ + title: label, + isPrompt: false, + onChanged: this.onKeyChanged, + + allowedFlags: [BxKeyBindingButtonFlag.KEYBOARD_PRESS, BxKeyBindingButtonFlag.KEYBOARD_MODIFIER], + }); + $btn.classList.add('bx-full-width'); + $btn.dataset.action = action; + this.allKeyElements.push($btn); + + const $row = createSettingRow(label, CE('div', { class: 'bx-binding-button-wrapper' }, $btn)); + $fieldSet.appendChild($row); + } + + // Don't append empty
+ if ($fieldSet.childElementCount > 1) { + $rows.appendChild($fieldSet); + } + } + + this.$content = CE('div', {}, $rows); + } + + private onKeyChanged = (e: Event) => { + const $current = e.target as BxKeyBindingButton; + const keyInfo = $current.keyInfo; + + // Unbind duplicated keys + if (keyInfo) { + for (const $elm of this.allKeyElements) { + if ($elm === $current) { + continue; + } + + if ($elm.keyInfo?.code === keyInfo.code && $elm.keyInfo?.modifiers === keyInfo.modifiers) { + // Unbind manually + $elm.unbindKey(true); + } + } + } + + // Save preset + this.savePreset(); + } + + private parseDataset($btn: BxKeyBindingButton): KeyboardShortcutButtonDataset { + const dataset = $btn.dataset; + return { + action: dataset.action as ShortcutAction, + }; + } + + protected switchPreset(id: number): void { + const preset = this.allPresets.data[id]; + if (!preset) { + this.currentPresetId = KeyboardShortcutDefaultId.OFF; + return; + } + + const presetData = preset.data; + this.currentPresetId = id; + const isDefaultPreset = id <= 0; + this.updateButtonStates(); + + // Update buttons + for (const $elm of this.allKeyElements) { + const { action } = this.parseDataset($elm); + + const keyInfo = presetData.mapping[action]; + if (keyInfo) { + $elm.bindKey(keyInfo, true) + } else { + $elm.unbindKey(true); + } + + $elm.disabled = isDefaultPreset; + } + } + + private savePreset() { + const presetData = deepClone(this.BLANK_PRESET_DATA) as KeyboardShortcutPresetData; + + // Get mapping + for (const $elm of this.allKeyElements) { + const { action } = this.parseDataset($elm); + + const mapping = presetData.mapping; + if ($elm.keyInfo) { + mapping[action] = $elm.keyInfo; + } + } + + const oldPreset = this.allPresets.data[this.currentPresetId]; + const newPreset = { + id: this.currentPresetId, + name: oldPreset.name, + data: presetData, + }; + this.presetsDb.updatePreset(newPreset); + + this.allPresets.data[this.currentPresetId] = newPreset; + StreamSettings.refreshKeyboardShortcuts(); + } +} diff --git a/src/modules/ui/dialog/profile-manger/mkb-mapping-manager-dialog.ts b/src/modules/ui/dialog/profile-manger/mkb-mapping-manager-dialog.ts new file mode 100755 index 0000000..469f8ee --- /dev/null +++ b/src/modules/ui/dialog/profile-manger/mkb-mapping-manager-dialog.ts @@ -0,0 +1,254 @@ +import type { MkbPresetData, MkbPresetRecord } from "@/types/presets"; +import { BaseProfileManagerDialog } from "./base-profile-manager-dialog"; +import { t } from "@/utils/translation"; +import { MkbMappingPresetsTable } from "@/utils/local-db/mkb-mapping-presets-table"; +import { GamepadKey, GamepadKeyName } from "@/enums/gamepad"; +import { CE, createSettingRow } from "@/utils/html"; +import { MouseMapTo, MkbPresetKey, type KeyCode } from "@/enums/mkb"; +import { BxKeyBindingButton, BxKeyBindingButtonFlag } from "@/web-components/bx-key-binding-button"; +import { StreamSettings } from "@/utils/stream-settings"; +import { BxNumberStepper } from "@/web-components/bx-number-stepper"; +import { deepClone } from "@/utils/global"; +import { BxSelectElement } from "@/web-components/bx-select"; + +type MkbButtonDataset = { + keySlot: number, + buttonIndex: GamepadKey, +} + +export class MkbMappingManagerDialog extends BaseProfileManagerDialog { + private static instance: MkbMappingManagerDialog; + public static getInstance = () => MkbMappingManagerDialog.instance ?? (MkbMappingManagerDialog.instance = new MkbMappingManagerDialog(t('virtual-controller'))); + + declare protected $content: HTMLElement; + + private readonly KEYS_PER_BUTTON = 2; + private readonly BUTTONS_ORDER = [ + GamepadKey.HOME, + GamepadKey.UP, GamepadKey.DOWN, GamepadKey.LEFT, GamepadKey.RIGHT, + GamepadKey.A, GamepadKey.B, GamepadKey.X, GamepadKey.Y, + GamepadKey.LB, GamepadKey.RB, GamepadKey.LT, GamepadKey.RT, + GamepadKey.SELECT, GamepadKey.START, + GamepadKey.L3, GamepadKey.LS_UP, GamepadKey.LS_DOWN, GamepadKey.LS_LEFT, GamepadKey.LS_RIGHT, + GamepadKey.R3, GamepadKey.RS_UP, GamepadKey.RS_DOWN, GamepadKey.RS_LEFT, GamepadKey.RS_RIGHT, + ]; + + protected readonly BLANK_PRESET_DATA: MkbPresetData = { + mapping: {}, + mouse: { + [MkbPresetKey.MOUSE_MAP_TO]: MouseMapTo.RS, + [MkbPresetKey.MOUSE_SENSITIVITY_X]: 100, + [MkbPresetKey.MOUSE_SENSITIVITY_Y]: 100, + [MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT]: 20, + }, + }; + + private readonly allKeyElements: BxKeyBindingButton[] = []; + private $mouseMapTo!: BxSelectElement; + private $mouseSensitivityX!: BxNumberStepper; + private $mouseSensitivityY!: BxNumberStepper; + private $mouseDeadzone!: BxNumberStepper; + + constructor(title: string) { + super(title, MkbMappingPresetsTable.getInstance()); + this.render(); + } + + private onBindingKey = (e: MouseEvent) => { + const $btn = e.target as HTMLButtonElement; + if ($btn.disabled) { + return; + } + + if (e.button !== 0) { + return; + } + }; + + private parseDataset($btn: BxKeyBindingButton): MkbButtonDataset { + const dataset = $btn.dataset; + return { + keySlot: parseInt(dataset.keySlot!), + buttonIndex: parseInt(dataset.buttonIndex!), + }; + } + + private onKeyChanged = (e: Event) => { + const $current = e.target as BxKeyBindingButton; + const keyInfo = $current.keyInfo; + + // Unbind duplicated keys + if (keyInfo) { + for (const $elm of this.allKeyElements) { + if ($elm !== $current && $elm.keyInfo?.code === keyInfo.code) { + // Unbind manually + $elm.unbindKey(true); + } + } + } + + // Save preset + this.savePreset(); + } + + private render() { + const $rows = CE('div', {}, + CE('i', { class: 'bx-mkb-note' }, t('right-click-to-unbind')), + ); + + for (const buttonIndex of this.BUTTONS_ORDER) { + const [buttonName, buttonPrompt] = GamepadKeyName[buttonIndex]; + + let $elm; + const $fragment = document.createDocumentFragment(); + for (let i = 0; i < this.KEYS_PER_BUTTON; i++) { + $elm = BxKeyBindingButton.create({ + title: buttonPrompt, + isPrompt: true, + allowedFlags: [BxKeyBindingButtonFlag.KEYBOARD_PRESS, BxKeyBindingButtonFlag.MOUSE_CLICK, BxKeyBindingButtonFlag.MOUSE_WHEEL], + onChanged: this.onKeyChanged, + }); + + $elm.dataset.buttonIndex = buttonIndex.toString(); + $elm.dataset.keySlot = i.toString(); + + $elm.addEventListener('mouseup', this.onBindingKey); + + $fragment.appendChild($elm); + this.allKeyElements.push($elm); + } + + const $keyRow = CE('div', { + class: 'bx-mkb-key-row', + _nearby: { + orientation: 'horizontal', + }, + }, + CE('label', { title: buttonName }, buttonPrompt), + $fragment, + ); + + $rows.appendChild($keyRow); + } + + const savePreset = () => this.savePreset(); + const $extraSettings = CE('div', {}, + createSettingRow( + t('map-mouse-to'), + this.$mouseMapTo = BxSelectElement.create(CE('select', { _on: { input: savePreset } }, + CE('option', { value: MouseMapTo.RS }, t('right-stick')), + CE('option', { value: MouseMapTo.LS }, t('left-stick')), + CE('option', { value: MouseMapTo.OFF }, t('off')), + )), + ), + + createSettingRow( + t('horizontal-sensitivity'), + this.$mouseSensitivityX = BxNumberStepper.create('hor_sensitivity', 0, 1, 300, { + suffix: '%', + exactTicks: 50, + }, savePreset), + ), + + createSettingRow( + t('vertical-sensitivity'), + this.$mouseSensitivityY = BxNumberStepper.create('ver_sensitivity', 0, 1, 300, { + suffix: '%', + exactTicks: 50, + }, savePreset), + ), + + createSettingRow( + t('deadzone-counterweight'), + this.$mouseDeadzone = BxNumberStepper.create('deadzone_counterweight', 0, 1, 50, { + suffix: '%', + exactTicks: 10, + }, savePreset), + ), + ); + + this.$content = CE('div', {}, + $rows, + $extraSettings, + ); + } + + protected switchPreset(id: number): void { + const preset = this.allPresets.data[id]; + if (!preset) { + this.currentPresetId = 0; + return; + } + + const presetData = preset.data; + this.currentPresetId = id; + const isDefaultPreset = id <= 0; + this.updateButtonStates(); + + // Update buttons + for (const $elm of this.allKeyElements) { + const { buttonIndex, keySlot } = this.parseDataset($elm); + + const buttonKeys = presetData.mapping[buttonIndex]; + if (buttonKeys && buttonKeys[keySlot]) { + $elm.bindKey({ + code: buttonKeys[keySlot], + }, true) + } else { + $elm.unbindKey(true); + } + + $elm.disabled = isDefaultPreset; + } + + // Update mouse settings + const mouse = presetData.mouse; + this.$mouseMapTo.value = mouse.mapTo.toString(); + this.$mouseSensitivityX.value = mouse.sensitivityX.toString(); + this.$mouseSensitivityY.value = mouse.sensitivityY.toString(); + this.$mouseDeadzone.value = mouse.deadzoneCounterweight.toString(); + + this.$mouseMapTo.disabled = isDefaultPreset; + this.$mouseSensitivityX.dataset.disabled = isDefaultPreset.toString(); + this.$mouseSensitivityY.dataset.disabled = isDefaultPreset.toString(); + this.$mouseDeadzone.dataset.disabled = isDefaultPreset.toString(); + } + + private savePreset() { + const presetData = deepClone(this.BLANK_PRESET_DATA) as MkbPresetData; + + // Get mapping + for (const $elm of this.allKeyElements) { + const { buttonIndex, keySlot } = this.parseDataset($elm); + const mapping = presetData.mapping; + if (!mapping[buttonIndex]) { + mapping[buttonIndex] = []; + } + + if (!$elm.keyInfo) { + // Remove empty key from mapping + delete mapping[buttonIndex][keySlot]; + } else { + mapping[buttonIndex][keySlot] = $elm.keyInfo.code as KeyCode; + } + } + + // Get mouse settings + const mouse = presetData.mouse; + mouse.mapTo = parseInt(this.$mouseMapTo.value) as MouseMapTo; + mouse.sensitivityX = parseInt(this.$mouseSensitivityX.value); + mouse.sensitivityY = parseInt(this.$mouseSensitivityY.value); + mouse.deadzoneCounterweight = parseInt(this.$mouseDeadzone.value); + + const oldPreset = this.allPresets.data[this.currentPresetId]; + const newPreset = { + id: this.currentPresetId, + name: oldPreset.name, + data: presetData, + }; + this.presetsDb.updatePreset(newPreset); + + this.allPresets.data[this.currentPresetId] = newPreset; + StreamSettings.refreshMkbSettings(); + } +} diff --git a/src/modules/ui/dialog/remote-play-dialog.ts b/src/modules/ui/dialog/remote-play-dialog.ts old mode 100644 new mode 100755 index 8b1aeb1..370d934 --- a/src/modules/ui/dialog/remote-play-dialog.ts +++ b/src/modules/ui/dialog/remote-play-dialog.ts @@ -8,11 +8,12 @@ import { RemotePlayConsoleState, RemotePlayManager } from "@/modules/remote-play import { BxSelectElement } from "@/web-components/bx-select"; import { BxEvent } from "@/utils/bx-event"; import { BxLogger } from "@/utils/bx-logger"; +import { StreamResolution } from "@/enums/pref-values"; -export class RemotePlayNavigationDialog extends NavigationDialog { - private static instance: RemotePlayNavigationDialog; - public static getInstance = () => RemotePlayNavigationDialog.instance ?? (RemotePlayNavigationDialog.instance = new RemotePlayNavigationDialog()); +export class RemotePlayDialog extends NavigationDialog { + private static instance: RemotePlayDialog; + public static getInstance = () => RemotePlayDialog.instance ?? (RemotePlayDialog.instance = new RemotePlayDialog()); private readonly LOG_TAG = 'RemotePlayNavigationDialog'; private readonly STATE_LABELS: Record = { @@ -35,21 +36,18 @@ export class RemotePlayNavigationDialog extends NavigationDialog { const $settingNote = CE('p', {}); - const currentResolution = getPref(PrefKey.REMOTE_PLAY_RESOLUTION); + const currentResolution = getPref(PrefKey.REMOTE_PLAY_STREAM_RESOLUTION); let $resolutions : HTMLSelectElement | NavigationElement = CE('select', {}, - CE('option', {value: '1080p'}, '1080p'), - CE('option', {value: '720p'}, '720p'), + CE('option', { value: StreamResolution.DIM_720P }, '720p'), + CE('option', { value: StreamResolution.DIM_1080P }, '1080p'), ); - if (getPref(PrefKey.UI_CONTROLLER_FRIENDLY)) { - $resolutions = BxSelectElement.wrap($resolutions as HTMLSelectElement); - } - + $resolutions = BxSelectElement.create($resolutions as HTMLSelectElement); $resolutions.addEventListener('input', (e: Event) => { const value = (e.target as HTMLSelectElement).value; $settingNote.textContent = value === '1080p' ? '✅ ' + t('can-stream-xbox-360-games') : '❌ ' + t('cant-stream-xbox-360-games'); - setPref(PrefKey.REMOTE_PLAY_RESOLUTION, value); + setPref(PrefKey.REMOTE_PLAY_STREAM_RESOLUTION, value); }); ($resolutions as any).value = currentResolution; @@ -67,7 +65,7 @@ export class RemotePlayNavigationDialog extends NavigationDialog { $fragment.appendChild($qualitySettings); // Render consoles list - const manager = RemotePlayManager.getInstance(); + const manager = RemotePlayManager.getInstance()!; const consoles = manager.getConsoles(); for (let con of consoles) { diff --git a/src/modules/ui/dialog/settings-dialog.ts b/src/modules/ui/dialog/settings-dialog.ts old mode 100644 new mode 100755 index fccf39b..157eaeb --- a/src/modules/ui/dialog/settings-dialog.ts +++ b/src/modules/ui/dialog/settings-dialog.ts @@ -1,15 +1,11 @@ import { isFullVersion } from "@macros/build" with {type: "macro"}; import { limitVideoPlayerFps, onChangeVideoPlayerType, updateVideoPlayer } from "@/modules/stream/stream-settings-utils"; -import { ButtonStyle, CE, createButton, createSvgIcon, removeChildElements, type BxButton } from "@/utils/html"; +import { ButtonStyle, CE, createButton, createSettingRow, createSvgIcon, escapeCssSelector, type BxButtonOptions } from "@/utils/html"; import { NavigationDialog, NavigationDirection } from "./navigation-dialog"; -import { ControllerShortcut } from "@/modules/controller-shortcut"; -import { MkbRemapper } from "@/modules/mkb/mkb-remapper"; -import { NativeMkbHandler } from "@/modules/mkb/native-mkb-handler"; -import { SoundShortcut } from "@/modules/shortcuts/shortcut-sound"; +import { SoundShortcut } from "@/modules/shortcuts/sound-shortcut"; import { StreamStats } from "@/modules/stream/stream-stats"; import { TouchController } from "@/modules/touch-controller"; -import { VibrationManager } from "@/modules/vibration-manager"; import { BxEvent } from "@/utils/bx-event"; import { BxIcon } from "@/utils/bx-icon"; import { STATES, AppInterface, deepClone, SCRIPT_VERSION, STORAGE, SCRIPT_VARIANT } from "@/utils/global"; @@ -19,20 +15,26 @@ import { setNearby } from "@/utils/navigation-utils"; import { PatcherCache } from "@/modules/patcher"; import { UserAgentProfile } from "@/enums/user-agent"; import { UserAgent } from "@/utils/user-agent"; -import { BX_FLAGS, NATIVE_FETCH, type BxFlags } from "@/utils/bx-flags"; -import { copyToClipboard } from "@/utils/utils"; -import { GamepadKey } from "@/enums/mkb"; +import { BX_FLAGS } from "@/utils/bx-flags"; +import { clearAllData, copyToClipboard } from "@/utils/utils"; import { PrefKey, StorageKey } from "@/enums/pref-keys"; -import { ControllerDeviceVibration, getPref, getPrefDefinition, setPref, StreamTouchController } from "@/utils/settings-storages/global-settings-storage"; -import { SettingElement, type BxHtmlSettingElement } from "@/utils/setting-element"; -import type { RecommendedSettings, SettingDefinition, SuggestedSettingCategory as SuggestedSettingProfile } from "@/types/setting-definition"; +import { getPref, getPrefDefinition, setPref } from "@/utils/settings-storages/global-settings-storage"; +import { SettingElement } from "@/utils/setting-element"; +import type { SettingDefinition, SuggestedSettingProfile } from "@/types/setting-definition"; import { FullscreenText } from "../fullscreen-text"; import { BxLogger } from "@/utils/bx-logger"; -import { updatePollingRate } from "@/utils/gamepad"; +import { GamepadKey } from "@/enums/gamepad"; +import { NativeMkbHandler } from "@/modules/mkb/native-mkb-handler"; +import { ControllerExtraSettings } from "./settings/controller-extra"; +import { SuggestionsSetting } from "./settings/suggestions"; +import { StreamSettings } from "@/utils/stream-settings"; +import { MkbExtraSettings } from "./settings/mkb-extra"; +import { BxExposed } from "@/utils/bx-exposed"; -type SettingTabContentItem = Partial<{ +type SettingTabSectionItem = Partial<{ pref: PrefKey; + multiLines: boolean; label: string; note: string | (() => HTMLElement); experimental: string; @@ -41,36 +43,39 @@ type SettingTabContentItem = Partial<{ unsupported: boolean; unsupportedNote: string; onChange: (e: any, value: number) => void; - onCreated: (setting: SettingTabContentItem, $control: any) => void; + onCreated: (setting: SettingTabSectionItem, $control: any) => void; params: any; requiredVariants?: BuildVariant | Array; }> -type SettingTabContent = { - group: 'general' | 'server' | 'stream' | 'game-bar' | 'co-op' | 'mkb' | 'touch-control' | 'loading-screen' | 'ui' | 'other' | 'advanced' | 'footer' | 'audio' | 'video' | 'controller' | 'native-mkb' | 'stats' | 'controller-shortcuts'; +type SettingTabSection = { + group: 'general' | 'server' | 'stream' | 'game-bar' | 'mkb' | 'touch-control' | 'loading-screen' | 'ui' | 'other' | 'advanced' | 'footer' + | 'audio' | 'video' + | 'device' | 'controller' | 'mkb' | 'native-mkb' + | 'stats'; label?: string; unsupported?: boolean; unsupportedNote?: string | Text | null; helpUrl?: string; - content?: any; + content?: HTMLElement; lazyContent?: boolean | (() => HTMLElement); - items?: Array void) | false>; + items?: Array void) | false>; requiredVariants?: BuildVariant | Array; }; type SettingTab = { icon: SVGElement; group: SettingTabGroup, - items: Array | (() => Array); + items: Array | (() => Array); requiredVariants?: BuildVariant | Array; lazyContent?: boolean; }; -type SettingTabGroup = 'global' | 'stream' | 'controller' | 'mkb' | 'native-mkb' | 'shortcuts' | 'stats'; +type SettingTabGroup = 'global' | 'stream' | 'controller' | 'mkb' | 'stats'; -export class SettingsNavigationDialog extends NavigationDialog { - private static instance: SettingsNavigationDialog; - public static getInstance = () => SettingsNavigationDialog.instance ?? (SettingsNavigationDialog.instance = new SettingsNavigationDialog()); +export class SettingsDialog extends NavigationDialog { + private static instance: SettingsDialog; + public static getInstance = () => SettingsDialog.instance ?? (SettingsDialog.instance = new SettingsDialog()); private readonly LOG_TAG = 'SettingsNavigationDialog'; $container!: HTMLElement; @@ -84,23 +89,23 @@ export class SettingsNavigationDialog extends NavigationDialog { private renderFullSettings: boolean; - private suggestedSettings: Record> = { + protected suggestedSettings: Record> = { recommended: {}, default: {}, lowest: {}, highest: {}, }; - private suggestedSettingLabels: PartialRecord = {}; - private settingElements: PartialRecord = {}; + protected suggestedSettingLabels: PartialRecord = {}; + protected settingElements: PartialRecord = {}; - private readonly TAB_GLOBAL_ITEMS: Array = [{ + private readonly TAB_GLOBAL_ITEMS: Array = [{ group: 'general', label: t('better-xcloud'), helpUrl: 'https://better-xcloud.github.io/features/', items: [ // Top buttons ($parent) => { - const PREF_LATEST_VERSION = getPref(PrefKey.LATEST_VERSION); + const PREF_LATEST_VERSION = getPref(PrefKey.VERSION_LATEST); const topButtons = []; // "New version available" button @@ -109,7 +114,7 @@ export class SettingsNavigationDialog extends NavigationDialog { const opts = { label: '🌟 ' + t('new-version-available', {version: PREF_LATEST_VERSION}), style: ButtonStyle.PRIMARY | ButtonStyle.FOCUSABLE | ButtonStyle.FULL_WIDTH, - } as BxButton; + } as BxButtonOptions; if (AppInterface && AppInterface.updateLatestScript) { opts.onClick = e => AppInterface.updateLatestScript(); @@ -167,7 +172,7 @@ export class SettingsNavigationDialog extends NavigationDialog { }, CE('label', {}, t('suggest-settings')), CE('span', {}, '❯'), ); - this.$btnSuggestion.addEventListener('click', this.renderSuggestions.bind(this)); + this.$btnSuggestion.addEventListener('click', SuggestionsSetting.renderSuggestions.bind(this)); topButtons.push(this.$btnSuggestion); @@ -181,7 +186,10 @@ export class SettingsNavigationDialog extends NavigationDialog { $parent.appendChild($div); }, - PrefKey.BETTER_XCLOUD_LOCALE, + { + pref: PrefKey.SCRIPT_LOCALE, + multiLines: true, + }, PrefKey.SERVER_BYPASS_RESTRICTION, PrefKey.UI_CONTROLLER_FRIENDLY, PrefKey.REMOTE_PLAY_ENABLED, @@ -190,21 +198,25 @@ export class SettingsNavigationDialog extends NavigationDialog { group: 'server', label: t('server'), items: [ - PrefKey.SERVER_REGION, - PrefKey.STREAM_PREFERRED_LOCALE, - PrefKey.PREFER_IPV6_SERVER, + { + pref: PrefKey.SERVER_REGION, + multiLines: true, + }, + { + pref: PrefKey.STREAM_PREFERRED_LOCALE, + multiLines: true, + }, + PrefKey.SERVER_PREFER_IPV6, ], }, { group: 'stream', label: t('stream'), items: [ - PrefKey.STREAM_TARGET_RESOLUTION, + PrefKey.STREAM_RESOLUTION, PrefKey.STREAM_CODEC_PROFILE, + PrefKey.STREAM_MAX_VIDEO_BITRATE, - PrefKey.BITRATE_VIDEO_MAX, - - PrefKey.AUDIO_ENABLE_VOLUME_CONTROL, - PrefKey.STREAM_DISABLE_FEEDBACK_DIALOG, + PrefKey.AUDIO_VOLUME_CONTROL_ENABLED, PrefKey.SCREENSHOT_APPLY_FILTERS, @@ -212,55 +224,68 @@ export class SettingsNavigationDialog extends NavigationDialog { PrefKey.GAME_FORTNITE_FORCE_CONSOLE, PrefKey.STREAM_COMBINE_SOURCES, ], - }, { - requiredVariants: 'full', - group: 'co-op', - label: t('local-co-op'), - items: [ - PrefKey.LOCAL_CO_OP_ENABLED, - ], }, { requiredVariants: 'full', group: 'mkb', label: t('mouse-and-keyboard'), - unsupportedNote: !STATES.userAgent.capabilities.mkb ? CE('a', { - href: 'https://github.com/redphx/better-xcloud/issues/206#issuecomment-1920475657', - target: '_blank', - }, '⚠️ ' + t('browser-unsupported-feature')) : null, - unsupported: !STATES.userAgent.capabilities.mkb, items: [ - PrefKey.NATIVE_MKB_ENABLED, - PrefKey.GAME_MSFS2020_FORCE_NATIVE_MKB, + PrefKey.NATIVE_MKB_MODE, + { + pref: PrefKey.FORCE_NATIVE_MKB_GAMES, + multiLines: true, + }, + PrefKey.MKB_ENABLED, PrefKey.MKB_HIDE_IDLE_CURSOR, ], + + // Unsupported + ...(!STATES.browser.capabilities.emulatedNativeMkb && (!STATES.userAgent.capabilities.mkb || !STATES.browser.capabilities.mkb) ? { + unsupported: true, + unsupportedNote: CE('a', { + href: 'https://github.com/redphx/better-xcloud/issues/206#issuecomment-1920475657', + target: '_blank', + }, '⚠️ ' + t('browser-unsupported-feature')), + } : {}), }, { requiredVariants: 'full', group: 'touch-control', label: t('touch-controller'), - unsupported: !STATES.userAgent.capabilities.touch, - unsupportedNote: !STATES.userAgent.capabilities.touch ? '⚠️ ' + t('device-unsupported-touch') : null, items: [ - PrefKey.STREAM_TOUCH_CONTROLLER, - PrefKey.STREAM_TOUCH_CONTROLLER_AUTO_OFF, - PrefKey.STREAM_TOUCH_CONTROLLER_DEFAULT_OPACITY, - PrefKey.STREAM_TOUCH_CONTROLLER_STYLE_STANDARD, - PrefKey.STREAM_TOUCH_CONTROLLER_STYLE_CUSTOM, + { + pref: PrefKey.TOUCH_CONTROLLER_MODE, + note: CE('a', { href: 'https://github.com/redphx/better-xcloud/discussions/241', target: '_blank' }, t('unofficial-game-list')), + }, + PrefKey.TOUCH_CONTROLLER_AUTO_OFF, + PrefKey.TOUCH_CONTROLLER_DEFAULT_OPACITY, + PrefKey.TOUCH_CONTROLLER_STYLE_STANDARD, + PrefKey.TOUCH_CONTROLLER_STYLE_CUSTOM, ], + + // Unsupported + ...(!STATES.userAgent.capabilities.touch ? { + unsupported: true, + unsupportedNote: '⚠️ ' + t('device-unsupported-touch'), + } : {}), }, { group: 'ui', label: t('ui'), items: [ PrefKey.UI_LAYOUT, PrefKey.UI_GAME_CARD_SHOW_WAIT_TIME, - PrefKey.CONTROLLER_SHOW_CONNECTION_STATUS, - PrefKey.STREAM_SIMPLIFY_MENU, - PrefKey.SKIP_SPLASH_VIDEO, + PrefKey.UI_CONTROLLER_SHOW_STATUS, + PrefKey.UI_SIMPLIFY_STREAM_MENU, + PrefKey.UI_SKIP_SPLASH_VIDEO, !AppInterface && PrefKey.UI_SCROLLBAR_HIDE, - PrefKey.HIDE_DOTS_ICON, - PrefKey.REDUCE_ANIMATIONS, + PrefKey.UI_HIDE_SYSTEM_MENU_ICON, + PrefKey.UI_DISABLE_FEEDBACK_DIALOG, + PrefKey.UI_REDUCE_ANIMATIONS, PrefKey.BLOCK_SOCIAL_FEATURES, - PrefKey.UI_HIDE_SECTIONS, + PrefKey.BYOG_DISABLED, + { + pref: PrefKey.UI_HIDE_SECTIONS, + multiLines: true, + }, ], }, { requiredVariants: 'full', @@ -273,9 +298,9 @@ export class SettingsNavigationDialog extends NavigationDialog { group: 'loading-screen', label: t('loading-screen'), items: [ - PrefKey.UI_LOADING_SCREEN_GAME_ART, - PrefKey.UI_LOADING_SCREEN_WAIT_TIME, - PrefKey.UI_LOADING_SCREEN_ROCKET, + PrefKey.LOADING_SCREEN_GAME_ART, + PrefKey.LOADING_SCREEN_SHOW_WAIT_TIME, + PrefKey.LOADING_SCREEN_ROCKET, ], }, { group: 'other', @@ -289,15 +314,15 @@ export class SettingsNavigationDialog extends NavigationDialog { items: [ { pref: PrefKey.USER_AGENT_PROFILE, + multiLines: true, onCreated: (setting, $control) => { const defaultUserAgent = (window.navigator as any).orgUserAgent || window.navigator.userAgent; const $inpCustomUserAgent = CE('input', { - id: `bx_setting_inp_${setting.pref}`, type: 'text', placeholder: defaultUserAgent, autocomplete: 'off', - 'class': 'bx-settings-custom-user-agent', + class: 'bx-settings-custom-user-agent', tabindex: 0, }); @@ -320,18 +345,8 @@ export class SettingsNavigationDialog extends NavigationDialog { }, { group: 'footer', items: [ - // Donation link - ($parent) => { - $parent.appendChild(CE('a', { - class: 'bx-donation-link', - href: 'https://ko-fi.com/redphx', - target: '_blank', - tabindex: 0, - }, `❤️ ${t('support-better-xcloud')}`)); - }, - // xCloud version - ($parent) => { + $parent => { try { const appVersion = document.querySelector('meta[name=gamepass-app-version]')!.content; const appDate = new Date(document.querySelector('meta[name=gamepass-app-date]')!.content).toISOString().substring(0, 10); @@ -341,39 +356,69 @@ export class SettingsNavigationDialog extends NavigationDialog { } catch (e) {} }, - // Debug info - ($parent) => { - const debugInfo = deepClone(BX_FLAGS.DeviceInfo); - debugInfo['settings'] = JSON.parse(window.localStorage.getItem('better_xcloud') || '{}'); + // Donation link + $parent => { + $parent.appendChild(CE('a', { + class: 'bx-donation-link', + href: 'https://ko-fi.com/redphx', + target: '_blank', + tabindex: 0, + }, `❤️ ${t('support-better-xcloud')}`)); + }, - const $debugInfo = CE('div', {class: 'bx-debug-info'}, + // Clear data + $parent => { + $parent.appendChild(createButton({ + label: t('clear-data'), + style: ButtonStyle.GHOST | ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE, + onClick: e => { + if (confirm(t('clear-data-confirm'))) { + clearAllData(); + } + }, + })); + }, + + // Debug info + $parent => { + $parent.appendChild(CE('div', { class: 'bx-debug-info' }, createButton({ label: 'Debug info', style: ButtonStyle.GHOST | ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE, onClick: e => { - const $pre = (e.target as HTMLElement).closest('button')?.nextElementSibling!; + const $button = (e.target as HTMLElement).closest('button'); + if (!$button) { + return; + } + + let $pre = $button.nextElementSibling!; + if (!$pre) { + const debugInfo = deepClone(BX_FLAGS.DeviceInfo); + debugInfo['settings'] = JSON.parse(window.localStorage.getItem(StorageKey.GLOBAL) || '{}'); + + $pre = CE('pre', { + class: 'bx-focusable bx-gone', + tabindex: 0, + _on: { + click: async (e: Event) => { + await copyToClipboard((e.target as HTMLElement).innerText); + }, + }, + }, '```\n' + JSON.stringify(debugInfo, null, ' ') + '\n```'); + + $button.insertAdjacentElement('afterend', $pre); + } $pre.classList.toggle('bx-gone'); $pre.scrollIntoView(); }, }), - CE('pre', { - class: 'bx-focusable bx-gone', - tabindex: 0, - on: { - click: async (e: Event) => { - await copyToClipboard((e.target as HTMLElement).innerText); - }, - }, - }, '```\n' + JSON.stringify(debugInfo, null, ' ') + '\n```'), - ); - - $parent.appendChild($debugInfo); + )); }, ], }]; - private readonly TAB_DISPLAY_ITEMS: Array = [{ + private readonly TAB_DISPLAY_ITEMS: Array = [{ requiredVariants: 'full', group: 'audio', label: t('audio'), @@ -384,9 +429,9 @@ export class SettingsNavigationDialog extends NavigationDialog { SoundShortcut.setGainNodeVolume(value); }, params: { - disabled: !getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL), + disabled: !getPref(PrefKey.AUDIO_VOLUME_CONTROL_ENABLED), }, - onCreated: (setting: SettingTabContentItem, $elm: HTMLElement) => { + onCreated: (setting: SettingTabSectionItem, $elm: HTMLElement) => { const $range = $elm.querySelector('input[type=range')!; window.addEventListener(BxEvent.SETTINGS_CHANGED, e => { const { storageKey, settingKey, settingValue } = e as any; @@ -445,26 +490,34 @@ export class SettingsNavigationDialog extends NavigationDialog { }], }]; - private readonly TAB_CONTROLLER_ITEMS: Array = [{ + private readonly TAB_CONTROLLER_ITEMS: Array = [isFullVersion() && STATES.browser.capabilities.deviceVibration && { + group: 'device', + label: t('device'), + items: [{ + pref: PrefKey.DEVICE_VIBRATION_MODE, + multiLines: true, + unsupported: !STATES.browser.capabilities.deviceVibration, + onChange: () => StreamSettings.refreshControllerSettings(), + }, { + pref: PrefKey.DEVICE_VIBRATION_INTENSITY, + unsupported: !STATES.browser.capabilities.deviceVibration, + onChange: () => StreamSettings.refreshControllerSettings(), + }], + }, { group: 'controller', label: t('controller'), helpUrl: 'https://better-xcloud.github.io/ingame-features/#controller', - items: [{ - pref: PrefKey.CONTROLLER_ENABLE_VIBRATION, - unsupported: !VibrationManager.supportControllerVibration(), - onChange: () => VibrationManager.updateGlobalVars(), - }, { - pref: PrefKey.CONTROLLER_DEVICE_VIBRATION, - unsupported: !VibrationManager.supportDeviceVibration(), - onChange: () => VibrationManager.updateGlobalVars(), - }, (VibrationManager.supportControllerVibration() || VibrationManager.supportDeviceVibration()) && { - pref: PrefKey.CONTROLLER_VIBRATION_INTENSITY, - unsupported: !VibrationManager.supportDeviceVibration(), - onChange: () => VibrationManager.updateGlobalVars(), - }, isFullVersion() && { + items: [ + isFullVersion() && { + pref: PrefKey.LOCAL_CO_OP_ENABLED, + onChange: () => { BxExposed.toggleLocalCoOp(getPref(PrefKey.LOCAL_CO_OP_ENABLED)); }, + }, + isFullVersion() && { pref: PrefKey.CONTROLLER_POLLING_RATE, - onChange: () => updatePollingRate(), - }], + onChange: () => StreamSettings.refreshControllerSettings(), + }, isFullVersion() && ($parent => { + $parent.appendChild(ControllerExtraSettings.renderSettings.apply(this)); + })], }, isFullVersion() && STATES.userAgent.capabilities.touch && { @@ -475,7 +528,7 @@ export class SettingsNavigationDialog extends NavigationDialog { content: CE('select', { disabled: true, }, CE('option', {}, t('default'))), - onCreated: (setting: SettingTabContentItem, $elm: HTMLSelectElement) => { + onCreated: (setting: SettingTabSectionItem, $elm: HTMLSelectElement) => { $elm.addEventListener('input', e => { TouchController.applyCustomLayout($elm.value, 1000); }); @@ -521,45 +574,44 @@ export class SettingsNavigationDialog extends NavigationDialog { }], }]; - private readonly TAB_VIRTUAL_CONTROLLER_ITEMS: (() => Array) = () => [{ - group: 'mkb', - label: t('virtual-controller'), - helpUrl: 'https://better-xcloud.github.io/mouse-and-keyboard/', - content: MkbRemapper.getInstance().render(), - }]; + private readonly TAB_MKB_ITEMS: (() => Array) = () => [ + isFullVersion() && { + requiredVariants: 'full', + group: 'mkb', + label: t('mouse-and-keyboard'), + helpUrl: 'https://better-xcloud.github.io/mouse-and-keyboard/', + items: [ + isFullVersion() && (($parent: HTMLElement) => { + $parent.appendChild(MkbExtraSettings.renderSettings.apply(this)); + }) + ], + }, - private readonly TAB_NATIVE_MKB_ITEMS: Array = [{ - requiredVariants: 'full', - group: 'native-mkb', - label: t('native-mkb'), - items: isFullVersion() ? [{ - pref: PrefKey.NATIVE_MKB_SCROLL_VERTICAL_SENSITIVITY, - onChange: (e: any, value: number) => { - NativeMkbHandler.getInstance().setVerticalScrollMultiplier(value / 100); - }, - }, { - pref: PrefKey.NATIVE_MKB_SCROLL_HORIZONTAL_SENSITIVITY, - onChange: (e: any, value: number) => { - NativeMkbHandler.getInstance().setHorizontalScrollMultiplier(value / 100); - }, + isFullVersion() && NativeMkbHandler.isAllowed() && { + requiredVariants: 'full', + group: 'native-mkb', + label: t('native-mkb'), + items: isFullVersion() ? [{ + pref: PrefKey.NATIVE_MKB_SCROLL_VERTICAL_SENSITIVITY, + onChange: (e: any, value: number) => { + NativeMkbHandler.getInstance()?.setVerticalScrollMultiplier(value / 100); + }, + }, { + pref: PrefKey.NATIVE_MKB_SCROLL_HORIZONTAL_SENSITIVITY, + onChange: (e: any, value: number) => { + NativeMkbHandler.getInstance()?.setHorizontalScrollMultiplier(value / 100); + }, }] : [], }]; - private readonly TAB_SHORTCUTS_ITEMS: (() => Array) = () => [{ - requiredVariants: 'full', - group: 'controller-shortcuts', - label: t('controller-shortcuts'), - content: isFullVersion() && ControllerShortcut.renderSettings(), - }]; - - private readonly TAB_STATS_ITEMS: Array = [{ + private readonly TAB_STATS_ITEMS: Array = [{ group: 'stats', label: t('stream-stats'), helpUrl: 'https://better-xcloud.github.io/stream-stats/', items: [{ pref: PrefKey.STATS_SHOW_WHEN_PLAYING, }, { - pref: PrefKey.STATS_QUICK_GLANCE, + pref: PrefKey.STATS_QUICK_GLANCE_ENABLED, onChange: (e: InputEvent) => { const streamStats = StreamStats.getInstance(); (e.target! as HTMLInputElement).checked ? streamStats.quickGlanceSetup() : streamStats.quickGlanceStop(); @@ -586,7 +638,7 @@ export class SettingsNavigationDialog extends NavigationDialog { ], }]; - private readonly SETTINGS_UI: Record = { + protected readonly SETTINGS_UI: Record = { global: { group: 'global', icon: BxIcon.HOME, @@ -606,25 +658,10 @@ export class SettingsNavigationDialog extends NavigationDialog { requiredVariants: 'full', }, - mkb: isFullVersion() && getPref(PrefKey.MKB_ENABLED) && { + mkb: isFullVersion() && { group: 'mkb', - icon: BxIcon.VIRTUAL_CONTROLLER, - items: this.TAB_VIRTUAL_CONTROLLER_ITEMS, - lazyContent: true, - requiredVariants: 'full', - }, - - 'native-mkb': isFullVersion() && AppInterface && getPref(PrefKey.NATIVE_MKB_ENABLED) === 'on' && { - group: 'native-mkb', icon: BxIcon.NATIVE_MKB, - items: this.TAB_NATIVE_MKB_ITEMS, - requiredVariants: 'full', - }, - - shortcuts: { - group: 'shortcuts', - icon: BxIcon.COMMAND, - items: this.TAB_SHORTCUTS_ITEMS, + items: this.TAB_MKB_ITEMS, lazyContent: true, requiredVariants: 'full', }, @@ -642,6 +679,24 @@ export class SettingsNavigationDialog extends NavigationDialog { this.renderFullSettings = STATES.supportedRegion && STATES.isSignedIn; this.setupDialog(); + + this.onMountedCallbacks.push(() => { + // Update video's settings + onChangeVideoPlayerType(); + + // Render custom layouts list + if (STATES.userAgent.capabilities.touch) { + BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED); + } + + // Trigger event + const $selectUserAgent = document.querySelector(`#bx_setting_${escapeCssSelector(PrefKey.USER_AGENT_PROFILE)}`); + if ($selectUserAgent) { + $selectUserAgent.disabled = true; + BxEvent.dispatch($selectUserAgent, 'input', {}); + $selectUserAgent.disabled = false; + } + }); } getDialog(): NavigationDialog { @@ -653,25 +708,11 @@ export class SettingsNavigationDialog extends NavigationDialog { } onMounted(): void { - if (!this.renderFullSettings) { - return; - } + super.onMounted(); + } - // Update video's settings - onChangeVideoPlayerType(); - - // Render custom layouts list - if (STATES.userAgent.capabilities.touch) { - BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED); - } - - // Trigger event - const $selectUserAgent = document.querySelector(`#bx_setting_${PrefKey.USER_AGENT_PROFILE}`); - if ($selectUserAgent) { - $selectUserAgent.disabled = true; - BxEvent.dispatch($selectUserAgent, 'input', {}); - $selectUserAgent.disabled = false; - } + isOverlayVisible(): boolean { + return !STATES.isPlaying; } private reloadPage() { @@ -683,86 +724,6 @@ export class SettingsNavigationDialog extends NavigationDialog { window.location.reload(); } - private async getRecommendedSettings(androidInfo: BxFlags['DeviceInfo']['androidInfo']): Promise { - function normalize(str: string) { - return str.toLowerCase() - .trim() - .replaceAll(/\s+/g, '-') - .replaceAll(/-+/g, '-'); - } - - // Get recommended settings from GitHub - try { - let {brand, board, model} = androidInfo!; - brand = normalize(brand); - board = normalize(board); - model = normalize(model); - - const url = `https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/devices/${brand}/${board}-${model}.json`; - const response = await NATIVE_FETCH(url); - const json = (await response.json()) as RecommendedSettings; - const recommended: PartialRecord = {}; - - // Only supports schema version 1 - if (json.schema_version !== 1) { - return null; - } - - const scriptSettings = json.settings.script; - - // Set base settings - if (scriptSettings._base) { - let base = typeof scriptSettings._base === 'string' ? [scriptSettings._base] : scriptSettings._base; - for (const profile of base) { - Object.assign(recommended, this.suggestedSettings[profile]); - } - - delete scriptSettings._base; - } - - // Override settings - let key: Exclude; - // @ts-ignore - for (key in scriptSettings) { - recommended[key] = scriptSettings[key]; - } - - // Update device type in BxFlags - BX_FLAGS.DeviceInfo.deviceType = json.device_type; - - this.suggestedSettings.recommended = recommended; - - return json.device_name; - } catch (e) {} - - return null; - } - - private addDefaultSuggestedSetting(prefKey: PrefKey, value: any) { - let key: keyof typeof this.suggestedSettings; - for (key in this.suggestedSettings) { - if (key !== 'default' && !(prefKey in this.suggestedSettings)) { - this.suggestedSettings[key][prefKey] = value; - } - } - } - - private generateDefaultSuggestedSettings() { - let key: keyof typeof this.suggestedSettings; - for (key in this.suggestedSettings) { - if (key === 'default') { - continue; - } - - let prefKey: PrefKey; - for (prefKey in this.suggestedSettings[key]) { - if (!(prefKey in this.suggestedSettings.default)) { - this.suggestedSettings.default[prefKey] = getPrefDefinition(prefKey).default; - } - } - } - } - private isSupportedVariant(requiredVariants: BuildVariant | Array | undefined) { if (typeof requiredVariants === 'undefined') { return true; @@ -772,234 +733,7 @@ export class SettingsNavigationDialog extends NavigationDialog { return requiredVariants.includes(SCRIPT_VARIANT); } - private async renderSuggestions(e: Event) { - const $btnSuggest = (e.target as HTMLElement).closest('div')!; - $btnSuggest.toggleAttribute('bx-open'); - - let $content = $btnSuggest.nextElementSibling as HTMLElement; - if ($content) { - BxEvent.dispatch($content.querySelector('select'), 'input'); - return; - } - - // Get labels - let settingTabGroup: keyof typeof this.SETTINGS_UI; - for (settingTabGroup in this.SETTINGS_UI) { - const settingTab = this.SETTINGS_UI[settingTabGroup]; - - if (!settingTab || !settingTab.items || typeof settingTab.items === 'function') { - continue; - } - - for (const settingTabContent of settingTab.items) { - if (!settingTabContent || !settingTabContent.items) { - continue; - } - - for (const setting of settingTabContent.items) { - let prefKey: PrefKey | undefined; - - if (typeof setting === 'string') { - prefKey = setting; - } else if (typeof setting === 'object') { - prefKey = setting.pref as PrefKey; - } - - if (prefKey) { - this.suggestedSettingLabels[prefKey] = settingTabContent.label; - } - } - } - } - - // Get recommended settings for Android devices - let recommendedDevice: string | null = ''; - - if (BX_FLAGS.DeviceInfo.deviceType.includes('android')) { - if (BX_FLAGS.DeviceInfo.androidInfo) { - recommendedDevice = await this.getRecommendedSettings(BX_FLAGS.DeviceInfo.androidInfo); - } - } - - /* - recommendedDevice = await this.getRecommendedSettings({ - manufacturer: 'Lenovo', - board: 'kona', - model: 'Lenovo TB-9707F', - }); - */ - - const hasRecommendedSettings = Object.keys(this.suggestedSettings.recommended).length > 0; - - // Add some specific setings based on device type - const deviceType = BX_FLAGS.DeviceInfo.deviceType; - if (deviceType === 'android-handheld') { - // Disable touch - this.addDefaultSuggestedSetting(PrefKey.STREAM_TOUCH_CONTROLLER, StreamTouchController.OFF); - // Enable device vibration - this.addDefaultSuggestedSetting(PrefKey.CONTROLLER_DEVICE_VIBRATION, ControllerDeviceVibration.ON); - } else if (deviceType === 'android') { - // Enable device vibration - this.addDefaultSuggestedSetting(PrefKey.CONTROLLER_DEVICE_VIBRATION, ControllerDeviceVibration.AUTO); - } else if (deviceType === 'android-tv') { - // Disable touch - this.addDefaultSuggestedSetting(PrefKey.STREAM_TOUCH_CONTROLLER, StreamTouchController.OFF); - } - - // Set value for Default profile - this.generateDefaultSuggestedSettings(); - - // Start rendering - const $suggestedSettings = CE('div', {class: 'bx-suggest-wrapper'}); - const $select = CE('select', {}, - hasRecommendedSettings && CE('option', {value: 'recommended'}, t('recommended')), - !hasRecommendedSettings && CE('option', {value: 'highest'}, t('highest-quality')), - CE('option', {value: 'default'}, t('default')), - CE('option', {value: 'lowest'}, t('lowest-quality')), - ); - $select.addEventListener('input', e => { - const profile = $select.value as SuggestedSettingProfile; - - // Empty children - removeChildElements($suggestedSettings); - const fragment = document.createDocumentFragment(); - - let note: HTMLElement | string | undefined; - if (profile === 'recommended') { - note = t('recommended-settings-for-device', {device: recommendedDevice}); - } else if (profile === 'highest') { - // Add note for "Highest quality" profile - note = '⚠️ ' + t('highest-quality-note'); - } - - note && fragment.appendChild(CE('div', {class: 'bx-suggest-note'}, note)); - - const settings = this.suggestedSettings[profile]; - let prefKey: PrefKey; - for (prefKey in settings) { - const currentValue = getPref(prefKey, false); - const suggestedValue = settings[prefKey]; - const currentValueText = STORAGE.Global.getValueText(prefKey, currentValue); - const isSameValue = currentValue === suggestedValue; - - let $child: HTMLElement; - let $value: HTMLElement | string; - if (isSameValue) { - // No changes - $value = currentValueText; - } else { - const suggestedValueText = STORAGE.Global.getValueText(prefKey, suggestedValue); - $value = currentValueText + ' ➔ ' + suggestedValueText; - } - - let $checkbox: HTMLInputElement; - const breadcrumb = this.suggestedSettingLabels[prefKey] + ' ❯ ' + STORAGE.Global.getLabel(prefKey); - - $child = CE('div', { - class: `bx-suggest-row ${isSameValue ? 'bx-suggest-ok' : 'bx-suggest-change'}`, - }, - $checkbox = CE('input', { - type: 'checkbox', - tabindex: 0, - checked: true, - id: `bx_suggest_${prefKey}`, - }), - CE('label', { - for: `bx_suggest_${prefKey}`, - }, - CE('div', { - class: 'bx-suggest-label', - }, breadcrumb), - CE('div', { - class: 'bx-suggest-value', - }, $value), - ), - ); - - if (isSameValue) { - $checkbox.disabled = true; - $checkbox.checked = true; - } - - fragment.appendChild($child); - } - - $suggestedSettings.appendChild(fragment); - }); - - BxEvent.dispatch($select, 'input'); - - const onClickApply = () => { - const profile = $select.value as SuggestedSettingProfile; - const settings = this.suggestedSettings[profile]; - - let prefKey: PrefKey; - for (prefKey in settings) { - const suggestedValue = settings[prefKey]; - const $checkBox = $content.querySelector(`#bx_suggest_${prefKey}`)!; - if (!$checkBox.checked || $checkBox.disabled) { - continue; - } - - const $control = this.settingElements[prefKey] as HTMLElement; - - // Set value directly if the control element is not available - if (!$control) { - setPref(prefKey, suggestedValue); - continue; - } - - if ('setValue' in $control) { - ($control as BxHtmlSettingElement).setValue(suggestedValue); - } else { - ($control as HTMLInputElement).value = suggestedValue; - } - - BxEvent.dispatch($control, 'input', { - manualTrigger: true, - }); - } - - // Refresh suggested settings - BxEvent.dispatch($select, 'input'); - }; - - // Apply button - const $btnApply = createButton({ - label: t('apply'), - style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE, - onClick: onClickApply, - }); - - $content = CE('div', { - class: 'bx-suggest-box', - _nearby: { - orientation: 'vertical', - } - }, - BxSelectElement.wrap($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', - tabindex: 0, - }, '🤓 ' + t('how-to-improve-app-performance')), - - BX_FLAGS.DeviceInfo.deviceType.includes('android') && !hasRecommendedSettings && CE('a', { - class: 'bx-suggest-link bx-focusable', - href: 'https://github.com/redphx/better-xcloud-devices', - target: '_blank', - tabindex: 0, - }, t('suggest-settings-link')), - ); - - $btnSuggest.insertAdjacentElement('afterend', $content); - } - - private onTabClicked(e: Event) { + private onTabClicked = (e: Event) => { const $svg = (e.target as SVGElement).closest('svg')!; // Render tab content lazily @@ -1008,9 +742,12 @@ export class SettingsNavigationDialog extends NavigationDialog { delete $svg.dataset.lazy; // Render data const settingTab = this.SETTINGS_UI[$svg.dataset.group as SettingTabGroup]; + if (!settingTab) { + return; + } const items = (settingTab.items as Function)(); - const $tabContent = this.renderTabContent.call(this, settingTab, items); + const $tabContent = this.renderSettingsSection.call(this, settingTab, items); this.$tabContents.appendChild($tabContent); } @@ -1046,12 +783,12 @@ export class SettingsNavigationDialog extends NavigationDialog { $svg.tabIndex = 0; settingTab.lazyContent && ($svg.dataset.lazy = settingTab.lazyContent.toString()); - $svg.addEventListener('click', this.onTabClicked.bind(this)); + $svg.addEventListener('click', this.onTabClicked); return $svg; } - private onGlobalSettingChanged(e: Event) { + private onGlobalSettingChanged = (e: Event) => { // Clear PatcherCache; isFullVersion() && PatcherCache.getInstance().clear(); @@ -1062,8 +799,8 @@ export class SettingsNavigationDialog extends NavigationDialog { this.$btnGlobalReload.classList.add('bx-danger'); } - private renderServerSetting(setting: SettingTabContentItem): HTMLElement { - let selectedValue =getPref(PrefKey.SERVER_REGION); + private renderServerSetting(setting: SettingTabSectionItem): HTMLElement { + let selectedValue = getPref(PrefKey.SERVER_REGION); const continents: Record('select', { - id: `bx_setting_${setting.pref}`, + id: `bx_setting_${escapeCssSelector(setting.pref!)}`, title: setting.label, tabindex: 0, }); @@ -1148,11 +885,11 @@ export class SettingsNavigationDialog extends NavigationDialog { return $control; } - private renderSettingRow(settingTab: SettingTab, $tabContent: HTMLElement, settingTabContent: SettingTabContent, setting: SettingTabContentItem | string) { + private renderSettingRow(settingTab: SettingTab, $tabContent: HTMLElement, settingTabContent: SettingTabSection, setting: SettingTabSectionItem | string) { if (typeof setting === 'string') { setting = { pref: setting as PrefKey, - } satisfies SettingTabContentItem; + } satisfies SettingTabSectionItem; } const pref = setting.pref; @@ -1167,7 +904,7 @@ export class SettingsNavigationDialog extends NavigationDialog { } else if (!setting.unsupported) { if (pref === PrefKey.SERVER_REGION) { $control = this.renderServerSetting(setting); - } else if (pref === PrefKey.BETTER_XCLOUD_LOCALE) { + } else if (pref === PrefKey.SCRIPT_LOCALE) { $control = SettingElement.fromPref(pref, STORAGE.Global, async (e: Event) => { const newLocale = (e.target as HTMLSelectElement).value; @@ -1205,15 +942,15 @@ export class SettingsNavigationDialog extends NavigationDialog { } else { let onChange = setting.onChange; if (!onChange && settingTab.group === 'global') { - onChange = this.onGlobalSettingChanged.bind(this); + onChange = this.onGlobalSettingChanged; } $control = SettingElement.fromPref(pref as PrefKey, STORAGE.Global, onChange, setting.params); } // Replace if not using controller-friendly UI + if (!forceFriendly && !getPref(PrefKey.UI_CONTROLLER_FRIENDLY)) { + $select.classList.add('bx-select'); + // @ts-ignore + return $select; + } -export class BxSelectElement { - static wrap($select: HTMLSelectElement) { // Remove "tabindex" attribute from