diff --git a/dist/better-xcloud.user.js b/dist/better-xcloud.user.js index 06b04a3..2cdab38 100644 --- a/dist/better-xcloud.user.js +++ b/dist/better-xcloud.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name Better xCloud // @namespace https://github.com/redphx -// @version 5.0.1 +// @version 5.1.0-beta // @description Improve Xbox Cloud Gaming (xCloud) experience // @author redphx // @license MIT @@ -56,7 +56,7 @@ class UserAgent { UserAgent.spoof(); } static updateStorage(profile, custom) { - const clonedConfig = structuredClone(UserAgent.#config); + const clonedConfig = deepClone(UserAgent.#config); if (clonedConfig.profile = profile, typeof custom !== "undefined") clonedConfig.custom = custom; window.localStorage.setItem(UserAgent.STORAGE_KEY, JSON.stringify(clonedConfig)); @@ -106,7 +106,14 @@ class UserAgent { } // src/utils/global.ts -var SCRIPT_VERSION = "5.0.1", AppInterface = window.AppInterface; +function deepClone(obj) { + if ("structuredClone" in window) + return structuredClone(obj); + if (!obj) + return obj; + return JSON.parse(JSON.stringify(obj)); +} +var SCRIPT_VERSION = "5.1.0-beta", AppInterface = window.AppInterface; UserAgent.init(); var userAgent = window.navigator.userAgent.toLowerCase(), isTv = userAgent.includes("smart-tv") || userAgent.includes("smarttv") || /\baft.*\b/.test(userAgent), isVr = window.navigator.userAgent.includes("VR") && window.navigator.userAgent.includes("OculusBrowser"), browserHasTouchSupport = "ontouchstart" in window || navigator.maxTouchPoints > 0, userAgentHasTouchSupport = !isTv && !isVr && browserHasTouchSupport, STATES = { isPlaying: !1, @@ -183,7 +190,8 @@ var DEFAULT_FLAGS = { SafariWorkaround: !0, UseDevTouchLayout: !1, ForceNativeMkbTitles: [], - FeatureGates: null + FeatureGates: null, + ScriptUi: "default" }, BX_FLAGS = Object.assign(DEFAULT_FLAGS, window.BX_FLAGS || {}); try { delete window.BX_FLAGS; @@ -249,9 +257,13 @@ var ButtonStyleIndices = Object.keys(ButtonStyle).splice(0, Object.keys(ButtonSt else $btn = CE("button", { class: "bx-button", type: "button" }); const style = options.style || 0; - return style && ButtonStyleIndices.forEach((index) => { + style && ButtonStyleIndices.forEach((index) => { style & index && $btn.classList.add(ButtonStyle[index]); - }), 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; + }), 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); + for (let key in options.attributes) + if (!$btn.hasOwnProperty(key)) + $btn.setAttribute(key, options.attributes[key]); + return $btn; }, CTN = document.createTextNode.bind(document); window.BX_CE = createElement; @@ -304,6 +316,8 @@ var SUPPORTED_LANGUAGES = { activated: "Activated", active: "Active", advanced: "Advanced", + "always-off": "Always off", + "always-on": "Always on", "amd-fidelity-cas": "AMD FidelityFX CAS", "android-app-settings": "Android app settings", apply: "Apply", @@ -617,7 +631,9 @@ class Translations { static async downloadTranslations(locale) { try { const translations = await (await NATIVE_FETCH(`https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/translations/${locale}.json`)).json(); - return window.localStorage.setItem(Translations.#KEY_TRANSLATIONS, JSON.stringify(translations)), Translations.#foreignTranslations = translations, !0; + if (localStorage.getItem(Translations.#KEY_LOCALE) === locale) + window.localStorage.setItem(Translations.#KEY_TRANSLATIONS, JSON.stringify(translations)), Translations.#foreignTranslations = translations; + return !0; } catch (e) { debugger; } @@ -652,7 +668,7 @@ class SettingElement { const label = setting.options[value], $option = CE("option", { value }, label); $control.appendChild($option); } - return $control.value = currentValue, onChange && $control.addEventListener("change", (e) => { + return $control.value = currentValue, onChange && $control.addEventListener("input", (e) => { const target = e.target, value = setting.type && setting.type === "number" ? parseInt(target.value) : target.value; onChange(e, value); }), $control.setValue = (value) => { @@ -674,13 +690,13 @@ class SettingElement { const target = e.target; target.selected = !target.selected; const $parent = target.parentElement; - $parent.focus(), $parent.dispatchEvent(new Event("change")); + $parent.focus(), $parent.dispatchEvent(new Event("input")); }), $control.appendChild($option); } return $control.addEventListener("mousedown", function(e) { const self = this, orgScrollTop = self.scrollTop; window.setTimeout(() => self.scrollTop = orgScrollTop, 0); - }), $control.addEventListener("mousemove", (e) => e.preventDefault()), onChange && $control.addEventListener("change", (e) => { + }), $control.addEventListener("mousemove", (e) => e.preventDefault()), onChange && $control.addEventListener("input", (e) => { const target = e.target, values = Array.from(target.selectedOptions).map((i) => i.value); onChange(e, values); }), $control; @@ -821,6 +837,9 @@ class StreamStats { #$br; #lastVideoStat; #quickGlanceObserver; + constructor() { + this.#render(); + } start(glancing = !1) { if (!this.isHidden() || glancing && this.isGlancing()) return; @@ -846,9 +865,11 @@ class StreamStats { isHidden = () => this.#$container && this.#$container.classList.contains("bx-gone"); isGlancing = () => this.#$container && this.#$container.dataset.display === "glancing"; quickGlanceSetup() { - if (this.#quickGlanceObserver) + if (!STATES.isPlaying || this.#quickGlanceObserver) return; const $uiContainer = document.querySelector("div[data-testid=ui-container]"); + if (!$uiContainer) + return; this.#quickGlanceObserver = new MutationObserver((mutationList, observer) => { for (let record of mutationList) if (record.attributeName && record.attributeName === "aria-expanded") @@ -911,8 +932,6 @@ class StreamStats { this.stop(); } #render() { - if (this.#$container) - return; const stats = { [StreamStat.PING]: [t("stat-ping"), this.#$ping = CE("span", {}, "0")], [StreamStat.FPS]: [t("stat-fps"), this.#$fps = CE("span", {}, "0")], @@ -932,9 +951,7 @@ class StreamStats { this.#$container = CE("div", { class: "bx-stats-bar bx-gone" }, $barFragment), this.refreshStyles(), document.documentElement.appendChild(this.#$container); } static setupEvents() { - window.addEventListener(BxEvent.STREAM_LOADING, (e) => { - StreamStats.getInstance().#render(); - }), window.addEventListener(BxEvent.STREAM_PLAYING, (e) => { + window.addEventListener(BxEvent.STREAM_PLAYING, (e) => { const PREF_STATS_QUICK_GLANCE = getPref(PrefKey.STATS_QUICK_GLANCE), PREF_STATS_SHOW_WHEN_PLAYING = getPref(PrefKey.STATS_SHOW_WHEN_PLAYING), streamStats = StreamStats.getInstance(); if (PREF_STATS_SHOW_WHEN_PLAYING) streamStats.start(); @@ -1087,7 +1104,7 @@ class Preferences { const options = { default: t("default") }; - if (!("getCapabilities" in RTCRtpReceiver) || typeof RTCRtpTransceiver === "undefined" || !("setCodecPreferences" in RTCRtpTransceiver.prototype)) + if (!("getCapabilities" in RTCRtpReceiver)) return options; let hasLowCodec = !1, hasNormalCodec = !1, hasHighCodec = !1; const codecs = RTCRtpReceiver.getCapabilities("video").codecs; @@ -1413,7 +1430,7 @@ class Preferences { [UserAgentProfile.SMARTTV_GENERIC]: "Smart TV", [UserAgentProfile.SMARTTV_TIZEN]: "Samsung Smart TV", [UserAgentProfile.VR_OCULUS]: "Meta Quest VR", - [UserAgentProfile.ANDROID_KIWI_V123]: "Kiwi Browser v123", + [UserAgentProfile.ANDROID_KIWI_V123]: "Kiwi Browser v124 Fix", [UserAgentProfile.CUSTOM]: t("custom") } }, @@ -2141,468 +2158,6 @@ class KeyHelper { } } -// src/assets/svg/command.svg -var command_default = "\n \n \n\n"; - -// src/assets/svg/controller.svg -var controller_default = "\n \n\n"; - -// src/assets/svg/copy.svg -var copy_default = "\n \n\n"; - -// src/assets/svg/cursor-text.svg -var cursor_text_default = "\n \n\n"; - -// src/assets/svg/display.svg -var display_default = "\n \n\n"; - -// src/assets/svg/home.svg -var home_default = "\n \n\n"; - -// src/assets/svg/native-mkb.svg -var native_mkb_default = "\n \n \n \n \n \n \n \n \n\n"; - -// src/assets/svg/new.svg -var new_default = "\n \n\n"; - -// src/assets/svg/question.svg -var question_default = "\n \n\n"; - -// src/assets/svg/refresh.svg -var refresh_default = "\n \n\n"; - -// src/assets/svg/remote-play.svg -var remote_play_default = "\n \n\n"; - -// src/assets/svg/stream-settings.svg -var stream_settings_default = "\n \n\n"; - -// src/assets/svg/stream-stats.svg -var stream_stats_default = "\n \n\n"; - -// src/assets/svg/trash.svg -var trash_default = "\n \n\n"; - -// src/assets/svg/touch-control-enable.svg -var touch_control_enable_default = "\n \n \n \n \n\n"; - -// src/assets/svg/touch-control-disable.svg -var touch_control_disable_default = "\n \n \n \n \n \n \n \n\n"; - -// src/assets/svg/virtual-controller.svg -var virtual_controller_default = "\n \n \n \n \n \n \n \n \n \n\n"; - -// src/assets/svg/caret-left.svg -var caret_left_default = "\n \n\n"; - -// src/assets/svg/caret-right.svg -var caret_right_default = "\n \n\n"; - -// src/assets/svg/camera.svg -var camera_default = "\n \n \n \n \n\n"; - -// src/assets/svg/microphone.svg -var microphone_default = "\n \n\n"; - -// src/assets/svg/microphone-slash.svg -var microphone_slash_default = "\n \n \n\n"; - -// src/assets/svg/battery-full.svg -var battery_full_default = "\n \n\n"; - -// src/assets/svg/clock.svg -var clock_default = "\n \n \n \n \n\n"; - -// src/assets/svg/cloud.svg -var cloud_default = "\n \n\n"; - -// src/assets/svg/download.svg -var download_default = "\n \n \n\n"; - -// src/assets/svg/speaker-high.svg -var speaker_high_default = "\n \n \n\n"; - -// src/assets/svg/upload.svg -var upload_default = "\n \n \n\n"; - -// src/utils/bx-icon.ts -var BxIcon = { - STREAM_SETTINGS: stream_settings_default, - STREAM_STATS: stream_stats_default, - COMMAND: command_default, - CONTROLLER: controller_default, - DISPLAY: display_default, - HOME: home_default, - NATIVE_MKB: native_mkb_default, - NEW: new_default, - COPY: copy_default, - TRASH: trash_default, - CURSOR_TEXT: cursor_text_default, - QUESTION: question_default, - REFRESH: refresh_default, - VIRTUAL_CONTROLLER: virtual_controller_default, - REMOTE_PLAY: remote_play_default, - CARET_LEFT: caret_left_default, - CARET_RIGHT: caret_right_default, - SCREENSHOT: camera_default, - TOUCH_CONTROL_ENABLE: touch_control_enable_default, - TOUCH_CONTROL_DISABLE: touch_control_disable_default, - MICROPHONE: microphone_default, - MICROPHONE_MUTED: microphone_slash_default, - BATTERY: battery_full_default, - PLAYTIME: clock_default, - SERVER: cloud_default, - DOWNLOAD: download_default, - UPLOAD: upload_default, - AUDIO: speaker_high_default -}; - -// src/modules/stream/stream-badges.ts -var StreamBadge; -(function(StreamBadge2) { - StreamBadge2["PLAYTIME"] = "playtime"; - StreamBadge2["BATTERY"] = "battery"; - StreamBadge2["DOWNLOAD"] = "in"; - StreamBadge2["UPLOAD"] = "out"; - StreamBadge2["SERVER"] = "server"; - StreamBadge2["VIDEO"] = "video"; - StreamBadge2["AUDIO"] = "audio"; -})(StreamBadge || (StreamBadge = {})); -var StreamBadgeIcon = { - [StreamBadge.PLAYTIME]: BxIcon.PLAYTIME, - [StreamBadge.VIDEO]: BxIcon.DISPLAY, - [StreamBadge.BATTERY]: BxIcon.BATTERY, - [StreamBadge.DOWNLOAD]: BxIcon.DOWNLOAD, - [StreamBadge.UPLOAD]: BxIcon.UPLOAD, - [StreamBadge.SERVER]: BxIcon.SERVER, - [StreamBadge.AUDIO]: BxIcon.AUDIO -}; - -class StreamBadges { - static instance; - static getInstance() { - if (!StreamBadges.instance) - StreamBadges.instance = new StreamBadges; - return StreamBadges.instance; - } - #ipv6 = !1; - #resolution = null; - #video = null; - #audio = null; - #region = ""; - startBatteryLevel = 100; - startTimestamp = 0; - #$container; - #cachedDoms = {}; - #interval; - #REFRESH_INTERVAL = 3000; - setRegion(region) { - this.#region = region; - } - #renderBadge(name, value, color) { - let $badge; - if (this.#cachedDoms[name]) - return $badge = this.#cachedDoms[name], $badge.lastElementChild.textContent = value, $badge; - if ($badge = CE("div", { class: "bx-badge", title: t(`badge-${name}`) }, CE("span", { class: "bx-badge-name" }, createSvgIcon(StreamBadgeIcon[name])), CE("span", { class: "bx-badge-value", style: `background-color: ${color}` }, value)), name === StreamBadge.BATTERY) - $badge.classList.add("bx-badge-battery"); - return this.#cachedDoms[name] = $badge, $badge; - } - async#updateBadges(forceUpdate = !1) { - if (!this.#$container || !forceUpdate && !this.#$container.isConnected) { - this.#stop(); - return; - } - let now = +new Date; - const diffSeconds = Math.ceil((now - this.startTimestamp) / 1000), playtime = this.#secondsToHm(diffSeconds); - let batteryLevel = "100%", batteryLevelInt = 100, isCharging = !1; - if ("getBattery" in navigator) - try { - const bm = await navigator.getBattery(); - if (isCharging = bm.charging, batteryLevelInt = Math.round(bm.level * 100), batteryLevel = `${batteryLevelInt}%`, batteryLevelInt != this.startBatteryLevel) { - const diffLevel = Math.round(batteryLevelInt - this.startBatteryLevel), sign = diffLevel > 0 ? "+" : ""; - batteryLevel += ` (${sign}${diffLevel}%)`; - } - } catch (e) { - } - const stats = await STATES.currentStream.peerConnection?.getStats(); - let totalIn = 0, totalOut = 0; - stats.forEach((stat) => { - if (stat.type === "candidate-pair" && stat.packetsReceived > 0 && stat.state === "succeeded") - totalIn += stat.bytesReceived, totalOut += stat.bytesSent; - }); - const badges = { - [StreamBadge.DOWNLOAD]: totalIn ? this.#humanFileSize(totalIn) : null, - [StreamBadge.UPLOAD]: totalOut ? this.#humanFileSize(totalOut) : null, - [StreamBadge.PLAYTIME]: playtime, - [StreamBadge.BATTERY]: batteryLevel - }; - let name; - for (name in badges) { - const value = badges[name]; - if (value === null) - continue; - const $elm = this.#cachedDoms[name]; - if ($elm && ($elm.lastElementChild.textContent = value), name === StreamBadge.BATTERY) - if (this.startBatteryLevel === 100 && batteryLevelInt === 100) - $elm.classList.add("bx-gone"); - else - $elm.dataset.charging = isCharging.toString(), $elm.classList.remove("bx-gone"); - } - } - async#start() { - await this.#updateBadges(!0), this.#stop(), this.#interval = window.setInterval(this.#updateBadges.bind(this), this.#REFRESH_INTERVAL); - } - #stop() { - this.#interval && clearInterval(this.#interval), this.#interval = null; - } - #secondsToHm(seconds) { - let h = Math.floor(seconds / 3600), m = Math.floor(seconds % 3600 / 60) + 1; - if (m === 60) - h += 1, m = 0; - const output = []; - return h > 0 && output.push(`${h}h`), m > 0 && output.push(`${m}m`), output.join(" "); - } - #humanFileSize(size) { - const units = ["B", "KB", "MB", "GB", "TB"], i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024)); - return (size / Math.pow(1024, i)).toFixed(2) + " " + units[i]; - } - async render() { - if (this.#$container) - return this.#start(), this.#$container; - await this.#getServerStats(); - let video = ""; - if (this.#resolution) - video = `${this.#resolution.height}p`; - if (this.#video) { - if (video && (video += "/"), video += this.#video.codec, this.#video.profile) { - const profile = this.#video.profile; - let quality = profile; - if (profile.startsWith("4d")) - quality = t("visual-quality-high"); - else if (profile.startsWith("42e")) - quality = t("visual-quality-normal"); - else if (profile.startsWith("420")) - quality = t("visual-quality-low"); - video += ` (${quality})`; - } - } - let audio; - if (this.#audio) { - audio = this.#audio.codec; - const bitrate = this.#audio.bitrate / 1000; - audio += ` (${bitrate} kHz)`; - } - let batteryLevel = ""; - if ("getBattery" in navigator) - batteryLevel = "100%"; - let server = this.#region; - server += "@" + (this.#ipv6 ? "IPv6" : "IPv4"); - const BADGES = [ - [StreamBadge.PLAYTIME, "1m", "#ff004d"], - [StreamBadge.BATTERY, batteryLevel, "#00b543"], - [StreamBadge.DOWNLOAD, this.#humanFileSize(0), "#29adff"], - [StreamBadge.UPLOAD, this.#humanFileSize(0), "#ff77a8"], - [StreamBadge.SERVER, server, "#ff6c24"], - video ? [StreamBadge.VIDEO, video, "#742f29"] : null, - audio ? [StreamBadge.AUDIO, audio, "#5f574f"] : null - ], $container = CE("div", { class: "bx-badges" }); - return BADGES.forEach((item2) => { - if (!item2) - return; - const $badge = this.#renderBadge(...item2); - $container.appendChild($badge); - }), this.#$container = $container, await this.#start(), $container; - } - async#getServerStats() { - const stats = await STATES.currentStream.peerConnection.getStats(), allVideoCodecs = {}; - let videoCodecId; - const allAudioCodecs = {}; - let audioCodecId; - const allCandidates = {}; - let candidateId; - if (stats.forEach((stat) => { - if (stat.type === "codec") { - const mimeType = stat.mimeType.split("/")[0]; - if (mimeType === "video") - allVideoCodecs[stat.id] = stat; - else if (mimeType === "audio") - allAudioCodecs[stat.id] = stat; - } else if (stat.type === "inbound-rtp" && stat.packetsReceived > 0) { - if (stat.kind === "video") - videoCodecId = stat.codecId; - else if (stat.kind === "audio") - audioCodecId = stat.codecId; - } else if (stat.type === "candidate-pair" && stat.packetsReceived > 0 && stat.state === "succeeded") - candidateId = stat.remoteCandidateId; - else if (stat.type === "remote-candidate") - allCandidates[stat.id] = stat.address; - }), videoCodecId) { - const videoStat = allVideoCodecs[videoCodecId], video = { - codec: videoStat.mimeType.substring(6) - }; - if (video.codec === "H264") { - const match = /profile-level-id=([0-9a-f]{6})/.exec(videoStat.sdpFmtpLine); - video.profile = match ? match[1] : null; - } - this.#video = video; - } - if (audioCodecId) { - const audioStat = allAudioCodecs[audioCodecId]; - this.#audio = { - codec: audioStat.mimeType.substring(6), - bitrate: audioStat.clockRate - }; - } - if (candidateId) - BxLogger.info("candidate", candidateId, allCandidates), this.#ipv6 = allCandidates[candidateId].includes(":"); - } - static setupEvents() { - window.addEventListener(BxEvent.STREAM_PLAYING, (e) => { - const $video = e.$video, streamBadges = StreamBadges.getInstance(); - streamBadges.#resolution = { - width: $video.videoWidth, - height: $video.videoHeight - }, streamBadges.startTimestamp = +new Date; - try { - "getBattery" in navigator && navigator.getBattery().then((bm) => { - streamBadges.startBatteryLevel = Math.round(bm.level * 100); - }); - } catch (e2) { - } - }); - } -} - -// src/modules/stream/stream-ui.ts -var cloneStreamHudButton = function($orgButton, label, svgIcon) { - const $container = $orgButton.cloneNode(!0); - let timeout; - const onTransitionStart = (e) => { - if (e.propertyName !== "opacity") - return; - timeout && clearTimeout(timeout), $container.style.pointerEvents = "none"; - }, onTransitionEnd = (e) => { - if (e.propertyName !== "opacity") - return; - if (document.getElementById("StreamHud")?.style.left === "0px") - timeout && clearTimeout(timeout), timeout = window.setTimeout(() => { - $container.style.pointerEvents = "auto"; - }, 100); - }; - if (STATES.browserHasTouchSupport) - $container.addEventListener("transitionstart", onTransitionStart), $container.addEventListener("transitionend", onTransitionEnd); - const $button = $container.querySelector("button"); - $button.setAttribute("title", label); - const $orgSvg = $button.querySelector("svg"), $svg = createSvgIcon(svgIcon); - return $svg.style.fill = "none", $svg.setAttribute("class", $orgSvg.getAttribute("class") || ""), $svg.ariaHidden = "true", $orgSvg.replaceWith($svg), $container; -}, cloneCloseButton = function($$btnOrg, icon, className, onChange) { - const $btn = $$btnOrg.cloneNode(!0), $svg = createSvgIcon(icon); - return $svg.setAttribute("class", $btn.firstElementChild.getAttribute("class") || ""), $svg.style.fill = "none", $btn.classList.add(className), $btn.removeChild($btn.firstElementChild), $btn.appendChild($svg), $btn.addEventListener("click", onChange), $btn; -}; -function injectStreamMenuButtons() { - const $screen = document.querySelector("#PageContent section[class*=PureScreens]"); - if (!$screen) - return; - if ($screen.xObserving) - return; - $screen.xObserving = !0; - const $settingsDialog = document.querySelector(".bx-stream-settings-dialog"), $parent = $screen.parentElement, hideSettingsFunc = (e) => { - if (e) { - const $target = e.target; - if (e.stopPropagation(), $target != $parent && $target.id !== "MultiTouchSurface" && !$target.querySelector("#BabylonCanvasContainer-main")) - return; - if ($target.id === "MultiTouchSurface") - $target.removeEventListener("touchstart", hideSettingsFunc); - } - $settingsDialog.classList.add("bx-gone"), $parent?.removeEventListener("click", hideSettingsFunc); - }; - let $btnStreamSettings, $btnStreamStats; - const streamStats = StreamStats.getInstance(); - new MutationObserver((mutationList) => { - mutationList.forEach((item2) => { - if (item2.type !== "childList") - return; - item2.addedNodes.forEach(async ($node) => { - if (!$node || $node.nodeType !== Node.ELEMENT_NODE) - return; - let $elm = $node; - if ($elm instanceof SVGSVGElement) - return; - if ($elm.className?.includes("PureErrorPage")) { - BxEvent.dispatch(window, BxEvent.STREAM_ERROR_PAGE); - return; - } - if ($elm.className?.startsWith("StreamMenu-module__container")) { - const $btnCloseHud = document.querySelector("button[class*=StreamMenu-module__backButton]"); - if (!$btnCloseHud) - return; - $btnCloseHud.addEventListener("click", (e) => { - $settingsDialog.classList.add("bx-gone"); - }); - const $btnRefresh = cloneCloseButton($btnCloseHud, BxIcon.REFRESH, "bx-stream-refresh-button", () => { - confirm(t("confirm-reload-stream")) && window.location.reload(); - }), $btnHome = cloneCloseButton($btnCloseHud, BxIcon.HOME, "bx-stream-home-button", () => { - confirm(t("back-to-home-confirm")) && (window.location.href = window.location.href.substring(0, 31)); - }); - $btnCloseHud.insertAdjacentElement("afterend", $btnRefresh), $btnRefresh.insertAdjacentElement("afterend", $btnHome), document.querySelector("div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module]")?.appendChild(await StreamBadges.getInstance().render()), hideSettingsFunc(); - return; - } - if ($elm.className?.startsWith("Overlay-module_") || $elm.className?.startsWith("InProgressScreen")) - $elm = $elm.querySelector("#StreamHud"); - if (!$elm || ($elm.id || "") !== "StreamHud") - return; - const $gripHandle = $elm.querySelector("button[class^=GripHandle]"), hideGripHandle = () => { - if (!$gripHandle) - return; - $gripHandle.dispatchEvent(new PointerEvent("pointerdown")), $gripHandle.click(), $gripHandle.dispatchEvent(new PointerEvent("pointerdown")), $gripHandle.click(); - }, $orgButton = $elm.querySelector("div[class^=HUDButton]"); - if (!$orgButton) - return; - if (!$btnStreamSettings) - $btnStreamSettings = cloneStreamHudButton($orgButton, t("stream-settings"), BxIcon.STREAM_SETTINGS), $btnStreamSettings.addEventListener("click", (e) => { - hideGripHandle(), e.preventDefault(), $settingsDialog.classList.remove("bx-gone"), $parent?.addEventListener("click", hideSettingsFunc); - const $touchSurface = document.getElementById("MultiTouchSurface"); - $touchSurface && $touchSurface.style.display != "none" && $touchSurface.addEventListener("touchstart", hideSettingsFunc); - }); - if (!$btnStreamStats) - $btnStreamStats = cloneStreamHudButton($orgButton, t("stream-stats"), BxIcon.STREAM_STATS), $btnStreamStats.addEventListener("click", (e) => { - hideGripHandle(), e.preventDefault(), streamStats.toggle(); - const btnStreamStatsOn2 = !streamStats.isHidden() && !streamStats.isGlancing(); - $btnStreamStats.classList.toggle("bx-stream-menu-button-on", btnStreamStatsOn2); - }); - const btnStreamStatsOn = !streamStats.isHidden() && !streamStats.isGlancing(); - if ($btnStreamStats.classList.toggle("bx-stream-menu-button-on", btnStreamStatsOn), $orgButton) { - const $btnParent = $orgButton.parentElement; - $btnParent.insertBefore($btnStreamStats, $btnParent.lastElementChild), $btnParent.insertBefore($btnStreamSettings, $btnStreamStats); - const $dotsButton = $btnParent.lastElementChild; - $dotsButton.parentElement.insertBefore($dotsButton, $dotsButton.parentElement.firstElementChild); - } - }); - }); - }).observe($screen, { subtree: !0, childList: !0 }); -} -function showStreamSettings(tabId) { - const $wrapper = document.querySelector(".bx-stream-settings-dialog"); - if (!$wrapper) - return; - if (tabId) { - const $tab = $wrapper.querySelector(`.bx-stream-settings-tabs svg[data-group=${tabId}]`); - $tab && $tab.dispatchEvent(new Event("click")); - } - $wrapper.classList.remove("bx-gone"); - const $screen = document.querySelector("#PageContent section[class*=PureScreens]"); - if ($screen && $screen.parentElement) { - const $parent = $screen.parentElement; - if (!$parent || $parent.bxClick) - return; - $parent.bxClick = !0; - const onClick = (e) => { - $wrapper.classList.add("bx-gone"), $parent.bxClick = !1, $parent.removeEventListener("click", onClick); - }; - $parent.addEventListener("click", onClick); - } -} - // src/modules/mkb/pointer-client.ts var LOG_TAG = "PointerClient", PointerAction; (function(PointerAction2) { @@ -2877,8 +2432,1180 @@ class NativeMkbHandler extends MkbHandler { } } +// src/assets/svg/command.svg +var command_default = "\n \n \n\n"; + +// src/assets/svg/controller.svg +var controller_default = "\n \n\n"; + +// src/assets/svg/copy.svg +var copy_default = "\n \n\n"; + +// src/assets/svg/cursor-text.svg +var cursor_text_default = "\n \n\n"; + +// src/assets/svg/display.svg +var display_default = "\n \n\n"; + +// src/assets/svg/home.svg +var home_default = "\n \n\n"; + +// src/assets/svg/native-mkb.svg +var native_mkb_default = "\n \n \n \n \n \n \n \n \n\n"; + +// src/assets/svg/new.svg +var new_default = "\n \n\n"; + +// src/assets/svg/question.svg +var question_default = "\n \n\n"; + +// src/assets/svg/refresh.svg +var refresh_default = "\n \n\n"; + +// src/assets/svg/remote-play.svg +var remote_play_default = "\n \n\n"; + +// src/assets/svg/stream-settings.svg +var stream_settings_default = "\n \n\n"; + +// src/assets/svg/stream-stats.svg +var stream_stats_default = "\n \n\n"; + +// src/assets/svg/trash.svg +var trash_default = "\n \n\n"; + +// src/assets/svg/touch-control-enable.svg +var touch_control_enable_default = "\n \n \n \n \n\n"; + +// src/assets/svg/touch-control-disable.svg +var touch_control_disable_default = "\n \n \n \n \n \n \n \n\n"; + +// src/assets/svg/virtual-controller.svg +var virtual_controller_default = "\n \n \n \n \n \n \n \n \n \n\n"; + +// src/assets/svg/caret-left.svg +var caret_left_default = "\n \n\n"; + +// src/assets/svg/caret-right.svg +var caret_right_default = "\n \n\n"; + +// src/assets/svg/camera.svg +var camera_default = "\n \n \n \n \n\n"; + +// src/assets/svg/microphone.svg +var microphone_default = "\n \n\n"; + +// src/assets/svg/microphone-slash.svg +var microphone_slash_default = "\n \n \n\n"; + +// src/assets/svg/battery-full.svg +var battery_full_default = "\n \n\n"; + +// src/assets/svg/clock.svg +var clock_default = "\n \n \n \n \n\n"; + +// src/assets/svg/cloud.svg +var cloud_default = "\n \n\n"; + +// src/assets/svg/download.svg +var download_default = "\n \n \n\n"; + +// src/assets/svg/speaker-high.svg +var speaker_high_default = "\n \n \n\n"; + +// src/assets/svg/upload.svg +var upload_default = "\n \n \n\n"; + +// src/utils/bx-icon.ts +var BxIcon = { + STREAM_SETTINGS: stream_settings_default, + STREAM_STATS: stream_stats_default, + COMMAND: command_default, + CONTROLLER: controller_default, + DISPLAY: display_default, + HOME: home_default, + NATIVE_MKB: native_mkb_default, + NEW: new_default, + COPY: copy_default, + TRASH: trash_default, + CURSOR_TEXT: cursor_text_default, + QUESTION: question_default, + REFRESH: refresh_default, + VIRTUAL_CONTROLLER: virtual_controller_default, + REMOTE_PLAY: remote_play_default, + CARET_LEFT: caret_left_default, + CARET_RIGHT: caret_right_default, + SCREENSHOT: camera_default, + TOUCH_CONTROL_ENABLE: touch_control_enable_default, + TOUCH_CONTROL_DISABLE: touch_control_disable_default, + MICROPHONE: microphone_default, + MICROPHONE_MUTED: microphone_slash_default, + BATTERY: battery_full_default, + PLAYTIME: clock_default, + SERVER: cloud_default, + DOWNLOAD: download_default, + UPLOAD: upload_default, + AUDIO: speaker_high_default +}; + +// src/modules/dialog.ts +class Dialog { + $dialog; + $title; + $content; + $overlay; + onClose; + constructor(options) { + const { + 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: 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"), 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"); + } +} + +// src/modules/mkb/mkb-remapper.ts +class MkbRemapper { + #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 + ]; + static #instance; + static get INSTANCE() { + if (!MkbRemapper.#instance) + MkbRemapper.#instance = new MkbRemapper; + return MkbRemapper.#instance; + } + #STATE = { + currentPresetId: 0, + presets: {}, + editingPresetData: null, + isEditing: !1 + }; + #$ = { + wrapper: null, + presetsSelect: null, + activateButton: null, + currentBindingKey: null, + allKeyElements: [], + allMouseElements: {} + }; + bindingDialog; + constructor() { + this.#STATE.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: !0 + }); + } + #clearEventListeners = () => { + window.removeEventListener("keydown", this.#onKeyDown), window.removeEventListener("mousedown", this.#onMouseDown), window.removeEventListener("wheel", this.#onWheel); + }; + #bindKey = ($elm, key) => { + const buttonIndex = parseInt($elm.getAttribute("data-button-index")), keySlot = parseInt($elm.getAttribute("data-key-slot")); + if ($elm.getAttribute("data-key-code") === key.code) + return; + for (let $otherElm of this.#$.allKeyElements) + if ($otherElm.getAttribute("data-key-code") === key.code) + this.#unbindKey($otherElm); + this.#STATE.editingPresetData.mapping[buttonIndex][keySlot] = key.code, $elm.textContent = key.name, $elm.setAttribute("data-key-code", key.code); + }; + #unbindKey = ($elm) => { + const buttonIndex = parseInt($elm.getAttribute("data-button-index")), keySlot = parseInt($elm.getAttribute("data-key-slot")); + this.#STATE.editingPresetData.mapping[buttonIndex][keySlot] = null, $elm.textContent = "", $elm.removeAttribute("data-key-code"); + }; + #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.#STATE.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.getAttribute("data-prompt") }); + }; + #onContextMenu = (e) => { + if (e.preventDefault(), !this.#STATE.isEditing) + return; + this.#unbindKey(e.target); + }; + #getPreset = (presetId) => { + return this.#STATE.presets[presetId]; + }; + #getCurrentPreset = () => { + return this.#getPreset(this.#STATE.currentPresetId); + }; + #switchPreset = (presetId) => { + this.#STATE.currentPresetId = presetId; + const presetData = this.#getCurrentPreset().data; + for (let $elm of this.#$.allKeyElements) { + const buttonIndex = parseInt($elm.getAttribute("data-button-index")), keySlot = parseInt($elm.getAttribute("data-key-slot")), buttonKeys = presetData.mapping[buttonIndex]; + if (buttonKeys && buttonKeys[keySlot]) + $elm.textContent = KeyHelper.codeToKeyName(buttonKeys[keySlot]), $elm.setAttribute("data-key-code", buttonKeys[keySlot]); + else + $elm.textContent = "", $elm.removeAttribute("data-key-code"); + } + let key; + 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.setValue(value); + } + const activated = getPref(PrefKey.MKB_DEFAULT_PRESET_ID) === this.#STATE.currentPresetId; + this.#$.activateButton.disabled = activated, this.#$.activateButton.querySelector("span").textContent = activated ? t("activated") : t("activate"); + }; + #refresh() { + while (this.#$.presetsSelect.firstChild) + this.#$.presetsSelect.removeChild(this.#$.presetsSelect.firstChild); + LocalDb.INSTANCE.getPresets().then((presets) => { + this.#STATE.presets = presets; + const $fragment = document.createDocumentFragment(); + let defaultPresetId; + if (this.#STATE.currentPresetId === 0) + this.#STATE.currentPresetId = parseInt(Object.keys(presets)[0]), defaultPresetId = this.#STATE.currentPresetId, setPref(PrefKey.MKB_DEFAULT_PRESET_ID, defaultPresetId), EmulatedMkbHandler.getInstance().refreshPresetData(); + else + defaultPresetId = getPref(PrefKey.MKB_DEFAULT_PRESET_ID); + for (let id2 in presets) { + let name = presets[id2].name; + if (id2 === defaultPresetId) + name = "🎮 " + name; + const $options = CE("option", { value: id2 }, name); + $options.selected = parseInt(id2) === this.#STATE.currentPresetId, $fragment.appendChild($options); + } + this.#$.presetsSelect.appendChild($fragment); + const activated = defaultPresetId === this.#STATE.currentPresetId; + this.#$.activateButton.disabled = activated, this.#$.activateButton.querySelector("span").textContent = activated ? t("activated") : t("activate"), !this.#STATE.isEditing && this.#switchPreset(this.#STATE.currentPresetId); + }); + } + #toggleEditing = (force) => { + if (this.#STATE.isEditing = typeof force !== "undefined" ? force : !this.#STATE.isEditing, this.#$.wrapper.classList.toggle("bx-editing", this.#STATE.isEditing), this.#STATE.isEditing) + this.#STATE.editingPresetData = deepClone(this.#getCurrentPreset().data); + else + this.#STATE.editingPresetData = null; + const 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.#STATE.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", {}), this.#$.presetsSelect.addEventListener("change", (e) => { + this.#switchPreset(parseInt(e.target.value)); + }); + const promptNewName = (value) => { + let newName = ""; + while (!newName) { + if (newName = prompt(t("prompt-preset-name"), value), newName === null) + return !1; + newName = newName.trim(); + } + return newName ? newName : !1; + }, $header = CE("div", { class: "bx-mkb-preset-tools" }, this.#$.presetsSelect, createButton({ + title: t("rename"), + icon: BxIcon.CURSOR_TEXT, + onClick: (e) => { + const preset = this.#getCurrentPreset(); + let newName = promptNewName(preset.name); + if (!newName || newName === preset.name) + return; + preset.name = newName, LocalDb.INSTANCE.updatePreset(preset).then((id2) => this.#refresh()); + } + }), createButton({ + icon: BxIcon.NEW, + title: t("new"), + onClick: (e) => { + let newName = promptNewName(""); + if (!newName) + return; + LocalDb.INSTANCE.newPreset(newName, MkbPreset.DEFAULT_PRESET).then((id2) => { + this.#STATE.currentPresetId = id2, this.#refresh(); + }); + } + }), createButton({ + icon: BxIcon.COPY, + title: t("copy"), + onClick: (e) => { + const preset = this.#getCurrentPreset(); + let newName = promptNewName(`${preset.name} (2)`); + if (!newName) + return; + LocalDb.INSTANCE.newPreset(newName, preset.data).then((id2) => { + this.#STATE.currentPresetId = id2, this.#refresh(); + }); + } + }), createButton({ + icon: BxIcon.TRASH, + style: ButtonStyle.DANGER, + title: t("delete"), + onClick: (e) => { + if (!confirm(t("confirm-delete-preset"))) + return; + LocalDb.INSTANCE.deletePreset(this.#STATE.currentPresetId).then((id2) => { + this.#STATE.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"))), keysPerButton = 2; + for (let 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"))); + const $mouseSettings = document.createDocumentFragment(); + for (let key in MkbPreset.MOUSE_SETTINGS) { + const setting = MkbPreset.MOUSE_SETTINGS[key], value = setting.default; + let $elm; + const onChange = (e, value2) => { + this.#STATE.editingPresetData.mouse[key] = value2; + }, $row = CE("div", { class: "bx-stream-settings-row" }, CE("label", { for: `bx_setting_${key}` }, 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); + const $actionButtons = CE("div", { class: "bx-mkb-action-buttons" }, CE("div", {}, createButton({ + label: t("edit"), + onClick: (e) => this.#toggleEditing(!0) + }), this.#$.activateButton = createButton({ + label: t("activate"), + style: ButtonStyle.PRIMARY, + onClick: (e) => { + setPref(PrefKey.MKB_DEFAULT_PRESET_ID, this.#STATE.currentPresetId), EmulatedMkbHandler.getInstance().refreshPresetData(), this.#refresh(); + } + })), CE("div", {}, createButton({ + label: t("cancel"), + style: ButtonStyle.GHOST, + onClick: (e) => { + this.#switchPreset(this.#STATE.currentPresetId), this.#toggleEditing(!1); + } + }), createButton({ + label: t("save"), + style: ButtonStyle.PRIMARY, + onClick: (e) => { + const updatedPreset = deepClone(this.#getCurrentPreset()); + updatedPreset.data = this.#STATE.editingPresetData, LocalDb.INSTANCE.updatePreset(updatedPreset).then((id2) => { + if (id2 === getPref(PrefKey.MKB_DEFAULT_PRESET_ID)) + EmulatedMkbHandler.getInstance().refreshPresetData(); + this.#toggleEditing(!1), this.#refresh(); + }); + } + }))); + return this.#$.wrapper.appendChild($actionButtons), this.#toggleEditing(!1), this.#refresh(), this.#$.wrapper; + } +} + +// src/utils/utils.ts +function checkForUpdate() { + if (SCRIPT_VERSION.includes("beta")) + return; + const CHECK_INTERVAL_SECONDS = 7200, currentVersion = getPref(PrefKey.CURRENT_VERSION), lastCheck = getPref(PrefKey.LAST_UPDATE_CHECK), now = Math.round(+new Date / 1000); + if (currentVersion === SCRIPT_VERSION && now - lastCheck < CHECK_INTERVAL_SECONDS) + return; + setPref(PrefKey.LAST_UPDATE_CHECK, now), fetch("https://api.github.com/repos/redphx/better-xcloud/releases/latest").then((response) => response.json()).then((json) => { + setPref(PrefKey.LATEST_VERSION, json.tag_name.substring(1)), setPref(PrefKey.CURRENT_VERSION, SCRIPT_VERSION); + }), Translations.updateTranslations(currentVersion === SCRIPT_VERSION); +} +function disablePwa() { + if (!(window.navigator.orgUserAgent || window.navigator.userAgent || "").toLowerCase()) + return; + if (!!AppInterface || UserAgent.isSafariMobile()) + Object.defineProperty(window.navigator, "standalone", { + value: !0 + }); +} +function hashCode(str2) { + let hash = 0; + for (let i = 0, len = str2.length;i < len; i++) { + const chr = str2.charCodeAt(i); + hash = (hash << 5) - hash + chr, hash |= 0; + } + return hash; +} +function renderString(str2, obj) { + return str2.replace(/\$\{.+?\}/g, (match) => { + const key = match.substring(2, match.length - 1); + if (key in obj) + return obj[key]; + return match; + }); +} +function ceilToNearest(value, interval) { + return Math.ceil(value / interval) * interval; +} +function floorToNearest(value, interval) { + return Math.floor(value / interval) * interval; +} + +// src/modules/shortcuts/shortcut-sound.ts +class SoundShortcut { + static adjustGainNodeVolume(amount) { + if (!getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL)) + return 0; + const currentValue = getPref(PrefKey.AUDIO_VOLUME); + let 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(PrefKey.AUDIO_VOLUME, newValue), SoundShortcut.setGainNodeVolume(newValue), Toast.show(`${t("stream")} ❯ ${t("volume")}`, newValue + "%", { instant: !0 }), BxEvent.dispatch(window, BxEvent.GAINNODE_VOLUME_CHANGED, { + volume: newValue + }), newValue; + } + static setGainNodeVolume(value) { + STATES.currentStream.audioGainNode && (STATES.currentStream.audioGainNode.gain.value = value / 100); + } + static muteUnmute() { + if (getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && STATES.currentStream.audioGainNode) { + const gainValue = STATES.currentStream.audioGainNode.gain.value, settingValue = getPref(PrefKey.AUDIO_VOLUME); + let targetValue; + if (settingValue === 0) + targetValue = 100, setPref(PrefKey.AUDIO_VOLUME, targetValue), BxEvent.dispatch(window, BxEvent.GAINNODE_VOLUME_CHANGED, { + volume: targetValue + }); + 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 }); + return; + } + let $media; + if ($media = document.querySelector("div[data-testid=media-container] audio"), !$media) + $media = document.querySelector("div[data-testid=media-container] video"); + if ($media) { + $media.muted = !$media.muted; + const status = $media.muted ? t("muted") : t("unmuted"); + Toast.show(`${t("stream")} ❯ ${t("volume")}`, status, { instant: !0 }); + } + } +} + +// src/modules/touch-controller.ts +var LOG_TAG2 = "TouchController"; + +class TouchController { + static #EVENT_SHOW_DEFAULT_CONTROLLER = new MessageEvent("message", { + data: JSON.stringify({ + content: '{"layoutId":""}', + target: "/streaming/touchcontrols/showlayoutv2", + type: "Message" + }), + origin: "better-xcloud" + }); + static #$style; + static #enable = !1; + static #dataChannel; + static #customLayouts = {}; + static #baseCustomLayouts = {}; + static #currentLayoutId; + static #customList; + static enable() { + TouchController.#enable = !0; + } + static disable() { + TouchController.#enable = !1; + } + static isEnabled() { + return TouchController.#enable; + } + static #showDefault() { + TouchController.#dispatchMessage(TouchController.#EVENT_SHOW_DEFAULT_CONTROLLER); + } + static #show() { + document.querySelector("#BabylonCanvasContainer-main")?.parentElement?.classList.remove("bx-offscreen"); + } + static #hide() { + document.querySelector("#BabylonCanvasContainer-main")?.parentElement?.classList.add("bx-offscreen"); + } + static toggleVisibility(status) { + if (!TouchController.#dataChannel) + return; + status ? TouchController.#hide() : TouchController.#show(); + } + static reset() { + TouchController.#enable = !1, TouchController.#dataChannel = null, TouchController.#$style && (TouchController.#$style.textContent = ""); + } + static #dispatchMessage(msg) { + TouchController.#dataChannel && window.setTimeout(() => { + TouchController.#dataChannel.dispatchEvent(msg); + }, 10); + } + static #dispatchLayouts(data) { + BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED, { + data + }); + } + static async getCustomLayouts(xboxTitleId, retries = 1) { + if (xboxTitleId in TouchController.#customLayouts) { + TouchController.#dispatchLayouts(TouchController.#customLayouts[xboxTitleId]); + return; + } + if (retries = retries || 1, retries > 2) { + TouchController.#customLayouts[xboxTitleId] = null, window.setTimeout(() => TouchController.#dispatchLayouts(null), 1000); + return; + } + const baseUrl = `https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/touch-layouts${BX_FLAGS.UseDevTouchLayout ? "/dev" : ""}`, url = `${baseUrl}/${xboxTitleId}.json`; + try { + const json = await (await NATIVE_FETCH(url)).json(), layouts = {}; + json.layouts.forEach(async (layoutName) => { + let baseLayouts = {}; + if (layoutName in TouchController.#baseCustomLayouts) + baseLayouts = TouchController.#baseCustomLayouts[layoutName]; + else + try { + const layoutUrl = `${baseUrl}/layouts/${layoutName}.json`; + baseLayouts = (await (await NATIVE_FETCH(layoutUrl)).json()).layouts, TouchController.#baseCustomLayouts[layoutName] = baseLayouts; + } catch (e) { + } + Object.assign(layouts, baseLayouts); + }), json.layouts = layouts, TouchController.#customLayouts[xboxTitleId] = json, window.setTimeout(() => TouchController.#dispatchLayouts(json), 1000); + } catch (e) { + TouchController.getCustomLayouts(xboxTitleId, retries + 1); + } + } + static loadCustomLayout(xboxTitleId, layoutId, delay = 0) { + if (!window.BX_EXPOSED.touchLayoutManager) { + const listener = (e) => { + if (window.removeEventListener(BxEvent.TOUCH_LAYOUT_MANAGER_READY, listener), TouchController.#enable) + TouchController.loadCustomLayout(xboxTitleId, layoutId, 0); + }; + window.addEventListener(BxEvent.TOUCH_LAYOUT_MANAGER_READY, listener); + return; + } + const layoutChanged = TouchController.#currentLayoutId !== layoutId; + TouchController.#currentLayoutId = layoutId; + const layoutData = TouchController.#customLayouts[xboxTitleId]; + if (!xboxTitleId || !layoutId || !layoutData) { + TouchController.#enable && TouchController.#showDefault(); + return; + } + const layout = layoutData.layouts[layoutId] || layoutData.layouts[layoutData.default_layout]; + if (!layout) + return; + let msg, html10 = !1; + if (layout.author) { + const author = `${escapeHtml(layout.author)}`; + msg = t("touch-control-layout-by", { name: author }), html10 = !0; + } else + msg = t("touch-control-layout"); + layoutChanged && Toast.show(msg, layout.name, { html: html10 }), window.setTimeout(() => { + window.BX_EXPOSED.shouldShowSensorControls = JSON.stringify(layout).includes("gyroscope"), window.BX_EXPOSED.touchLayoutManager.changeLayoutForScope({ + type: "showLayout", + scope: xboxTitleId, + subscope: "base", + layout: { + id: "System.Standard", + displayName: "System", + layoutFile: layout + } + }); + }, delay); + } + static updateCustomList() { + TouchController.#customList = JSON.parse(window.localStorage.getItem("better_xcloud_custom_touch_layouts") || "[]"), 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("better_xcloud_custom_touch_layouts", JSON.stringify(json)); + }); + } + static getCustomList() { + return TouchController.#customList; + } + static setup() { + window.testTouchLayout = (layout) => { + const { touchLayoutManager } = window.BX_EXPOSED; + touchLayoutManager && touchLayoutManager.changeLayoutForScope({ + type: "showLayout", + scope: "" + STATES.currentStream?.xboxTitleId, + subscope: "base", + layout: { + id: "System.Standard", + displayName: "Custom", + layoutFile: layout + } + }); + }; + const $style = document.createElement("style"); + document.documentElement.appendChild($style), TouchController.#$style = $style; + const PREF_STYLE_STANDARD = getPref(PrefKey.STREAM_TOUCH_CONTROLLER_STYLE_STANDARD), PREF_STYLE_CUSTOM = getPref(PrefKey.STREAM_TOUCH_CONTROLLER_STYLE_CUSTOM); + window.addEventListener(BxEvent.DATA_CHANNEL_CREATED, (e) => { + const dataChannel = e.dataChannel; + if (!dataChannel || dataChannel.label !== "message") + return; + let filter = ""; + if (TouchController.#enable) { + if (PREF_STYLE_STANDARD === "white") + filter = "grayscale(1) brightness(2)"; + else if (PREF_STYLE_STANDARD === "muted") + filter = "sepia(0.5)"; + } else if (PREF_STYLE_CUSTOM === "muted") + filter = "sepia(0.5)"; + if (filter) + $style.textContent = `#babylon-canvas { filter: ${filter} !important; }`; + else + $style.textContent = ""; + TouchController.#dataChannel = dataChannel, dataChannel.addEventListener("open", () => { + window.setTimeout(TouchController.#show, 1000); + }); + let focused = !1; + dataChannel.addEventListener("message", (msg) => { + if (msg.origin === "better-xcloud" || typeof msg.data !== "string") + return; + if (msg.data.includes("touchcontrols/showtitledefault")) { + if (TouchController.#enable) + if (focused) + TouchController.getCustomLayouts(STATES.currentStream?.xboxTitleId); + else + TouchController.#showDefault(); + return; + } + try { + if (msg.data.includes("/titleinfo")) { + const json = JSON.parse(JSON.parse(msg.data).content); + if (focused = json.focused, !json.focused) + TouchController.#show(); + STATES.currentStream.xboxTitleId = parseInt(json.titleid, 16).toString(); + } + } catch (e2) { + BxLogger.error(LOG_TAG2, "Load custom layout", e2); + } + }); + }); + } +} + +// src/modules/vibration-manager.ts +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; + } + const 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; + } + const 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 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(PrefKey.CONTROLLER_ENABLE_VIBRATION) : !1, window.BX_VIBRATION_INTENSITY = getPref(PrefKey.CONTROLLER_VIBRATION_INTENSITY) / 100, !VibrationManager.supportDeviceVibration()) { + window.BX_ENABLE_DEVICE_VIBRATION = !1; + return; + } + stopVibration && window.navigator.vibrate(0); + const value = getPref(PrefKey.CONTROLLER_DEVICE_VIBRATION); + let enabled; + if (value === "on") + enabled = !0; + else if (value === "auto") { + enabled = !0; + const 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; + const dataView = new DataView(e.data); + let 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; + const vibrationType = dataView.getUint8(offset); + if (offset += Uint8Array.BYTES_PER_ELEMENT, vibrationType !== 0) + return; + const data = {}; + let 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) => { + const dataChannel = e.dataChannel; + if (!dataChannel || dataChannel.label !== "input") + return; + dataChannel.addEventListener("message", VibrationManager.#onMessage); + }); + } +} + +// src/web-components/bx-select.ts +class BxSelectElement { + static wrap($select) { + const $btnPrev = createButton({ + label: "<", + style: ButtonStyle.FOCUSABLE, + attributes: { + tabindex: 0 + } + }), $btnNext = createButton({ + label: ">", + style: ButtonStyle.FOCUSABLE, + attributes: { + tabindex: 0 + } + }), isMultiple = $select.multiple; + let visibleIndex = $select.selectedIndex, $checkBox, $label; + const $content = CE("div", {}, $checkBox = CE("input", { type: "checkbox", id: $select.id + "_checkbox" }), $label = CE("label", { for: $select.id + "_checkbox" }, "")); + isMultiple && $checkBox.addEventListener("input", (e) => { + const $option = getOptionAtIndex(visibleIndex); + $option && ($option.selected = e.target.checked), $select.dispatchEvent(new Event("input")); + }), $checkBox.classList.toggle("bx-gone", !isMultiple); + const getOptionAtIndex = (index) => { + return $select.querySelector(`option:nth-of-type(${visibleIndex + 1})`); + }, render = () => { + visibleIndex = normalizeIndex(visibleIndex); + const $option = getOptionAtIndex(visibleIndex); + let content = ""; + if ($option) + content = $option.textContent || ""; + $label.textContent = content, isMultiple && ($checkBox.checked = $option?.selected || !1), $checkBox.classList.toggle("bx-gone", !isMultiple || !content); + const disablePrev = visibleIndex <= 0, disableNext = visibleIndex === $select.querySelectorAll("option").length - 1; + $btnPrev.classList.toggle("bx-inactive", disablePrev), disablePrev && $btnNext.focus(), $btnNext.classList.toggle("bx-inactive", disableNext), disableNext && $btnPrev.focus(); + }, normalizeIndex = (index) => { + return Math.min(Math.max(index, 0), $select.querySelectorAll("option").length - 1); + }, onPrevNext = (e) => { + const goNext = e.target === $btnNext, currentIndex = visibleIndex; + let newIndex = goNext ? currentIndex + 1 : currentIndex - 1; + if (newIndex = normalizeIndex(newIndex), visibleIndex = newIndex, !isMultiple && newIndex !== currentIndex) + $select.selectedIndex = newIndex; + $select.dispatchEvent(new Event("input")); + }; + return $select.addEventListener("input", (e) => render()), $btnPrev.addEventListener("click", onPrevNext), $btnNext.addEventListener("click", onPrevNext), new MutationObserver((mutationList, observer2) => { + mutationList.forEach((mutation) => { + mutation.type === "childList" && render(); + }); + }).observe($select, { + subtree: !0, + childList: !0 + }), render(), CE("div", { class: "bx-select" }, $select, $btnPrev, $content, $btnNext); + } +} + +// src/modules/stream/stream-settings-utils.ts +function onChangeVideoPlayerType() { + const playerType = getPref(PrefKey.VIDEO_PLAYER_TYPE), $videoProcessing = document.getElementById("bx_setting_video_processing"), $videoSharpness = document.getElementById("bx_setting_video_sharpness"); + let isDisabled = !1; + if (playerType === StreamPlayerType.WEBGL2) + $videoProcessing.querySelector(`option[value=${StreamVideoProcessing.CAS}]`).disabled = !1; + else if ($videoProcessing.value = StreamVideoProcessing.USM, setPref(PrefKey.VIDEO_PROCESSING, StreamVideoProcessing.USM), $videoProcessing.querySelector(`option[value=${StreamVideoProcessing.CAS}]`).disabled = !0, UserAgent.isSafari()) + isDisabled = !0; + $videoProcessing.disabled = isDisabled, $videoSharpness.dataset.disabled = isDisabled.toString(), updateVideoPlayer(); +} +function updateVideoPlayer() { + const streamPlayer = STATES.currentStream.streamPlayer; + if (!streamPlayer) + return; + const options = { + processing: getPref(PrefKey.VIDEO_PROCESSING), + sharpness: getPref(PrefKey.VIDEO_SHARPNESS), + saturation: getPref(PrefKey.VIDEO_SATURATION), + contrast: getPref(PrefKey.VIDEO_CONTRAST), + brightness: getPref(PrefKey.VIDEO_BRIGHTNESS) + }; + streamPlayer.setPlayerType(getPref(PrefKey.VIDEO_PLAYER_TYPE)), streamPlayer.updateOptions(options), streamPlayer.refreshPlayer(); +} +window.addEventListener("resize", updateVideoPlayer); + +// src/modules/stream/stream-settings.ts +class StreamSettings { + static instance; + static getInstance() { + if (!StreamSettings.instance) + StreamSettings.instance = new StreamSettings; + return StreamSettings.instance; + } + $container; + $overlay; + SETTINGS_UI = [ + { + icon: BxIcon.DISPLAY, + group: "stream", + items: [{ + group: "audio", + label: t("audio"), + help_url: "https://better-xcloud.github.io/ingame-features/#audio", + items: [{ + pref: PrefKey.AUDIO_VOLUME, + onChange: (e, value) => { + SoundShortcut.setGainNodeVolume(value); + }, + params: { + disabled: !getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) + }, + onMounted: ($elm) => { + const $range = $elm.querySelector("input[type=range"); + window.addEventListener(BxEvent.GAINNODE_VOLUME_CHANGED, (e) => { + $range.value = e.volume, BxEvent.dispatch($range, "input", { + ignoreOnChange: !0 + }); + }); + } + }] + }, { + group: "video", + label: t("video"), + help_url: "https://better-xcloud.github.io/ingame-features/#video", + items: [{ + pref: PrefKey.VIDEO_PLAYER_TYPE, + onChange: onChangeVideoPlayerType + }, { + pref: PrefKey.VIDEO_RATIO, + onChange: updateVideoPlayer + }, { + pref: PrefKey.VIDEO_PROCESSING, + onChange: updateVideoPlayer + }, { + pref: PrefKey.VIDEO_SHARPNESS, + onChange: updateVideoPlayer + }, { + pref: PrefKey.VIDEO_SATURATION, + onChange: updateVideoPlayer + }, { + pref: PrefKey.VIDEO_CONTRAST, + onChange: updateVideoPlayer + }, { + pref: PrefKey.VIDEO_BRIGHTNESS, + onChange: updateVideoPlayer + }] + }] + }, + { + icon: BxIcon.CONTROLLER, + group: "controller", + items: [ + { + group: "controller", + label: t("controller"), + help_url: "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() + }] + }, + STATES.userAgentHasTouchSupport && { + group: "touch-controller", + label: t("touch-controller"), + items: [{ + label: t("layout"), + content: CE("select", { disabled: !0 }, CE("option", {}, t("default"))), + onMounted: ($elm) => { + $elm.addEventListener("change", (e) => { + TouchController.loadCustomLayout(STATES.currentStream?.xboxTitleId, $elm.value, 1000); + }), window.addEventListener(BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED, (e) => { + const data = e.data; + if (STATES.currentStream?.xboxTitleId && $elm.xboxTitleId === STATES.currentStream?.xboxTitleId) { + $elm.dispatchEvent(new Event("change")); + return; + } + $elm.xboxTitleId = STATES.currentStream?.xboxTitleId; + while ($elm.firstChild) + $elm.removeChild($elm.firstChild); + if ($elm.disabled = !data, !data) { + $elm.appendChild(CE("option", { value: "" }, t("default"))), $elm.value = "", $elm.dispatchEvent(new Event("change")); + return; + } + const $fragment = document.createDocumentFragment(); + for (let key in data.layouts) { + const layout = data.layouts[key]; + let name; + if (layout.author) + name = `${layout.name} (${layout.author})`; + else + name = layout.name; + const $option = CE("option", { value: key }, name); + $fragment.appendChild($option); + } + $elm.appendChild($fragment), $elm.value = data.default_layout, $elm.dispatchEvent(new Event("change")); + }); + } + }] + } + ] + }, + getPref(PrefKey.MKB_ENABLED) && { + icon: BxIcon.VIRTUAL_CONTROLLER, + group: "mkb", + items: [{ + group: "mkb", + label: t("virtual-controller"), + help_url: "https://better-xcloud.github.io/mouse-and-keyboard/", + content: MkbRemapper.INSTANCE.render() + }] + }, + AppInterface && getPref(PrefKey.NATIVE_MKB_ENABLED) === "on" && { + icon: BxIcon.NATIVE_MKB, + group: "native-mkb", + items: [{ + group: "native-mkb", + label: t("native-mkb"), + items: [{ + pref: PrefKey.NATIVE_MKB_SCROLL_VERTICAL_SENSITIVITY, + onChange: (e, value) => { + NativeMkbHandler.getInstance().setVerticalScrollMultiplier(value / 100); + } + }, { + pref: PrefKey.NATIVE_MKB_SCROLL_HORIZONTAL_SENSITIVITY, + onChange: (e, value) => { + NativeMkbHandler.getInstance().setHorizontalScrollMultiplier(value / 100); + } + }] + }] + }, + { + icon: BxIcon.COMMAND, + group: "shortcuts", + items: [{ + group: "shortcuts_controller", + label: t("controller-shortcuts"), + content: ControllerShortcut.renderSettings() + }] + }, + { + icon: BxIcon.STREAM_STATS, + group: "stats", + items: [{ + group: "stats", + label: t("stream-stats"), + help_url: "https://better-xcloud.github.io/stream-stats/", + items: [ + { + pref: PrefKey.STATS_SHOW_WHEN_PLAYING + }, + { + pref: PrefKey.STATS_QUICK_GLANCE, + onChange: (e) => { + const streamStats = StreamStats.getInstance(); + e.target.checked ? streamStats.quickGlanceSetup() : streamStats.quickGlanceStop(); + } + }, + { + pref: PrefKey.STATS_ITEMS, + onChange: StreamStats.refreshStyles + }, + { + pref: PrefKey.STATS_POSITION, + onChange: StreamStats.refreshStyles + }, + { + pref: PrefKey.STATS_TEXT_SIZE, + onChange: StreamStats.refreshStyles + }, + { + pref: PrefKey.STATS_OPACITY, + onChange: StreamStats.refreshStyles + }, + { + pref: PrefKey.STATS_TRANSPARENT, + onChange: StreamStats.refreshStyles + }, + { + pref: PrefKey.STATS_CONDITIONAL_FORMATTING, + onChange: StreamStats.refreshStyles + } + ] + }] + } + ]; + constructor() { + this.#setupDialog(); + } + show(tabId) { + const $container = this.$container; + if (tabId) { + const $tab = $container.querySelector(`.bx-stream-settings-tabs svg[data-group=${tabId}]`); + $tab && $tab.dispatchEvent(new Event("click")); + } + this.$overlay.classList.remove("bx-gone"), this.$overlay.dataset.isPlaying = STATES.isPlaying.toString(), $container.classList.remove("bx-gone"), document.body.classList.add("bx-no-scroll"); + } + hide() { + this.$overlay.classList.add("bx-gone"), this.$container.classList.add("bx-gone"), document.body.classList.remove("bx-no-scroll"); + } + #setupDialog() { + let $tabs, $settings; + const $overlay = CE("div", { class: "bx-stream-settings-overlay bx-gone" }); + this.$overlay = $overlay; + const $container = CE("div", { class: "bx-stream-settings-dialog bx-gone" }, $tabs = CE("div", { class: "bx-stream-settings-tabs" }), $settings = CE("div", { class: "bx-stream-settings-tab-contents" })); + this.$container = $container, $overlay.addEventListener("click", (e) => { + e.preventDefault(), e.stopPropagation(), this.hide(); + }); + for (let settingTab of this.SETTINGS_UI) { + if (!settingTab) + continue; + const $svg = createSvgIcon(settingTab.icon); + $svg.addEventListener("click", (e) => { + for (let $child of Array.from($settings.children)) + if ($child.getAttribute("data-group") === settingTab.group) + $child.classList.remove("bx-gone"); + else + $child.classList.add("bx-gone"); + for (let $child of Array.from($tabs.children)) + $child.classList.remove("bx-active"); + $svg.classList.add("bx-active"); + }), $tabs.appendChild($svg); + const $group = CE("div", { "data-group": settingTab.group, class: "bx-gone" }); + for (let settingGroup of settingTab.items) { + if (!settingGroup) + continue; + if ($group.appendChild(CE("h2", {}, CE("span", {}, settingGroup.label), settingGroup.help_url && createButton({ + icon: BxIcon.QUESTION, + style: ButtonStyle.GHOST, + url: settingGroup.help_url, + title: t("help") + }))), settingGroup.note) { + if (typeof settingGroup.note === "string") + settingGroup.note = document.createTextNode(settingGroup.note); + $group.appendChild(settingGroup.note); + } + if (settingGroup.content) { + $group.appendChild(settingGroup.content); + continue; + } + if (!settingGroup.items) + settingGroup.items = []; + for (let setting of settingGroup.items) { + if (!setting) + continue; + const pref = setting.pref; + let $control; + if (setting.content) + $control = setting.content; + else if (!setting.unsupported) { + if ($control = toPrefElement(pref, setting.onChange, setting.params), $control instanceof HTMLSelectElement && BX_FLAGS.ScriptUi === "tv") + $control = BxSelectElement.wrap($control); + } + const label = Preferences.SETTINGS[pref]?.label || setting.label, note = Preferences.SETTINGS[pref]?.note || setting.note, $content = CE("div", { class: "bx-stream-settings-row", "data-type": settingGroup.group }, CE("label", { for: `bx_setting_${pref}` }, label, note && CE("div", { class: "bx-stream-settings-dialog-note" }, note), setting.unsupported && CE("div", { class: "bx-stream-settings-dialog-note" }, t("browser-unsupported-feature"))), !setting.unsupported && $control); + $group.appendChild($content), setting.onMounted && setting.onMounted($control); + } + } + $settings.appendChild($group); + } + $tabs.firstElementChild.dispatchEvent(new Event("click")), document.documentElement.appendChild($overlay), document.documentElement.appendChild($container); + } +} + // src/modules/mkb/mkb-handler.ts -var LOG_TAG2 = "MkbHandler", PointerToMouseButton = { +var LOG_TAG3 = "MkbHandler", PointerToMouseButton = { 1: 0, 2: 2, 4: 1 @@ -3171,7 +3898,7 @@ class EmulatedMkbHandler extends MkbHandler { }), createButton({ label: t("edit"), onClick: (e) => { - e.preventDefault(), e.stopPropagation(), showStreamSettings("mkb"); + e.preventDefault(), e.stopPropagation(), StreamSettings.getInstance().show("mkb"); } })))); if (!this.#$message.isConnected) @@ -3247,7 +3974,7 @@ class EmulatedMkbHandler extends MkbHandler { if (AppInterface && getPref(PrefKey.NATIVE_MKB_ENABLED) === "on") AppInterface && NativeMkbHandler.getInstance().init(); } else if (getPref(PrefKey.MKB_ENABLED) && (AppInterface || !UserAgent.isMobile())) - BxLogger.info(LOG_TAG2, "Emulate MKB"), EmulatedMkbHandler.getInstance().init(); + BxLogger.info(LOG_TAG3, "Emulate MKB"), EmulatedMkbHandler.getInstance().init(); }); } } @@ -3283,102 +4010,6 @@ class StreamUiShortcut { } } -// src/utils/utils.ts -function checkForUpdate() { - if (SCRIPT_VERSION.includes("beta")) - return; - const CHECK_INTERVAL_SECONDS = 7200, currentVersion = getPref(PrefKey.CURRENT_VERSION), lastCheck = getPref(PrefKey.LAST_UPDATE_CHECK), now = Math.round(+new Date / 1000); - if (currentVersion === SCRIPT_VERSION && now - lastCheck < CHECK_INTERVAL_SECONDS) - return; - setPref(PrefKey.LAST_UPDATE_CHECK, now), fetch("https://api.github.com/repos/redphx/better-xcloud/releases/latest").then((response) => response.json()).then((json) => { - setPref(PrefKey.LATEST_VERSION, json.tag_name.substring(1)), setPref(PrefKey.CURRENT_VERSION, SCRIPT_VERSION); - }), Translations.updateTranslations(currentVersion === SCRIPT_VERSION); -} -function disablePwa() { - if (!(window.navigator.orgUserAgent || window.navigator.userAgent || "").toLowerCase()) - return; - if (!!AppInterface || UserAgent.isSafariMobile()) - Object.defineProperty(window.navigator, "standalone", { - value: !0 - }); -} -function hashCode(str2) { - let hash = 0; - for (let i = 0, len = str2.length;i < len; i++) { - const chr = str2.charCodeAt(i); - hash = (hash << 5) - hash + chr, hash |= 0; - } - return hash; -} -function renderString(str2, obj) { - return str2.replace(/\$\{.+?\}/g, (match) => { - const key = match.substring(2, match.length - 1); - if (key in obj) - return obj[key]; - return match; - }); -} -function ceilToNearest(value, interval) { - return Math.ceil(value / interval) * interval; -} -function floorToNearest(value, interval) { - return Math.floor(value / interval) * interval; -} - -// src/modules/shortcuts/shortcut-sound.ts -class SoundShortcut { - static adjustGainNodeVolume(amount) { - if (!getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL)) - return 0; - const currentValue = getPref(PrefKey.AUDIO_VOLUME); - let 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(PrefKey.AUDIO_VOLUME, newValue), SoundShortcut.setGainNodeVolume(newValue), Toast.show(`${t("stream")} ❯ ${t("volume")}`, newValue + "%", { instant: !0 }), BxEvent.dispatch(window, BxEvent.GAINNODE_VOLUME_CHANGED, { - volume: newValue - }), newValue; - } - static setGainNodeVolume(value) { - STATES.currentStream.audioGainNode && (STATES.currentStream.audioGainNode.gain.value = value / 100); - } - static muteUnmute() { - if (getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && STATES.currentStream.audioGainNode) { - const gainValue = STATES.currentStream.audioGainNode.gain.value, settingValue = getPref(PrefKey.AUDIO_VOLUME); - let targetValue; - if (settingValue === 0) - targetValue = 100, setPref(PrefKey.AUDIO_VOLUME, targetValue), BxEvent.dispatch(window, BxEvent.GAINNODE_VOLUME_CHANGED, { - volume: targetValue - }); - 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 }); - return; - } - let $media; - if ($media = document.querySelector("div[data-testid=media-container] audio"), !$media) - $media = document.querySelector("div[data-testid=media-container] video"); - if ($media) { - $media.muted = !$media.muted; - const status = $media.muted ? t("muted") : t("unmuted"); - Toast.show(`${t("stream")} ❯ ${t("volume")}`, status, { instant: !0 }); - } - } -} - // src/modules/controller-shortcut.ts var ShortcutAction; (function(ShortcutAction2) { @@ -3577,7 +4208,7 @@ var InputType; var BxExposed = { getTitleInfo: () => STATES.currentStream.titleInfo, modifyTitleInfo: (titleInfo) => { - titleInfo = structuredClone(titleInfo); + titleInfo = deepClone(titleInfo); let supportedInputTypes = titleInfo.details.supportedInputTypes; if (BX_FLAGS.ForceNativeMkbTitles?.includes(titleInfo.details.productId)) supportedInputTypes.push(InputType.MKB); @@ -3743,602 +4374,6 @@ class LoadingScreen { } } -// src/modules/dialog.ts -class Dialog { - $dialog; - $title; - $content; - $overlay; - onClose; - constructor(options) { - const { - 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: 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"), 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"); - } -} - -// src/modules/mkb/mkb-remapper.ts -class MkbRemapper { - #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 - ]; - static #instance; - static get INSTANCE() { - if (!MkbRemapper.#instance) - MkbRemapper.#instance = new MkbRemapper; - return MkbRemapper.#instance; - } - #STATE = { - currentPresetId: 0, - presets: {}, - editingPresetData: null, - isEditing: !1 - }; - #$ = { - wrapper: null, - presetsSelect: null, - activateButton: null, - currentBindingKey: null, - allKeyElements: [], - allMouseElements: {} - }; - bindingDialog; - constructor() { - this.#STATE.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: !0 - }); - } - #clearEventListeners = () => { - window.removeEventListener("keydown", this.#onKeyDown), window.removeEventListener("mousedown", this.#onMouseDown), window.removeEventListener("wheel", this.#onWheel); - }; - #bindKey = ($elm, key) => { - const buttonIndex = parseInt($elm.getAttribute("data-button-index")), keySlot = parseInt($elm.getAttribute("data-key-slot")); - if ($elm.getAttribute("data-key-code") === key.code) - return; - for (let $otherElm of this.#$.allKeyElements) - if ($otherElm.getAttribute("data-key-code") === key.code) - this.#unbindKey($otherElm); - this.#STATE.editingPresetData.mapping[buttonIndex][keySlot] = key.code, $elm.textContent = key.name, $elm.setAttribute("data-key-code", key.code); - }; - #unbindKey = ($elm) => { - const buttonIndex = parseInt($elm.getAttribute("data-button-index")), keySlot = parseInt($elm.getAttribute("data-key-slot")); - this.#STATE.editingPresetData.mapping[buttonIndex][keySlot] = null, $elm.textContent = "", $elm.removeAttribute("data-key-code"); - }; - #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.#STATE.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.getAttribute("data-prompt") }); - }; - #onContextMenu = (e) => { - if (e.preventDefault(), !this.#STATE.isEditing) - return; - this.#unbindKey(e.target); - }; - #getPreset = (presetId) => { - return this.#STATE.presets[presetId]; - }; - #getCurrentPreset = () => { - return this.#getPreset(this.#STATE.currentPresetId); - }; - #switchPreset = (presetId) => { - this.#STATE.currentPresetId = presetId; - const presetData = this.#getCurrentPreset().data; - for (let $elm of this.#$.allKeyElements) { - const buttonIndex = parseInt($elm.getAttribute("data-button-index")), keySlot = parseInt($elm.getAttribute("data-key-slot")), buttonKeys = presetData.mapping[buttonIndex]; - if (buttonKeys && buttonKeys[keySlot]) - $elm.textContent = KeyHelper.codeToKeyName(buttonKeys[keySlot]), $elm.setAttribute("data-key-code", buttonKeys[keySlot]); - else - $elm.textContent = "", $elm.removeAttribute("data-key-code"); - } - let key; - 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.setValue(value); - } - const activated = getPref(PrefKey.MKB_DEFAULT_PRESET_ID) === this.#STATE.currentPresetId; - this.#$.activateButton.disabled = activated, this.#$.activateButton.querySelector("span").textContent = activated ? t("activated") : t("activate"); - }; - #refresh() { - while (this.#$.presetsSelect.firstChild) - this.#$.presetsSelect.removeChild(this.#$.presetsSelect.firstChild); - LocalDb.INSTANCE.getPresets().then((presets) => { - this.#STATE.presets = presets; - const $fragment = document.createDocumentFragment(); - let defaultPresetId; - if (this.#STATE.currentPresetId === 0) - this.#STATE.currentPresetId = parseInt(Object.keys(presets)[0]), defaultPresetId = this.#STATE.currentPresetId, setPref(PrefKey.MKB_DEFAULT_PRESET_ID, defaultPresetId), EmulatedMkbHandler.getInstance().refreshPresetData(); - else - defaultPresetId = getPref(PrefKey.MKB_DEFAULT_PRESET_ID); - for (let id2 in presets) { - let name = presets[id2].name; - if (id2 === defaultPresetId) - name = "🎮 " + name; - const $options = CE("option", { value: id2 }, name); - $options.selected = parseInt(id2) === this.#STATE.currentPresetId, $fragment.appendChild($options); - } - this.#$.presetsSelect.appendChild($fragment); - const activated = defaultPresetId === this.#STATE.currentPresetId; - this.#$.activateButton.disabled = activated, this.#$.activateButton.querySelector("span").textContent = activated ? t("activated") : t("activate"), !this.#STATE.isEditing && this.#switchPreset(this.#STATE.currentPresetId); - }); - } - #toggleEditing = (force) => { - if (this.#STATE.isEditing = typeof force !== "undefined" ? force : !this.#STATE.isEditing, this.#$.wrapper.classList.toggle("bx-editing", this.#STATE.isEditing), this.#STATE.isEditing) - this.#STATE.editingPresetData = structuredClone(this.#getCurrentPreset().data); - else - this.#STATE.editingPresetData = null; - const 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.#STATE.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", {}), this.#$.presetsSelect.addEventListener("change", (e) => { - this.#switchPreset(parseInt(e.target.value)); - }); - const promptNewName = (value) => { - let newName = ""; - while (!newName) { - if (newName = prompt(t("prompt-preset-name"), value), newName === null) - return !1; - newName = newName.trim(); - } - return newName ? newName : !1; - }, $header = CE("div", { class: "bx-mkb-preset-tools" }, this.#$.presetsSelect, createButton({ - title: t("rename"), - icon: BxIcon.CURSOR_TEXT, - onClick: (e) => { - const preset = this.#getCurrentPreset(); - let newName = promptNewName(preset.name); - if (!newName || newName === preset.name) - return; - preset.name = newName, LocalDb.INSTANCE.updatePreset(preset).then((id2) => this.#refresh()); - } - }), createButton({ - icon: BxIcon.NEW, - title: t("new"), - onClick: (e) => { - let newName = promptNewName(""); - if (!newName) - return; - LocalDb.INSTANCE.newPreset(newName, MkbPreset.DEFAULT_PRESET).then((id2) => { - this.#STATE.currentPresetId = id2, this.#refresh(); - }); - } - }), createButton({ - icon: BxIcon.COPY, - title: t("copy"), - onClick: (e) => { - const preset = this.#getCurrentPreset(); - let newName = promptNewName(`${preset.name} (2)`); - if (!newName) - return; - LocalDb.INSTANCE.newPreset(newName, preset.data).then((id2) => { - this.#STATE.currentPresetId = id2, this.#refresh(); - }); - } - }), createButton({ - icon: BxIcon.TRASH, - style: ButtonStyle.DANGER, - title: t("delete"), - onClick: (e) => { - if (!confirm(t("confirm-delete-preset"))) - return; - LocalDb.INSTANCE.deletePreset(this.#STATE.currentPresetId).then((id2) => { - this.#STATE.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"))), keysPerButton = 2; - for (let 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"))); - const $mouseSettings = document.createDocumentFragment(); - for (let key in MkbPreset.MOUSE_SETTINGS) { - const setting = MkbPreset.MOUSE_SETTINGS[key], value = setting.default; - let $elm; - const onChange = (e, value2) => { - this.#STATE.editingPresetData.mouse[key] = value2; - }, $row = CE("div", { class: "bx-stream-settings-row" }, CE("label", { for: `bx_setting_${key}` }, 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); - const $actionButtons = CE("div", { class: "bx-mkb-action-buttons" }, CE("div", {}, createButton({ - label: t("edit"), - onClick: (e) => this.#toggleEditing(!0) - }), this.#$.activateButton = createButton({ - label: t("activate"), - style: ButtonStyle.PRIMARY, - onClick: (e) => { - setPref(PrefKey.MKB_DEFAULT_PRESET_ID, this.#STATE.currentPresetId), EmulatedMkbHandler.getInstance().refreshPresetData(), this.#refresh(); - } - })), CE("div", {}, createButton({ - label: t("cancel"), - style: ButtonStyle.GHOST, - onClick: (e) => { - this.#switchPreset(this.#STATE.currentPresetId), this.#toggleEditing(!1); - } - }), createButton({ - label: t("save"), - style: ButtonStyle.PRIMARY, - onClick: (e) => { - const updatedPreset = structuredClone(this.#getCurrentPreset()); - updatedPreset.data = this.#STATE.editingPresetData, LocalDb.INSTANCE.updatePreset(updatedPreset).then((id2) => { - if (id2 === getPref(PrefKey.MKB_DEFAULT_PRESET_ID)) - EmulatedMkbHandler.getInstance().refreshPresetData(); - this.#toggleEditing(!1), this.#refresh(); - }); - } - }))); - return this.#$.wrapper.appendChild($actionButtons), this.#toggleEditing(!1), this.#refresh(), this.#$.wrapper; - } -} - -// src/modules/touch-controller.ts -var LOG_TAG3 = "TouchController"; - -class TouchController { - static #EVENT_SHOW_DEFAULT_CONTROLLER = new MessageEvent("message", { - data: JSON.stringify({ - content: '{"layoutId":""}', - target: "/streaming/touchcontrols/showlayoutv2", - type: "Message" - }), - origin: "better-xcloud" - }); - static #$style; - static #enable = !1; - static #dataChannel; - static #customLayouts = {}; - static #baseCustomLayouts = {}; - static #currentLayoutId; - static #customList; - static enable() { - TouchController.#enable = !0; - } - static disable() { - TouchController.#enable = !1; - } - static isEnabled() { - return TouchController.#enable; - } - static #showDefault() { - TouchController.#dispatchMessage(TouchController.#EVENT_SHOW_DEFAULT_CONTROLLER); - } - static #show() { - document.querySelector("#BabylonCanvasContainer-main")?.parentElement?.classList.remove("bx-offscreen"); - } - static #hide() { - document.querySelector("#BabylonCanvasContainer-main")?.parentElement?.classList.add("bx-offscreen"); - } - static toggleVisibility(status) { - if (!TouchController.#dataChannel) - return; - status ? TouchController.#hide() : TouchController.#show(); - } - static reset() { - TouchController.#enable = !1, TouchController.#dataChannel = null, TouchController.#$style && (TouchController.#$style.textContent = ""); - } - static #dispatchMessage(msg) { - TouchController.#dataChannel && window.setTimeout(() => { - TouchController.#dataChannel.dispatchEvent(msg); - }, 10); - } - static #dispatchLayouts(data) { - BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED, { - data - }); - } - static async getCustomLayouts(xboxTitleId, retries = 1) { - if (xboxTitleId in TouchController.#customLayouts) { - TouchController.#dispatchLayouts(TouchController.#customLayouts[xboxTitleId]); - return; - } - if (retries = retries || 1, retries > 2) { - TouchController.#customLayouts[xboxTitleId] = null, window.setTimeout(() => TouchController.#dispatchLayouts(null), 1000); - return; - } - const baseUrl = `https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/touch-layouts${BX_FLAGS.UseDevTouchLayout ? "/dev" : ""}`, url = `${baseUrl}/${xboxTitleId}.json`; - try { - const json = await (await NATIVE_FETCH(url)).json(), layouts = {}; - json.layouts.forEach(async (layoutName) => { - let baseLayouts = {}; - if (layoutName in TouchController.#baseCustomLayouts) - baseLayouts = TouchController.#baseCustomLayouts[layoutName]; - else - try { - const layoutUrl = `${baseUrl}/layouts/${layoutName}.json`; - baseLayouts = (await (await NATIVE_FETCH(layoutUrl)).json()).layouts, TouchController.#baseCustomLayouts[layoutName] = baseLayouts; - } catch (e) { - } - Object.assign(layouts, baseLayouts); - }), json.layouts = layouts, TouchController.#customLayouts[xboxTitleId] = json, window.setTimeout(() => TouchController.#dispatchLayouts(json), 1000); - } catch (e) { - TouchController.getCustomLayouts(xboxTitleId, retries + 1); - } - } - static loadCustomLayout(xboxTitleId, layoutId, delay = 0) { - if (!window.BX_EXPOSED.touchLayoutManager) { - const listener = (e) => { - if (window.removeEventListener(BxEvent.TOUCH_LAYOUT_MANAGER_READY, listener), TouchController.#enable) - TouchController.loadCustomLayout(xboxTitleId, layoutId, 0); - }; - window.addEventListener(BxEvent.TOUCH_LAYOUT_MANAGER_READY, listener); - return; - } - const layoutChanged = TouchController.#currentLayoutId !== layoutId; - TouchController.#currentLayoutId = layoutId; - const layoutData = TouchController.#customLayouts[xboxTitleId]; - if (!xboxTitleId || !layoutId || !layoutData) { - TouchController.#enable && TouchController.#showDefault(); - return; - } - const layout = layoutData.layouts[layoutId] || layoutData.layouts[layoutData.default_layout]; - if (!layout) - return; - let msg, html15 = !1; - if (layout.author) { - const author = `${escapeHtml(layout.author)}`; - msg = t("touch-control-layout-by", { name: author }), html15 = !0; - } else - msg = t("touch-control-layout"); - layoutChanged && Toast.show(msg, layout.name, { html: html15 }), window.setTimeout(() => { - window.BX_EXPOSED.shouldShowSensorControls = JSON.stringify(layout).includes("gyroscope"), window.BX_EXPOSED.touchLayoutManager.changeLayoutForScope({ - type: "showLayout", - scope: xboxTitleId, - subscope: "base", - layout: { - id: "System.Standard", - displayName: "System", - layoutFile: layout - } - }); - }, delay); - } - static updateCustomList() { - TouchController.#customList = JSON.parse(window.localStorage.getItem("better_xcloud_custom_touch_layouts") || "[]"), 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("better_xcloud_custom_touch_layouts", JSON.stringify(json)); - }); - } - static getCustomList() { - return TouchController.#customList; - } - static setup() { - window.testTouchLayout = (layout) => { - const { touchLayoutManager } = window.BX_EXPOSED; - touchLayoutManager && touchLayoutManager.changeLayoutForScope({ - type: "showLayout", - scope: "" + STATES.currentStream?.xboxTitleId, - subscope: "base", - layout: { - id: "System.Standard", - displayName: "Custom", - layoutFile: layout - } - }); - }; - const $style = document.createElement("style"); - document.documentElement.appendChild($style), TouchController.#$style = $style; - const PREF_STYLE_STANDARD = getPref(PrefKey.STREAM_TOUCH_CONTROLLER_STYLE_STANDARD), PREF_STYLE_CUSTOM = getPref(PrefKey.STREAM_TOUCH_CONTROLLER_STYLE_CUSTOM); - window.addEventListener(BxEvent.DATA_CHANNEL_CREATED, (e) => { - const dataChannel = e.dataChannel; - if (!dataChannel || dataChannel.label !== "message") - return; - let filter = ""; - if (TouchController.#enable) { - if (PREF_STYLE_STANDARD === "white") - filter = "grayscale(1) brightness(2)"; - else if (PREF_STYLE_STANDARD === "muted") - filter = "sepia(0.5)"; - } else if (PREF_STYLE_CUSTOM === "muted") - filter = "sepia(0.5)"; - if (filter) - $style.textContent = `#babylon-canvas { filter: ${filter} !important; }`; - else - $style.textContent = ""; - TouchController.#dataChannel = dataChannel, dataChannel.addEventListener("open", () => { - window.setTimeout(TouchController.#show, 1000); - }); - let focused = !1; - dataChannel.addEventListener("message", (msg) => { - if (msg.origin === "better-xcloud" || typeof msg.data !== "string") - return; - if (msg.data.includes("touchcontrols/showtitledefault")) { - if (TouchController.#enable) - if (focused) - TouchController.getCustomLayouts(STATES.currentStream?.xboxTitleId); - else - TouchController.#showDefault(); - return; - } - try { - if (msg.data.includes("/titleinfo")) { - const json = JSON.parse(JSON.parse(msg.data).content); - if (focused = json.focused, !json.focused) - TouchController.#show(); - STATES.currentStream.xboxTitleId = parseInt(json.titleid, 16).toString(); - } - } catch (e2) { - BxLogger.error(LOG_TAG3, "Load custom layout", e2); - } - }); - }); - } -} - -// src/modules/vibration-manager.ts -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; - } - const 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; - } - const 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 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(PrefKey.CONTROLLER_ENABLE_VIBRATION) : !1, window.BX_VIBRATION_INTENSITY = getPref(PrefKey.CONTROLLER_VIBRATION_INTENSITY) / 100, !VibrationManager.supportDeviceVibration()) { - window.BX_ENABLE_DEVICE_VIBRATION = !1; - return; - } - stopVibration && window.navigator.vibrate(0); - const value = getPref(PrefKey.CONTROLLER_DEVICE_VIBRATION); - let enabled; - if (value === "on") - enabled = !0; - else if (value === "auto") { - enabled = !0; - const 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; - const dataView = new DataView(e.data); - let 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; - const vibrationType = dataView.getUint8(offset); - if (offset += Uint8Array.BYTES_PER_ELEMENT, vibrationType !== 0) - return; - const data = {}; - let 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) => { - const dataChannel = e.dataChannel; - if (!dataChannel || dataChannel.label !== "input") - return; - dataChannel.addEventListener("message", VibrationManager.#onMessage); - }); - } -} - // src/modules/ui/ui.ts function localRedirect(path) { const url = window.location.href.substring(0, 31) + path, $pageContent = document.getElementById("PageContent"); @@ -4354,324 +4389,8 @@ function localRedirect(path) { }, 1000); }), $pageContent.appendChild($anchor), $anchor.click(); } -var setupStreamSettingsDialog = function() { - const SETTINGS_UI = [ - { - icon: BxIcon.DISPLAY, - group: "stream", - items: [ - { - group: "audio", - label: t("audio"), - help_url: "https://better-xcloud.github.io/ingame-features/#audio", - items: [ - { - pref: PrefKey.AUDIO_VOLUME, - onChange: (e, value) => { - SoundShortcut.setGainNodeVolume(value); - }, - params: { - disabled: !getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) - }, - onMounted: ($elm) => { - const $range = $elm.querySelector("input[type=range"); - window.addEventListener(BxEvent.GAINNODE_VOLUME_CHANGED, (e) => { - $range.value = e.volume, BxEvent.dispatch($range, "input", { - ignoreOnChange: !0 - }); - }); - } - } - ] - }, - { - group: "video", - label: t("video"), - help_url: "https://better-xcloud.github.io/ingame-features/#video", - items: [ - { - pref: PrefKey.VIDEO_PLAYER_TYPE, - onChange: onChangeVideoPlayerType - }, - { - pref: PrefKey.VIDEO_RATIO, - onChange: updateVideoPlayer - }, - { - pref: PrefKey.VIDEO_PROCESSING, - onChange: updateVideoPlayer - }, - { - pref: PrefKey.VIDEO_SHARPNESS, - onChange: updateVideoPlayer - }, - { - pref: PrefKey.VIDEO_SATURATION, - onChange: updateVideoPlayer - }, - { - pref: PrefKey.VIDEO_CONTRAST, - onChange: updateVideoPlayer - }, - { - pref: PrefKey.VIDEO_BRIGHTNESS, - onChange: updateVideoPlayer - } - ] - } - ] - }, - { - icon: BxIcon.CONTROLLER, - group: "controller", - items: [ - { - group: "controller", - label: t("controller"), - help_url: "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() - } - ] - }, - STATES.userAgentHasTouchSupport && { - group: "touch-controller", - label: t("touch-controller"), - items: [ - { - label: t("layout"), - content: CE("select", { disabled: !0 }, CE("option", {}, t("default"))), - onMounted: ($elm) => { - $elm.addEventListener("change", (e) => { - TouchController.loadCustomLayout(STATES.currentStream?.xboxTitleId, $elm.value, 1000); - }), window.addEventListener(BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED, (e) => { - const data = e.data; - if (STATES.currentStream?.xboxTitleId && $elm.xboxTitleId === STATES.currentStream?.xboxTitleId) { - $elm.dispatchEvent(new Event("change")); - return; - } - $elm.xboxTitleId = STATES.currentStream?.xboxTitleId; - while ($elm.firstChild) - $elm.removeChild($elm.firstChild); - if ($elm.disabled = !data, !data) { - $elm.appendChild(CE("option", { value: "" }, t("default"))), $elm.value = "", $elm.dispatchEvent(new Event("change")); - return; - } - const $fragment = document.createDocumentFragment(); - for (let key in data.layouts) { - const layout = data.layouts[key]; - let name; - if (layout.author) - name = `${layout.name} (${layout.author})`; - else - name = layout.name; - const $option = CE("option", { value: key }, name); - $fragment.appendChild($option); - } - $elm.appendChild($fragment), $elm.value = data.default_layout, $elm.dispatchEvent(new Event("change")); - }); - } - } - ] - } - ] - }, - getPref(PrefKey.MKB_ENABLED) && { - icon: BxIcon.VIRTUAL_CONTROLLER, - group: "mkb", - items: [ - { - group: "mkb", - label: t("virtual-controller"), - help_url: "https://better-xcloud.github.io/mouse-and-keyboard/", - content: MkbRemapper.INSTANCE.render() - } - ] - }, - AppInterface && getPref(PrefKey.NATIVE_MKB_ENABLED) === "on" && { - icon: BxIcon.NATIVE_MKB, - group: "native-mkb", - items: [ - { - group: "native-mkb", - label: t("native-mkb"), - items: [ - { - pref: PrefKey.NATIVE_MKB_SCROLL_VERTICAL_SENSITIVITY, - onChange: (e, value) => { - NativeMkbHandler.getInstance().setVerticalScrollMultiplier(value / 100); - } - }, - { - pref: PrefKey.NATIVE_MKB_SCROLL_HORIZONTAL_SENSITIVITY, - onChange: (e, value) => { - NativeMkbHandler.getInstance().setHorizontalScrollMultiplier(value / 100); - } - } - ] - } - ] - }, - { - icon: BxIcon.COMMAND, - group: "shortcuts", - items: [ - { - group: "shortcuts_controller", - label: t("controller-shortcuts"), - content: ControllerShortcut.renderSettings() - } - ] - }, - { - icon: BxIcon.STREAM_STATS, - group: "stats", - items: [ - { - group: "stats", - label: t("stream-stats"), - help_url: "https://better-xcloud.github.io/stream-stats/", - items: [ - { - pref: PrefKey.STATS_SHOW_WHEN_PLAYING - }, - { - pref: PrefKey.STATS_QUICK_GLANCE, - onChange: (e) => { - const streamStats = StreamStats.getInstance(); - e.target.checked ? streamStats.quickGlanceSetup() : streamStats.quickGlanceStop(); - } - }, - { - pref: PrefKey.STATS_ITEMS, - onChange: StreamStats.refreshStyles - }, - { - pref: PrefKey.STATS_POSITION, - onChange: StreamStats.refreshStyles - }, - { - pref: PrefKey.STATS_TEXT_SIZE, - onChange: StreamStats.refreshStyles - }, - { - pref: PrefKey.STATS_OPACITY, - onChange: StreamStats.refreshStyles - }, - { - pref: PrefKey.STATS_TRANSPARENT, - onChange: StreamStats.refreshStyles - }, - { - pref: PrefKey.STATS_CONDITIONAL_FORMATTING, - onChange: StreamStats.refreshStyles - } - ] - } - ] - } - ]; - let $tabs, $settings; - const $wrapper = CE("div", { class: "bx-stream-settings-dialog bx-gone" }, $tabs = CE("div", { class: "bx-stream-settings-tabs" }), $settings = CE("div", { class: "bx-stream-settings-tab-contents" })); - for (let settingTab of SETTINGS_UI) { - if (!settingTab) - continue; - const $svg = createSvgIcon(settingTab.icon); - $svg.addEventListener("click", (e) => { - for (let $child of Array.from($settings.children)) - if ($child.getAttribute("data-group") === settingTab.group) - $child.classList.remove("bx-gone"); - else - $child.classList.add("bx-gone"); - for (let $child of Array.from($tabs.children)) - $child.classList.remove("bx-active"); - $svg.classList.add("bx-active"); - }), $tabs.appendChild($svg); - const $group = CE("div", { "data-group": settingTab.group, class: "bx-gone" }); - for (let settingGroup of settingTab.items) { - if (!settingGroup) - continue; - if ($group.appendChild(CE("h2", {}, CE("span", {}, settingGroup.label), settingGroup.help_url && createButton({ - icon: BxIcon.QUESTION, - style: ButtonStyle.GHOST, - url: settingGroup.help_url, - title: t("help") - }))), settingGroup.note) { - if (typeof settingGroup.note === "string") - settingGroup.note = document.createTextNode(settingGroup.note); - $group.appendChild(settingGroup.note); - } - if (settingGroup.content) { - $group.appendChild(settingGroup.content); - continue; - } - if (!settingGroup.items) - settingGroup.items = []; - for (let setting of settingGroup.items) { - if (!setting) - continue; - const pref = setting.pref; - let $control; - if (setting.content) - $control = setting.content; - else if (!setting.unsupported) - $control = toPrefElement(pref, setting.onChange, setting.params); - const label = Preferences.SETTINGS[pref]?.label || setting.label, note = Preferences.SETTINGS[pref]?.note || setting.note, $content = CE("div", { class: "bx-stream-settings-row", "data-type": settingGroup.group }, CE("label", { for: `bx_setting_${pref}` }, label, note && CE("div", { class: "bx-stream-settings-dialog-note" }, note), setting.unsupported && CE("div", { class: "bx-stream-settings-dialog-note" }, t("browser-unsupported-feature"))), !setting.unsupported && $control); - $group.appendChild($content), setting.onMounted && setting.onMounted($control); - } - } - $settings.appendChild($group); - } - $tabs.firstElementChild.dispatchEvent(new Event("click")), document.documentElement.appendChild($wrapper); -}, onChangeVideoPlayerType = function() { - const playerType = getPref(PrefKey.VIDEO_PLAYER_TYPE), $videoProcessing = document.getElementById("bx_setting_video_processing"), $videoSharpness = document.getElementById("bx_setting_video_sharpness"); - let isDisabled = !1; - if (playerType === StreamPlayerType.WEBGL2) - $videoProcessing.querySelector(`option[value=${StreamVideoProcessing.CAS}]`).disabled = !1; - else if ($videoProcessing.value = StreamVideoProcessing.USM, setPref(PrefKey.VIDEO_PROCESSING, StreamVideoProcessing.USM), $videoProcessing.querySelector(`option[value=${StreamVideoProcessing.CAS}]`).disabled = !0, UserAgent.isSafari()) - isDisabled = !0; - $videoProcessing.disabled = isDisabled, $videoSharpness.dataset.disabled = isDisabled.toString(), updateVideoPlayer(); -}; -function updateVideoPlayer() { - const streamPlayer = STATES.currentStream.streamPlayer; - if (!streamPlayer) - return; - const options = { - processing: getPref(PrefKey.VIDEO_PROCESSING), - sharpness: getPref(PrefKey.VIDEO_SHARPNESS), - saturation: getPref(PrefKey.VIDEO_SATURATION), - contrast: getPref(PrefKey.VIDEO_CONTRAST), - brightness: getPref(PrefKey.VIDEO_BRIGHTNESS) - }; - streamPlayer.setPlayerType(getPref(PrefKey.VIDEO_PLAYER_TYPE)), streamPlayer.updateOptions(options), streamPlayer.refreshPlayer(); -} -var preloadFonts = function() { - const $link = CE("link", { - rel: "preload", - href: "https://redphx.github.io/better-xcloud/fonts/promptfont.otf", - as: "font", - type: "font/otf", - crossorigin: "" - }); - document.querySelector("head")?.appendChild($link); -}; function setupStreamUi() { - if (!document.querySelector(".bx-stream-settings-dialog")) - preloadFonts(), window.addEventListener("resize", updateVideoPlayer), setupStreamSettingsDialog(), Screenshot.setup(); - onChangeVideoPlayerType(); + StreamSettings.getInstance(), onChangeVideoPlayerType(); } // src/modules/remote-play.ts @@ -4893,6 +4612,222 @@ class RemotePlay { } } +// src/modules/stream/stream-badges.ts +var StreamBadge; +(function(StreamBadge2) { + StreamBadge2["PLAYTIME"] = "playtime"; + StreamBadge2["BATTERY"] = "battery"; + StreamBadge2["DOWNLOAD"] = "in"; + StreamBadge2["UPLOAD"] = "out"; + StreamBadge2["SERVER"] = "server"; + StreamBadge2["VIDEO"] = "video"; + StreamBadge2["AUDIO"] = "audio"; +})(StreamBadge || (StreamBadge = {})); +var StreamBadgeIcon = { + [StreamBadge.PLAYTIME]: BxIcon.PLAYTIME, + [StreamBadge.VIDEO]: BxIcon.DISPLAY, + [StreamBadge.BATTERY]: BxIcon.BATTERY, + [StreamBadge.DOWNLOAD]: BxIcon.DOWNLOAD, + [StreamBadge.UPLOAD]: BxIcon.UPLOAD, + [StreamBadge.SERVER]: BxIcon.SERVER, + [StreamBadge.AUDIO]: BxIcon.AUDIO +}; + +class StreamBadges { + static instance; + static getInstance() { + if (!StreamBadges.instance) + StreamBadges.instance = new StreamBadges; + return StreamBadges.instance; + } + #ipv6 = !1; + #resolution = null; + #video = null; + #audio = null; + #region = ""; + startBatteryLevel = 100; + startTimestamp = 0; + #$container; + #cachedDoms = {}; + #interval; + #REFRESH_INTERVAL = 3000; + setRegion(region2) { + this.#region = region2; + } + #renderBadge(name, value, color) { + let $badge; + if (this.#cachedDoms[name]) + return $badge = this.#cachedDoms[name], $badge.lastElementChild.textContent = value, $badge; + if ($badge = CE("div", { class: "bx-badge", title: t(`badge-${name}`) }, CE("span", { class: "bx-badge-name" }, createSvgIcon(StreamBadgeIcon[name])), CE("span", { class: "bx-badge-value", style: `background-color: ${color}` }, value)), name === StreamBadge.BATTERY) + $badge.classList.add("bx-badge-battery"); + return this.#cachedDoms[name] = $badge, $badge; + } + async#updateBadges(forceUpdate = !1) { + if (!this.#$container || !forceUpdate && !this.#$container.isConnected) { + this.#stop(); + return; + } + let now = +new Date; + const diffSeconds = Math.ceil((now - this.startTimestamp) / 1000), playtime = this.#secondsToHm(diffSeconds); + let batteryLevel = "100%", batteryLevelInt = 100, isCharging = !1; + if ("getBattery" in navigator) + try { + const bm = await navigator.getBattery(); + if (isCharging = bm.charging, batteryLevelInt = Math.round(bm.level * 100), batteryLevel = `${batteryLevelInt}%`, batteryLevelInt != this.startBatteryLevel) { + const diffLevel = Math.round(batteryLevelInt - this.startBatteryLevel), sign = diffLevel > 0 ? "+" : ""; + batteryLevel += ` (${sign}${diffLevel}%)`; + } + } catch (e) { + } + const stats = await STATES.currentStream.peerConnection?.getStats(); + let totalIn = 0, totalOut = 0; + stats.forEach((stat) => { + if (stat.type === "candidate-pair" && stat.packetsReceived > 0 && stat.state === "succeeded") + totalIn += stat.bytesReceived, totalOut += stat.bytesSent; + }); + const badges = { + [StreamBadge.DOWNLOAD]: totalIn ? this.#humanFileSize(totalIn) : null, + [StreamBadge.UPLOAD]: totalOut ? this.#humanFileSize(totalOut) : null, + [StreamBadge.PLAYTIME]: playtime, + [StreamBadge.BATTERY]: batteryLevel + }; + let name; + for (name in badges) { + const value = badges[name]; + if (value === null) + continue; + const $elm = this.#cachedDoms[name]; + if ($elm && ($elm.lastElementChild.textContent = value), name === StreamBadge.BATTERY) + if (this.startBatteryLevel === 100 && batteryLevelInt === 100) + $elm.classList.add("bx-gone"); + else + $elm.dataset.charging = isCharging.toString(), $elm.classList.remove("bx-gone"); + } + } + async#start() { + await this.#updateBadges(!0), this.#stop(), this.#interval = window.setInterval(this.#updateBadges.bind(this), this.#REFRESH_INTERVAL); + } + #stop() { + this.#interval && clearInterval(this.#interval), this.#interval = null; + } + #secondsToHm(seconds) { + let h = Math.floor(seconds / 3600), m = Math.floor(seconds % 3600 / 60) + 1; + if (m === 60) + h += 1, m = 0; + const output = []; + return h > 0 && output.push(`${h}h`), m > 0 && output.push(`${m}m`), output.join(" "); + } + #humanFileSize(size) { + const units = ["B", "KB", "MB", "GB", "TB"], i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024)); + return (size / Math.pow(1024, i)).toFixed(2) + " " + units[i]; + } + async render() { + if (this.#$container) + return this.#start(), this.#$container; + await this.#getServerStats(); + let video = ""; + if (this.#resolution) + video = `${this.#resolution.height}p`; + if (this.#video) { + if (video && (video += "/"), video += this.#video.codec, this.#video.profile) { + const profile = this.#video.profile; + let quality = profile; + if (profile.startsWith("4d")) + quality = t("visual-quality-high"); + else if (profile.startsWith("42e")) + quality = t("visual-quality-normal"); + else if (profile.startsWith("420")) + quality = t("visual-quality-low"); + video += ` (${quality})`; + } + } + let audio; + if (this.#audio) { + audio = this.#audio.codec; + const bitrate = this.#audio.bitrate / 1000; + audio += ` (${bitrate} kHz)`; + } + let batteryLevel = ""; + if ("getBattery" in navigator) + batteryLevel = "100%"; + let server = this.#region; + server += "@" + (this.#ipv6 ? "IPv6" : "IPv4"); + const BADGES = [ + [StreamBadge.PLAYTIME, "1m", "#ff004d"], + [StreamBadge.BATTERY, batteryLevel, "#00b543"], + [StreamBadge.DOWNLOAD, this.#humanFileSize(0), "#29adff"], + [StreamBadge.UPLOAD, this.#humanFileSize(0), "#ff77a8"], + [StreamBadge.SERVER, server, "#ff6c24"], + video ? [StreamBadge.VIDEO, video, "#742f29"] : null, + audio ? [StreamBadge.AUDIO, audio, "#5f574f"] : null + ], $container = CE("div", { class: "bx-badges" }); + return BADGES.forEach((item2) => { + if (!item2) + return; + const $badge = this.#renderBadge(...item2); + $container.appendChild($badge); + }), this.#$container = $container, await this.#start(), $container; + } + async#getServerStats() { + const stats = await STATES.currentStream.peerConnection.getStats(), allVideoCodecs = {}; + let videoCodecId; + const allAudioCodecs = {}; + let audioCodecId; + const allCandidates = {}; + let candidateId; + if (stats.forEach((stat) => { + if (stat.type === "codec") { + const mimeType = stat.mimeType.split("/")[0]; + if (mimeType === "video") + allVideoCodecs[stat.id] = stat; + else if (mimeType === "audio") + allAudioCodecs[stat.id] = stat; + } else if (stat.type === "inbound-rtp" && stat.packetsReceived > 0) { + if (stat.kind === "video") + videoCodecId = stat.codecId; + else if (stat.kind === "audio") + audioCodecId = stat.codecId; + } else if (stat.type === "candidate-pair" && stat.packetsReceived > 0 && stat.state === "succeeded") + candidateId = stat.remoteCandidateId; + else if (stat.type === "remote-candidate") + allCandidates[stat.id] = stat.address; + }), videoCodecId) { + const videoStat = allVideoCodecs[videoCodecId], video = { + codec: videoStat.mimeType.substring(6) + }; + if (video.codec === "H264") { + const match = /profile-level-id=([0-9a-f]{6})/.exec(videoStat.sdpFmtpLine); + video.profile = match ? match[1] : null; + } + this.#video = video; + } + if (audioCodecId) { + const audioStat = allAudioCodecs[audioCodecId]; + this.#audio = { + codec: audioStat.mimeType.substring(6), + bitrate: audioStat.clockRate + }; + } + if (candidateId) + BxLogger.info("candidate", candidateId, allCandidates), this.#ipv6 = allCandidates[candidateId].includes(":"); + } + static setupEvents() { + window.addEventListener(BxEvent.STREAM_PLAYING, (e) => { + const $video = e.$video, streamBadges = StreamBadges.getInstance(); + streamBadges.#resolution = { + width: $video.videoWidth, + height: $video.videoHeight + }, streamBadges.startTimestamp = +new Date; + try { + "getBattery" in navigator && navigator.getBattery().then((bm) => { + streamBadges.startBatteryLevel = Math.round(bm.level * 100); + }); + } catch (e2) { + } + }); + } +} + // src/enums/game-pass-gallery.ts var GamePassCloudGallery; (function(GamePassCloudGallery2) { @@ -5017,8 +4952,9 @@ function interceptHttpRequests() { if (url.startsWith("https://emerald.xboxservices.com/xboxcomfd/experimentation")) try { const response = await NATIVE_FETCH(request, init), json = await response.json(); - for (let key in FeatureGates) - json.exp.treatments[key] = FeatureGates[key]; + if (json && json.exp && json.treatments) + for (let key in FeatureGates) + json.exp.treatments[key] = FeatureGates[key]; return response.json = () => Promise.resolve(json), response; } catch (e) { console.log(e); @@ -5262,7 +5198,7 @@ class XcloudInterceptor { enableMouseInput: overrideMkb, enableKeyboardInput: overrideMkb }); - if (overrides.videoConfiguration = overrides.videoConfiguration || {}, overrides.videoConfiguration.setCodecPreferences = !0, TouchController.isEnabled()) + if (TouchController.isEnabled()) overrides.inputConfiguration.enableTouchInput = !0, overrides.inputConfiguration.maxTouchPoints = 10; if (getPref(PrefKey.AUDIO_MIC_ON_PLAYING)) overrides.audioConfiguration = overrides.audioConfiguration || {}, overrides.audioConfiguration.enableMicrophone = !0; @@ -5304,1314 +5240,7 @@ function showGamepadToast(gamepad) { // src/utils/css.ts 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: 36px; - --bx-default-button-color: #2d3036; - --bx-default-button-hover-color: #515863; - --bx-default-button-disabled-color: #8e8e8e; - --bx-primary-button-color: #008746; - --bx-primary-button-hover-color: #04b358; - --bx-primary-button-disabled-color: #448262; - --bx-danger-button-color: #c10404; - --bx-danger-button-hover-color: #e61d1d; - --bx-danger-button-disabled-color: #a26c6c; - --bx-toast-z-index: 9999; - --bx-dialog-z-index: 9101; - --bx-dialog-overlay-z-index: 9100; - --bx-remote-play-popup-z-index: 9090; - --bx-stats-bar-z-index: 9001; - --bx-stream-settings-z-index: 9000; - --bx-mkb-pointer-lock-msg-z-index: 8999; - --bx-game-bar-z-index: 8888; - --bx-wait-time-box-z-index: 100; - --bx-screenshot-animation-z-index: 1; -} -@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: 600px) { - 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-gone { - display: none !important; -} -.bx-offscreen { - position: absolute !important; - top: -9999px !important; - left: -9999px !important; - visibility: hidden !important; -} -.bx-hidden { - visibility: hidden !important; -} -.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); -} -#headerArea, -#uhfSkipToMain, -.uhf-footer { - display: none; -} -div[class*=NotFocusedDialog] { - position: absolute !important; - top: -9999px !important; - left: -9999px !important; - width: 0px !important; - height: 0px !important; -} -#game-stream video:not([src]) { - visibility: hidden; -} -.bx-button { - background-color: var(--bx-default-button-color); - 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:focus { - outline: none !important; -} -.bx-button:hover, -.bx-button.bx-focusable:focus { - background-color: var(--bx-default-button-hover-color); -} -.bx-button:disabled { - cursor: default; - background-color: var(--bx-default-button-disabled-color); -} -.bx-button.bx-ghost { - background-color: transparent; -} -.bx-button.bx-ghost:hover, -.bx-button.bx-ghost.bx-focusable:focus { - background-color: var(--bx-default-button-hover-color); -} -.bx-button.bx-primary { - background-color: var(--bx-primary-button-color); -} -.bx-button.bx-primary:hover, -.bx-button.bx-primary.bx-focusable:focus { - background-color: var(--bx-primary-button-hover-color); -} -.bx-button.bx-primary:disabled { - background-color: var(--bx-primary-button-disabled-color); -} -.bx-button.bx-danger { - background-color: var(--bx-danger-button-color); -} -.bx-button.bx-danger:hover, -.bx-button.bx-danger.bx-focusable:focus { - background-color: var(--bx-danger-button-hover-color); -} -.bx-button.bx-danger:disabled { - background-color: var(--bx-danger-button-disabled-color); -} -.bx-button.bx-tall { - height: calc(var(--bx-button-height) * 1.5) !important; -} -.bx-button svg { - display: inline-block; - width: 16px; - height: var(--bx-button-height); -} -.bx-button svg:not(:only-child) { - margin-right: 4px; -} -.bx-button span { - display: inline-block; -/* height: var(--bx-button-height); */ - line-height: var(--bx-button-height); - vertical-align: middle; -/* vertical-align: -webkit-baseline-middle; */ - color: #fff; - overflow: hidden; - white-space: nowrap; -} -.bx-button.bx-focusable { - position: relative; -} -.bx-button.bx-focusable::after { - border: 2px solid transparent; - border-radius: 4px; -} -.bx-button.bx-focusable:focus::after { - content: ''; - border-color: #fff; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; -} -a.bx-button { - display: inline-block; -} -a.bx-button.bx-full-width { - text-align: center; -} -.bx-header-remote-play-button { - height: auto; - margin-right: 8px !important; -} -.bx-header-remote-play-button svg { - width: 24px; - height: 46px; -} -.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-settings-reload-button { - margin-top: 10px; -} -.bx-settings-container { - background-color: #151515; - user-select: none; - -webkit-user-select: none; - color: #fff; - font-family: var(--bx-normal-font); -} -@media (hover: hover) { - .bx-settings-wrapper a.bx-settings-title:hover { - color: #83f73a; - } -} -.bx-settings-wrapper { - width: 450px; - margin: auto; - padding: 12px 6px; -} -@media screen and (max-width: 450px) { - .bx-settings-wrapper { - width: 100%; - } -} -.bx-settings-wrapper *:focus { - outline: none !important; -} -.bx-settings-wrapper .bx-settings-title-wrapper { - display: flex; - margin-bottom: 10px; - align-items: center; -} -.bx-settings-wrapper a.bx-settings-title { - font-family: var(--bx-title-font); - font-size: 1.4rem; - text-decoration: none; - font-weight: bold; - display: block; - color: #5dc21e; - flex: 1; -} -.bx-settings-wrapper a.bx-settings-title:focus { - color: #83f73a; -} -.bx-settings-wrapper .bx-button.bx-primary { - margin-top: 8px; -} -.bx-settings-wrapper a.bx-settings-update { - display: block; - color: #ff834b; - text-decoration: none; - margin-bottom: 8px; - text-align: center; - background: #222; - border-radius: 4px; - padding: 4px; -} -@media (hover: hover) { - .bx-settings-wrapper a.bx-settings-update:hover { - color: #ff9869; - text-decoration: underline; - } -} -.bx-settings-wrapper a.bx-settings-update:focus { - color: #ff9869; - text-decoration: underline; -} -.bx-settings-group-label { - font-weight: bold; - display: block; - font-size: 1.1rem; -} -.bx-settings-row { - display: flex; - padding: 6px 12px; - position: relative; -} -.bx-settings-row label { - flex: 1; - align-self: center; - margin-bottom: 0; -} -.bx-settings-row:hover, -.bx-settings-row:focus-within { - background-color: #242424; -} -.bx-settings-row input { - align-self: center; - accent-color: var(--bx-primary-button-color); -} -.bx-settings-row input:focus { - accent-color: var(--bx-danger-button-color); -} -.bx-settings-row select:disabled { - -webkit-appearance: none; - background: transparent; - text-align-last: right; - border: none; - color: #fff; -} -.bx-settings-row input[type=checkbox]:focus, -.bx-settings-row 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-row:has(input:focus)::before, -.bx-settings-row:has(select:focus)::before { - content: ' '; - border-radius: 4px; - border: 2px solid #fff; - position: absolute; - top: 0; - left: 0; - bottom: 0; -} -.bx-settings-group-label b, -.bx-settings-row label b { - display: block; - font-size: 12px; - font-style: italic; - font-weight: normal; - color: #828282; -} -.bx-settings-group-label b { - margin-bottom: 8px; -} -.bx-settings-app-version { - margin-top: 10px; - text-align: center; - color: #747474; - font-size: 12px; -} -.bx-donation-link { - display: block; - text-align: center; - text-decoration: none; - height: 20px; - line-height: 20px; - font-size: 14px; - margin-top: 10px; - color: #5dc21e; -} -.bx-donation-link:hover { - color: #6dd72b; -} -.bx-donation-link:focus { - text-decoration: underline; -} -.bx-settings-custom-user-agent { - display: block; - width: 100%; -} -.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-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 0.2s ease-in; -} -.bx-toast.bx-show { - opacity: 0.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-popup { - width: 100%; - max-width: 1920px; - margin: auto; - position: relative; - height: 0.1px; - overflow: visible; - z-index: var(--bx-remote-play-popup-z-index); -} -.bx-remote-play-container { - position: absolute; - right: 10px; - top: 0; - background: #1a1b1e; - border-radius: 10px; - width: 420px; - max-width: calc(100vw - 20px); - margin: 0 0 0 auto; - padding: 20px; - box-shadow: rgba(0,0,0,0.502) 0px 0px 12px 0px; -} -@media (min-width: 480px) and (min-height: calc(480px + 1px)) { - .bx-remote-play-container { - right: calc(env(safe-area-inset-right, 0px) + 32px); - } -} -@media (min-width: 768px) and (min-height: calc(480px + 1px)) { - .bx-remote-play-container { - right: calc(env(safe-area-inset-right, 0px) + 48px); - } -} -@media (min-width: 1920px) and (min-height: calc(480px + 1px)) { - .bx-remote-play-container { - right: calc(env(safe-area-inset-right, 0px) + 80px); - } -} -.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-settings span { - font-weight: bold; - font-size: 18px; - display: block; - margin-bottom: 8px; - text-align: center; -} -.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: 14px; -} -.bx-remote-play-connect-button { - min-height: 100%; - margin: 4px 0; -} -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 0.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: 0px solid rgba(255,255,255,0.502); - } - 50% { - border: 8px solid rgba(255,255,255,0.502); - } - 100% { - border: 0px solid rgba(255,255,255,0.502); - } -} -@-webkit-keyframes bx-anim-taking-screenshot { - 0% { - border: 0px solid rgba(255,255,255,0.502); - } - 50% { - border: 8px solid rgba(255,255,255,0.502); - } - 100% { - border: 0px solid rgba(255,255,255,0.502); - } -} -@-o-keyframes bx-anim-taking-screenshot { - 0% { - border: 0px solid rgba(255,255,255,0.502); - } - 50% { - border: 8px solid rgba(255,255,255,0.502); - } - 100% { - border: 0px solid rgba(255,255,255,0.502); - } -} -@keyframes bx-anim-taking-screenshot { - 0% { - border: 0px solid rgba(255,255,255,0.502); - } - 50% { - border: 8px solid rgba(255,255,255,0.502); - } - 100% { - border: 0px 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: 14px; -} -.bx-number-stepper button { - border: none; - width: 24px; - height: 24px; - margin: 0 4px; - 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); - color: #fff; -} -@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: 0px 0px 6px #1c1c1c; - transition: opacity 0.1s ease-in; -/* Touch controller buttons */ -/* Show enabled button */ -/* Show enable button */ -} -#bx-game-bar .bx-game-bar-container.bx-show { - opacity: 0.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 0.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(0.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-enabled] button { - display: none; -} -#bx-game-bar .bx-game-bar-container div[data-enabled='true'] button:first-of-type { - display: block; -} -#bx-game-bar .bx-game-bar-container div[data-enabled='false'] 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: 0px; - 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: 0px 0px 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; -/* height: 30px; */ - 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: block; - user-select: none; - -webkit-user-select: none; - position: fixed; - top: 0; - background-color: #000; - color: #fff; - font-family: var(--bx-monospaced-font); - font-size: 0.9rem; - padding-left: 8px; - z-index: var(--bx-stats-bar-z-index); - text-wrap: nowrap; -} -.bx-stats-bar[data-stats*="[fps]"] > .bx-stat-fps, -.bx-stats-bar[data-stats*="[ping]"] > .bx-stat-ping, -.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 { - display: inline-block; -} -.bx-stats-bar[data-stats$="[fps]"] > .bx-stat-fps, -.bx-stats-bar[data-stats$="[ping]"] > .bx-stat-ping, -.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 { - margin-right: 0; - 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; - margin-right: 8px; - 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: inherit; - 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-stream-settings-dialog { - display: flex; - position: fixed; - z-index: var(--bx-stream-settings-z-index); - opacity: 0.98; - user-select: none; - -webkit-user-select: none; -} -.bx-stream-settings-tabs { - position: fixed; - top: 0; - right: 420px; - display: flex; - flex-direction: column; - border-radius: 0 0 0 8px; - box-shadow: 0px 0px 6px #000; - overflow: clip; -} -.bx-stream-settings-tabs svg { - width: 32px; - height: 32px; - padding: 10px; - box-sizing: content-box; - background: #131313; - cursor: pointer; - border-left: 4px solid #1e1e1e; -} -.bx-stream-settings-tabs svg.bx-active { - background: #222; - border-color: #008746; -} -.bx-stream-settings-tabs svg:not(.bx-active):hover { - background: #2f2f2f; - border-color: #484848; -} -.bx-stream-settings-tab-contents { - flex-direction: column; - position: fixed; - right: 0; - top: 0; - bottom: 0; - padding: 14px 14px 0; - width: 420px; - background: #1a1b1e; - color: #fff; - font-weight: 400; - font-size: 16px; - font-family: var(--bx-title-font); - text-align: center; - box-shadow: 0px 0px 6px #000; - overflow: overlay; -} -.bx-stream-settings-tab-contents > div[data-group=mkb] { - display: flex; - flex-direction: column; - height: 100%; - overflow: hidden; -} -.bx-stream-settings-tab-contents *:focus { - outline: none !important; -} -.bx-stream-settings-tab-contents h2 { - margin-bottom: 8px; - display: flex; - align-item: center; -} -.bx-stream-settings-tab-contents h2 span { - display: inline-block; - font-size: 24px; - font-weight: bold; - text-transform: uppercase; - text-align: left; - flex: 1; - height: var(--bx-button-height); - line-height: calc(var(--bx-button-height) + 4px); - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; -} -.bx-stream-settings-row { - display: flex; - border-bottom: 1px solid rgba(64,64,64,0.502); - margin-bottom: 16px; - padding-bottom: 16px; -} -.bx-stream-settings-row label { - font-size: 16px; - display: block; - text-align: left; - flex: 1; - align-self: center; - margin-bottom: 0 !important; -} -.bx-stream-settings-row input { - accent-color: var(--bx-primary-button-color); -} -.bx-stream-settings-row select:disabled { - -webkit-appearance: none; - background: transparent; - text-align-last: right; - border: none; - color: #fff; -} -.bx-stream-settings-row select option:disabled { - display: none; -} -.bx-stream-settings-dialog-note { - display: block; - font-size: 12px; - font-weight: lighter; - font-style: italic; -} -.bx-stream-settings-tab-contents div[data-group="shortcuts"] > div[data-has-gamepad=true] > div:first-of-type { - display: none; -} -.bx-stream-settings-tab-contents div[data-group="shortcuts"] > div[data-has-gamepad=true] > div:last-of-type { - display: block; -} -.bx-stream-settings-tab-contents div[data-group="shortcuts"] > div[data-has-gamepad=false] > div:first-of-type { - display: block; -} -.bx-stream-settings-tab-contents div[data-group="shortcuts"] > div[data-has-gamepad=false] > div:last-of-type { - display: none; -} -.bx-stream-settings-tab-contents div[data-group="shortcuts"] .bx-shortcut-profile { - width: 100%; - height: 36px; - display: block; - margin-bottom: 10px; -} -.bx-stream-settings-tab-contents div[data-group="shortcuts"] .bx-shortcut-note { - font-size: 14px; -} -.bx-stream-settings-tab-contents div[data-group="shortcuts"] .bx-shortcut-row { - display: flex; - margin-bottom: 10px; -} -.bx-stream-settings-tab-contents div[data-group="shortcuts"] .bx-shortcut-row label.bx-prompt { - flex: 1; - font-size: 26px; - margin-bottom: 0; -} -.bx-stream-settings-tab-contents div[data-group="shortcuts"] .bx-shortcut-row .bx-shortcut-actions { - flex: 2; - position: relative; -} -.bx-stream-settings-tab-contents div[data-group="shortcuts"] .bx-shortcut-row .bx-shortcut-actions select { - position: absolute; - width: 100%; - height: 100%; - display: block; -} -.bx-stream-settings-tab-contents div[data-group="shortcuts"] .bx-shortcut-row .bx-shortcut-actions select:last-of-type { - opacity: 0; - z-index: calc(var(--bx-stream-settings-z-index) + 1); -} -.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: 0.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; -} -`; + 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:36px;--bx-default-button-color:#2d3036;--bx-default-button-hover-color:#515863;--bx-default-button-disabled-color:#8e8e8e;--bx-primary-button-color:#008746;--bx-primary-button-hover-color:#04b358;--bx-primary-button-disabled-color:#448262;--bx-danger-button-color:#c10404;--bx-danger-button-hover-color:#e61d1d;--bx-danger-button-disabled-color:#a26c6c;--bx-toast-z-index:9999;--bx-dialog-z-index:9101;--bx-dialog-overlay-z-index:9100;--bx-remote-play-popup-z-index:9090;--bx-stats-bar-z-index:9010;--bx-stream-settings-z-index:9001;--bx-mkb-pointer-lock-msg-z-index:9000;--bx-stream-settings-overlay-z-index:8999;--bx-game-bar-z-index:8888;--bx-wait-time-box-z-index:100;--bx-screenshot-animation-z-index:1}@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:600px){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-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)}#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-button{background-color:var(--bx-default-button-color);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:focus{outline:none !important}.bx-button:hover,.bx-button.bx-focusable:focus{background-color:var(--bx-default-button-hover-color)}.bx-button:disabled{cursor:default;background-color:var(--bx-default-button-disabled-color)}.bx-button.bx-ghost{background-color:transparent}.bx-button.bx-ghost:hover,.bx-button.bx-ghost.bx-focusable:focus{background-color:var(--bx-default-button-hover-color)}.bx-button.bx-primary{background-color:var(--bx-primary-button-color)}.bx-button.bx-primary:hover,.bx-button.bx-primary.bx-focusable:focus{background-color:var(--bx-primary-button-hover-color)}.bx-button.bx-primary:disabled{background-color:var(--bx-primary-button-disabled-color)}.bx-button.bx-danger{background-color:var(--bx-danger-button-color)}.bx-button.bx-danger:hover,.bx-button.bx-danger.bx-focusable:focus{background-color:var(--bx-danger-button-hover-color)}.bx-button.bx-danger:disabled{background-color:var(--bx-danger-button-disabled-color)}.bx-button.bx-tall{height:calc(var(--bx-button-height) * 1.5) !important}.bx-button svg{display:inline-block;width:16px;height:var(--bx-button-height)}.bx-button svg:not(:only-child){margin-right:4px}.bx-button span{display:inline-block;line-height:var(--bx-button-height);vertical-align:middle;color:#fff;overflow:hidden;white-space:nowrap}.bx-button.bx-focusable{position:relative}.bx-button.bx-focusable::after{border:2px solid transparent;border-radius:4px}.bx-button.bx-focusable:focus::after{content:'';border-color:#fff;position:absolute;top:0;left:0;right:0;bottom:0}a.bx-button{display:inline-block}a.bx-button.bx-full-width{text-align:center}.bx-header-remote-play-button{height:auto;margin-right:8px !important}.bx-header-remote-play-button svg{width:24px;height:46px}.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-settings-reload-button{margin-top:10px}.bx-settings-container{background-color:#151515;user-select:none;-webkit-user-select:none;color:#fff;font-family:var(--bx-normal-font)}@media (hover:hover){.bx-settings-wrapper a.bx-settings-title:hover{color:#83f73a}}.bx-settings-wrapper{width:450px;margin:auto;padding:12px 6px}@media screen and (max-width:450px){.bx-settings-wrapper{width:100%}}.bx-settings-wrapper *:focus{outline:none !important}.bx-settings-wrapper .bx-top-buttons .bx-button{display:block;margin-bottom:8px}.bx-settings-wrapper .bx-settings-title-wrapper{display:flex;margin-bottom:10px;align-items:center}.bx-settings-wrapper a.bx-settings-title{font-family:var(--bx-title-font);font-size:1.4rem;text-decoration:none;font-weight:bold;display:block;color:#5dc21e;flex:1}.bx-settings-wrapper a.bx-settings-title:focus{color:#83f73a}.bx-settings-wrapper a.bx-settings-update{display:block;color:#ff834b;text-decoration:none;margin-bottom:8px;text-align:center;background:#222;border-radius:4px;padding:4px}@media (hover:hover){.bx-settings-wrapper a.bx-settings-update:hover{color:#ff9869;text-decoration:underline}}.bx-settings-wrapper a.bx-settings-update:focus{color:#ff9869;text-decoration:underline}.bx-settings-group-label{font-weight:bold;display:block;font-size:1.1rem}.bx-settings-row{display:flex;padding:6px 12px;position:relative}.bx-settings-row label{flex:1;align-self:center;margin-bottom:0}.bx-settings-row:hover,.bx-settings-row:focus-within{background-color:#242424}.bx-settings-row input{align-self:center;accent-color:var(--bx-primary-button-color)}.bx-settings-row input:focus{accent-color:var(--bx-danger-button-color)}.bx-settings-row select:disabled{-webkit-appearance:none;background:transparent;text-align-last:right;border:none;color:#fff}.bx-settings-row input[type=checkbox]:focus,.bx-settings-row 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-row:has(input:focus)::before,.bx-settings-row:has(select:focus)::before{content:' ';border-radius:4px;border:2px solid #fff;position:absolute;top:0;left:0;bottom:0}.bx-settings-group-label b,.bx-settings-row label b{display:block;font-size:12px;font-style:italic;font-weight:normal;color:#828282}.bx-settings-group-label b{margin-bottom:8px}.bx-settings-app-version{margin-top:10px;text-align:center;color:#747474;font-size:12px}.bx-donation-link{display:block;text-align:center;text-decoration:none;height:20px;line-height:20px;font-size:14px;margin-top:10px;color:#5dc21e}.bx-donation-link:hover{color:#6dd72b}.bx-donation-link:focus{text-decoration:underline}.bx-settings-custom-user-agent{display:block;width:100%}.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-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-popup{width:100%;max-width:1920px;margin:auto;position:relative;height:.1px;overflow:visible;z-index:var(--bx-remote-play-popup-z-index)}.bx-remote-play-container{position:absolute;right:10px;top:0;background:#1a1b1e;border-radius:10px;width:420px;max-width:calc(100vw - 20px);margin:0 0 0 auto;padding:20px;box-shadow:rgba(0,0,0,0.502) 0 0 12px 0}@media (min-width:480px) and (min-height:calc(480px + 1px)){.bx-remote-play-container{right:calc(env(safe-area-inset-right, 0px) + 32px)}}@media (min-width:768px) and (min-height:calc(480px + 1px)){.bx-remote-play-container{right:calc(env(safe-area-inset-right, 0px) + 48px)}}@media (min-width:1920px) and (min-height:calc(480px + 1px)){.bx-remote-play-container{right:calc(env(safe-area-inset-right, 0px) + 80px)}}.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-settings span{font-weight:bold;font-size:18px;display:block;margin-bottom:8px;text-align:center}.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:14px}.bx-remote-play-connect-button{min-height:100%;margin:4px 0}.bx-select select{display:none}.bx-select > div{display:inline-block;min-width:110px;text-align:center;margin:0 10px;line-height:24px;vertical-align:middle;background:#fff;color:#000;border-radius:4px;padding:2px 4px}.bx-select > div input{display:inline-block;margin-right:8px}.bx-select > div label{margin-bottom:0}.bx-select button{border:none;width:24px;height:24px;line-height:24px;color:#fff;border-radius:4px;font-weight:bold;font-size:14px;font-family:var(--bx-monospaced-font)}.bx-select button.bx-inactive{pointer-events:none;opacity:.2}.bx-select button span{line-height:unset}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:14px}.bx-number-stepper button{border:none;width:24px;height:24px;margin:0 4px;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-enabled] button{display:none}#bx-game-bar .bx-game-bar-container div[data-enabled='true'] button:first-of-type{display:block}#bx-game-bar .bx-game-bar-container div[data-enabled='false'] 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:block;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*="[fps]"] > .bx-stat-fps,.bx-stats-bar[data-stats*="[ping]"] > .bx-stat-ping,.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{display:inline-block}.bx-stats-bar[data-stats$="[fps]"] > .bx-stat-fps,.bx-stats-bar[data-stats$="[ping]"] > .bx-stat-ping,.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{margin-right:0;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;margin-right:8px;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:inherit;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-stream-settings-dialog{display:flex;position:fixed;z-index:var(--bx-stream-settings-z-index);opacity:.98;user-select:none;-webkit-user-select:none}.bx-stream-settings-overlay{position:fixed;background:rgba(11,11,11,0.89);top:0;left:0;right:0;bottom:0;z-index:var(--bx-stream-settings-overlay-z-index)}.bx-stream-settings-overlay[data-is-playing="true"]{background:transparent}.bx-stream-settings-tabs{position:fixed;top:0;right:420px;display:flex;flex-direction:column;border-radius:0 0 0 8px;box-shadow:0 0 6px #000;overflow:clip}.bx-stream-settings-tabs svg{width:32px;height:32px;padding:10px;box-sizing:content-box;background:#131313;cursor:pointer;border-left:4px solid #1e1e1e}.bx-stream-settings-tabs svg.bx-active{background:#222;border-color:#008746}.bx-stream-settings-tabs svg:not(.bx-active):hover{background:#2f2f2f;border-color:#484848}.bx-stream-settings-tab-contents{flex-direction:column;position:fixed;right:0;top:0;bottom:0;padding:14px 14px 0;width:420px;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}.bx-stream-settings-tab-contents > div[data-group=mkb]{display:flex;flex-direction:column;height:100%;overflow:hidden}.bx-stream-settings-tab-contents *:focus{outline:none !important}.bx-stream-settings-tab-contents h2{margin-bottom:8px;display:flex;align-item:center}.bx-stream-settings-tab-contents h2 span{display:inline-block;font-size:24px;font-weight:bold;text-transform:uppercase;text-align:left;flex:1;height:var(--bx-button-height);line-height:calc(var(--bx-button-height) + 4px);text-overflow:ellipsis;overflow:hidden;white-space:nowrap}.bx-stream-settings-row{display:flex;border-bottom:1px solid rgba(64,64,64,0.502);margin-bottom:16px;padding-bottom:16px}.bx-stream-settings-row > label{font-size:16px;display:block;text-align:left;flex:1;align-self:center;margin-bottom:0 !important}.bx-stream-settings-row input{accent-color:var(--bx-primary-button-color)}.bx-stream-settings-row select:disabled{-webkit-appearance:none;background:transparent;text-align-last:right;border:none;color:#fff}.bx-stream-settings-row select option:disabled{display:none}.bx-stream-settings-dialog-note{display:block;font-size:12px;font-weight:lighter;font-style:italic}.bx-stream-settings-tab-contents div[data-group="shortcuts"] > div[data-has-gamepad=true] > div:first-of-type{display:none}.bx-stream-settings-tab-contents div[data-group="shortcuts"] > div[data-has-gamepad=true] > div:last-of-type{display:block}.bx-stream-settings-tab-contents div[data-group="shortcuts"] > div[data-has-gamepad=false] > div:first-of-type{display:block}.bx-stream-settings-tab-contents div[data-group="shortcuts"] > div[data-has-gamepad=false] > div:last-of-type{display:none}.bx-stream-settings-tab-contents div[data-group="shortcuts"] .bx-shortcut-profile{width:100%;height:36px;display:block;margin-bottom:10px}.bx-stream-settings-tab-contents div[data-group="shortcuts"] .bx-shortcut-note{font-size:14px}.bx-stream-settings-tab-contents div[data-group="shortcuts"] .bx-shortcut-row{display:flex;margin-bottom:10px}.bx-stream-settings-tab-contents div[data-group="shortcuts"] .bx-shortcut-row label.bx-prompt{flex:1;font-size:26px;margin-bottom:0}.bx-stream-settings-tab-contents div[data-group="shortcuts"] .bx-shortcut-row .bx-shortcut-actions{flex:2;position:relative}.bx-stream-settings-tab-contents div[data-group="shortcuts"] .bx-shortcut-row .bx-shortcut-actions select{position:absolute;width:100%;height:100%;display:block}.bx-stream-settings-tab-contents div[data-group="shortcuts"] .bx-shortcut-row .bx-shortcut-actions select:last-of-type{opacity:0;z-index:calc(var(--bx-stream-settings-z-index) + 1)}.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}`; if (getPref(PrefKey.BLOCK_SOCIAL_FEATURES)) css += ` /* Hide "Play with friends" section */ @@ -6729,6 +5358,16 @@ body::-webkit-scrollbar { const $style = CE("style", {}, css); document.documentElement.appendChild($style); } +function preloadFonts() { + const $link = CE("link", { + rel: "preload", + href: "https://redphx.github.io/better-xcloud/fonts/promptfont.otf", + as: "font", + type: "font/otf", + crossorigin: "" + }); + document.querySelector("head")?.appendChild($link); +} // src/modules/mkb/mouse-cursor-hider.ts class MouseCursorHider { @@ -7098,6 +5737,11 @@ true` + ",this._connectionType="; if (!str2.includes("async requestPointerLock(){")) return !1; return str2 = str2.replace("async requestPointerLock(){", "async requestPointerLock(){return;"), str2; + }, + patchRequestInfoCrash(str2) { + if (!str2.includes('if(!e)throw new Error("RequestInfo.origin is falsy");')) + return !1; + return str2 = str2.replace('if(!e)throw new Error("RequestInfo.origin is falsy");', 'if (!e) e = "https://www.xbox.com";'), str2; } }, PATCH_ORDERS = [ ...getPref(PrefKey.NATIVE_MKB_ENABLED) === "on" ? [ @@ -7106,6 +5750,7 @@ true` + ",this._connectionType="; "disableNativeRequestPointerLock", "exposeInputSink" ] : [], + "patchRequestInfoCrash", "disableStreamGate", "overrideSettings", "broadcastPollingMode", @@ -7274,9 +5919,7 @@ function setupSettingsUi() { let $btnReload; const $container = CE("div", { class: "bx-settings-container bx-gone" - }); - let $updateAvailable; - const $wrapper = CE("div", { class: "bx-settings-wrapper" }, CE("div", { class: "bx-settings-title-wrapper" }, CE("a", { + }), $wrapper = CE("div", { class: "bx-settings-wrapper" }, CE("div", { class: "bx-settings-title-wrapper" }, CE("a", { class: "bx-settings-title", href: "https://github.com/redphx/better-xcloud/releases", target: "_blank" @@ -7285,36 +5928,48 @@ function setupSettingsUi() { style: ButtonStyle.FOCUSABLE, label: t("help"), url: "https://better-xcloud.github.io/features/" - }))); - if ($updateAvailable = CE("a", { - class: "bx-settings-update bx-gone", - href: "https://github.com/redphx/better-xcloud/releases/latest", - target: "_blank" - }), $wrapper.appendChild($updateAvailable), !SCRIPT_VERSION.includes("beta") && PREF_LATEST_VERSION && PREF_LATEST_VERSION != SCRIPT_VERSION) - $updateAvailable.textContent = `🌟 Version ${PREF_LATEST_VERSION} available`, $updateAvailable.classList.remove("bx-gone"); - if (AppInterface) { - const $btn = createButton({ + }))), topButtons = []; + if (!SCRIPT_VERSION.includes("beta") && PREF_LATEST_VERSION && PREF_LATEST_VERSION != SCRIPT_VERSION) + topButtons.push(createButton({ + label: `🌟 Version ${PREF_LATEST_VERSION} available`, + style: ButtonStyle.PRIMARY | ButtonStyle.FOCUSABLE | ButtonStyle.FULL_WIDTH, + url: "https://github.com/redphx/better-xcloud/releases/latest" + })); + if (topButtons.push(createButton({ + label: t("stream-settings"), + icon: BxIcon.STREAM_SETTINGS, + style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE, + onClick: (e) => { + StreamSettings.getInstance().show(); + } + })), AppInterface) + topButtons.push(createButton({ label: t("android-app-settings"), icon: BxIcon.STREAM_SETTINGS, style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE, onClick: (e) => { AppInterface.openAppSettings && AppInterface.openAppSettings(); } - }); - $wrapper.appendChild($btn); - } else if (UserAgent.getDefault().toLowerCase().includes("android")) { - const $btn = createButton({ + })); + else if (UserAgent.getDefault().toLowerCase().includes("android")) + topButtons.push(createButton({ label: "🔥 " + t("install-android"), style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE, url: "https://better-xcloud.github.io/android" - }); - $wrapper.appendChild($btn); + })); + if (topButtons.length) { + const $div = CE("div", { class: "bx-top-buttons" }); + for (let $button of topButtons) + $div.appendChild($button); + $wrapper.appendChild($div); } const onChange = async (e) => { PatcherCache.clear(), $btnReload.classList.add("bx-danger"); const $btnHeaderSettings = document.querySelector(".bx-header-settings-button"); - if ($btnHeaderSettings && $btnHeaderSettings.classList.add("bx-danger"), e.target.id === "bx_setting_" + PrefKey.BETTER_XCLOUD_LOCALE) - Translations.refreshCurrentLocale(), await Translations.updateTranslations(), $btnReload.textContent = t("settings-reloading"), $btnReload.click(); + if ($btnHeaderSettings && $btnHeaderSettings.classList.add("bx-danger"), e.target.id === "bx_setting_" + PrefKey.BETTER_XCLOUD_LOCALE) { + if (Translations.refreshCurrentLocale(), await Translations.updateTranslations(), BX_FLAGS.ScriptUi !== "tv") + $btnReload.textContent = t("settings-reloading"), $btnReload.click(); + } }; for (let groupLabel in SETTINGS_UI) { const $group = CE("span", { class: "bx-settings-group-label" }, groupLabel); @@ -7347,7 +6002,7 @@ function setupSettingsUi() { type: "text", placeholder: defaultUserAgent, class: "bx-settings-custom-user-agent" - }), $inpCustomUserAgent.addEventListener("change", (e) => { + }), $inpCustomUserAgent.addEventListener("input", (e) => { const profile = $control.value, custom = e.target.value.trim(); UserAgent.updateStorage(profile, custom), onChange(e); }), $control = toPrefElement(PrefKey.USER_AGENT_PROFILE, (e) => { @@ -7361,7 +6016,7 @@ function setupSettingsUi() { id: `bx_setting_${settingId}`, title: settingLabel, tabindex: 0 - }), $control.name = $control.id, $control.addEventListener("change", (e) => { + }), $control.name = $control.id, $control.addEventListener("input", (e) => { setPref(settingId, e.target.value), onChange(e); }), selectedValue = PREF_PREFERRED_REGION, setting.options = {}; for (let regionName in STATES.serverRegions) { @@ -7395,9 +6050,13 @@ function setupSettingsUi() { const $label = CE("label", labelAttrs, settingLabel); if (settingNote) $label.appendChild(CE("b", {}, settingNote)); - const $elm = CE("div", { class: "bx-settings-row" }, $label, $control); + let $elm; + if ($control instanceof HTMLSelectElement && BX_FLAGS.ScriptUi === "tv") + $elm = CE("div", { class: "bx-settings-row" }, $label, BxSelectElement.wrap($control)); + else + $elm = CE("div", { class: "bx-settings-row" }, $label, $control); if ($wrapper.appendChild($elm), settingId === PrefKey.USER_AGENT_PROFILE) - $wrapper.appendChild($inpCustomUserAgent), $control.disabled = !0, $control.dispatchEvent(new Event("change")), $control.disabled = !1; + $wrapper.appendChild($inpCustomUserAgent), $control.disabled = !0, $control.dispatchEvent(new Event("input")), $control.disabled = !1; } } $btnReload = createButton({ @@ -7607,15 +6266,36 @@ function overridePreloadState() { } catch (e) { BxLogger.error(LOG_TAG6, e); } - _state = state, STATES.appContext = structuredClone(state.appContext); + _state = state, STATES.appContext = deepClone(state.appContext); } }); } var LOG_TAG6 = "PreloadState"; // src/utils/sdp.ts +function setCodecPreferences(sdp, preferredCodec) { + const h264Pattern = /a=fmtp:(\d+).*profile-level-id=([0-9a-f]{6})/g, profilePrefix = preferredCodec === "high" ? "4d" : preferredCodec === "low" ? "420" : "42e", preferredCodecIds = [], matches = sdp.matchAll(h264Pattern) || []; + for (let match of matches) { + const id2 = match[1]; + if (match[2].startsWith(profilePrefix)) + preferredCodecIds.push(id2); + } + if (!preferredCodecIds.length) + return sdp; + const lines = sdp.split("\r\n"); + for (let lineIndex = 0;lineIndex < lines.length; lineIndex++) { + const line = lines[lineIndex]; + if (!line.startsWith("m=video")) + continue; + const tmp = line.trim().split(" "); + let ids = tmp.slice(3); + ids = ids.filter((item2) => !preferredCodecIds.includes(item2)), ids = preferredCodecIds.concat(ids), lines[lineIndex] = tmp.slice(0, 3).concat(ids).join(" "); + break; + } + return lines.join("\r\n"); +} function patchSdpBitrate(sdp, video, audio) { - const lines = sdp.split("\n"), mediaSet = new Set; + const lines = sdp.split("\r\n"), mediaSet = new Set; !!video && mediaSet.add("video"), !!audio && mediaSet.add("audio"); const bitrate = { video, @@ -7646,7 +6326,7 @@ function patchSdpBitrate(sdp, video, audio) { } } } - return lines.join("\n"); + return lines.join("\r\n"); } // src/modules/player/shaders/clarity_boost.vert @@ -7943,25 +6623,10 @@ function patchVideoApi() { }; } function patchRtcCodecs() { - const codecProfile = getPref(PrefKey.STREAM_CODEC_PROFILE); - if (codecProfile === "default") + if (getPref(PrefKey.STREAM_CODEC_PROFILE) === "default") return; if (typeof RTCRtpTransceiver === "undefined" || !("setCodecPreferences" in RTCRtpTransceiver.prototype)) return !1; - const profileLevelId = `profile-level-id=${codecProfile === "high" ? "4d" : codecProfile === "low" ? "420" : "42e"}`, nativeSetCodecPreferences = RTCRtpTransceiver.prototype.setCodecPreferences; - RTCRtpTransceiver.prototype.setCodecPreferences = function(codecs) { - const newCodecs = codecs.slice(); - let pos = 0; - newCodecs.forEach((codec, i) => { - if (codec.sdpFmtpLine && codec.sdpFmtpLine.includes(profileLevelId)) - newCodecs.splice(i, 1), newCodecs.splice(pos, 0, codec), ++pos; - }); - try { - nativeSetCodecPreferences.apply(this, [newCodecs]); - } catch (e) { - BxLogger.error("setCodecPreferences", e), nativeSetCodecPreferences.apply(this, [codecs]); - } - }; } function patchRtcPeerConnection() { const nativeCreateDataChannel = RTCPeerConnection.prototype.createDataChannel; @@ -7971,17 +6636,19 @@ function patchRtcPeerConnection() { dataChannel }), dataChannel; }; - const maxVideoBitrate = getPref(PrefKey.BITRATE_VIDEO_MAX); - if (maxVideoBitrate > 0) { + const maxVideoBitrate = getPref(PrefKey.BITRATE_VIDEO_MAX), codec = getPref(PrefKey.STREAM_CODEC_PROFILE); + if (codec !== "default" || maxVideoBitrate > 0) { const nativeSetLocalDescription = RTCPeerConnection.prototype.setLocalDescription; RTCPeerConnection.prototype.setLocalDescription = function(description) { + if (codec !== "default") + arguments[0].sdp = setCodecPreferences(arguments[0].sdp, codec); try { - if (description) + if (maxVideoBitrate > 0 && description) arguments[0].sdp = patchSdpBitrate(arguments[0].sdp, Math.round(maxVideoBitrate / 1000)); } catch (e) { BxLogger.error("setLocalDescription", e); } - return nativeSetLocalDescription.apply(this, arguments); + return BxLogger.info("setLocalDescription", arguments[0].sdp), nativeSetLocalDescription.apply(this, arguments); }; } const OrgRTCPeerConnection = window.RTCPeerConnection; @@ -8074,6 +6741,103 @@ function patchPointerLockApi() { }; } +// src/modules/stream/stream-ui.ts +var cloneStreamHudButton = function($orgButton, label, svgIcon) { + const $container = $orgButton.cloneNode(!0); + let timeout; + const onTransitionStart = (e) => { + if (e.propertyName !== "opacity") + return; + timeout && clearTimeout(timeout), $container.style.pointerEvents = "none"; + }, onTransitionEnd = (e) => { + if (e.propertyName !== "opacity") + return; + if (document.getElementById("StreamHud")?.style.left === "0px") + timeout && clearTimeout(timeout), timeout = window.setTimeout(() => { + $container.style.pointerEvents = "auto"; + }, 100); + }; + if (STATES.browserHasTouchSupport) + $container.addEventListener("transitionstart", onTransitionStart), $container.addEventListener("transitionend", onTransitionEnd); + const $button = $container.querySelector("button"); + $button.setAttribute("title", label); + const $orgSvg = $button.querySelector("svg"), $svg = createSvgIcon(svgIcon); + return $svg.style.fill = "none", $svg.setAttribute("class", $orgSvg.getAttribute("class") || ""), $svg.ariaHidden = "true", $orgSvg.replaceWith($svg), $container; +}, cloneCloseButton = function($$btnOrg, icon, className, onChange) { + const $btn = $$btnOrg.cloneNode(!0), $svg = createSvgIcon(icon); + return $svg.setAttribute("class", $btn.firstElementChild.getAttribute("class") || ""), $svg.style.fill = "none", $btn.classList.add(className), $btn.removeChild($btn.firstElementChild), $btn.appendChild($svg), $btn.addEventListener("click", onChange), $btn; +}; +function injectStreamMenuButtons() { + const $screen = document.querySelector("#PageContent section[class*=PureScreens]"); + if (!$screen) + return; + if ($screen.xObserving) + return; + $screen.xObserving = !0; + let $btnStreamSettings, $btnStreamStats; + const streamStats = StreamStats.getInstance(); + new MutationObserver((mutationList) => { + mutationList.forEach((item2) => { + if (item2.type !== "childList") + return; + item2.addedNodes.forEach(async ($node) => { + if (!$node || $node.nodeType !== Node.ELEMENT_NODE) + return; + let $elm = $node; + if ($elm instanceof SVGSVGElement) + return; + if ($elm.className?.includes("PureErrorPage")) { + BxEvent.dispatch(window, BxEvent.STREAM_ERROR_PAGE); + return; + } + if ($elm.className?.startsWith("StreamMenu-module__container")) { + const $btnCloseHud = document.querySelector("button[class*=StreamMenu-module__backButton]"); + if (!$btnCloseHud) + return; + $btnCloseHud.addEventListener("click", (e) => { + StreamSettings.getInstance().hide(); + }); + const $btnRefresh = cloneCloseButton($btnCloseHud, BxIcon.REFRESH, "bx-stream-refresh-button", () => { + confirm(t("confirm-reload-stream")) && window.location.reload(); + }), $btnHome = cloneCloseButton($btnCloseHud, BxIcon.HOME, "bx-stream-home-button", () => { + confirm(t("back-to-home-confirm")) && (window.location.href = window.location.href.substring(0, 31)); + }); + $btnCloseHud.insertAdjacentElement("afterend", $btnRefresh), $btnRefresh.insertAdjacentElement("afterend", $btnHome), document.querySelector("div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module]")?.appendChild(await StreamBadges.getInstance().render()); + return; + } + if ($elm.className?.startsWith("Overlay-module_") || $elm.className?.startsWith("InProgressScreen")) + $elm = $elm.querySelector("#StreamHud"); + if (!$elm || ($elm.id || "") !== "StreamHud") + return; + const $gripHandle = $elm.querySelector("button[class^=GripHandle]"), hideGripHandle = () => { + if (!$gripHandle) + return; + $gripHandle.dispatchEvent(new PointerEvent("pointerdown")), $gripHandle.click(), $gripHandle.dispatchEvent(new PointerEvent("pointerdown")), $gripHandle.click(); + }, $orgButton = $elm.querySelector("div[class^=HUDButton]"); + if (!$orgButton) + return; + if (!$btnStreamSettings) + $btnStreamSettings = cloneStreamHudButton($orgButton, t("stream-settings"), BxIcon.STREAM_SETTINGS), $btnStreamSettings.addEventListener("click", (e) => { + hideGripHandle(), e.preventDefault(), StreamSettings.getInstance().show(); + }); + if (!$btnStreamStats) + $btnStreamStats = cloneStreamHudButton($orgButton, t("stream-stats"), BxIcon.STREAM_STATS), $btnStreamStats.addEventListener("click", (e) => { + hideGripHandle(), e.preventDefault(), streamStats.toggle(); + const btnStreamStatsOn2 = !streamStats.isHidden() && !streamStats.isGlancing(); + $btnStreamStats.classList.toggle("bx-stream-menu-button-on", btnStreamStatsOn2); + }); + const btnStreamStatsOn = !streamStats.isHidden() && !streamStats.isGlancing(); + if ($btnStreamStats.classList.toggle("bx-stream-menu-button-on", btnStreamStatsOn), $orgButton) { + const $btnParent = $orgButton.parentElement; + $btnParent.insertBefore($btnStreamStats, $btnParent.lastElementChild), $btnParent.insertBefore($btnStreamSettings, $btnStreamStats); + const $dotsButton = $btnParent.lastElementChild; + $dotsButton.parentElement.insertBefore($dotsButton, $dotsButton.parentElement.firstElementChild); + } + }); + }); + }).observe($screen, { subtree: !0, childList: !0 }); +} + // src/modules/game-bar/action-base.ts class BaseGameBarAction { constructor() { @@ -8301,11 +7065,7 @@ class GuideMenu { var unload = function() { if (!STATES.isPlaying) return; - EmulatedMkbHandler.getInstance().destroy(), NativeMkbHandler.getInstance().destroy(), STATES.currentStream.streamPlayer?.destroy(), STATES.isPlaying = !1, STATES.currentStream = {}, window.BX_EXPOSED.shouldShowSensorControls = !1, window.BX_EXPOSED.stopTakRendering = !1; - const $streamSettingsDialog = document.querySelector(".bx-stream-settings-dialog"); - if ($streamSettingsDialog) - $streamSettingsDialog.classList.add("bx-gone"); - StreamStats.getInstance().onStoppedPlaying(), MouseCursorHider.stop(), TouchController.reset(), GameBar.getInstance().disable(); + EmulatedMkbHandler.getInstance().destroy(), NativeMkbHandler.getInstance().destroy(), STATES.currentStream.streamPlayer?.destroy(), STATES.isPlaying = !1, STATES.currentStream = {}, window.BX_EXPOSED.shouldShowSensorControls = !1, window.BX_EXPOSED.stopTakRendering = !1, StreamSettings.getInstance().hide(), StreamStats.getInstance().onStoppedPlaying(), MouseCursorHider.stop(), TouchController.reset(), GameBar.getInstance().disable(); }, observeRootDialog = function($root) { let currentShown = !1; new MutationObserver((mutationList) => { @@ -8348,7 +7108,7 @@ var unload = function() { }); observer.observe(document.documentElement, { subtree: !0, childList: !0 }); }, main = function() { - if (waitForRootDialog(), patchRtcPeerConnection(), patchRtcCodecs(), interceptHttpRequests(), patchVideoApi(), patchCanvasContext(), AppInterface && patchPointerLockApi(), getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && patchAudioContext(), getPref(PrefKey.BLOCK_TRACKING) && patchMeControl(), STATES.userAgentHasTouchSupport && TouchController.updateCustomList(), overridePreloadState(), VibrationManager.initialSetup(), BX_FLAGS.CheckForUpdate && checkForUpdate(), addCss(), Toast.setup(), getPref(PrefKey.GAME_BAR_POSITION) !== "off" && GameBar.getInstance(), BX_FLAGS.PreloadUi && setupStreamUi(), GuideMenu.observe(), StreamBadges.setupEvents(), StreamStats.setupEvents(), EmulatedMkbHandler.setupEvents(), Patcher.init(), disablePwa(), window.addEventListener("gamepadconnected", (e) => showGamepadToast(e.gamepad)), window.addEventListener("gamepaddisconnected", (e) => showGamepadToast(e.gamepad)), getPref(PrefKey.REMOTE_PLAY_ENABLED)) + if (waitForRootDialog(), patchRtcPeerConnection(), patchRtcCodecs(), interceptHttpRequests(), patchVideoApi(), patchCanvasContext(), AppInterface && patchPointerLockApi(), getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && patchAudioContext(), getPref(PrefKey.BLOCK_TRACKING) && patchMeControl(), STATES.userAgentHasTouchSupport && TouchController.updateCustomList(), overridePreloadState(), VibrationManager.initialSetup(), BX_FLAGS.CheckForUpdate && checkForUpdate(), addCss(), preloadFonts(), Toast.setup(), getPref(PrefKey.GAME_BAR_POSITION) !== "off" && GameBar.getInstance(), BX_FLAGS.PreloadUi && setupStreamUi(), Screenshot.setup(), GuideMenu.observe(), StreamBadges.setupEvents(), StreamStats.setupEvents(), EmulatedMkbHandler.setupEvents(), Patcher.init(), disablePwa(), window.addEventListener("gamepadconnected", (e) => showGamepadToast(e.gamepad)), window.addEventListener("gamepaddisconnected", (e) => showGamepadToast(e.gamepad)), getPref(PrefKey.REMOTE_PLAY_ENABLED)) RemotePlay.detect(); if (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === "all") TouchController.setup();