* 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
This commit is contained in:
redphx 2024-05-10 18:35:40 +07:00 committed by GitHub
parent b66ca192b2
commit b2e932cc4c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 533 additions and 315 deletions

View File

@ -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;
}
}
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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';

View File

@ -0,0 +1,6 @@
<svg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='#fff' fill-rule='evenodd' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 32 32'>
<g transform="matrix(.150985 0 0 .150985 -3.32603 -2.72209)" fill="none" stroke="#fff" stroke-width="16">
<path d="M208 208H48c-8.777 0-16-7.223-16-16V80c0-8.777 7.223-16 16-16h32l16-24h64l16 24h32c8.777 0 16 7.223 16 16v112c0 8.777-7.223 16-16 16z"/>
<circle cx="128" cy="132" r="36"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 494 B

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="100%" stroke="#fff" fill="#fff" height="100%" viewBox="0 0 13 23" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2">
<g transform="matrix(.399603 0 0 .709528 0 0)">
<path d="M1.601 2.727l23.255 13.097L1.601 28.922c-.977.55-.977 1.443 0 1.993s2.563.55 3.539 0l25.025-14.094c.977-.55.977-1.443 0-1.993L5.14.734c-.977-.55-2.563-.55-3.539 0s-.977 1.443 0 1.993z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 452 B

View File

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#fff" viewBox="0 0 32 32" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2">
<g fill="none" stroke="#fff">
<path d="M6.021 5.021l20 22" stroke-width="2"/>
<path d="M8.735 8.559H2.909a.89.89 0 0 0-.889.889v13.146a.89.89 0 0 0 .889.888h19.34m4.289 0h2.594a.89.89 0 0 0 .889-.888V9.448a.89.89 0 0 0-.889-.889H12.971" stroke-miterlimit="1.5" stroke-width="2.083"/>
</g>
<path d="M8.147 11.981l-.053-.001-.054.001c-.55.028-.988.483-.988 1.04v6c0 .575.467 1.042 1.042 1.042l.053-.001c.55-.028.988-.484.988-1.04v-6a1.04 1.04 0 0 0-.988-1.04z"/>
<path d="M11.147 14.981l-.054-.001h-6a1.04 1.04 0 1 0 0 2.083h6c.575 0 1.042-.467 1.042-1.042a1.04 1.04 0 0 0-.988-1.04z"/>
<circle cx="25.345" cy="18.582" r="2.561" fill="none" stroke="#fff" stroke-width="1.78" transform="matrix(1.17131 0 0 1.17131 -5.74235 -5.74456)"/>
</svg>

After

Width:  |  Height:  |  Size: 915 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#fff" viewBox="0 0 32 32" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2">
<path d="M30.021 9.448a.89.89 0 0 0-.889-.889H2.909a.89.89 0 0 0-.889.889v13.146a.89.89 0 0 0 .889.888h26.223a.89.89 0 0 0 .889-.888V9.448z" fill="none" stroke="#fff" stroke-width="2.083"/>
<path d="M8.147 11.981l-.053-.001-.054.001c-.55.028-.988.483-.988 1.04v6c0 .575.467 1.042 1.042 1.042l.053-.001c.55-.028.988-.484.988-1.04v-6a1.04 1.04 0 0 0-.988-1.04z"/>
<path d="M11.147 14.981l-.054-.001h-6a1.04 1.04 0 1 0 0 2.083h6c.575 0 1.042-.467 1.042-1.042a1.04 1.04 0 0 0-.988-1.04z"/>
<circle cx="25.345" cy="18.582" r="2.561" fill="none" stroke="#fff" stroke-width="1.78" transform="matrix(1.17131 0 0 1.17131 -5.74235 -5.74456)"/>
</svg>

After

Width:  |  Height:  |  Size: 796 B

View File

@ -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();

View File

@ -0,0 +1,6 @@
export abstract class BaseGameBarAction {
constructor() {}
reset() {}
abstract render(): HTMLElement;
}

View File

@ -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<HTMLAnchorElement>('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');
}
}

View File

@ -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');
}
}

View File

@ -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;
}
}

View File

@ -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<HTMLAnchorElement>('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);
}

View File

@ -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) {

View File

@ -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,

View File

@ -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();

View File

@ -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 {

View File

@ -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,

View File

@ -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 = '<path d="M6.537 8.906c0-4.216 3.469-7.685 7.685-7.685s7.685 3.469 7.685 7.685M7.719 30.778l-4.333-7.389C3.133 22.944 3 22.44 3 21.928a2.97 2.97 0 0 1 2.956-2.956 2.96 2.96 0 0 1 2.55 1.461l2.761 4.433V8.906a2.97 2.97 0 0 1 2.956-2.956 2.97 2.97 0 0 1 2.956 2.956v8.276a2.97 2.97 0 0 1 2.956-2.956 2.97 2.97 0 0 1 2.956 2.956v2.365a2.97 2.97 0 0 1 2.956-2.956A2.97 2.97 0 0 1 29 19.547v5.32c0 3.547-1.182 5.911-1.182 5.911"/>',
} as const;

View File

@ -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);

View File

@ -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,

View File

@ -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",