From 784a31ce43a303f47718b1a99d307ebe34e8c332 Mon Sep 17 00:00:00 2001 From: redphx <96280+redphx@users.noreply.github.com> Date: Wed, 18 Sep 2024 20:14:49 +0700 Subject: [PATCH] Migrate Remote Play popup to Navigation dialog --- src/assets/css/navigation-dialog.styl | 5 + src/assets/css/remote-play.styl | 37 +- src/assets/css/root.styl | 2 - src/assets/css/settings-dialog.styl | 6 - src/index.ts | 6 +- src/modules/remote-play-manager.ts | 276 +++++++++++++++ src/modules/remote-play.ts | 368 -------------------- src/modules/ui/dialog/remote-play-dialog.ts | 148 ++++++++ src/modules/ui/header.ts | 4 +- src/utils/history.ts | 7 +- src/utils/monkey-patches.ts | 1 + src/utils/xcloud-interceptor.ts | 4 +- src/utils/xhome-interceptor.ts | 8 +- 13 files changed, 454 insertions(+), 418 deletions(-) create mode 100644 src/modules/remote-play-manager.ts delete mode 100644 src/modules/remote-play.ts create mode 100644 src/modules/ui/dialog/remote-play-dialog.ts diff --git a/src/assets/css/navigation-dialog.styl b/src/assets/css/navigation-dialog.styl index 0d83d81..b1b1d4a 100644 --- a/src/assets/css/navigation-dialog.styl +++ b/src/assets/css/navigation-dialog.styl @@ -1,6 +1,11 @@ .bx-navigation-dialog { position: absolute; z-index: var(--bx-navigation-dialog-z-index); + font-family: var(--bx-title-font); + + *:focus { + outline: none !important; + } } .bx-navigation-dialog-overlay { diff --git a/src/assets/css/remote-play.styl b/src/assets/css/remote-play.styl index 51743c8..be04b19 100644 --- a/src/assets/css/remote-play.styl +++ b/src/assets/css/remote-play.styl @@ -1,36 +1,16 @@ -.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; + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + + color: white; background: #1a1b1e; border-radius: 10px; width: 420px; max-width: calc(100vw - 20px); margin: 0 0 0 auto; padding: 20px; - box-shadow: #00000080 0px 0px 12px 0px; - - @media (min-width:480px) and (min-height:calc(480px + 1px)) { - right: calc(env(safe-area-inset-right, 0px) + 32px); - } - - @media (min-width:768px) and (min-height:calc(480px + 1px)) { - right: calc(env(safe-area-inset-right, 0px) + 48px); - } - - @media (min-width:1920px) and (min-height:calc(480px + 1px)) { - right: calc(env(safe-area-inset-right, 0px) + 80px); - } > .bx-button { display: table; @@ -121,3 +101,8 @@ min-height: 100%; margin: 4px 0; } + +.bx-remote-play-buttons { + display: flex; + justify-content: space-between; +} diff --git a/src/assets/css/root.styl b/src/assets/css/root.styl index c0ec2d3..a5aa734 100644 --- a/src/assets/css/root.styl +++ b/src/assets/css/root.styl @@ -37,8 +37,6 @@ button_color(name, normal, hover, active, disabled) --bx-navigation-dialog-z-index: 30100; --bx-navigation-dialog-overlay-z-index: 30000; - --bx-remote-play-popup-z-index: 20000; - --bx-game-bar-z-index: 10000; --bx-screenshot-animation-z-index: 9000; --bx-wait-time-box-z-index: 1000; diff --git a/src/assets/css/settings-dialog.styl b/src/assets/css/settings-dialog.styl index 078c639..8b18db2 100644 --- a/src/assets/css/settings-dialog.styl +++ b/src/assets/css/settings-dialog.styl @@ -130,7 +130,6 @@ &:focus { border-color: #fff; - outline: none; } &[data-group=global] { @@ -234,11 +233,6 @@ } } - &:focus, - *:focus { - outline: none !important; - } - .bx-top-buttons { display: flex; flex-direction: column; diff --git a/src/index.ts b/src/index.ts index 1c07abd..01b4d62 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,7 +16,7 @@ import { MouseCursorHider } from "@modules/mkb/mouse-cursor-hider"; import { TouchController } from "@modules/touch-controller"; import { checkForUpdate, disablePwa, productTitleToSlug } from "@utils/utils"; import { Patcher } from "@modules/patcher"; -import { RemotePlay } from "@modules/remote-play"; +import { RemotePlayManager } from "@/modules/remote-play-manager"; import { onHistoryChanged, patchHistoryMethod } from "@utils/history"; import { VibrationManager } from "@modules/vibration-manager"; import { overridePreloadState } from "@utils/preload-state"; @@ -157,7 +157,7 @@ document.addEventListener('readystatechange', e => { if (STATES.isSignedIn) { // Preload Remote Play - getPref(PrefKey.REMOTE_PLAY_ENABLED) && RemotePlay.preload(); + getPref(PrefKey.REMOTE_PLAY_ENABLED) && RemotePlayManager.getInstance().initialize(); } else { // Show Settings button in the header when not signed in window.setTimeout(HeaderSection.watchHeader, 2000); @@ -413,7 +413,7 @@ function main() { // Preload Remote Play if (getPref(PrefKey.REMOTE_PLAY_ENABLED)) { - RemotePlay.detect(); + RemotePlayManager.detect(); } if (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === StreamTouchController.ALL) { diff --git a/src/modules/remote-play-manager.ts b/src/modules/remote-play-manager.ts new file mode 100644 index 0000000..b9a0940 --- /dev/null +++ b/src/modules/remote-play-manager.ts @@ -0,0 +1,276 @@ +import { STATES, AppInterface } from "@utils/global"; +import { Toast } from "@utils/toast"; +import { BxEvent } from "@utils/bx-event"; +import { t } from "@utils/translation"; +import { localRedirect } from "@modules/ui/ui"; +import { BxLogger } from "@utils/bx-logger"; +import { HeaderSection } from "./ui/header"; +import { PrefKey } from "@/enums/pref-keys"; +import { getPref, setPref } from "@/utils/settings-storages/global-settings-storage"; +import { RemotePlayNavigationDialog } from "./ui/dialog/remote-play-dialog"; + +const LOG_TAG = 'RemotePlay'; + +export const enum RemotePlayConsoleState { + ON = 'On', + OFF = 'Off', + STANDBY = 'ConnectedStandby', + UNKNOWN = 'Unknown', +} + +type RemotePlayRegion = { + name: string; + baseUri: string; + isDefault: boolean; +}; + +type RemotePlayConsole = { + deviceName: string; + serverId: string; + powerState: RemotePlayConsoleState; + consoleType: string; + // playPath: string; + // outOfHomeWarning: string; + // wirelessWarning: string; + // isDevKit: string; +}; + +export class RemotePlayManager { + private static instance: RemotePlayManager; + public static getInstance(): RemotePlayManager { + if (!this.instance) { + this.instance = new RemotePlayManager(); + } + + return this.instance; + } + + private isInitialized = false; + + private XCLOUD_TOKEN!: string; + private XHOME_TOKEN!: string; + + private consoles!: Array; + private regions: Array = []; + + static readonly BASE_DEVICE_INFO = { + appInfo: { + env: { + clientAppId: window.location.host, + clientAppType: 'browser', + clientAppVersion: '24.17.36', + clientSdkVersion: '10.1.14', + httpEnvironment: 'prod', + sdkInstallId: '', + }, + }, + dev: { + displayInfo: { + dimensions: { + widthInPixels: 1920, + heightInPixels: 1080, + }, + pixelDensity: { + dpiX: 1, + dpiY: 1, + }, + }, + hw: { + make: 'Microsoft', + model: 'unknown', + sdktype: 'web', + }, + os: { + name: 'windows', + ver: '22631.2715', + platform: 'desktop', + }, + browser: { + browserName: 'chrome', + browserVersion: '125.0', + }, + }, + }; + + constructor() { + } + + initialize() { + if (this.isInitialized) { + return; + } + + this.isInitialized = true; + + this.getXhomeToken(() => { + this.getConsolesList(() => { + BxLogger.info(LOG_TAG, 'Consoles', this.consoles); + + STATES.supportedRegion && HeaderSection.showRemotePlayButton(); + BxEvent.dispatch(window, BxEvent.REMOTE_PLAY_READY); + }); + }); + } + + get xcloudToken() { + return this.XCLOUD_TOKEN; + } + + set xcloudToken(token: string) { + this.XCLOUD_TOKEN = token; + } + + get xhomeToken() { + return this.XHOME_TOKEN; + } + + getConsoles() { + return this.consoles; + } + + + private getXhomeToken(callback: any) { + if (this.XHOME_TOKEN) { + callback(); + return; + } + + let GSSV_TOKEN; + try { + GSSV_TOKEN = JSON.parse(localStorage.getItem('xboxcom_xbl_user_info')!).tokens['http://gssv.xboxlive.com/'].token; + } catch (e) { + for (let i = 0; i < localStorage.length; i++){ + const key = localStorage.key(i)!; + if (!key.startsWith('Auth.User.')) { + continue; + } + + const json = JSON.parse(localStorage.getItem(key)!); + for (const token of json.tokens) { + if (!token.relyingParty.includes('gssv.xboxlive.com')) { + continue; + } + + GSSV_TOKEN = token.tokenData.token; + break; + } + + break; + } + } + + const request = new Request('https://xhome.gssv-play-prod.xboxlive.com/v2/login/user', { + method: 'POST', + body: JSON.stringify({ + offeringId: 'xhome', + token: GSSV_TOKEN, + }), + headers: { + 'Content-Type': 'application/json; charset=utf-8', + }, + }); + + fetch(request).then(resp => resp.json()) + .then(json => { + this.regions = json.offeringSettings.regions; + this.XHOME_TOKEN = json.gsToken; + callback(); + }); + } + + private async getConsolesList(callback: any) { + if (this.consoles) { + callback(); + return; + } + + const options = { + method: 'GET', + headers: { + 'Authorization': `Bearer ${this.XHOME_TOKEN}`, + }, + }; + + // Test servers one by one + for (const region of this.regions) { + try { + const request = new Request(`${region.baseUri}/v6/servers/home?mr=50`, options); + const resp = await fetch(request); + + const json = await resp.json(); + if (json.results.length === 0) { + continue; + } + + this.consoles = json.results; + + // Store working server + STATES.remotePlay.server = region.baseUri; + } catch (e) {} + + if (this.consoles) { + break; + } + } + + // None of the servers worked + if (!STATES.remotePlay.server) { + this.consoles = []; + } + + callback(); + } + + play(serverId: string, resolution?: string) { + if (resolution) { + setPref(PrefKey.REMOTE_PLAY_RESOLUTION, resolution); + } + + STATES.remotePlay.config = { + serverId: serverId, + }; + window.BX_REMOTE_PLAY_CONFIG = STATES.remotePlay.config; + + localRedirect('/launch/fortnite/BT5P2X999VH2#remote-play'); + } + + togglePopup(force = null) { + if (!this.isReady()) { + Toast.show(t('getting-consoles-list')); + return; + } + + if (this.consoles.length === 0) { + Toast.show(t('no-consoles-found'), '', {instant: true}); + return; + } + + // Show native dialog in Android app + if (AppInterface && AppInterface.showRemotePlayDialog) { + AppInterface.showRemotePlayDialog(JSON.stringify(this.consoles)); + (document.activeElement as HTMLElement).blur(); + return; + } + + RemotePlayNavigationDialog.getInstance().show(); + } + + static detect() { + if (!getPref(PrefKey.REMOTE_PLAY_ENABLED)) { + return; + } + + STATES.remotePlay.isPlaying = window.location.pathname.includes('/launch/') && window.location.hash.startsWith('#remote-play'); + if (STATES.remotePlay?.isPlaying) { + window.BX_REMOTE_PLAY_CONFIG = STATES.remotePlay.config; + // Remove /launch/... from URL + window.history.replaceState({origin: 'better-xcloud'}, '', 'https://www.xbox.com/' + location.pathname.substring(1, 6) + '/play'); + } else { + window.BX_REMOTE_PLAY_CONFIG = null; + } + } + + private isReady() { + return this.consoles !== null; + } +} diff --git a/src/modules/remote-play.ts b/src/modules/remote-play.ts deleted file mode 100644 index eb108a5..0000000 --- a/src/modules/remote-play.ts +++ /dev/null @@ -1,368 +0,0 @@ -import { STATES, AppInterface } from "@utils/global"; -import { CE, createButton, ButtonStyle } from "@utils/html"; -import { BxIcon } from "@utils/bx-icon"; -import { Toast } from "@utils/toast"; -import { BxEvent } from "@utils/bx-event"; -import { t } from "@utils/translation"; -import { localRedirect } from "@modules/ui/ui"; -import { BxLogger } from "@utils/bx-logger"; -import { HeaderSection } from "./ui/header"; -import { PrefKey } from "@/enums/pref-keys"; -import { getPref, setPref } from "@/utils/settings-storages/global-settings-storage"; - -const LOG_TAG = 'RemotePlay'; - -const enum RemotePlayConsoleState { - ON = 'On', - OFF = 'Off', - STANDBY = 'ConnectedStandby', - UNKNOWN = 'Unknown', -} - -type RemotePlayRegion = { - name: string; - baseUri: string; - isDefault: boolean; -}; - -type RemotePlayConsole = { - deviceName: string; - serverId: string; - powerState: RemotePlayConsoleState; - consoleType: string; - // playPath: string; - // outOfHomeWarning: string; - // wirelessWarning: string; - // isDevKit: string; -}; - -export class RemotePlay { - static XCLOUD_TOKEN: string; - static XHOME_TOKEN: string; - static #CONSOLES: Array; - static #REGIONS: Array; - - static readonly #STATE_LABELS: {[key in RemotePlayConsoleState]: string} = { - [RemotePlayConsoleState.ON]: t('powered-on'), - [RemotePlayConsoleState.OFF]: t('powered-off'), - [RemotePlayConsoleState.STANDBY]: t('standby'), - [RemotePlayConsoleState.UNKNOWN]: t('unknown'), - }; - - static readonly BASE_DEVICE_INFO = { - appInfo: { - env: { - clientAppId: window.location.host, - clientAppType: 'browser', - clientAppVersion: '24.17.36', - clientSdkVersion: '10.1.14', - httpEnvironment: 'prod', - sdkInstallId: '', - }, - }, - dev: { - displayInfo: { - dimensions: { - widthInPixels: 1920, - heightInPixels: 1080, - }, - pixelDensity: { - dpiX: 1, - dpiY: 1, - }, - }, - hw: { - make: 'Microsoft', - model: 'unknown', - sdktype: 'web', - }, - os: { - name: 'windows', - ver: '22631.2715', - platform: 'desktop', - }, - browser: { - browserName: 'chrome', - browserVersion: '125.0', - }, - }, - }; - - static #$content: HTMLElement; - - static #initialize() { - if (RemotePlay.#$content) { - return; - } - - RemotePlay.#$content = CE('div', {}, t('getting-consoles-list')); - RemotePlay.#getXhomeToken(() => { - RemotePlay.#getConsolesList(() => { - BxLogger.info(LOG_TAG, 'Consoles', RemotePlay.#CONSOLES); - if (RemotePlay.#CONSOLES && RemotePlay.#CONSOLES.length > 0) { - STATES.supportedRegion && HeaderSection.showRemotePlayButton(); - } - - RemotePlay.#renderConsoles(); - BxEvent.dispatch(window, BxEvent.REMOTE_PLAY_READY); - }); - }); - } - - static #renderConsoles() { - const $fragment = CE('div', {'class': 'bx-remote-play-container'}); - - if (!RemotePlay.#CONSOLES || RemotePlay.#CONSOLES.length === 0) { - $fragment.appendChild(CE('span', {}, t('no-consoles-found'))); - RemotePlay.#$content = CE('div', {}, $fragment); - return; - } - - const $settingNote = CE('p', {}); - - const resolutions = [1080, 720]; - const currentResolution = getPref(PrefKey.REMOTE_PLAY_RESOLUTION); - const $resolutionGroup = CE('div', {}); - for (const resolution of resolutions) { - const value = `${resolution}p`; - const id = `bx_radio_xhome_resolution_${resolution}`; - - const $radio = CE('input', { - 'type': 'radio', - 'value': value, - 'id': id, - 'name': 'bx_radio_xhome_resolution', - }, value); - - $radio.addEventListener('change', e => { - const value = (e.target as HTMLInputElement).value; - - $settingNote.textContent = value === '1080p' ? '✅ ' + t('can-stream-xbox-360-games') : '❌ ' + t('cant-stream-xbox-360-games'); - setPref(PrefKey.REMOTE_PLAY_RESOLUTION, value); - }); - - const $label = CE('label', { - 'for': id, - 'class': 'bx-remote-play-resolution', - }, $radio, `${resolution}p`); - - $resolutionGroup.appendChild($label); - - if (currentResolution === value) { - $radio.checked = true; - $radio.dispatchEvent(new Event('change')); - } - } - - const $qualitySettings = CE('div', {'class': 'bx-remote-play-settings'}, - CE('div', {}, - CE('label', {}, t('target-resolution'), $settingNote), - $resolutionGroup, - ) - ); - - $fragment.appendChild($qualitySettings); - - // Render concoles list - for (let con of RemotePlay.#CONSOLES) { - const $child = CE('div', {'class': 'bx-remote-play-device-wrapper'}, - CE('div', {'class': 'bx-remote-play-device-info'}, - CE('div', {}, - CE('span', {'class': 'bx-remote-play-device-name'}, con.deviceName), - CE('span', {'class': 'bx-remote-play-console-type'}, con.consoleType.replace('Xbox', '')) - ), - CE('div', {'class': 'bx-remote-play-power-state'}, RemotePlay.#STATE_LABELS[con.powerState]), - ), - - // Connect button - createButton({ - classes: ['bx-remote-play-connect-button'], - label: t('console-connect'), - style: ButtonStyle.PRIMARY | ButtonStyle.FOCUSABLE, - onClick: e => { - RemotePlay.play(con.serverId); - }, - }), - ); - - $fragment.appendChild($child); - } - - // Add Help button - $fragment.appendChild(createButton({ - icon: BxIcon.QUESTION, - style: ButtonStyle.GHOST | ButtonStyle.FOCUSABLE, - url: 'https://better-xcloud.github.io/remote-play', - label: t('help'), - })); - - RemotePlay.#$content = CE('div', {}, $fragment); - } - - static #getXhomeToken(callback: any) { - if (RemotePlay.XHOME_TOKEN) { - callback(); - return; - } - - let GSSV_TOKEN; - try { - GSSV_TOKEN = JSON.parse(localStorage.getItem('xboxcom_xbl_user_info')!).tokens['http://gssv.xboxlive.com/'].token; - } catch (e) { - for (let i = 0; i < localStorage.length; i++){ - const key = localStorage.key(i)!; - if (!key.startsWith('Auth.User.')) { - continue; - } - - const json = JSON.parse(localStorage.getItem(key)!); - for (const token of json.tokens) { - if (!token.relyingParty.includes('gssv.xboxlive.com')) { - continue; - } - - GSSV_TOKEN = token.tokenData.token; - break; - } - - break; - } - } - - const request = new Request('https://xhome.gssv-play-prod.xboxlive.com/v2/login/user', { - method: 'POST', - body: JSON.stringify({ - offeringId: 'xhome', - token: GSSV_TOKEN, - }), - headers: { - 'Content-Type': 'application/json; charset=utf-8', - }, - }); - fetch(request).then(resp => resp.json()) - .then(json => { - RemotePlay.#REGIONS = json.offeringSettings.regions; - RemotePlay.XHOME_TOKEN = json.gsToken; - callback(); - }); - } - - static async #getConsolesList(callback: any) { - if (RemotePlay.#CONSOLES) { - callback(); - return; - } - - const options = { - method: 'GET', - headers: { - 'Authorization': `Bearer ${RemotePlay.XHOME_TOKEN}`, - }, - }; - - // Test servers one by one - for (const region of RemotePlay.#REGIONS) { - try { - const request = new Request(`${region.baseUri}/v6/servers/home?mr=50`, options); - const resp = await fetch(request); - - const json = await resp.json(); - if (json.results.length === 0) { - continue; - } - - RemotePlay.#CONSOLES = json.results; - - // Store working server - STATES.remotePlay.server = region.baseUri; - - callback(); - } catch (e) {} - - if (RemotePlay.#CONSOLES && RemotePlay.#CONSOLES.length > 0) { - break; - } - } - - // None of the servers worked - if (!STATES.remotePlay.server) { - RemotePlay.#CONSOLES = []; - } - } - - static play(serverId: string, resolution?: string) { - if (resolution) { - setPref(PrefKey.REMOTE_PLAY_RESOLUTION, resolution); - } - - STATES.remotePlay.config = { - serverId: serverId, - }; - window.BX_REMOTE_PLAY_CONFIG = STATES.remotePlay.config; - - localRedirect('/launch/fortnite/BT5P2X999VH2#remote-play'); - RemotePlay.detachPopup(); - } - - static preload() { - RemotePlay.#initialize(); - } - - static detachPopup() { - // Detach popup from body - const $popup = document.querySelector('.bx-remote-play-popup'); - $popup && $popup.remove(); - } - - static togglePopup(force = null) { - if (!getPref(PrefKey.REMOTE_PLAY_ENABLED) || !RemotePlay.isReady()) { - Toast.show(t('getting-consoles-list')); - return; - } - - RemotePlay.#initialize(); - - if (AppInterface && AppInterface.showRemotePlayDialog) { - AppInterface.showRemotePlayDialog(JSON.stringify(RemotePlay.#CONSOLES)); - (document.activeElement as HTMLElement).blur(); - return; - } - - if (document.querySelector('.bx-remote-play-popup')) { - if (force === false) { - RemotePlay.#$content.classList.add('bx-gone'); - } else { - RemotePlay.#$content.classList.toggle('bx-gone'); - } - return; - } - - const $header = document.querySelector('#gamepass-root header')!; - - const group = $header.firstElementChild!.getAttribute('data-group')!; - RemotePlay.#$content.setAttribute('data-group', group); - RemotePlay.#$content.classList.add('bx-remote-play-popup'); - RemotePlay.#$content.classList.remove('bx-gone'); - - $header.insertAdjacentElement('afterend', RemotePlay.#$content); - } - - static detect() { - if (!getPref(PrefKey.REMOTE_PLAY_ENABLED)) { - return; - } - - STATES.remotePlay.isPlaying = window.location.pathname.includes('/launch/') && window.location.hash.startsWith('#remote-play'); - if (STATES.remotePlay?.isPlaying) { - window.BX_REMOTE_PLAY_CONFIG = STATES.remotePlay.config; - // Remove /launch/... from URL - window.history.replaceState({origin: 'better-xcloud'}, '', 'https://www.xbox.com/' + location.pathname.substring(1, 6) + '/play'); - } else { - window.BX_REMOTE_PLAY_CONFIG = null; - } - } - - static isReady() { - return RemotePlay.#CONSOLES !== null && RemotePlay.#CONSOLES.length > 0; - } -} diff --git a/src/modules/ui/dialog/remote-play-dialog.ts b/src/modules/ui/dialog/remote-play-dialog.ts new file mode 100644 index 0000000..164595b --- /dev/null +++ b/src/modules/ui/dialog/remote-play-dialog.ts @@ -0,0 +1,148 @@ +import { ButtonStyle, CE, createButton } from "@/utils/html"; +import { NavigationDialog } from "./navigation-dialog"; +import { PrefKey } from "@/enums/pref-keys"; +import { BxIcon } from "@/utils/bx-icon"; +import { getPref, setPref } from "@/utils/settings-storages/global-settings-storage"; +import { t } from "@/utils/translation"; +import { RemotePlayConsoleState, RemotePlayManager } from "@/modules/remote-play-manager"; + + +export class RemotePlayNavigationDialog extends NavigationDialog { + private static instance: RemotePlayNavigationDialog; + public static getInstance(): RemotePlayNavigationDialog { + if (!RemotePlayNavigationDialog.instance) { + RemotePlayNavigationDialog.instance = new RemotePlayNavigationDialog(); + } + return RemotePlayNavigationDialog.instance; + } + + private readonly STATE_LABELS: Record = { + [RemotePlayConsoleState.ON]: t('powered-on'), + [RemotePlayConsoleState.OFF]: t('powered-off'), + [RemotePlayConsoleState.STANDBY]: t('standby'), + [RemotePlayConsoleState.UNKNOWN]: t('unknown'), + }; + + $container!: HTMLElement; + + constructor() { + super(); + this.setupDialog(); + } + + private setupDialog() { + const $fragment = CE('div', {'class': 'bx-remote-play-container'}); + + const $settingNote = CE('p', {}); + + const resolutions = [1080, 720]; + const currentResolution = getPref(PrefKey.REMOTE_PLAY_RESOLUTION); + const $resolutionGroup = CE('div', {}); + + const onResolutionChange = (e: Event) => { + const value = (e.target as HTMLInputElement).value; + + $settingNote.textContent = value === '1080p' ? '✅ ' + t('can-stream-xbox-360-games') : '❌ ' + t('cant-stream-xbox-360-games'); + setPref(PrefKey.REMOTE_PLAY_RESOLUTION, value); + }; + + for (const resolution of resolutions) { + const value = `${resolution}p`; + const id = `bx_radio_xhome_resolution_${resolution}`; + + const $radio = CE('input', { + type: 'radio', + value: value, + id: id, + name: 'bx_radio_xhome_resolution', + }, value); + + $radio.addEventListener('input', onResolutionChange); + + const $label = CE('label', { + for: id, + class: 'bx-remote-play-resolution', + }, $radio, `${resolution}p`); + + $resolutionGroup.appendChild($label); + + if (currentResolution === value) { + $radio.checked = true; + $radio.dispatchEvent(new Event('input')); + } + } + + const $qualitySettings = CE('div', { + class: 'bx-remote-play-settings', + }, CE('div', {}, + CE('label', {}, t('target-resolution'), $settingNote), + $resolutionGroup, + )); + + $fragment.appendChild($qualitySettings); + + // Render consoles list + const manager = RemotePlayManager.getInstance(); + const consoles = manager.getConsoles(); + + for (let con of consoles) { + const $child = CE('div', {class: 'bx-remote-play-device-wrapper'}, + CE('div', {class: 'bx-remote-play-device-info'}, + CE('div', {}, + CE('span', {class: 'bx-remote-play-device-name'}, con.deviceName), + CE('span', {class: 'bx-remote-play-console-type'}, con.consoleType.replace('Xbox', '')) + ), + CE('div', {class: 'bx-remote-play-power-state'}, this.STATE_LABELS[con.powerState]), + ), + + // Connect button + createButton({ + classes: ['bx-remote-play-connect-button'], + label: t('console-connect'), + style: ButtonStyle.PRIMARY | ButtonStyle.FOCUSABLE, + onClick: e => manager.play(con.serverId), + }), + ); + + $fragment.appendChild($child); + } + + // Add buttons + $fragment.appendChild( + CE('div', { + class: 'bx-remote-play-buttons', + _nearby: { + orientation: 'horizontal', + }, + }, + createButton({ + icon: BxIcon.QUESTION, + style: ButtonStyle.GHOST | ButtonStyle.FOCUSABLE, + url: 'https://better-xcloud.github.io/remote-play', + label: t('help'), + }), + + createButton({ + icon: BxIcon.CLOSE, + style: ButtonStyle.GHOST | ButtonStyle.FOCUSABLE, + label: t('close'), + }), + ), + ); + + this.$container = $fragment; + } + + getDialog(): NavigationDialog { + return this; + } + + getContent(): HTMLElement { + return this.$container; + } + + focusIfNeeded(): void { + const $btnConnect = this.$container.querySelector('.bx-remote-play-device-wrapper button') as HTMLElement; + $btnConnect && $btnConnect.focus(); + } +} diff --git a/src/modules/ui/header.ts b/src/modules/ui/header.ts index 1aa6044..2085349 100644 --- a/src/modules/ui/header.ts +++ b/src/modules/ui/header.ts @@ -2,7 +2,7 @@ import { SCRIPT_VERSION } from "@utils/global"; import { createButton, ButtonStyle, CE, isElementVisible } from "@utils/html"; import { BxIcon } from "@utils/bx-icon"; import { getPreferredServerRegion } from "@utils/region"; -import { RemotePlay } from "@modules/remote-play"; +import { RemotePlayManager } from "@/modules/remote-play-manager"; import { t } from "@utils/translation"; import { SettingsNavigationDialog } from "./dialog/settings-dialog"; import { PrefKey } from "@/enums/pref-keys"; @@ -15,7 +15,7 @@ export class HeaderSection { title: t('remote-play'), style: ButtonStyle.GHOST | ButtonStyle.FOCUSABLE | ButtonStyle.CIRCULAR, onClick: e => { - RemotePlay.togglePopup(); + RemotePlayManager.getInstance().togglePopup(); }, }); diff --git a/src/utils/history.ts b/src/utils/history.ts index c29c40a..4dfb104 100644 --- a/src/utils/history.ts +++ b/src/utils/history.ts @@ -1,6 +1,6 @@ import { BxEvent } from "@utils/bx-event"; import { LoadingScreen } from "@modules/loading-screen"; -import { RemotePlay } from "@modules/remote-play"; +import { RemotePlayManager } from "@/modules/remote-play-manager"; import { HeaderSection } from "@/modules/ui/header"; import { NavigationDialogManager } from "@/modules/ui/dialog/navigation-dialog"; @@ -24,7 +24,7 @@ export function onHistoryChanged(e: PopStateEvent) { return; } - window.setTimeout(RemotePlay.detect, 10); + window.setTimeout(RemotePlayManager.detect, 10); // Hide Global settings const $settings = document.querySelector('.bx-settings-container'); @@ -35,9 +35,6 @@ export function onHistoryChanged(e: PopStateEvent) { // Hide Navigation dialog NavigationDialogManager.getInstance().hide(); - // Hide Remote Play popup - RemotePlay.detachPopup(); - LoadingScreen.reset(); window.setTimeout(HeaderSection.watchHeader, 2000); diff --git a/src/utils/monkey-patches.ts b/src/utils/monkey-patches.ts index 94a7e2e..56941ba 100644 --- a/src/utils/monkey-patches.ts +++ b/src/utils/monkey-patches.ts @@ -254,6 +254,7 @@ export function patchPointerLockApi() { }); // const nativeRequestPointerLock = HTMLElement.prototype.requestPointerLock; + // @ts-ignore HTMLElement.prototype.requestPointerLock = function() { pointerLockElement = document.documentElement; window.dispatchEvent(new Event(BxEvent.POINTER_LOCK_REQUESTED)); diff --git a/src/utils/xcloud-interceptor.ts b/src/utils/xcloud-interceptor.ts index f4d9bdb..da8eb86 100644 --- a/src/utils/xcloud-interceptor.ts +++ b/src/utils/xcloud-interceptor.ts @@ -1,5 +1,5 @@ import { LoadingScreen } from "@modules/loading-screen"; -import { RemotePlay } from "@modules/remote-play"; +import { RemotePlayManager } from "@/modules/remote-play-manager"; import { StreamBadges } from "@modules/stream/stream-badges"; import { TouchController } from "@modules/touch-controller"; import { BxEvent } from "./bx-event"; @@ -30,7 +30,7 @@ class XcloudInterceptor { const obj = await response.clone().json(); // Store xCloud token - RemotePlay.XCLOUD_TOKEN = obj.gsToken; + RemotePlayManager.getInstance().xcloudToken = obj.gsToken; // Get server list const serverEmojis = { diff --git a/src/utils/xhome-interceptor.ts b/src/utils/xhome-interceptor.ts index a072fa5..8ea9153 100644 --- a/src/utils/xhome-interceptor.ts +++ b/src/utils/xhome-interceptor.ts @@ -1,4 +1,3 @@ -import { RemotePlay } from "@/modules/remote-play"; import { TouchController } from "@/modules/touch-controller"; import { BxEvent } from "./bx-event"; import { SupportedInputType } from "./bx-exposed"; @@ -8,6 +7,7 @@ import { patchIceCandidates } from "./network"; import { PrefKey } from "@/enums/pref-keys"; import { getPref, StreamResolution, StreamTouchController } from "./settings-storages/global-settings-storage"; import type { RemotePlayConsoleAddresses } from "@/types/network"; +import { RemotePlayManager } from "@/modules/remote-play-manager"; export class XhomeInterceptor { static #consoleAddrs: RemotePlayConsoleAddresses = {}; @@ -111,7 +111,7 @@ export class XhomeInterceptor { for (const pair of (clone.headers as any).entries()) { headers[pair[0]] = pair[1]; } - headers.authorization = `Bearer ${RemotePlay.XCLOUD_TOKEN}`; + headers.authorization = `Bearer ${RemotePlayManager.getInstance().xcloudToken}`; const index = request.url.indexOf('.xboxlive.com'); request = new Request('https://wus.core.gssv-play-prod' + request.url.substring(index), { @@ -146,10 +146,10 @@ export class XhomeInterceptor { headers[pair[0]] = pair[1]; } // Add xHome token to headers - headers.authorization = `Bearer ${RemotePlay.XHOME_TOKEN}`; + headers.authorization = `Bearer ${RemotePlayManager.getInstance().xhomeToken}`; // Patch resolution - const deviceInfo = RemotePlay.BASE_DEVICE_INFO; + const deviceInfo = RemotePlayManager.BASE_DEVICE_INFO; if (getPref(PrefKey.REMOTE_PLAY_RESOLUTION) === StreamResolution.DIM_720P) { deviceInfo.dev.os.name = 'android'; }