import { escapeHtml } from "@utils/html"; import { Toast } from "@utils/toast"; import { BxEvent } from "@utils/bx-event"; import { NATIVE_FETCH } from "@utils/bx-flags"; import { t } from "@utils/translation"; import { BxLogger } from "@utils/bx-logger"; import { PrefKey } from "@/enums/pref-keys"; import { getPref } from "@/utils/settings-storages/global-settings-storage"; import { TouchControllerStyleCustom, TouchControllerStyleStandard } from "@/enums/pref-values"; import { GhPagesUtils } from "@/utils/gh-pages"; import { BxEventBus } from "@/utils/bx-event-bus"; const LOG_TAG = 'TouchController'; type TouchControlLayout = { name: string, author: string, content: any, }; type TouchControlDefinition = { name: string, product_id: string, default_layout: string, layouts: Record, }; export class TouchController { static readonly #EVENT_SHOW_DEFAULT_CONTROLLER = new MessageEvent('message', { data: JSON.stringify({ content: '{"layoutId":""}', target: '/streaming/touchcontrols/showlayoutv2', type: 'Message', }), origin: 'better-xcloud', }); /* static readonly #EVENT_HIDE_CONTROLLER = new MessageEvent('message', { data: '{"content":"","target":"/streaming/touchcontrols/hide","type":"Message"}', origin: 'better-xcloud', }); */ static #$style: HTMLStyleElement; static #enabled = false; static #dataChannel: RTCDataChannel | null; static #customLayouts: Record = {}; static #baseCustomLayouts: Record> = {}; static #currentLayoutId: string; static #customList: string[]; static #xboxTitleId: string | null = null; static setXboxTitleId(xboxTitleId: string) { TouchController.#xboxTitleId = xboxTitleId; } static getCustomLayouts() { const xboxTitleId = TouchController.#xboxTitleId; if (!xboxTitleId) { return null; } return TouchController.#customLayouts[xboxTitleId]; } static enable() { TouchController.#enabled = true; } static disable() { TouchController.#enabled = false; } static isEnabled() { return TouchController.#enabled; } 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(): boolean { if (!TouchController.#dataChannel) { return false; } const $container = document.querySelector('#BabylonCanvasContainer-main')?.parentElement; if (!$container) { return false; } $container.classList.toggle('bx-offscreen'); return !$container.classList.contains('bx-offscreen'); } static reset() { TouchController.#enabled = false; TouchController.#dataChannel = null; TouchController.#xboxTitleId = null; TouchController.#$style && (TouchController.#$style.textContent = ''); } static #dispatchMessage(msg: any) { TouchController.#dataChannel && window.setTimeout(() => { TouchController.#dataChannel!.dispatchEvent(msg); }, 10); } static #dispatchLayouts(data: any) { // Load default layout TouchController.applyCustomLayout(null, 1000); BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED); }; static async requestCustomLayouts(retries: number=1) { const xboxTitleId = TouchController.#xboxTitleId; if (!xboxTitleId) { return; } if (xboxTitleId in TouchController.#customLayouts) { TouchController.#dispatchLayouts(TouchController.#customLayouts[xboxTitleId]); return; } retries = retries || 1; if (retries > 2) { TouchController.#customLayouts[xboxTitleId] = null; // Wait for BX_EXPOSED.touchLayoutManager window.setTimeout(() => TouchController.#dispatchLayouts(null), 1000); return; } // Get layout info try { const resp = await NATIVE_FETCH(GhPagesUtils.getUrl(`touch-layouts/${xboxTitleId}.json`)); const json = await resp.json(); const layouts = {}; json.layouts.forEach(async (layoutName: string) => { let baseLayouts = {}; if (layoutName in TouchController.#baseCustomLayouts) { baseLayouts = TouchController.#baseCustomLayouts[layoutName]; } else { try { const layoutUrl = GhPagesUtils.getUrl(`touch-layouts/layouts/${layoutName}.json`); const resp = await NATIVE_FETCH(layoutUrl); const json = await resp.json(); baseLayouts = json.layouts; TouchController.#baseCustomLayouts[layoutName] = baseLayouts; } catch (e) {} } Object.assign(layouts, baseLayouts); }); json.layouts = layouts; TouchController.#customLayouts[xboxTitleId] = json; // Wait for BX_EXPOSED.touchLayoutManager window.setTimeout(() => TouchController.#dispatchLayouts(json), 1000); } catch (e) { // Retry TouchController.requestCustomLayouts(retries + 1); } } static applyCustomLayout(layoutId: string | null, delay: number=0) { // TODO: fix this if (!window.BX_EXPOSED.touchLayoutManager) { const listener = (e: Event) => { if (TouchController.#enabled) { TouchController.applyCustomLayout(layoutId, 0); } }; window.addEventListener(BxEvent.TOUCH_LAYOUT_MANAGER_READY, listener, { once: true }); return; } const xboxTitleId = TouchController.#xboxTitleId; if (!xboxTitleId) { BxLogger.error(LOG_TAG, 'Invalid xboxTitleId'); return; } if (!layoutId) { // Get default layout ID from definition layoutId = TouchController.#customLayouts[xboxTitleId]?.default_layout || null; } if (!layoutId) { BxLogger.error(LOG_TAG, 'Invalid layoutId, show default controller'); TouchController.#enabled && TouchController.#showDefault(); return; } const layoutChanged = TouchController.#currentLayoutId !== layoutId; TouchController.#currentLayoutId = layoutId; // Get layout data const layoutData = TouchController.#customLayouts[xboxTitleId]; if (!xboxTitleId || !layoutId || !layoutData) { TouchController.#enabled && TouchController.#showDefault(); return; } const layout = (layoutData.layouts[layoutId] || layoutData.layouts[layoutData.default_layout]); if (!layout) { return; } // Show a toast with layout's name let msg: string; let html = false; if (layout.author) { const author = `${escapeHtml(layout.author)}`; msg = t('touch-control-layout-by', { name: author }); html = true; } else { msg = t('touch-control-layout'); } layoutChanged && Toast.show(msg, layout.name, { html }); window.setTimeout(() => { // Show gyroscope control in the "More options" dialog if this layout has gyroscope 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 = GhPagesUtils.getTouchControlCustomList(); } static getCustomList(): string[] { return TouchController.#customList; } static setup() { // Function for testing touch control window.testTouchLayout = (layout: any) => { const { touchLayoutManager } = window.BX_EXPOSED; touchLayoutManager && touchLayoutManager.changeLayoutForScope({ type: 'showLayout', scope: '' + TouchController.#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.TOUCH_CONTROLLER_STYLE_STANDARD); const PREF_STYLE_CUSTOM = getPref(PrefKey.TOUCH_CONTROLLER_STYLE_CUSTOM); BxEventBus.Stream.on('dataChannelCreated', payload => { const { dataChannel } = payload; if (dataChannel?.label !== 'message') { return; } // Apply touch controller's style let filter = ''; if (TouchController.#enabled) { if (PREF_STYLE_STANDARD === TouchControllerStyleStandard.WHITE) { filter = 'grayscale(1) brightness(2)'; } else if (PREF_STYLE_STANDARD === TouchControllerStyleStandard.MUTED) { filter = 'sepia(0.5)'; } } else if (PREF_STYLE_CUSTOM === TouchControllerStyleCustom.MUTED) { filter = 'sepia(0.5)'; } if (filter) { $style.textContent = `#babylon-canvas { filter: ${filter} !important; }`; } else { $style.textContent = ''; } TouchController.#dataChannel = dataChannel; // Fix sometimes the touch controller doesn't show at the beginning dataChannel.addEventListener('open', () => { window.setTimeout(TouchController.#show, 1000); }); let focused = false; dataChannel.addEventListener('message', (msg: MessageEvent) => { if (msg.origin === 'better-xcloud' || typeof msg.data !== 'string') { return; } // Dispatch a message to display generic touch controller if (msg.data.includes('touchcontrols/showtitledefault')) { if (TouchController.#enabled) { if (focused) { TouchController.requestCustomLayouts(); } else { TouchController.#showDefault(); } } return; } // Load custom touch layout try { if (msg.data.includes('/titleinfo')) { const json = JSON.parse(JSON.parse(msg.data).content); focused = json.focused; if (!json.focused) { TouchController.#show(); } TouchController.setXboxTitleId(parseInt(json.titleid, 16).toString()); } } catch (e) { BxLogger.error(LOG_TAG, 'Load custom layout', e); } }); }); } }