diff --git a/src/assets/css/stream-settings.styl b/src/assets/css/stream-settings.styl index b42b355..0d87214 100644 --- a/src/assets/css/stream-settings.styl +++ b/src/assets/css/stream-settings.styl @@ -124,3 +124,29 @@ font-style: italic; padding-top: 16px; } + +.bx-quick-settings-tab-contents { + div[data-group="shortcuts"] { + .bx-shortcut-profile { + width: 100%; + height: 36px; + display: block; + margin-bottom: 10px; + } + + .bx-shortcut-row { + display: flex; + margin-bottom: 10px; + + label.bx-prompt { + flex: 1; + font-family: var(--bx-promptfont-font); + font-size: 26px; + } + + select { + flex: 2; + } + } + } +} diff --git a/src/assets/svg/command.svg b/src/assets/svg/command.svg new file mode 100644 index 0000000..c14a01a --- /dev/null +++ b/src/assets/svg/command.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/modules/controller-shortcut.ts b/src/modules/controller-shortcut.ts new file mode 100644 index 0000000..aafdab5 --- /dev/null +++ b/src/modules/controller-shortcut.ts @@ -0,0 +1,321 @@ +import { Screenshot } from "@utils/screenshot"; +import { GamepadKey } from "./mkb/definitions"; +import { PrompFont } from "@utils/prompt-font"; +import { CE } from "@utils/html"; +import { t } from "@utils/translation"; +import { PrefKey, getPref } from "@/utils/preferences"; +import { AppInterface } from "@/utils/global"; +import { MkbHandler } from "./mkb/mkb-handler"; +import { StreamStat, StreamStats } from "./stream/stream-stats"; +import { MicrophoneShortcut } from "./shortcuts/shortcut-microphone"; +import { StreamUiShortcut } from "./shortcuts/shortcut-stream-ui"; + +enum ShortcutAction { + STREAM_SCREENSHOT_CAPTURE = 'stream-screenshot-capture', + + STREAM_MENU_TOGGLE = 'stream-menu-toggle', + STREAM_STATS_TOGGLE = 'stream-stats-toggle', + STREAM_SOUND_TOGGLE = 'stream-sound-toggle', + STREAM_MICROPHONE_TOGGLE = 'stream-microphone-toggle', + + STREAM_VOLUME_INC = 'stream-volume-inc', + STREAM_VOLUME_DEC = 'stream-volume-dec', + + DEVICE_VOLUME_INC = 'device-volume-inc', + DEVICE_VOLUME_DEC = 'device-volume-dec', + + SCREEN_BRIGHTNESS_INC = 'screen-brightness-inc', + SCREEN_BRIGHTNESS_DEC = 'screen-brightness-dec', +} + +export class ControllerShortcut { + static readonly #STORAGE_KEY = 'better_xcloud_controller_shortcuts'; + + static #buttonsCache: {[key: string]: boolean[]} = {}; + static #buttonsStatus: {[key: string]: boolean[]} = {}; + + static #$selectProfile: HTMLSelectElement; + static #$selectActions: Partial<{[key in GamepadKey]: HTMLSelectElement}> = {}; + static #$remap: HTMLElement; + + static #ACTIONS: {[key: string]: (ShortcutAction | null)[]} = {}; + + static reset(index: number) { + ControllerShortcut.#buttonsCache[index] = []; + ControllerShortcut.#buttonsStatus[index] = []; + } + + static handle(gamepad: Gamepad): boolean { + const gamepadIndex = gamepad.index; + const actions = ControllerShortcut.#ACTIONS[gamepad.id]; + if (!actions) { + return false; + } + + // Move the buttons status from the previous frame to the cache + ControllerShortcut.#buttonsCache[gamepadIndex] = ControllerShortcut.#buttonsStatus[gamepadIndex].slice(0); + // Clear the buttons status + ControllerShortcut.#buttonsStatus[gamepadIndex] = []; + + const pressed: boolean[] = []; + let otherButtonPressed = false; + + gamepad.buttons.forEach((button, index) => { + // Only add the newly pressed button to the array (holding doesn't count) + if (button.pressed && index !== GamepadKey.HOME) { + otherButtonPressed = true; + pressed[index] = true; + + // If this is newly pressed button > run action + if (actions[index] && !ControllerShortcut.#buttonsCache[gamepadIndex][index]) { + ControllerShortcut.#runAction(actions[index]!); + } + } + }); + + ControllerShortcut.#buttonsStatus[gamepadIndex] = pressed; + return otherButtonPressed; + } + + static #runAction(action: ShortcutAction) { + switch (action) { + case ShortcutAction.STREAM_SCREENSHOT_CAPTURE: + Screenshot.takeScreenshot(); + break; + + case ShortcutAction.STREAM_STATS_TOGGLE: + StreamStats.toggle(); + break; + + case ShortcutAction.STREAM_MICROPHONE_TOGGLE: + MicrophoneShortcut.toggle(); + break; + + case ShortcutAction.STREAM_MENU_TOGGLE: + StreamUiShortcut.showHideStreamMenu(); + break; + } + } + + static #updateAction(profile: string, button: GamepadKey, action: ShortcutAction | null) { + if (!(profile in ControllerShortcut.#ACTIONS)) { + ControllerShortcut.#ACTIONS[profile] = []; + } + + if (!action) { + action = null; + } + + ControllerShortcut.#ACTIONS[profile][button] = action; + + // Remove empty profiles + for (const key in ControllerShortcut.#ACTIONS) { + let empty = true; + for (const value of ControllerShortcut.#ACTIONS[key]) { + if (!!value) { + empty = false; + break; + } + } + + if (empty) { + delete ControllerShortcut.#ACTIONS[key]; + } + } + + // Save to storage + window.localStorage.setItem(ControllerShortcut.#STORAGE_KEY, JSON.stringify(ControllerShortcut.#ACTIONS)); + + console.log(ControllerShortcut.#ACTIONS); + } + + static #updateProfileList(e?: GamepadEvent) { + const $select = ControllerShortcut.#$selectProfile; + const $remap = ControllerShortcut.#$remap; + + const $fragment = document.createDocumentFragment(); + + // Remove old profiles + while ($select.firstElementChild) { + $select.firstElementChild.remove(); + } + + const gamepads = navigator.getGamepads(); + let hasGamepad = false; + + for (const gamepad of gamepads) { + if (!gamepad || !gamepad.connected) { + continue; + } + + // Ignore emulated gamepad + if (gamepad.id === MkbHandler.VIRTUAL_GAMEPAD_ID) { + continue; + } + + hasGamepad = true; + + const $option = CE('option', {value: gamepad.id}, gamepad.id); + $fragment.appendChild($option); + } + + if (hasGamepad) { + $select.appendChild($fragment); + + $remap.classList.remove('bx-gone'); + + $select.disabled = false; + $select.selectedIndex = 0; + $select.dispatchEvent(new Event('change')); + } else { + $remap.classList.add('bx-gone'); + + $select.disabled = true; + const $option = CE('option', {}, '---'); + $fragment.appendChild($option); + + $select.appendChild($fragment); + } + } + + static #switchProfile(profile: string) { + let actions = ControllerShortcut.#ACTIONS[profile]; + if (!actions) { + actions = []; + } + + // Reset selects' values + let button: any; + for (button in ControllerShortcut.#$selectActions) { + const $select = ControllerShortcut.#$selectActions[button as GamepadKey]!; + $select.value = actions[button] || ''; + } + } + + static renderSettings() { + // Read actions from localStorage + ControllerShortcut.#ACTIONS = JSON.parse(window.localStorage.getItem(ControllerShortcut.#STORAGE_KEY) || '{}'); + + const buttons = { + [GamepadKey.A]: PrompFont.A, + [GamepadKey.B]: PrompFont.B, + [GamepadKey.X]: PrompFont.X, + [GamepadKey.Y]: PrompFont.Y, + + [GamepadKey.LB]: PrompFont.LB, + [GamepadKey.RB]: PrompFont.RB, + + [GamepadKey.LT]: PrompFont.LT, + [GamepadKey.RT]: PrompFont.RT, + + [GamepadKey.SELECT]: PrompFont.SELECT, + [GamepadKey.START]: PrompFont.START, + + [GamepadKey.UP]: PrompFont.UP, + [GamepadKey.DOWN]: PrompFont.DOWN, + [GamepadKey.LEFT]: PrompFont.LEFT, + [GamepadKey.RIGHT]: PrompFont.RIGHT, + }; + + const actions: {[key: string]: Partial<{[key in ShortcutAction]: string | string[]}>} = { + /* + [t('device')]: AppInterface && { + [ShortcutAction.DEVICE_VOLUME_INC]: [t('device'), t('volume'), t('increase')], + [ShortcutAction.DEVICE_VOLUME_DEC]: [t('device'), t('volume'), t('decrease')], + + [ShortcutAction.SCREEN_BRIGHTNESS_INC]: [t('screen'), t('brightness'), t('increase')], + [ShortcutAction.SCREEN_BRIGHTNESS_DEC]: [t('screen'), t('brightness'), t('decrease')], + }, + */ + + [t('stream')]: { + [ShortcutAction.STREAM_SCREENSHOT_CAPTURE]: [t('stream'), t('take-screenshot')], + [ShortcutAction.STREAM_STATS_TOGGLE]: [t('stream'), t('stats'), t('show-hide')], + [ShortcutAction.STREAM_MICROPHONE_TOGGLE]: [t('stream'), t('microphone'), t('toggle')], + [ShortcutAction.STREAM_MENU_TOGGLE]: [t('stream'), t('menu'), t('show')], + // [ShortcutAction.STREAM_SOUND_TOGGLE]: [t('stream'), t('sound'), t('toggle')], + // [ShortcutAction.STREAM_VOLUME_INC]: getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && [t('stream'), t('volume'), t('increase')], + // [ShortcutAction.STREAM_VOLUME_DEC]: getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && [t('stream'), t('volume'), t('decrease')], + } + }; + + const $baseSelect = CE('select', {autocomplete: 'off'}, CE('option', {value: ''}, '---')); + for (const groupLabel in actions) { + const items = actions[groupLabel]; + if (!items) { + continue; + } + + const $optGroup = CE('optgroup', {'label': groupLabel}); + + for (const action in items) { + let label = items[action as keyof typeof items]; + if (!label) { + continue; + } + + if (Array.isArray(label)) { + label = label.join(' > '); + } + + const $option = CE('option', {value: action}, label); + $optGroup.appendChild($option); + } + + $baseSelect.appendChild($optGroup); + } + + const $container = CE('div', {}); + + const $profile = CE('select', {'class': 'bx-shortcut-profile', autocomplete: 'off'}); + $profile.addEventListener('change', e => { + ControllerShortcut.#switchProfile($profile.value); + }); + + $container.appendChild($profile); + + const onActionChanged = (e: Event) => { + const $target = e.target as HTMLSelectElement; + + const profile = $profile.value; + const button: unknown = $target.dataset.button; + const action = $target.value as ShortcutAction; + + ControllerShortcut.#updateAction(profile, button as GamepadKey, action); + }; + + const $remap = CE('div', {'class': 'bx-gone'}); + let button: keyof typeof buttons; + // @ts-ignore + for (button in buttons) { + const $row = CE('div', {'class': 'bx-shortcut-row'}); + + const prompt = buttons[button]; + const $label = CE('label', {'class': 'bx-prompt'}, `${PrompFont.HOME} + ${prompt}`); + + const $select = $baseSelect.cloneNode(true) as HTMLSelectElement; + $select.dataset.button = button.toString(); + $select.addEventListener('change', onActionChanged); + + ControllerShortcut.#$selectActions[button] = $select; + + $row.appendChild($label); + $row.appendChild($select); + + $remap.appendChild($row); + } + + $container.appendChild($remap); + + ControllerShortcut.#$selectProfile = $profile; + ControllerShortcut.#$remap = $remap; + + // Detect when gamepad connected/disconnect + window.addEventListener('gamepadconnected', ControllerShortcut.#updateProfileList); + window.addEventListener('gamepaddisconnected', ControllerShortcut.#updateProfileList); + + ControllerShortcut.#updateProfileList(); + + return $container; + } +} diff --git a/src/modules/game-bar/action-microphone.ts b/src/modules/game-bar/action-microphone.ts index b3fe35e..4ebbfd7 100644 --- a/src/modules/game-bar/action-microphone.ts +++ b/src/modules/game-bar/action-microphone.ts @@ -3,6 +3,7 @@ import { BxIcon } from "@utils/bx-icon"; import { createButton, ButtonStyle, CE } from "@utils/html"; import { t } from "@utils/translation"; import { BaseGameBarAction } from "./action-base"; +import { MicrophoneShortcut } from "../shortcuts/shortcut-microphone"; enum MicrophoneState { REQUESTED = 'Requested', @@ -22,15 +23,9 @@ export class MicrophoneAction extends BaseGameBarAction { const onClick = (e: Event) => { BxEvent.dispatch(window, BxEvent.GAME_BAR_ACTION_ACTIVATED); - const state = this.$content.getAttribute('data-enabled'); - const enableMic = state === 'true' ? false : true; - try { - window.BX_EXPOSED.streamSession.tryEnableChatAsync(enableMic); - this.$content.setAttribute('data-enabled', enableMic.toString()); - } catch (e) { - console.log(e); - } + const enabled = MicrophoneShortcut.toggle(false); + this.$content.setAttribute('data-enabled', enabled.toString()); }; const $btnDefault = createButton({ diff --git a/src/modules/mkb/definitions.ts b/src/modules/mkb/definitions.ts index d4bf87a..181f4f6 100644 --- a/src/modules/mkb/definitions.ts +++ b/src/modules/mkb/definitions.ts @@ -1,4 +1,5 @@ import type { GamepadKeyNameType } from "@/types/mkb"; +import { PrompFont } from "@/utils/prompt-font"; export enum GamepadKey { A = 0, @@ -18,6 +19,7 @@ export enum GamepadKey { LEFT = 14, RIGHT = 15, HOME = 16, + SHARE = 17, LS_UP = 100, LS_DOWN = 101, @@ -32,36 +34,36 @@ export enum GamepadKey { export const GamepadKeyName: GamepadKeyNameType = { - [GamepadKey.A]: ['A', '⇓'], - [GamepadKey.B]: ['B', '⇒'], - [GamepadKey.X]: ['X', '⇐'], - [GamepadKey.Y]: ['Y', '⇑'], + [GamepadKey.A]: ['A', PrompFont.A], + [GamepadKey.B]: ['B', PrompFont.B], + [GamepadKey.X]: ['X', PrompFont.X], + [GamepadKey.Y]: ['Y', PrompFont.Y], - [GamepadKey.LB]: ['LB', '↘'], - [GamepadKey.RB]: ['RB', '↙'], - [GamepadKey.LT]: ['LT', '↖'], - [GamepadKey.RT]: ['RT', '↗'], + [GamepadKey.LB]: ['LB', PrompFont.LB], + [GamepadKey.RB]: ['RB', PrompFont.RB], + [GamepadKey.LT]: ['LT', PrompFont.LT], + [GamepadKey.RT]: ['RT', PrompFont.RT], - [GamepadKey.SELECT]: ['Select', '⇺'], - [GamepadKey.START]: ['Start', '⇻'], - [GamepadKey.HOME]: ['Home', ''], + [GamepadKey.SELECT]: ['Select', PrompFont.SELECT], + [GamepadKey.START]: ['Start', PrompFont.START], + [GamepadKey.HOME]: ['Home', PrompFont.HOME], - [GamepadKey.UP]: ['D-Pad Up', '≻'], - [GamepadKey.DOWN]: ['D-Pad Down', '≽'], - [GamepadKey.LEFT]: ['D-Pad Left', '≺'], - [GamepadKey.RIGHT]: ['D-Pad Right', '≼'], + [GamepadKey.UP]: ['D-Pad Up', PrompFont.UP], + [GamepadKey.DOWN]: ['D-Pad Down', PrompFont.DOWN], + [GamepadKey.LEFT]: ['D-Pad Left', PrompFont.LEFT], + [GamepadKey.RIGHT]: ['D-Pad Right', PrompFont.RIGHT], - [GamepadKey.L3]: ['L3', '↺'], - [GamepadKey.LS_UP]: ['Left Stick Up', '↾'], - [GamepadKey.LS_DOWN]: ['Left Stick Down', '⇂'], - [GamepadKey.LS_LEFT]: ['Left Stick Left', '↼'], - [GamepadKey.LS_RIGHT]: ['Left Stick Right', '⇀'], + [GamepadKey.L3]: ['L3', PrompFont.L3], + [GamepadKey.LS_UP]: ['Left Stick Up', PrompFont.LS_UP], + [GamepadKey.LS_DOWN]: ['Left Stick Down', PrompFont.LS_DOWN], + [GamepadKey.LS_LEFT]: ['Left Stick Left', PrompFont.LS_LEFT], + [GamepadKey.LS_RIGHT]: ['Left Stick Right', PrompFont.LS_RIGHT], - [GamepadKey.R3]: ['R3', '↻'], - [GamepadKey.RS_UP]: ['Right Stick Up', '↿'], - [GamepadKey.RS_DOWN]: ['Right Stick Down', '⇃'], - [GamepadKey.RS_LEFT]: ['Right Stick Left', '↽'], - [GamepadKey.RS_RIGHT]: ['Right Stick Right', '⇁'], + [GamepadKey.R3]: ['R3', PrompFont.R3], + [GamepadKey.RS_UP]: ['Right Stick Up', PrompFont.RS_UP], + [GamepadKey.RS_DOWN]: ['Right Stick Down', PrompFont.RS_DOWN], + [GamepadKey.RS_LEFT]: ['Right Stick Left', PrompFont.RS_LEFT], + [GamepadKey.RS_RIGHT]: ['Right Stick Right', PrompFont.RS_RIGHT], }; diff --git a/src/modules/patcher.ts b/src/modules/patcher.ts index d50eb2e..0dc46e8 100644 --- a/src/modules/patcher.ts +++ b/src/modules/patcher.ts @@ -384,13 +384,19 @@ if (!!window.BX_REMOTE_PLAY_CONFIG) { return false; } - // Restore the "..." button - str = str.replace(text, 'e.guideUI = null;' + text); + let newCode = ` +// Expose onShowStreamMenu +window.BX_EXPOSED.showStreamMenu = e.onShowStreamMenu; +// Restore the "..." button +e.guideUI = null; +`; // Remove the TAK Edit button when the touch controller is disabled if (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === 'off') { - str = str.replace(text, 'e.canShowTakHUD = false;' + text); + newCode += 'e.canShowTakHUD = false;'; } + + str = str.replace(text, newCode + text); return str; }, diff --git a/src/modules/patches/controller-shortcuts.js b/src/modules/patches/controller-shortcuts.js index c8da989..f030e0a 100644 --- a/src/modules/patches/controller-shortcuts.js +++ b/src/modules/patches/controller-shortcuts.js @@ -1,5 +1,87 @@ const currentGamepad = ${gamepadVar}; +// Share button on XS controller if (currentGamepad.buttons[17] && currentGamepad.buttons[17].value === 1) { window.dispatchEvent(new Event(BxEvent.CAPTURE_SCREENSHOT)); } + +const btnHome = currentGamepad.buttons[16]; +if (btnHome) { + if (!this.bxHomeStates) { + this.bxHomeStates = {}; + } + + if (btnHome.pressed) { + this.gamepadIsIdle.set(currentGamepad.index, false); + + if (this.bxHomeStates[currentGamepad.index]) { + const lastTimestamp = this.bxHomeStates[currentGamepad.index].timestamp; + + if (currentGamepad.timestamp !== lastTimestamp) { + this.bxHomeStates[currentGamepad.index].timestamp = currentGamepad.timestamp; + + const handled = window.BX_EXPOSED.handleControllerShortcut(currentGamepad); + if (handled) { + this.bxHomeStates[currentGamepad.index].shortcutPressed += 1; + } + } + } else { + // First time pressing > save current timestamp + window.BX_EXPOSED.resetControllerShortcut(currentGamepad.index); + this.bxHomeStates[currentGamepad.index] = { + shortcutPressed: 0, + timestamp: currentGamepad.timestamp, + }; + } + + // Listen to next button press + const intervalMs = 50; + this.inputConfiguration.useIntervalWorkerThreadForInput && this.intervalWorker ? this.intervalWorker.scheduleTimer(intervalMs) : this.pollGamepadssetTimeoutTimerID = setTimeout(this.pollGamepads, intervalMs); + + // Hijack this button + return; + } else if (this.bxHomeStates[currentGamepad.index]) { + const info = structuredClone(this.bxHomeStates[currentGamepad.index]); + + // Home button released + this.bxHomeStates[currentGamepad.index] = null; + + if (info.shortcutPressed === 0) { + const fakeGamepadMappings = [{ + GamepadIndex: 0, + A: 0, + B: 0, + X: 0, + Y: 0, + LeftShoulder: 0, + RightShoulder: 0, + LeftTrigger: 0, + RightTrigger: 0, + View: 0, + Menu: 0, + LeftThumb: 0, + RightThumb: 0, + DPadUp: 0, + DPadDown: 0, + DPadLeft: 0, + DPadRight: 0, + Nexus: 1, + LeftThumbXAxis: 0, + LeftThumbYAxis: 0, + RightThumbXAxis: 0, + RightThumbYAxis: 0, + PhysicalPhysicality: 0, + VirtualPhysicality: 0, + Dirty: true, + Virtual: false, + }]; + + const isLongPress = (currentGamepad.timestamp - info.timestamp) >= 500; + const intervalMs = isLongPress ? 500 : 100; + + this.inputSink.onGamepadInput(performance.now() - intervalMs, fakeGamepadMappings); + this.inputConfiguration.useIntervalWorkerThreadForInput && this.intervalWorker ? this.intervalWorker.scheduleTimer(intervalMs) : this.pollGamepadssetTimeoutTimerID = setTimeout(this.pollGamepads, intervalMs); + return; + } + } +} diff --git a/src/modules/shortcuts/shortcut-microphone.ts b/src/modules/shortcuts/shortcut-microphone.ts new file mode 100644 index 0000000..af31ab9 --- /dev/null +++ b/src/modules/shortcuts/shortcut-microphone.ts @@ -0,0 +1,24 @@ +import { t } from "@utils/translation"; +import { Toast } from "@utils/toast"; + +export class MicrophoneShortcut { + static toggle(showToast: boolean = true): boolean { + if (!window.BX_EXPOSED.streamSession) { + return false; + } + + const state = window.BX_EXPOSED.streamSession._microphoneState; + const enableMic = state === 'Enabled' ? false : true; + + try { + window.BX_EXPOSED.streamSession.tryEnableChatAsync(enableMic); + showToast && Toast.show(t('microphone'), t(enableMic ? 'unmuted': 'muted'), {instant: true}); + + return enableMic; + } catch (e) { + console.log(e); + } + + return false; + } +} diff --git a/src/modules/shortcuts/shortcut-stream-ui.ts b/src/modules/shortcuts/shortcut-stream-ui.ts new file mode 100644 index 0000000..181a556 --- /dev/null +++ b/src/modules/shortcuts/shortcut-stream-ui.ts @@ -0,0 +1,6 @@ +export class StreamUiShortcut { + static showHideStreamMenu() { + // Show menu + window.BX_EXPOSED.showStreamMenu && window.BX_EXPOSED.showStreamMenu(); + } +} diff --git a/src/modules/stream/stream-ui.ts b/src/modules/stream/stream-ui.ts index 6ded232..5c71a9a 100644 --- a/src/modules/stream/stream-ui.ts +++ b/src/modules/stream/stream-ui.ts @@ -210,7 +210,7 @@ export function injectStreamMenuButtons() { // Create Stream Settings button if (!$btnStreamSettings) { - $btnStreamSettings = cloneStreamHudButton($orgButton, t('menu-stream-settings'), BxIcon.STREAM_SETTINGS); + $btnStreamSettings = cloneStreamHudButton($orgButton, t('stream-settings'), BxIcon.STREAM_SETTINGS); $btnStreamSettings.addEventListener('click', e => { hideGripHandle(); e.preventDefault(); @@ -228,7 +228,7 @@ export function injectStreamMenuButtons() { // Create Stream Stats button if (!$btnStreamStats) { - $btnStreamStats = cloneStreamHudButton($orgButton, t('menu-stream-stats'), BxIcon.STREAM_STATS); + $btnStreamStats = cloneStreamHudButton($orgButton, t('stream-stats'), BxIcon.STREAM_STATS); $btnStreamStats.addEventListener('click', e => { hideGripHandle(); e.preventDefault(); diff --git a/src/modules/ui/ui.ts b/src/modules/ui/ui.ts index 0a02724..d1a6dfa 100644 --- a/src/modules/ui/ui.ts +++ b/src/modules/ui/ui.ts @@ -10,6 +10,7 @@ import { TouchController } from "@modules/touch-controller"; import { t } from "@utils/translation"; import { VibrationManager } from "@modules/vibration-manager"; import { Screenshot } from "@/utils/screenshot"; +import { ControllerShortcut } from "../controller-shortcut"; export function localRedirect(path: string) { @@ -239,13 +240,25 @@ function setupQuickSettingsBar() { ], }, + { + 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('menu-stream-stats'), + label: t('stream-stats'), help_url: 'https://better-xcloud.github.io/stream-stats/', items: [ { @@ -490,9 +503,24 @@ function resizeVideoPlayer() { } +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); +} + + export function setupStreamUi() { // Prevent initializing multiple times if (!document.querySelector('.bx-quick-settings-bar')) { + preloadFonts(); + window.addEventListener('resize', updateVideoPlayerCss); setupQuickSettingsBar(); StreamStats.render(); diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 2409859..f533aea 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -38,8 +38,6 @@ type BxStates = { titleInfo: XcloudTitleInfo; $video: HTMLVideoElement | null; - $screenshotCanvas: HTMLCanvasElement | null; - screenshotCanvasContext: CanvasRenderingContext2D | null; peerConnection: RTCPeerConnection; audioContext: AudioContext | null; diff --git a/src/utils/bx-exposed.ts b/src/utils/bx-exposed.ts index 6c96149..f15649b 100644 --- a/src/utils/bx-exposed.ts +++ b/src/utils/bx-exposed.ts @@ -1,3 +1,4 @@ +import { ControllerShortcut } from "@/modules/controller-shortcut"; import { GameBar } from "@modules/game-bar/game-bar"; import { BxEvent } from "@utils/bx-event"; import { STATES } from "@utils/global"; @@ -111,5 +112,8 @@ export const BxExposed = { const gainNode = audioCtx.createGain(); // call monkey-patched createGain() in BxAudioContext source.connect(gainNode).connect(audioCtx.destination); - } + }, + + handleControllerShortcut: ControllerShortcut.handle, + resetControllerShortcut: ControllerShortcut.reset, }; diff --git a/src/utils/bx-icon.ts b/src/utils/bx-icon.ts index e4341f8..a6e83d3 100644 --- a/src/utils/bx-icon.ts +++ b/src/utils/bx-icon.ts @@ -1,3 +1,4 @@ +import iconCommand from "@assets/svg/command.svg" with { type: "text" }; import iconController from "@assets/svg/controller.svg" with { type: "text" }; import iconCopy from "@assets/svg/copy.svg" with { type: "text" }; import iconCursorText from "@assets/svg/cursor-text.svg" with { type: "text" }; @@ -24,6 +25,7 @@ import iconMicrophoneMuted from "@assets/svg/microphone-slash.svg" with { type: export const BxIcon = { STREAM_SETTINGS: iconStreamSettings, STREAM_STATS: iconStreamStats, + COMMAND: iconCommand, CONTROLLER: iconController, DISPLAY: iconDisplay, MOUSE: iconMouse, diff --git a/src/utils/prompt-font.ts b/src/utils/prompt-font.ts new file mode 100644 index 0000000..a64f45b --- /dev/null +++ b/src/utils/prompt-font.ts @@ -0,0 +1,32 @@ +export enum PrompFont { + A = '⇓', + B = '⇒', + X = '⇐', + Y = '⇑', + + LB = '↘', + RB = '↙', + LT = '↖', + RT = '↗', + + SELECT = '⇺', + START = '⇻', + HOME = '', + + UP = '≻', + DOWN = '≽', + LEFT = '≺', + RIGHT = '≼', + + L3 = '↺', + LS_UP = '↾', + LS_DOWN = '⇂', + LS_LEFT = '↼', + LS_RIGHT = '⇀', + + R3 = '↻', + RS_UP = '↿', + RS_DOWN = '⇃', + RS_LEFT = '↽', + RS_RIGHT = '⇁', +} diff --git a/src/utils/screenshot.ts b/src/utils/screenshot.ts index dd298bc..88a77ee 100644 --- a/src/utils/screenshot.ts +++ b/src/utils/screenshot.ts @@ -3,21 +3,24 @@ import { CE } from "./html"; export class Screenshot { - static setup() { - const currentStream = STATES.currentStream; - if (!currentStream.$screenshotCanvas) { - currentStream.$screenshotCanvas = CE('canvas', {'class': 'bx-gone'}); + static #$canvas: HTMLCanvasElement; + static #canvasContext: CanvasRenderingContext2D; - currentStream.screenshotCanvasContext = currentStream.$screenshotCanvas.getContext('2d', { - alpha: false, - willReadFrequently: false, - }); + static setup() { + if (Screenshot.#$canvas) { + return; } - // document.documentElement.appendChild(currentStream.$screenshotCanvas!); + + Screenshot.#$canvas = CE('canvas', {'class': 'bx-gone'}); + + Screenshot.#canvasContext = Screenshot.#$canvas.getContext('2d', { + alpha: false, + willReadFrequently: false, + })!; } static updateCanvasSize(width: number, height: number) { - const $canvas = STATES.currentStream.$screenshotCanvas; + const $canvas = Screenshot.#$canvas; if ($canvas) { $canvas.width = width; $canvas.height = height; @@ -25,7 +28,7 @@ export class Screenshot { } static updateCanvasFilters(filters: string) { - STATES.currentStream.screenshotCanvasContext && (STATES.currentStream.screenshotCanvasContext.filter = filters); + Screenshot.#canvasContext.filter = filters; } private static onAnimationEnd(e: Event) { @@ -35,7 +38,7 @@ export class Screenshot { static takeScreenshot(callback?: any) { const currentStream = STATES.currentStream; const $video = currentStream.$video; - const $canvas = currentStream.$screenshotCanvas; + const $canvas = Screenshot.#$canvas; if (!$video || !$canvas) { return; } @@ -43,7 +46,7 @@ export class Screenshot { $video.parentElement?.addEventListener('animationend', this.onAnimationEnd); $video.parentElement?.classList.add('bx-taking-screenshot'); - const canvasContext = currentStream.screenshotCanvasContext!; + const canvasContext = Screenshot.#canvasContext; canvasContext.drawImage($video, 0, 0, $canvas.width, $canvas.height); // Get data URL and pass to parent app diff --git a/src/utils/translation.ts b/src/utils/translation.ts index 2a79feb..23b5d1d 100644 --- a/src/utils/translation.ts +++ b/src/utils/translation.ts @@ -13,6 +13,7 @@ export const SUPPORTED_LANGUAGES = { 'pl-PL': 'polski', 'pt-BR': 'português (Brasil)', 'ru-RU': 'русский', + 'th-TH': 'ภาษาไทย', 'tr-TR': 'Türkçe', 'uk-UA': 'українська', 'vi-VN': 'Tiếng Việt', @@ -62,8 +63,10 @@ const Texts = { "copy": "Copy", "custom": "Custom", "deadzone-counterweight": "Deadzone counterweight", + "decrease": "Decrease", "default": "Default", "delete": "Delete", + "device": "Device", "device-unsupported-touch": "Your device doesn't have touch support", "device-vibration": "Device vibration", "device-vibration-not-using-gamepad": "On when not using gamepad", @@ -93,12 +96,14 @@ const Texts = { "game-bar": "Game Bar", "getting-consoles-list": "Getting the list of consoles...", "help": "Help", + "hide": "Hide", "hide-idle-cursor": "Hide mouse cursor on idle", "hide-scrollbar": "Hide web page's scrollbar", "hide-system-menu-icon": "Hide System menu's icon", "hide-touch-controller": "Hide touch controller", "horizontal-sensitivity": "Horizontal sensitivity", "import": "Import", + "increase": "Increase", "install-android": "Install Better xCloud app for Android", "keyboard-shortcuts": "Keyboard shortcuts", "language": "Language", @@ -109,13 +114,13 @@ const Texts = { "local-co-op": "Local co-op", "map-mouse-to": "Map mouse to", "may-not-work-properly": "May not work properly!", - "menu-stream-settings": "Stream settings", - "menu-stream-stats": "Stream stats", + "menu": "Menu", "microphone": "Microphone", "mkb-adjust-ingame-settings": "You may also need to adjust the in-game sensitivity & deadzone settings", "mkb-click-to-activate": "Click to activate", "mkb-disclaimer": "Using this feature when playing online could be viewed as cheating", "mouse-and-keyboard": "Mouse & Keyboard", + "mute-unmute-sound": "Mute/unmute sound", "muted": "Muted", "name": "Name", "new": "New", @@ -146,6 +151,7 @@ const Texts = { (e: any) => `Naciśnij ${e.key}, aby przełączyć funkcję myszy i klawiatury`, (e: any) => `Pressione ${e.key} para ativar/desativar a função de Mouse e Teclado`, (e: any) => `Нажмите ${e.key} для переключения функции мыши и клавиатуры`, + , (e: any) => `Klavye ve fare özelliğini açmak için ${e.key} tuşuna basın`, (e: any) => `Натисніть ${e.key}, щоб увімкнути або вимкнути функцію миші та клавіатури`, (e: any) => `Nhấn ${e.key} để bật/tắt tính năng Chuột và Bàn phím`, @@ -167,6 +173,7 @@ const Texts = { "safari-failed-message": "Failed to run Better xCloud. Retrying, please wait...", "saturation": "Saturation", "save": "Save", + "screen": "Screen", "screenshot-apply-filters": "Applies video filters to screenshots", "separate-touch-controller": "Separate Touch controller & Controller #1", "separate-touch-controller-note": "Touch controller is Player 1, Controller #1 is Player 2", @@ -174,7 +181,9 @@ const Texts = { "settings-reload": "Reload page to reflect changes", "settings-reloading": "Reloading...", "shortcut-keys": "Shortcut keys", + "show": "Show", "show-game-art": "Show game art", + "show-hide": "Show/hide", "show-stats-on-startup": "Show stats when starting the game", "show-touch-controller": "Show touch controller", "show-wait-time": "Show the estimated wait time", @@ -195,6 +204,8 @@ const Texts = { "stick-decay-minimum": "Stick decay minimum", "stick-decay-strength": "Stick decay strength", "stream": "Stream", + "stream-settings": "Stream settings", + "stream-stats": "Stream stats", "stretch": "Stretch", "stretch-note": "Don't use with native touch games", "support-better-xcloud": "Support Better xCloud", @@ -210,6 +221,9 @@ const Texts = { "tc-muted-colors": "Muted colors", "tc-standard-layout-style": "Standard layout's button style", "text-size": "Text size", + "toggle": "Toggle", + "toggle-microphone": "Toggle microphone", + "toggle-stream-stats": "Toggle stream stats", "top-center": "Top-center", "top-left": "Top-left", "top-right": "Top-right", @@ -226,6 +240,7 @@ const Texts = { (e: any) => `Układ sterowania dotykowego stworzony przez ${e.name}`, (e: any) => `Disposição de controle por toque feito por ${e.name}`, (e: any) => `Сенсорная раскладка по ${e.name}`, + , (e: any) => `${e.name} kişisinin dokunmatik kontrolcü tuş şeması`, (e: any) => `Розташування сенсорного керування від ${e.name}`, (e: any) => `Bố cục điều khiển cảm ứng tạo bởi ${e.name}`, @@ -292,7 +307,7 @@ export class Translations { static get(key: keyof typeof Texts, values?: any): T { let text = null; - if (Translations.#selectedLocale !== Translations.#EN_US) { + if (Translations.#foreignTranslations && Translations.#selectedLocale !== Translations.#EN_US) { text = Translations.#foreignTranslations[key]; }