From b2e932cc4c438827b920fdcfb60ae026e714490f Mon Sep 17 00:00:00 2001 From: redphx <96280+redphx@users.noreply.github.com> Date: Fri, 10 May 2024 18:35:40 +0700 Subject: [PATCH] Game bar (#392) * Fix games with custom touch control sometimes not showing touch icon * Create game-bar with screenshot button * Disable Game bar when opening the Guide * Remove SCREENSHOT_BUTTON_POSITION pref * Make the touch control action functional * Show game bar when the game starts * Fix 720p/High not working (#387) * Update icons * Update game bar's animations * Reset states of Game bar actions before playing * Don't show Touch control action on non-touch-supported devices * Clean up * Update translations * Update actions' texts * Clean up --- src/assets/css/game-bar.styl | 91 ++++++++++ src/assets/css/root.styl | 3 +- src/assets/css/stream-actions.styl | 46 ----- src/assets/css/styles.styl | 2 +- src/assets/svg/camera.svg | 6 + src/assets/svg/caret-right.svg | 5 + src/assets/svg/touch-control-disable.svg | 9 + src/assets/svg/touch-control-enable.svg | 6 + src/index.ts | 41 +---- src/modules/game-bar/action-base.ts | 6 + src/modules/game-bar/action-screenshot.ts | 78 +++++++++ src/modules/game-bar/action-touch-control.ts | 51 ++++++ src/modules/game-bar/game-bar.ts | 116 +++++++++++++ src/modules/screenshot.ts | 100 ----------- src/modules/touch-controller.ts | 59 ++----- src/modules/ui/global-settings.ts | 1 - src/modules/ui/ui.ts | 7 +- src/utils/bx-event.ts | 2 + src/utils/bx-exposed.ts | 22 +-- src/utils/bx-icon.ts | 9 + src/utils/network.ts | 7 +- src/utils/preferences.ts | 10 -- src/utils/translation.ts | 171 ++++++++++++------- 23 files changed, 533 insertions(+), 315 deletions(-) create mode 100644 src/assets/css/game-bar.styl delete mode 100644 src/assets/css/stream-actions.styl create mode 100644 src/assets/svg/camera.svg create mode 100644 src/assets/svg/caret-right.svg create mode 100644 src/assets/svg/touch-control-disable.svg create mode 100644 src/assets/svg/touch-control-enable.svg create mode 100644 src/modules/game-bar/action-base.ts create mode 100644 src/modules/game-bar/action-screenshot.ts create mode 100644 src/modules/game-bar/action-touch-control.ts create mode 100644 src/modules/game-bar/game-bar.ts delete mode 100644 src/modules/screenshot.ts diff --git a/src/assets/css/game-bar.styl b/src/assets/css/game-bar.styl new file mode 100644 index 0000000..84cd586 --- /dev/null +++ b/src/assets/css/game-bar.styl @@ -0,0 +1,91 @@ +#bx-game-bar { + z-index: var(--bx-game-bar-z-index); + position: fixed; + left: 0; + bottom: 0; + width: 40px; + height: 90px; + overflow: visible; + cursor: pointer; + + > svg { + display: none; + pointer-events: none; + position: absolute; + height: 28px; + margin-top: 16px; + } + + @media (hover: hover) { + &:hover { + > svg { + display: block; + } + } + } + + .bx-game-bar-container { + opacity: 0; + position absolute; + display: flex; + overflow: hidden; + background: #1a1b1ee8; + border-radius: 0 10px 10px 0; + box-shadow: 0px 0px 6px #1c1c1c; + transition: opacity 0.1s ease-in; + + &.bx-show { + opacity: 1; + + + svg { + display: none !important; + } + } + + &.bx-hide { + opacity: 0; + } + + button { + width: 60px; + height: 60px; + + svg { + width: 28px; + height: 28px; + transition: transform 0.08s ease 0s; + } + + &:hover { + border-radius: 0; + } + + &:active { + svg { + transform: scale(0.75); + } + } + } + + /* Touch controller buttons */ + div[data-enabled] { + button { + display: none; + } + } + + /* Show disable button */ + div[data-enabled='true'] { + button:last-of-type { + display: block; + } + } + + /* Show enable button */ + div[data-enabled='false'] { + button:first-of-type { + display: block; + } + } + } +} diff --git a/src/assets/css/root.styl b/src/assets/css/root.styl index 4ac4a7e..56f5e13 100644 --- a/src/assets/css/root.styl +++ b/src/assets/css/root.styl @@ -27,8 +27,7 @@ --bx-stats-bar-z-index: 9001; --bx-stream-settings-z-index: 9000; --bx-mkb-pointer-lock-msg-z-index: 8999; - --bx-screenshot-z-index: 8888; - --bx-touch-controller-bar-z-index: 5555; + --bx-game-bar-z-index: 8888; --bx-wait-time-box-z-index: 100; } diff --git a/src/assets/css/stream-actions.styl b/src/assets/css/stream-actions.styl deleted file mode 100644 index db6d1de..0000000 --- a/src/assets/css/stream-actions.styl +++ /dev/null @@ -1,46 +0,0 @@ -.bx-screenshot-button { - display: none; - opacity: 0; - position: fixed; - bottom: 0; - box-sizing: border-box; - width: 60px; - height: 90px; - padding: 16px 16px 46px 16px; - background-size: cover; - background-repeat: no-repeat; - background-origin: content-box; - filter: drop-shadow(0 0 2px #000000B0); - transition: opacity 0.1s ease-in-out 0s, padding 0.1s ease-in 0s; - z-index: var(--bx-screenshot-z-index); - - /* Credit: https://phosphoricons.com */ - background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDMyIDMyIiBmaWxsPSIjZmZmIj48cGF0aCBkPSJNMjguMzA4IDUuMDM4aC00LjI2NWwtMi4wOTctMy4xNDVhMS4yMyAxLjIzIDAgMCAwLTEuMDIzLS41NDhoLTkuODQ2YTEuMjMgMS4yMyAwIDAgMC0xLjAyMy41NDhMNy45NTYgNS4wMzhIMy42OTJBMy43MSAzLjcxIDAgMCAwIDAgOC43MzF2MTcuMjMxYTMuNzEgMy43MSAwIDAgMCAzLjY5MiAzLjY5MmgyNC42MTVBMy43MSAzLjcxIDAgMCAwIDMyIDI1Ljk2MlY4LjczMWEzLjcxIDMuNzEgMCAwIDAtMy42OTItMy42OTJ6bS02Ljc2OSAxMS42OTJjMCAzLjAzOS0yLjUgNS41MzgtNS41MzggNS41MzhzLTUuNTM4LTIuNS01LjUzOC01LjUzOCAyLjUtNS41MzggNS41MzgtNS41MzggNS41MzggMi41IDUuNTM4IDUuNTM4eiIvPjwvc3ZnPgo='); - - &[data-showing=true] { - opacity: 0.9; - } - - &[data-capturing=true] { - padding: 8px 8px 38px 8px; - } -} - -.bx-screenshot-canvas { - display: none; -} - -#bx-touch-controller-bar { - display: none; - opacity: 0; - position: fixed; - bottom: 0; - left: 0; - right: 0; - height: 6vh; - z-index: var(--bx-touch-controller-bar-z-index); - - &[data-showing=true] { - display: block; - } -} diff --git a/src/assets/css/styles.styl b/src/assets/css/styles.styl index 475bea2..d9c6780 100644 --- a/src/assets/css/styles.styl +++ b/src/assets/css/styles.styl @@ -10,7 +10,7 @@ @import 'stream.styl'; @import 'number-stepper.styl'; -@import 'stream-actions.styl'; +@import 'game-bar.styl'; @import 'stream-stats.styl'; @import 'stream-settings.styl'; @import 'mkb.styl'; diff --git a/src/assets/svg/camera.svg b/src/assets/svg/camera.svg new file mode 100644 index 0000000..5be4147 --- /dev/null +++ b/src/assets/svg/camera.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/svg/caret-right.svg b/src/assets/svg/caret-right.svg new file mode 100644 index 0000000..804de29 --- /dev/null +++ b/src/assets/svg/caret-right.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/svg/touch-control-disable.svg b/src/assets/svg/touch-control-disable.svg new file mode 100644 index 0000000..a3a29d7 --- /dev/null +++ b/src/assets/svg/touch-control-disable.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/assets/svg/touch-control-enable.svg b/src/assets/svg/touch-control-enable.svg new file mode 100644 index 0000000..2764d91 --- /dev/null +++ b/src/assets/svg/touch-control-enable.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/index.ts b/src/index.ts index 899008f..be9ad9f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,7 +11,7 @@ import { StreamBadges } from "@modules/stream/stream-badges"; import { StreamStats } from "@modules/stream/stream-stats"; import { addCss } from "@utils/css"; import { Toast } from "@utils/toast"; -import { setupBxUi, updateVideoPlayerCss } from "@modules/ui/ui"; +import { setupStreamUi, updateVideoPlayerCss } from "@modules/ui/ui"; import { PrefKey, getPref } from "@utils/preferences"; import { LoadingScreen } from "@modules/loading-screen"; import { MouseCursorHider } from "@modules/mkb/mouse-cursor-hider"; @@ -27,6 +27,7 @@ import { patchAudioContext, patchCanvasContext, patchMeControl, patchRtcCodecs, import { STATES } from "@utils/global"; import { injectStreamMenuButtons } from "@modules/stream/stream-ui"; import { BxLogger } from "@utils/bx-logger"; +import { GameBar } from "./modules/game-bar/game-bar"; // Handle login page if (window.location.pathname.includes('/auth/msa')) { @@ -123,9 +124,7 @@ window.addEventListener(BxEvent.STREAM_LOADING, e => { } // Setup UI - setupBxUi(); - - + setupStreamUi(); }); // Setup loading screen @@ -148,32 +147,14 @@ window.addEventListener(BxEvent.STREAM_PLAYING, e => { STATES.isPlaying = true; injectStreamMenuButtons(); - /* - if (getPref(Preferences.CONTROLLER_ENABLE_SHORTCUTS)) { - GamepadHandler.startPolling(); - } - */ - const PREF_SCREENSHOT_BUTTON_POSITION = getPref(PrefKey.SCREENSHOT_BUTTON_POSITION); + GameBar.reset(); + GameBar.enable(); + GameBar.showBar(); + STATES.currentStream.$screenshotCanvas!.width = $video.videoWidth; STATES.currentStream.$screenshotCanvas!.height = $video.videoHeight; updateVideoPlayerCss(); - - // Setup screenshot button - if (PREF_SCREENSHOT_BUTTON_POSITION !== 'none') { - const $btn = document.querySelector('.bx-screenshot-button')! as HTMLElement; - $btn.classList.remove('bx-gone'); - $btn.style.display = 'block'; - - if (PREF_SCREENSHOT_BUTTON_POSITION === 'bottom-right') { - $btn.style.right = '0'; - } else { - $btn.style.left = '0'; - } - } - - const $touchControllerBar = document.getElementById('bx-touch-controller-bar'); - $touchControllerBar && $touchControllerBar.classList.remove('bx-gone'); }); window.addEventListener(BxEvent.STREAM_ERROR_PAGE, e => { @@ -199,13 +180,9 @@ window.addEventListener(BxEvent.STREAM_STOPPED, e => { STATES.currentStream.$video = null; StreamStats.onStoppedPlaying(); - const $screenshotBtn = document.querySelector('.bx-screenshot-button'); - if ($screenshotBtn) { - $screenshotBtn.removeAttribute('style'); - } - MouseCursorHider.stop(); TouchController.reset(); + GameBar.disable(); }); @@ -231,7 +208,7 @@ function main() { // Setup UI addCss(); Toast.setup(); - BX_FLAGS.PreloadUi && setupBxUi(); + BX_FLAGS.PreloadUi && setupStreamUi(); StreamBadges.setupEvents(); StreamStats.setupEvents(); diff --git a/src/modules/game-bar/action-base.ts b/src/modules/game-bar/action-base.ts new file mode 100644 index 0000000..ecd678e --- /dev/null +++ b/src/modules/game-bar/action-base.ts @@ -0,0 +1,6 @@ +export abstract class BaseGameBarAction { + constructor() {} + reset() {} + + abstract render(): HTMLElement; +} diff --git a/src/modules/game-bar/action-screenshot.ts b/src/modules/game-bar/action-screenshot.ts new file mode 100644 index 0000000..3d6121a --- /dev/null +++ b/src/modules/game-bar/action-screenshot.ts @@ -0,0 +1,78 @@ +import { BxEvent } from "@utils/bx-event"; +import { AppInterface, STATES } from "@utils/global"; +import { BxIcon } from "@utils/bx-icon"; +import { createButton, ButtonStyle, CE } from "@utils/html"; +import { BaseGameBarAction } from "./action-base"; +import { t } from "@utils/translation"; + +export class ScreenshotAction extends BaseGameBarAction { + $content: HTMLElement; + + constructor() { + super(); + + const currentStream = STATES.currentStream; + currentStream.$screenshotCanvas = CE('canvas', {'class': 'bx-gone'}); + document.documentElement.appendChild(currentStream.$screenshotCanvas!); + + const onClick = (e: Event) => { + BxEvent.dispatch(window, BxEvent.GAME_BAR_ACTION_ACTIVATED); + this.takeScreenshot(); + }; + + this.$content = createButton({ + style: ButtonStyle.GHOST, + icon: BxIcon.SCREENSHOT, + title: t('take-screenshot'), + onClick: onClick, + }); + } + + render(): HTMLElement { + return this.$content; + } + + takeScreenshot(callback?: any) { + const currentStream = STATES.currentStream; + const $video = currentStream.$video; + const $canvas = currentStream.$screenshotCanvas; + if (!$video || !$canvas) { + return; + } + + const $canvasContext = $canvas.getContext('2d', { + alpha: false, + willReadFrequently: false, + })!; + + $canvasContext.drawImage($video, 0, 0, $canvas.width, $canvas.height); + + // Get data URL and pass to parent app + if (AppInterface) { + const data = $canvas.toDataURL('image/png').split(';base64,')[1]; + AppInterface.saveScreenshot(currentStream.titleId, data); + + // Free screenshot from memory + $canvasContext.clearRect(0, 0, $canvas.width, $canvas.height); + + callback && callback(); + return; + } + + $canvas && $canvas.toBlob(blob => { + // Download screenshot + const now = +new Date; + const $anchor = CE('a', { + 'download': `${currentStream.titleId}-${now}.png`, + 'href': URL.createObjectURL(blob!), + }); + $anchor.click(); + + // Free screenshot from memory + URL.revokeObjectURL($anchor.href); + $canvasContext.clearRect(0, 0, $canvas.width, $canvas.height); + + callback && callback(); + }, 'image/png'); + } +} diff --git a/src/modules/game-bar/action-touch-control.ts b/src/modules/game-bar/action-touch-control.ts new file mode 100644 index 0000000..03bb13c --- /dev/null +++ b/src/modules/game-bar/action-touch-control.ts @@ -0,0 +1,51 @@ +import { BxEvent } from "@utils/bx-event"; +import { BxIcon } from "@utils/bx-icon"; +import { createButton, ButtonStyle, CE } from "@utils/html"; +import { TouchController } from "@modules/touch-controller"; +import { BaseGameBarAction } from "./action-base"; +import { t } from "@utils/translation"; + +export class TouchControlAction extends BaseGameBarAction { + $content: HTMLElement; + + constructor() { + super(); + + const onClick = (e: Event) => { + BxEvent.dispatch(window, BxEvent.GAME_BAR_ACTION_ACTIVATED); + + const $parent = (e as any).target.closest('div[data-enabled]'); + let enabled = $parent.getAttribute('data-enabled', 'true') === 'true'; + $parent.setAttribute('data-enabled', (!enabled).toString()); + + TouchController.toggleVisibility(enabled); + }; + + const $btnEnable = createButton({ + style: ButtonStyle.GHOST, + icon: BxIcon.TOUCH_CONTROL_ENABLE, + title: t('show-touch-controller'), + onClick: onClick, + }); + + const $btnDisable = createButton({ + style: ButtonStyle.GHOST, + icon: BxIcon.TOUCH_CONTROL_DISABLE, + title: t('hide-touch-controller'), + onClick: onClick, + }); + + this.$content = CE('div', {'data-enabled': 'true'}, + $btnEnable, + $btnDisable, + ); + } + + render(): HTMLElement { + return this.$content; + } + + reset(): void { + this.$content.setAttribute('data-enabled', 'true'); + } +} diff --git a/src/modules/game-bar/game-bar.ts b/src/modules/game-bar/game-bar.ts new file mode 100644 index 0000000..11e7ef4 --- /dev/null +++ b/src/modules/game-bar/game-bar.ts @@ -0,0 +1,116 @@ +import { CE, createSvgIcon } from "@utils/html"; +import { ScreenshotAction } from "./action-screenshot"; +import { TouchControlAction } from "./action-touch-control"; +import { BxEvent } from "@utils/bx-event"; +import { BxIcon } from "@utils/bx-icon"; +import type { BaseGameBarAction } from "./action-base"; +import { STATES } from "@utils/global"; +import { PrefKey, getPref } from "@utils/preferences"; + + +export class GameBar { + static readonly #VISIBLE_DURATION = 2000; + static #timeout: number | null; + + static #$gameBar: HTMLElement; + static #$container: HTMLElement; + + static #$actions: BaseGameBarAction[] = []; + + static #beginHideTimeout() { + GameBar.#clearHideTimeout(); + + GameBar.#timeout = window.setTimeout(() => { + GameBar.#timeout = null; + GameBar.hideBar(); + }, GameBar.#VISIBLE_DURATION); + } + + static #clearHideTimeout() { + GameBar.#timeout && clearTimeout(GameBar.#timeout); + GameBar.#timeout = null; + } + + static enable() { + GameBar.#$gameBar && GameBar.#$gameBar.classList.remove('bx-gone'); + } + + static disable() { + GameBar.#$gameBar && GameBar.#$gameBar.classList.add('bx-gone'); + GameBar.hideBar(); + } + + static showBar() { + if (!GameBar.#$container) { + return; + } + + GameBar.#$container.classList.remove('bx-offscreen', 'bx-hide'); + GameBar.#$container.classList.add('bx-show'); + + GameBar.#beginHideTimeout(); + } + + static hideBar() { + if (!GameBar.#$container) { + return; + } + + GameBar.#$container.classList.remove('bx-show'); + GameBar.#$container.classList.add('bx-hide'); + } + + // Reset all states + static reset() { + for (const action of GameBar.#$actions) { + action.reset(); + } + } + + static setup() { + let $container; + const $gameBar = CE('div', {id: 'bx-game-bar', class: 'bx-gone'}, + $container = CE('div', {class: 'bx-game-bar-container bx-offscreen'}), + createSvgIcon(BxIcon.CARET_RIGHT), + ); + + GameBar.#$actions = [ + new ScreenshotAction(), + ...(STATES.hasTouchSupport && (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) !== 'off') ? [new TouchControlAction()] : []), + ]; + + for (const action of GameBar.#$actions) { + $container.appendChild(action.render()); + } + + // Toggle game bar when clicking on the game bar box + $gameBar.addEventListener('click', e => { + if (e.target === $gameBar) { + if ($container.classList.contains('bx-show')) { + GameBar.hideBar(); + } else { + GameBar.showBar(); + } + } + }); + + // Hide game bar after clicking on an action + window.addEventListener(BxEvent.GAME_BAR_ACTION_ACTIVATED, GameBar.hideBar); + + $container.addEventListener('pointerover', GameBar.#clearHideTimeout); + $container.addEventListener('pointerout', GameBar.#beginHideTimeout); + + // Add animation when hiding game bar + $container.addEventListener('transitionend', e => { + const classList = $container.classList; + if (classList.contains('bx-hide')) { + classList.remove('bx-offscreen', 'bx-hide'); + classList.add('bx-offscreen'); + } + }); + + document.documentElement.appendChild($gameBar); + GameBar.#$gameBar = $gameBar; + GameBar.#$container = $container; + } +} diff --git a/src/modules/screenshot.ts b/src/modules/screenshot.ts deleted file mode 100644 index a02cb37..0000000 --- a/src/modules/screenshot.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { STATES, AppInterface } from "@utils/global"; -import { CE } from "@utils/html"; - -export function takeScreenshot(callback: any) { - const currentStream = STATES.currentStream!; - const $video = currentStream.$video; - const $canvas = currentStream.$screenshotCanvas; - if (!$video || !$canvas) { - return; - } - - const $canvasContext = $canvas.getContext('2d', { - alpha: false, - willReadFrequently: false, - })!; - - $canvasContext.drawImage($video, 0, 0, $canvas.width, $canvas.height); - - // Get data URL and pass to parent app - if (AppInterface) { - const data = $canvas.toDataURL('image/png').split(';base64,')[1]; - AppInterface.saveScreenshot(currentStream.titleId, data); - - // Free screenshot from memory - $canvasContext.clearRect(0, 0, $canvas.width, $canvas.height); - - callback && callback(); - return; - } - - $canvas && $canvas.toBlob(blob => { - // Download screenshot - const now = +new Date; - const $anchor = CE('a', { - 'download': `${currentStream.titleId}-${now}.png`, - 'href': URL.createObjectURL(blob!), - }); - $anchor.click(); - - // Free screenshot from memory - URL.revokeObjectURL($anchor.href); - $canvasContext.clearRect(0, 0, $canvas.width, $canvas.height); - - callback && callback(); - }, 'image/png'); -} - - -export function setupScreenshotButton() { - const currentStream = STATES.currentStream! - currentStream.$screenshotCanvas = CE('canvas', {'class': 'bx-screenshot-canvas'}); - document.documentElement.appendChild(currentStream.$screenshotCanvas!); - - const delay = 2000; - const $btn = CE('div', {'class': 'bx-screenshot-button', 'data-showing': false}); - - let timeout: number | null; - const detectDbClick = (e: MouseEvent) => { - if (!currentStream.$video) { - timeout = null; - $btn.style.display = 'none'; - return; - } - - if (timeout) { - clearTimeout(timeout); - timeout = null; - $btn.setAttribute('data-capturing', 'true'); - - takeScreenshot(() => { - // Hide button - $btn.setAttribute('data-showing', 'false'); - window.setTimeout(() => { - if (!timeout) { - $btn.setAttribute('data-capturing', 'false'); - } - }, 100); - }); - - return; - } - - const isShowing = $btn.getAttribute('data-showing') === 'true'; - if (!isShowing) { - // Show button - $btn.setAttribute('data-showing', 'true'); - $btn.setAttribute('data-capturing', 'false'); - - timeout && clearTimeout(timeout); - timeout = window.setTimeout(() => { - timeout = null; - $btn.setAttribute('data-showing', 'false'); - $btn.setAttribute('data-capturing', 'false'); - }, delay); - } - } - - $btn.addEventListener('mousedown', detectDbClick); - document.documentElement.appendChild($btn); -} diff --git a/src/modules/touch-controller.ts b/src/modules/touch-controller.ts index 466de00..b9ec819 100644 --- a/src/modules/touch-controller.ts +++ b/src/modules/touch-controller.ts @@ -1,5 +1,5 @@ import { STATES } from "@utils/global"; -import { CE, escapeHtml } from "@utils/html"; +import { escapeHtml } from "@utils/html"; import { Toast } from "@utils/toast"; import { BxEvent } from "@utils/bx-event"; import { BX_FLAGS } from "@utils/bx-flags"; @@ -12,7 +12,11 @@ const LOG_TAG = 'TouchController'; export class TouchController { static readonly #EVENT_SHOW_DEFAULT_CONTROLLER = new MessageEvent('message', { - data: '{"content":"{\\"layoutId\\":\\"\\"}","target":"/streaming/touchcontrols/showlayoutv2","type":"Message"}', + data: JSON.stringify({ + content: '{"layoutId":""}', + target: '/streaming/touchcontrols/showlayoutv2', + type: 'Message', + }), origin: 'better-xcloud', }); @@ -23,17 +27,17 @@ export class TouchController { }); */ - static #$bar: HTMLElement; static #$style: HTMLStyleElement; static #enable = false; - static #showing = false; static #dataChannel: RTCDataChannel | null; static #customLayouts: {[index: string]: any} = {}; static #baseCustomLayouts: {[index: string]: any} = {}; static #currentLayoutId: string; + static #customList: string[]; + static enable() { TouchController.#enable = true; } @@ -48,37 +52,28 @@ export class TouchController { static #showDefault() { TouchController.#dispatchMessage(TouchController.#EVENT_SHOW_DEFAULT_CONTROLLER); - TouchController.#showing = true; } static #show() { document.querySelector('#BabylonCanvasContainer-main')?.parentElement?.classList.remove('bx-offscreen'); - TouchController.#showing = true; } static #hide() { document.querySelector('#BabylonCanvasContainer-main')?.parentElement?.classList.add('bx-offscreen'); - TouchController.#showing = false; } - static #toggleVisibility() { + static toggleVisibility(status: boolean) { if (!TouchController.#dataChannel) { return; } - TouchController.#showing ? TouchController.#hide() : TouchController.#show(); - } - - static #toggleBar(value: boolean) { - TouchController.#$bar && TouchController.#$bar.setAttribute('data-showing', value.toString()); + status ? TouchController.#hide() : TouchController.#show(); } static reset() { TouchController.#enable = false; - TouchController.#showing = false; TouchController.#dataChannel = null; - TouchController.#$bar && TouchController.#$bar.removeAttribute('data-showing'); TouchController.#$style && (TouchController.#$style.textContent = ''); } @@ -195,15 +190,19 @@ export class TouchController { } static updateCustomList() { + const key = 'better_xcloud_custom_touch_layouts'; + TouchController.#customList = JSON.parse(window.localStorage.getItem(key) || '[]'); + NATIVE_FETCH('https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/touch-layouts/ids.json') .then(response => response.json()) .then(json => { - window.localStorage.setItem('better_xcloud_custom_touch_layouts', JSON.stringify(json)); + TouchController.#customList = json; + window.localStorage.setItem(key, JSON.stringify(json)); }); } static getCustomList(): string[] { - return JSON.parse(window.localStorage.getItem('better_xcloud_custom_touch_layouts') || '[]'); + return TouchController.#customList; } static setup() { @@ -223,32 +222,9 @@ export class TouchController { }); }; - const $fragment = document.createDocumentFragment(); const $style = document.createElement('style'); - $fragment.appendChild($style); + document.documentElement.appendChild($style); - const $bar = CE('div', {'id': 'bx-touch-controller-bar'}); - $fragment.appendChild($bar); - - document.documentElement.appendChild($fragment); - - // Setup double-tap event - let clickTimeout: number | null; - $bar.addEventListener('mousedown', (e: MouseEvent) => { - clickTimeout && clearTimeout(clickTimeout); - if (clickTimeout) { - // Double-clicked - clickTimeout = null; - TouchController.#toggleVisibility(); - return; - } - - clickTimeout = window.setTimeout(() => { - clickTimeout = null; - }, 400); - }); - - TouchController.#$bar = $bar; TouchController.#$style = $style; const PREF_STYLE_STANDARD = getPref(PrefKey.STREAM_TOUCH_CONTROLLER_STYLE_STANDARD); @@ -307,7 +283,6 @@ export class TouchController { try { if (msg.data.includes('/titleinfo')) { const json = JSON.parse(JSON.parse(msg.data).content); - TouchController.#toggleBar(json.focused); focused = json.focused; if (!json.focused) { diff --git a/src/modules/ui/global-settings.ts b/src/modules/ui/global-settings.ts index d3eec15..9fcf8f9 100644 --- a/src/modules/ui/global-settings.ts +++ b/src/modules/ui/global-settings.ts @@ -31,7 +31,6 @@ const SETTINGS_UI = { PrefKey.AUDIO_MIC_ON_PLAYING, PrefKey.STREAM_DISABLE_FEEDBACK_DIALOG, - PrefKey.SCREENSHOT_BUTTON_POSITION, PrefKey.SCREENSHOT_APPLY_FILTERS, PrefKey.AUDIO_ENABLE_VOLUME_CONTROL, diff --git a/src/modules/ui/ui.ts b/src/modules/ui/ui.ts index dfd527a..49218a7 100644 --- a/src/modules/ui/ui.ts +++ b/src/modules/ui/ui.ts @@ -5,11 +5,11 @@ import { UserAgent } from "@utils/user-agent"; import { BxEvent } from "@utils/bx-event"; import { MkbRemapper } from "@modules/mkb/mkb-remapper"; import { getPref, PrefKey, toPrefElement } from "@utils/preferences"; -import { setupScreenshotButton } from "@modules/screenshot"; import { StreamStats } from "@modules/stream/stream-stats"; import { TouchController } from "@modules/touch-controller"; import { t } from "@utils/translation"; import { VibrationManager } from "@modules/vibration-manager"; +import { GameBar } from "../game-bar/game-bar"; export function localRedirect(path: string) { @@ -468,13 +468,14 @@ div[data-testid="media-container"] { $elm.textContent = css; } -export function setupBxUi() { +export function setupStreamUi() { // Prevent initializing multiple times if (!document.querySelector('.bx-quick-settings-bar')) { window.addEventListener('resize', updateVideoPlayerCss); setupQuickSettingsBar(); - setupScreenshotButton(); StreamStats.render(); + + GameBar.setup(); } updateVideoPlayerCss(); diff --git a/src/utils/bx-event.ts b/src/utils/bx-event.ts index 6264dbe..023c34f 100644 --- a/src/utils/bx-event.ts +++ b/src/utils/bx-event.ts @@ -27,6 +27,8 @@ export enum BxEvent { XCLOUD_SERVERS_READY = 'bx-servers-ready', DATA_CHANNEL_CREATED = 'bx-data-channel-created', + + GAME_BAR_ACTION_ACTIVATED = 'bx-game-bar-action-activated', } export namespace BxEvent { diff --git a/src/utils/bx-exposed.ts b/src/utils/bx-exposed.ts index e63c35f..29a2f04 100644 --- a/src/utils/bx-exposed.ts +++ b/src/utils/bx-exposed.ts @@ -1,3 +1,4 @@ +import { GameBar } from "@modules/game-bar/game-bar"; import { BxEvent } from "@utils/bx-event"; import { STATES } from "@utils/global"; import { getPref, PrefKey } from "@utils/preferences"; @@ -15,25 +16,12 @@ enum InputType { export const BxExposed = { onPollingModeChanged: (mode: 'All' | 'None') => { if (!STATES.isPlaying) { - return false; + GameBar.disable(); + return; } - const $screenshotBtn = document.querySelector('.bx-screenshot-button'); - const $touchControllerBar = document.getElementById('bx-touch-controller-bar'); - - if (mode !== 'None') { - // Hide screenshot button - $screenshotBtn && $screenshotBtn.classList.add('bx-gone'); - - // Hide touch controller bar - $touchControllerBar && $touchControllerBar.classList.add('bx-gone'); - } else { - // Show screenshot button - $screenshotBtn && $screenshotBtn.classList.remove('bx-gone'); - - // Show touch controller bar - $touchControllerBar && $touchControllerBar.classList.remove('bx-gone'); - } + // Toggle Game bar + mode !== 'None' ? GameBar.disable() : GameBar.enable(); }, getTitleInfo: () => STATES.currentStream.titleInfo, diff --git a/src/utils/bx-icon.ts b/src/utils/bx-icon.ts index 862dfab..2e16174 100644 --- a/src/utils/bx-icon.ts +++ b/src/utils/bx-icon.ts @@ -1,3 +1,5 @@ +import iconCadetRight from "@assets/svg/caret-right.svg" with { type: "text" }; +import iconCamera from "@assets/svg/camera.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" }; @@ -11,6 +13,8 @@ import iconRemotePlay from "@assets/svg/remote-play.svg" with { type: "text" }; import iconStreamSettings from "@assets/svg/stream-settings.svg" with { type: "text" }; import iconStreamStats from "@assets/svg/stream-stats.svg" with { type: "text" }; import iconTrash from "@assets/svg/trash.svg" with { type: "text" }; +import iconTouchControlEnable from "@assets/svg/touch-control-enable.svg" with { type: "text" }; +import iconTouchControlDisable from "@assets/svg/touch-control-disable.svg" with { type: "text" }; export const BxIcon = { STREAM_SETTINGS: iconStreamSettings, @@ -28,5 +32,10 @@ export const BxIcon = { REMOTE_PLAY: iconRemotePlay, + CARET_RIGHT: iconCadetRight, + SCREENSHOT: iconCamera, + TOUCH_CONTROL_ENABLE: iconTouchControlEnable, + TOUCH_CONTROL_DISABLE: iconTouchControlDisable, + // HAND_TAP = '', } as const; diff --git a/src/utils/network.ts b/src/utils/network.ts index 1c7f23e..01f92ca 100644 --- a/src/utils/network.ts +++ b/src/utils/network.ts @@ -438,6 +438,9 @@ class XcloudInterceptor { overrides.inputConfiguration = overrides.inputConfiguration || {}; overrides.inputConfiguration.enableVibration = true; + overrides.videoConfiguration = overrides.videoConfiguration || {}; + overrides.videoConfiguration.setCodecPreferences = true; + // Enable touch controller if (TouchController.isEnabled()) { overrides.inputConfiguration.enableTouchInput = true; @@ -570,7 +573,9 @@ export function interceptHttpRequests() { const newCustomList = customList.map(item => ({ id: item })); obj.push(...newCustomList); - } catch (e) {} + } catch (e) { + console.log(e); + } } response.json = () => Promise.resolve(obj); diff --git a/src/utils/preferences.ts b/src/utils/preferences.ts index d86c1da..58717d3 100644 --- a/src/utils/preferences.ts +++ b/src/utils/preferences.ts @@ -46,7 +46,6 @@ export enum PrefKey { MKB_ABSOLUTE_MOUSE = 'mkb_absolute_mouse', MKB_DEFAULT_PRESET_ID = 'mkb_default_preset_id', - SCREENSHOT_BUTTON_POSITION = 'screenshot_button_position', SCREENSHOT_APPLY_FILTERS = 'screenshot_apply_filters', BLOCK_TRACKING = 'block_tracking', @@ -227,15 +226,6 @@ export class Preferences { default: false, }, - [PrefKey.SCREENSHOT_BUTTON_POSITION]: { - label: t('screenshot-button-position'), - default: 'bottom-left', - options: { - 'bottom-left': t('bottom-left'), - 'bottom-right': t('bottom-right'), - 'none': t('disable'), - }, - }, [PrefKey.SCREENSHOT_APPLY_FILTERS]: { label: t('screenshot-apply-filters'), default: false, diff --git a/src/utils/translation.ts b/src/utils/translation.ts index 2ed7624..f43bad5 100644 --- a/src/utils/translation.ts +++ b/src/utils/translation.ts @@ -23,7 +23,7 @@ const Texts = { "Activate", "Activo", , - , + "Abilita", "設定する", "활성화", "Aktywuj", @@ -40,7 +40,7 @@ const Texts = { "Activated", "Activado", , - , + "In uso", "設定中", "활성화 됨", "Aktywowane", @@ -57,7 +57,7 @@ const Texts = { "Active", "Activo", , - , + "Attivo", "有効", "활성화", "Aktywny", @@ -91,7 +91,7 @@ const Texts = { "Apply", "Aplicar", , - , + "Applica", "適用", , "Zastosuj", @@ -303,7 +303,7 @@ const Texts = { "Яркость", "Aydınlık", "Яскравість", - "Độ sáng", + "Độ sáng", "亮度", ], "browser-unsupported-feature": [ @@ -414,7 +414,7 @@ const Texts = { "Clear", "Borrar", , - , + "Pulisci", "消去", "비우기", "Wyczyść", @@ -448,7 +448,7 @@ const Texts = { "Combine audio & video streams", "Combinar flujos de audio y vídeo", , - , + "Combinare i flussi audio e video", "音声を映像ストリーミングと統合", , "Połącz strumienie audio i wideo", @@ -465,7 +465,7 @@ const Texts = { "May fix the laggy audio problem", "Puede arreglar el problema de audio con retraso", , - , + "Potrebbe risolvere il problema dell'audio irregolare", "音声の遅延を改善できる可能性があります", , "Może rozwiązać problem z zacinającym dźwiękiem", @@ -499,7 +499,7 @@ const Texts = { "Do you want to delete this preset?", "¿Desea eliminar este preajuste?", "Voulez-vous supprimer ce préréglage?", - , + "Vuoi eliminare questo profilo?", "このプリセットを削除しますか?", "이 프리셋을 삭제하시겠습니까?", "Czy na pewno chcesz usunąć ten szablon?", @@ -533,7 +533,7 @@ const Texts = { "Connected", "Conectado", , - , + "Connesso", "接続済み", , "Połączony", @@ -618,7 +618,7 @@ const Texts = { "Controller vibration", "Vibración del mando", , - , + "Vibrazione del controller", "コントローラーの振動", "컨트롤러 진동", "Wibracje kontrolera", @@ -635,7 +635,7 @@ const Texts = { "Copy", "Copiar", , - , + "Duplica", "コピー", "복사", "Kopiuj", @@ -669,7 +669,7 @@ const Texts = { "Deadzone counterweight", "Contrapeso de la zona muerta", , - , + "Compensazione della zona morta", "デッドゾーンのカウンターウエイト", , "Przeciwwaga martwej strefy", @@ -737,7 +737,7 @@ const Texts = { "Device vibration", "Vibración del dispositivo", , - , + "Vibrazione del dispositivo", "デバイスの振動", "기기 진동", "Wibracje urządzenia", @@ -754,7 +754,7 @@ const Texts = { "On when not using gamepad", "Activado cuando no se utiliza el mando", , - , + "Abilita quando non si usa un gamepad", "ゲームパッド未使用時にオン", "게임패드를 사용하지 않을 때", "Włączone, gdy nie używasz kontrolera", @@ -839,7 +839,7 @@ const Texts = { "Disabled", "Desactivado", , - , + "Disattivato", "無効", "비활성화됨", "Wyłączony", @@ -856,7 +856,7 @@ const Texts = { "Disconnected", "Desconectado", , - , + "Disconnesso", "切断", , "Rozłączony", @@ -907,7 +907,7 @@ const Texts = { "Enable local co-op support", "Habilitar soporte co-op local", , - , + "Abilita supporto cooperativo locale", "ローカルマルチプレイのサポートを有効化", , "Włącz lokalny co-op", @@ -924,7 +924,7 @@ const Texts = { "Only works if the game doesn't require a different profile", "Solo funciona si el juego no requiere un perfil diferente", , - , + "Funziona quando il gioco non richiede un profilo differente", "別アカウントでのサインインを必要としないゲームのみ動作します", , "Działa tylko wtedy, gdy gra nie wymaga innego profilu", @@ -1026,7 +1026,7 @@ const Texts = { "Enabled", "Activado", , - , + "Attivato", "有効", "활성화됨", "Włączony", @@ -1043,7 +1043,7 @@ const Texts = { "Experimental", "Experimental", , - , + "Sperimentale", "実験的機能", , "Eksperymentalne", @@ -1060,7 +1060,7 @@ const Texts = { "Export", "Exportar", , - , + "Esporta", "エクスポート(書出し)", "내보내기", "Eksportuj", @@ -1094,7 +1094,7 @@ const Texts = { "Allows playing STW mode on mobile", "Permitir jugar al modo STW en el móvil", , - , + "Consente di riprodurre la modalità Salva il Mondo sul cellulare", "モバイル版で「世界を救え」をプレイできるようになります", , "Zezwól na granie w tryb STW na urządzeniu mobilnym", @@ -1111,7 +1111,7 @@ const Texts = { "Fortnite: force console version", "Fortnite: forzar versión de consola", , - "Fortnite: Foza la versione console", + "Fortnite: forza la versione console", "Fortnite: 強制的にコンソール版を起動する", , "Fortnite: wymuś wersję konsolową", @@ -1145,7 +1145,7 @@ const Texts = { "Help", "Ayuda", , - , + "Guida", "ヘルプ", , "Pomoc", @@ -1179,7 +1179,7 @@ const Texts = { "Hide web page's scrollbar", "Oculta la barra de desplazamiento de la página", , - , + "Nascondi la barra di scorrimento della pagina web", "Webページのスクロールバーを隠す", , "Ukryj pasek przewijania strony", @@ -1207,13 +1207,30 @@ const Texts = { "Ẩn biểu tượng của menu Hệ thống", "隐藏系统菜单图标", ], + "hide-touch-controller": [ + "Touch-Controller ausblenden", + , + "Hide touch controller", + "Ocultar controles táctiles", + , + , + "タッチコントローラーを隠す", + , + , + , + "Скрыть сенсорный контроллер", + , + "Приховати сенсорний контролер", + "Ẩn bộ điều khiển cảm ứng", + , + ], "horizontal-sensitivity": [ "Horizontale Empfindlichkeit", "Sensitifitas horizontal", "Horizontal sensitivity", "Sensibilidad horizontal", , - , + "Sensibilità orizzontale", "左右方向の感度", , "Czułość pozioma", @@ -1230,7 +1247,7 @@ const Texts = { "Import", "Importar", , - , + "Importa", "インポート(読込み)", "가져오기", "Importuj", @@ -1247,7 +1264,7 @@ const Texts = { "Install Better xCloud app for Android", "Instale la aplicación Better xCloud para Android", , - , + "Installa l'applicazione Better xCloud per Android", "Android用のBetter xCloudをインストール", , "Zainstaluj aplikację Better xCloud na Androida", @@ -1264,7 +1281,7 @@ const Texts = { "Keyboard shortcuts", "Atajos del teclado", , - , + "Scorciatoie da tastiera", "キーボードショートカット", , "Skróty klawiszowe", @@ -1332,7 +1349,7 @@ const Texts = { "Left stick", "Joystick izquierdo", , - , + "Levetta sinistra", "左スティック", "왼쪽 스틱", "Lewy drążek analogowy", @@ -1366,7 +1383,7 @@ const Texts = { "Local co-op", "Co-op local", , - , + "Cooperativa locale", "ローカルマルチプレイ", , "Lokalna kooperacja", @@ -1383,7 +1400,7 @@ const Texts = { "Map mouse to", "Mapear ratón a", , - , + "Usa il mouse come", "マウスの割り当て", , "Przypisz myszkę do", @@ -1553,7 +1570,7 @@ const Texts = { "Name", "Nombre", , - , + "Nome", "名前", "이름", "Nazwa", @@ -1570,7 +1587,7 @@ const Texts = { "New", "Nuevo", , - , + "Nuovo", "新しい", "새로 만들기", "Nowy", @@ -1808,7 +1825,7 @@ const Texts = { "Preset", "Preajuste", , - , + "Profilo", "プリセット", "프리셋", "Szablon", @@ -1825,7 +1842,7 @@ const Texts = { "Press Esc to cancel", "Presione Esc para cancelar", , - , + "Premi Esc per annullare", "Escを押してキャンセル", "ESC를 눌러 취소", "Naciśnij Esc, aby anulować", @@ -1842,7 +1859,7 @@ const Texts = { (e: any) => `Press ${e.key} to toggle the Mouse and Keyboard feature`, (e: any) => `Pulsa ${e.key} para activar la función de ratón y teclado`, , - , + (e: any) => `Premi ${e.key} per attivare o disattivare la funzione Mouse e Tastiera`, (e: any) => `${e.key} キーでマウスとキーボードの機能を切り替える`, (e: any) => `${e.key} 키를 눌러 마우스와 키보드 기능을 활성화 하십시오`, (e: any) => `Naciśnij ${e.key}, aby przełączyć funkcję myszy i klawiatury`, @@ -1859,7 +1876,7 @@ const Texts = { "Press a key or do a mouse click to bind...", "Presione una tecla o haga un clic del ratón para enlazar...", , - , + "Premi un tasto o fai un clic del mouse per associare...", "キーを押すかマウスをクリックして割り当て...", "정지하려면 아무키나 마우스를 클릭해주세요...", "Naciśnij klawisz lub kliknij myszą, aby przypisać...", @@ -1876,7 +1893,7 @@ const Texts = { "Preset's name:", "Nombre del preajuste:", , - , + "Nome del profilo:", "プリセット名:", "프리셋 이름:", "Nazwa szablonu:", @@ -1961,7 +1978,7 @@ const Texts = { "Rename", "Renombrar", , - , + "Rinomina", "名前変更", "이름 바꾸기", "Zmień nazwę", @@ -1978,7 +1995,7 @@ const Texts = { "Right-click on a key to unbind it", "Clic derecho en una tecla para desvincularla", , - , + "Clic col tasto destro su una assegnazione per dissociarla", "右クリックで割り当て解除", "할당 해제하려면 키를 오른쪽 클릭하세요", "Kliknij prawym przyciskiem myszy na klawisz, aby anulować przypisanie", @@ -1995,7 +2012,7 @@ const Texts = { "Right stick", "Joystick derecho", , - , + "Levetta destra", "右スティック", "오른쪽 스틱", "Prawy drążek analogowy", @@ -2114,7 +2131,7 @@ const Texts = { "Save", "Guardar", , - , + "Conferma", "保存", "저장", "Zapisz", @@ -2131,7 +2148,7 @@ const Texts = { "Applies video filters to screenshots", "Aplica filtros de vídeo a las capturas de pantalla", , - , + "Applica filtri video agli screenshot", "スクリーンショットにビデオフィルターを適用", , "Stosuje filtry wideo do zrzutów ekranu", @@ -2165,7 +2182,7 @@ const Texts = { "Separate Touch controller & Controller #1", "Separar controlador táctil y controlador #1", , - , + "Controller su schermo e Controller #1 separati", "タッチコントローラーとコントローラー#1を分ける", , "Oddziel Kontroler dotykowy i Kontroler #1", @@ -2182,7 +2199,7 @@ const Texts = { "Touch controller is Player 1, Controller #1 is Player 2", "El controlador táctil es Jugador 1, Controlador #1 es Jugador 2", , - , + "Il Giocatore 1 userà il Controller su schermo, il Giocatore 2 userà il Controller #1", "タッチコントローラーがプレイヤー1、コントローラー#1がプレイヤー2に割り当てられます", , "Kontroler dotykowy to Gracz 1, Kontroler #1 to Gracz 2", @@ -2250,7 +2267,7 @@ const Texts = { "Shortcut keys", "Teclas de atajo", , - , + "Tasti di scelta rapida", "ショートカットキー", , "Skróty klawiszowe", @@ -2295,6 +2312,23 @@ const Texts = { "Hiển thị thông số khi vào game", "开始游戏时显示统计信息", ], + "show-touch-controller": [ + "Touch-Controller anzeigen", + , + "Show touch controller", + "Mostrar controles táctiles", + , + , + "タッチコントローラーを表示", + , + , + , + "Показать сенсорный контроллер", + , + "Показати сенсорний контролер", + "Hiện bộ điều khiển cảm ứng", + , + ], "show-wait-time": [ "Geschätzte Wartezeit anzeigen", "Tampilkan waktu antrian", @@ -2556,7 +2590,7 @@ const Texts = { "Stick decay minimum", "Disminuir mínimamente el analógico", , - , + "Tempo minimo di rilascio dello stick", "スティックの減衰の最小値", , "Minimalne opóźnienie drążka", @@ -2573,7 +2607,7 @@ const Texts = { "Stick decay strength", "Intensidad de decaimiento del analógico", , - , + "Velocità di rilascio dello stick", "スティックの減衰の強さ", , "Siła opóźnienia drążka", @@ -2624,7 +2658,7 @@ const Texts = { "Support Better xCloud", "Apoyar a Better xCloud", , - , + "Sostieni Better xCloud", "Better xCloudをサポート", , "Wesprzyj Better xCloud", @@ -2652,6 +2686,23 @@ const Texts = { "Hoán đổi nút", "交换按钮", ], + "take-screenshot": [ + "Screenshot aufnehmen", + , + "Take screenshot", + "Capturar pantalla", + , + , + "スクリーンショットを撮影", + , + , + , + "Сделать снимок экрана", + , + "Зробити знімок екрану", + "Lưu ảnh màn hình", + , + ], "target-resolution": [ "Festgelegte Auflösung", "Resolusi", @@ -2709,7 +2760,7 @@ const Texts = { "Off when controller found", "Desactivar cuando se encuentra el controlador", , - , + "Disabilitata quando un controllor viene rilevato", "コントローラー接続時に無効化", , "Wyłącz, gdy kontroler zostanie znaleziony", @@ -2756,11 +2807,11 @@ const Texts = { ], "tc-default-opacity": [ "Standard Deckkraft", - , + "Opasitas bawaan", "Default opacity", "Opacidad por defecto", , - , + "Opacità predefinita", "既定の透過度", , "Domyślna przezroczystość", @@ -2894,14 +2945,14 @@ const Texts = { (e: any) => `Touch-Steuerungslayout von ${e.name}`, , (e: any) => `Touch control layout by ${e.name}`, + (e: any) => `Disposición del control táctil por ${e.nombre}`, , - , - , + (e: any) => `Configurazione dei comandi su schermo creata da ${e.name}`, (e: any) => `タッチ操作レイアウト作成者: ${e.name}`, , (e: any) => `Układ sterowania dotykowego stworzony przez ${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}`, @@ -3015,7 +3066,7 @@ const Texts = { "Use mouse's absolute position", "Usar la posición absoluta del ratón", , - , + "Usa la posizione assoluta del mouse", "マウスの絶対座標を使用", "마우스 절대위치 사용", "Użyj pozycji bezwzględnej myszy", @@ -3066,7 +3117,7 @@ const Texts = { "Vibration intensity", "Intensidad de la vibración", , - , + "Intensità della vibrazione", "振動の強さ", "진동 세기", "Siła wibracji", @@ -3083,7 +3134,7 @@ const Texts = { "Vibration", "Vibración", , - , + "Vibrazione", "振動", , "Wibracje",