diff --git a/dist/better-xcloud.user.js b/dist/better-xcloud.user.js index fa021e6..edadcf9 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.6.2-beta +// @version 5.6.3-beta // @description Improve Xbox Cloud Gaming (xCloud) experience // @author redphx // @license MIT @@ -138,7 +138,7 @@ function deepClone(obj) { return {}; return JSON.parse(JSON.stringify(obj)); } -var SCRIPT_VERSION = "5.6.2-beta", AppInterface = window.AppInterface; +var SCRIPT_VERSION = "5.6.3-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 = { supportedRegion: !0, @@ -590,6 +590,7 @@ var SUPPORTED_LANGUAGES = { ], "touch-controller": "Touch controller", "transparent-background": "Transparent background", + "true-achievements": "TrueAchievements", ui: "UI", "unexpected-behavior": "May cause unexpected behavior", "united-states": "United States", @@ -1821,12 +1822,12 @@ class Screenshot { streamPlayer.getWebGL2Player().drawFrame(); if (canvasContext.drawImage($player, 0, 0, $canvas.width, $canvas.height), AppInterface) { const data = $canvas.toDataURL("image/png").split(";base64,")[1]; - AppInterface.saveScreenshot(currentStream.titleId, data), canvasContext.clearRect(0, 0, $canvas.width, $canvas.height), callback && callback(); + AppInterface.saveScreenshot(currentStream.titleSlug, data), canvasContext.clearRect(0, 0, $canvas.width, $canvas.height), callback && callback(); return; } $canvas && $canvas.toBlob((blob) => { const now = +new Date, $anchor = CE("a", { - download: `${currentStream.titleId}-${now}.png`, + download: `${currentStream.titleSlug}-${now}.png`, href: URL.createObjectURL(blob) }); $anchor.click(), URL.revokeObjectURL($anchor.href), canvasContext.clearRect(0, 0, $canvas.width, $canvas.height), callback && callback(); @@ -2753,70 +2754,73 @@ class NavigationDialogManager { } } -var better_xcloud_default = "\n \n \n\n"; +var better_xcloud_default = ""; -var close_default = "\n \n \n\n"; +var true_achievements_default = ""; -var command_default = "\n \n \n\n"; +var close_default = ""; -var controller_default = "\n \n\n"; +var command_default = ""; -var copy_default = "\n \n\n"; +var controller_default = ""; -var create_shortcut_default = "\n \n \n\n"; +var copy_default = ""; -var cursor_text_default = "\n \n\n"; +var create_shortcut_default = ""; -var display_default = "\n \n\n"; +var cursor_text_default = ""; -var home_default = "\n \n\n"; +var display_default = ""; -var native_mkb_default = "\n \n \n \n \n \n \n \n \n\n"; +var home_default = ""; -var new_default = "\n \n\n"; +var native_mkb_default = ""; -var question_default = "\n \n\n"; +var new_default = ""; -var refresh_default = "\n \n\n"; +var question_default = ""; -var remote_play_default = "\n \n\n"; +var refresh_default = ""; -var stream_settings_default = "\n \n\n"; +var remote_play_default = ""; -var stream_stats_default = "\n \n\n"; +var stream_settings_default = ""; -var touch_control_disable_default = "\n \n \n \n \n \n \n \n\n"; +var stream_stats_default = ""; -var touch_control_enable_default = "\n \n \n \n \n\n"; +var touch_control_disable_default = ""; -var trash_default = "\n \n\n"; +var touch_control_enable_default = ""; -var virtual_controller_default = "\n \n \n \n \n \n \n \n \n \n\n"; +var trash_default = ""; -var caret_left_default = "\n \n\n"; +var virtual_controller_default = ""; -var caret_right_default = "\n \n\n"; +var caret_left_default = ""; -var camera_default = "\n \n \n \n \n\n"; +var caret_right_default = ""; -var microphone_default = "\n \n\n"; +var camera_default = ""; -var microphone_slash_default = "\n \n \n\n"; +var microphone_default = ""; -var battery_full_default = "\n \n\n"; +var microphone_slash_default = ""; -var clock_default = "\n \n \n \n \n\n"; +var battery_full_default = ""; -var cloud_default = "\n \n\n"; +var clock_default = ""; -var download_default = "\n \n \n\n"; +var cloud_default = ""; -var speaker_high_default = "\n \n \n\n"; +var download_default = ""; -var upload_default = "\n \n \n\n"; +var speaker_high_default = ""; + +var upload_default = ""; var BxIcon = { BETTER_XCLOUD: better_xcloud_default, + TRUE_ACHIEVEMENTS: true_achievements_default, STREAM_SETTINGS: stream_settings_default, STREAM_STATS: stream_stats_default, CLOSE: close_default, @@ -3223,6 +3227,9 @@ async function copyToClipboard(text, showToast = !0) { } return !1; } +function productTitleToSlug(title) { + return title.replace(/[;,/?:@&=+_`~$%#^*()!^™\xae\xa9]/g, "").replace(/ {2,}/g, " ").trim().substr(0, 50).replace(/ /g, "-").toLowerCase(); +} class SoundShortcut { static adjustGainNodeVolume(amount) { @@ -7677,6 +7684,44 @@ class MicrophoneAction extends BaseGameBarAction { } } +class TrueAchievements { + static open() { + if (AppInterface) + ; + else { + let xboxTitleId; + xboxTitleId = STATES.currentStream.xboxTitleId || STATES.currentStream.titleInfo?.details.xboxTitleId; + let url = "https://www.trueachievements.com"; + if (xboxTitleId) + url += `/deeplink/${xboxTitleId}/1`; + CE("a", { + class: "bx-hidden bx-offscreen", + href: url, + target: "_blank" + }, "").click(); + } + } +} + +class TrueAchievementsAction extends BaseGameBarAction { + $content; + constructor() { + super(); + const onClick = (e) => { + BxEvent.dispatch(window, BxEvent.GAME_BAR_ACTION_ACTIVATED), TrueAchievements.open(); + }; + this.$content = createButton({ + style: 4, + icon: BxIcon.TRUE_ACHIEVEMENTS, + title: t("true-achievements"), + onClick + }); + } + render() { + return this.$content; + } +} + class GameBar { static instance; static getInstance() { @@ -7695,6 +7740,7 @@ class GameBar { if (this.actions = [ new ScreenshotAction, ...STATES.userAgent.capabilities.touch && getPref("stream_touch_controller") !== "off" ? [new TouchControlAction] : [], + new TrueAchievementsAction, new MicrophoneAction ], position === "bottom-right") this.actions.reverse(); @@ -7757,13 +7803,6 @@ class GuideMenu { }, { once: !0 }), window.BX_EXPOSED.dialogRoutes.closeAll(); } }), - appSettings: createButton({ - label: t("app-settings"), - style: 64 | 32, - onClick: (e) => { - window.BX_EXPOSED.dialogRoutes.closeAll(), AppInterface.openAppSettings && AppInterface.openAppSettings(); - } - }), closeApp: createButton({ label: t("close-app"), style: 64 | 32 | 2, @@ -7789,11 +7828,18 @@ class GuideMenu { label: t("back-to-home"), style: 64 | 32, onClick: (e) => { - confirm(t("back-to-home-confirm")) && (window.location.href = window.location.href.substring(0, 31)); + confirm(t("back-to-home-confirm")) && (window.location.href = window.location.href.substring(0, 31)), window.BX_EXPOSED.dialogRoutes.closeAll(); }, attributes: { "data-state": "playing" } + }), + trueAchievements: createButton({ + label: t("true-achievements"), + style: 64 | 32, + onClick: (e) => { + TrueAchievements.open(), window.BX_EXPOSED.dialogRoutes.closeAll(); + } }) }; static #$renderedButtons; @@ -7804,6 +7850,7 @@ class GuideMenu { class: "bx-guide-home-buttons" }), buttons = [ GuideMenu.#BUTTONS.scriptSettings, + GuideMenu.#BUTTONS.trueAchievements, GuideMenu.#BUTTONS.reloadPage, GuideMenu.#BUTTONS.backToHome, AppInterface && GuideMenu.#BUTTONS.closeApp @@ -8123,6 +8170,20 @@ class StreamUiHandler { } } +class XboxApi { + static CACHED_TITLES = {}; + static async getProductTitle(xboxTitleId) { + if (xboxTitleId = xboxTitleId.toString(), XboxApi.CACHED_TITLES[xboxTitleId]) + return XboxApi.CACHED_TITLES[xboxTitleId]; + try { + const url = `https://displaycatalog.mp.microsoft.com/v7.0/products/lookup?market=US&languages=en&value=${xboxTitleId}&alternateId=XboxTitleId&fieldsTemplate=browse`, productTitle = (await (await NATIVE_FETCH(url)).json()).Products[0].LocalizedProperties[0].ProductTitle; + return XboxApi.CACHED_TITLES[xboxTitleId] = productTitle, productTitle; + } catch (e) { + } + return null; + } +} + function unload() { if (!STATES.isPlaying) return; @@ -8243,12 +8304,10 @@ window.addEventListener(BxEvent.XCLOUD_SERVERS_READY, (e) => { STATES.isSignedIn = !0, window.setTimeout(HeaderSection.watchHeader, 2000); }); window.addEventListener(BxEvent.STREAM_LOADING, (e) => { - if (window.location.pathname.includes("/launch/")) { - const matches = /\/launch\/(?[^\/]+)\/(?\w+)/.exec(window.location.pathname); - if (matches?.groups) - STATES.currentStream.titleId = matches.groups.title_id, STATES.currentStream.productId = matches.groups.product_id; - } else - STATES.currentStream.titleId = "remote-play", STATES.currentStream.productId = ""; + if (window.location.pathname.includes("/launch/") && STATES.currentStream.titleInfo) + STATES.currentStream.titleSlug = productTitleToSlug(STATES.currentStream.titleInfo.product.title); + else + STATES.currentStream.titleSlug = "remote-play"; }); getPref("ui_loading_screen_game_art") && window.addEventListener(BxEvent.TITLE_INFO_READY, LoadingScreen.setup); window.addEventListener(BxEvent.STREAM_STARTING, (e) => { @@ -8270,6 +8329,23 @@ window.addEventListener(BxEvent.XCLOUD_RENDERING_COMPONENT, (e) => { if (e.component === "product-details") ProductDetailsPage.injectShortcutButton(); }); +window.addEventListener(BxEvent.DATA_CHANNEL_CREATED, (e) => { + const dataChannel = e.dataChannel; + if (!dataChannel || dataChannel.label !== "message") + return; + dataChannel.addEventListener("message", async (msg) => { + if (msg.origin === "better-xcloud" || typeof msg.data !== "string") + return; + if (msg.data.includes("/titleinfo")) { + const json = JSON.parse(JSON.parse(msg.data).content), xboxTitleId = parseInt(json.titleid, 16); + if (STATES.currentStream.xboxTitleId = xboxTitleId, STATES.currentStream.titleSlug = "remote-play", STATES.remotePlay.isPlaying && json.focused) { + const productTitle = await XboxApi.getProductTitle(xboxTitleId); + if (productTitle) + STATES.currentStream.titleSlug = productTitleToSlug(productTitle); + } + } + }); +}); window.addEventListener(BxEvent.STREAM_STOPPED, unload); window.addEventListener("pagehide", (e) => { BxEvent.dispatch(window, BxEvent.STREAM_STOPPED);