* 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-stats-bar-z-index: 9001;
--bx-stream-settings-z-index: 9000; --bx-stream-settings-z-index: 9000;
--bx-mkb-pointer-lock-msg-z-index: 8999; --bx-mkb-pointer-lock-msg-z-index: 8999;
--bx-screenshot-z-index: 8888; --bx-game-bar-z-index: 8888;
--bx-touch-controller-bar-z-index: 5555;
--bx-wait-time-box-z-index: 100; --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 'stream.styl';
@import 'number-stepper.styl'; @import 'number-stepper.styl';
@import 'stream-actions.styl'; @import 'game-bar.styl';
@import 'stream-stats.styl'; @import 'stream-stats.styl';
@import 'stream-settings.styl'; @import 'stream-settings.styl';
@import 'mkb.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 { StreamStats } from "@modules/stream/stream-stats";
import { addCss } from "@utils/css"; import { addCss } from "@utils/css";
import { Toast } from "@utils/toast"; 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 { PrefKey, getPref } from "@utils/preferences";
import { LoadingScreen } from "@modules/loading-screen"; import { LoadingScreen } from "@modules/loading-screen";
import { MouseCursorHider } from "@modules/mkb/mouse-cursor-hider"; import { MouseCursorHider } from "@modules/mkb/mouse-cursor-hider";
@ -27,6 +27,7 @@ import { patchAudioContext, patchCanvasContext, patchMeControl, patchRtcCodecs,
import { STATES } from "@utils/global"; import { STATES } from "@utils/global";
import { injectStreamMenuButtons } from "@modules/stream/stream-ui"; import { injectStreamMenuButtons } from "@modules/stream/stream-ui";
import { BxLogger } from "@utils/bx-logger"; import { BxLogger } from "@utils/bx-logger";
import { GameBar } from "./modules/game-bar/game-bar";
// Handle login page // Handle login page
if (window.location.pathname.includes('/auth/msa')) { if (window.location.pathname.includes('/auth/msa')) {
@ -123,9 +124,7 @@ window.addEventListener(BxEvent.STREAM_LOADING, e => {
} }
// Setup UI // Setup UI
setupBxUi(); setupStreamUi();
}); });
// Setup loading screen // Setup loading screen
@ -148,32 +147,14 @@ window.addEventListener(BxEvent.STREAM_PLAYING, e => {
STATES.isPlaying = true; STATES.isPlaying = true;
injectStreamMenuButtons(); 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!.width = $video.videoWidth;
STATES.currentStream.$screenshotCanvas!.height = $video.videoHeight; STATES.currentStream.$screenshotCanvas!.height = $video.videoHeight;
updateVideoPlayerCss(); 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 => { window.addEventListener(BxEvent.STREAM_ERROR_PAGE, e => {
@ -199,13 +180,9 @@ window.addEventListener(BxEvent.STREAM_STOPPED, e => {
STATES.currentStream.$video = null; STATES.currentStream.$video = null;
StreamStats.onStoppedPlaying(); StreamStats.onStoppedPlaying();
const $screenshotBtn = document.querySelector('.bx-screenshot-button');
if ($screenshotBtn) {
$screenshotBtn.removeAttribute('style');
}
MouseCursorHider.stop(); MouseCursorHider.stop();
TouchController.reset(); TouchController.reset();
GameBar.disable();
}); });
@ -231,7 +208,7 @@ function main() {
// Setup UI // Setup UI
addCss(); addCss();
Toast.setup(); Toast.setup();
BX_FLAGS.PreloadUi && setupBxUi(); BX_FLAGS.PreloadUi && setupStreamUi();
StreamBadges.setupEvents(); StreamBadges.setupEvents();
StreamStats.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 { STATES } from "@utils/global";
import { CE, escapeHtml } from "@utils/html"; import { escapeHtml } from "@utils/html";
import { Toast } from "@utils/toast"; import { Toast } from "@utils/toast";
import { BxEvent } from "@utils/bx-event"; import { BxEvent } from "@utils/bx-event";
import { BX_FLAGS } from "@utils/bx-flags"; import { BX_FLAGS } from "@utils/bx-flags";
@ -12,7 +12,11 @@ const LOG_TAG = 'TouchController';
export class TouchController { export class TouchController {
static readonly #EVENT_SHOW_DEFAULT_CONTROLLER = new MessageEvent('message', { 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', origin: 'better-xcloud',
}); });
@ -23,17 +27,17 @@ export class TouchController {
}); });
*/ */
static #$bar: HTMLElement;
static #$style: HTMLStyleElement; static #$style: HTMLStyleElement;
static #enable = false; static #enable = false;
static #showing = false;
static #dataChannel: RTCDataChannel | null; static #dataChannel: RTCDataChannel | null;
static #customLayouts: {[index: string]: any} = {}; static #customLayouts: {[index: string]: any} = {};
static #baseCustomLayouts: {[index: string]: any} = {}; static #baseCustomLayouts: {[index: string]: any} = {};
static #currentLayoutId: string; static #currentLayoutId: string;
static #customList: string[];
static enable() { static enable() {
TouchController.#enable = true; TouchController.#enable = true;
} }
@ -48,37 +52,28 @@ export class TouchController {
static #showDefault() { static #showDefault() {
TouchController.#dispatchMessage(TouchController.#EVENT_SHOW_DEFAULT_CONTROLLER); TouchController.#dispatchMessage(TouchController.#EVENT_SHOW_DEFAULT_CONTROLLER);
TouchController.#showing = true;
} }
static #show() { static #show() {
document.querySelector('#BabylonCanvasContainer-main')?.parentElement?.classList.remove('bx-offscreen'); document.querySelector('#BabylonCanvasContainer-main')?.parentElement?.classList.remove('bx-offscreen');
TouchController.#showing = true;
} }
static #hide() { static #hide() {
document.querySelector('#BabylonCanvasContainer-main')?.parentElement?.classList.add('bx-offscreen'); document.querySelector('#BabylonCanvasContainer-main')?.parentElement?.classList.add('bx-offscreen');
TouchController.#showing = false;
} }
static #toggleVisibility() { static toggleVisibility(status: boolean) {
if (!TouchController.#dataChannel) { if (!TouchController.#dataChannel) {
return; return;
} }
TouchController.#showing ? TouchController.#hide() : TouchController.#show(); status ? TouchController.#hide() : TouchController.#show();
}
static #toggleBar(value: boolean) {
TouchController.#$bar && TouchController.#$bar.setAttribute('data-showing', value.toString());
} }
static reset() { static reset() {
TouchController.#enable = false; TouchController.#enable = false;
TouchController.#showing = false;
TouchController.#dataChannel = null; TouchController.#dataChannel = null;
TouchController.#$bar && TouchController.#$bar.removeAttribute('data-showing');
TouchController.#$style && (TouchController.#$style.textContent = ''); TouchController.#$style && (TouchController.#$style.textContent = '');
} }
@ -195,15 +190,19 @@ export class TouchController {
} }
static updateCustomList() { 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') NATIVE_FETCH('https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/touch-layouts/ids.json')
.then(response => response.json()) .then(response => response.json())
.then(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[] { static getCustomList(): string[] {
return JSON.parse(window.localStorage.getItem('better_xcloud_custom_touch_layouts') || '[]'); return TouchController.#customList;
} }
static setup() { static setup() {
@ -223,32 +222,9 @@ export class TouchController {
}); });
}; };
const $fragment = document.createDocumentFragment();
const $style = document.createElement('style'); 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; TouchController.#$style = $style;
const PREF_STYLE_STANDARD = getPref(PrefKey.STREAM_TOUCH_CONTROLLER_STYLE_STANDARD); const PREF_STYLE_STANDARD = getPref(PrefKey.STREAM_TOUCH_CONTROLLER_STYLE_STANDARD);
@ -307,7 +283,6 @@ export class TouchController {
try { try {
if (msg.data.includes('/titleinfo')) { if (msg.data.includes('/titleinfo')) {
const json = JSON.parse(JSON.parse(msg.data).content); const json = JSON.parse(JSON.parse(msg.data).content);
TouchController.#toggleBar(json.focused);
focused = json.focused; focused = json.focused;
if (!json.focused) { if (!json.focused) {

View File

@ -31,7 +31,6 @@ const SETTINGS_UI = {
PrefKey.AUDIO_MIC_ON_PLAYING, PrefKey.AUDIO_MIC_ON_PLAYING,
PrefKey.STREAM_DISABLE_FEEDBACK_DIALOG, PrefKey.STREAM_DISABLE_FEEDBACK_DIALOG,
PrefKey.SCREENSHOT_BUTTON_POSITION,
PrefKey.SCREENSHOT_APPLY_FILTERS, PrefKey.SCREENSHOT_APPLY_FILTERS,
PrefKey.AUDIO_ENABLE_VOLUME_CONTROL, PrefKey.AUDIO_ENABLE_VOLUME_CONTROL,

View File

@ -5,11 +5,11 @@ import { UserAgent } from "@utils/user-agent";
import { BxEvent } from "@utils/bx-event"; import { BxEvent } from "@utils/bx-event";
import { MkbRemapper } from "@modules/mkb/mkb-remapper"; import { MkbRemapper } from "@modules/mkb/mkb-remapper";
import { getPref, PrefKey, toPrefElement } from "@utils/preferences"; import { getPref, PrefKey, toPrefElement } from "@utils/preferences";
import { setupScreenshotButton } from "@modules/screenshot";
import { StreamStats } from "@modules/stream/stream-stats"; import { StreamStats } from "@modules/stream/stream-stats";
import { TouchController } from "@modules/touch-controller"; import { TouchController } from "@modules/touch-controller";
import { t } from "@utils/translation"; import { t } from "@utils/translation";
import { VibrationManager } from "@modules/vibration-manager"; import { VibrationManager } from "@modules/vibration-manager";
import { GameBar } from "../game-bar/game-bar";
export function localRedirect(path: string) { export function localRedirect(path: string) {
@ -468,13 +468,14 @@ div[data-testid="media-container"] {
$elm.textContent = css; $elm.textContent = css;
} }
export function setupBxUi() { export function setupStreamUi() {
// Prevent initializing multiple times // Prevent initializing multiple times
if (!document.querySelector('.bx-quick-settings-bar')) { if (!document.querySelector('.bx-quick-settings-bar')) {
window.addEventListener('resize', updateVideoPlayerCss); window.addEventListener('resize', updateVideoPlayerCss);
setupQuickSettingsBar(); setupQuickSettingsBar();
setupScreenshotButton();
StreamStats.render(); StreamStats.render();
GameBar.setup();
} }
updateVideoPlayerCss(); updateVideoPlayerCss();

View File

@ -27,6 +27,8 @@ export enum BxEvent {
XCLOUD_SERVERS_READY = 'bx-servers-ready', XCLOUD_SERVERS_READY = 'bx-servers-ready',
DATA_CHANNEL_CREATED = 'bx-data-channel-created', DATA_CHANNEL_CREATED = 'bx-data-channel-created',
GAME_BAR_ACTION_ACTIVATED = 'bx-game-bar-action-activated',
} }
export namespace BxEvent { export namespace BxEvent {

View File

@ -1,3 +1,4 @@
import { GameBar } from "@modules/game-bar/game-bar";
import { BxEvent } from "@utils/bx-event"; import { BxEvent } from "@utils/bx-event";
import { STATES } from "@utils/global"; import { STATES } from "@utils/global";
import { getPref, PrefKey } from "@utils/preferences"; import { getPref, PrefKey } from "@utils/preferences";
@ -15,25 +16,12 @@ enum InputType {
export const BxExposed = { export const BxExposed = {
onPollingModeChanged: (mode: 'All' | 'None') => { onPollingModeChanged: (mode: 'All' | 'None') => {
if (!STATES.isPlaying) { if (!STATES.isPlaying) {
return false; GameBar.disable();
return;
} }
const $screenshotBtn = document.querySelector('.bx-screenshot-button'); // Toggle Game bar
const $touchControllerBar = document.getElementById('bx-touch-controller-bar'); mode !== 'None' ? GameBar.disable() : GameBar.enable();
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');
}
}, },
getTitleInfo: () => STATES.currentStream.titleInfo, 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 iconController from "@assets/svg/controller.svg" with { type: "text" };
import iconCopy from "@assets/svg/copy.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" }; 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 iconStreamSettings from "@assets/svg/stream-settings.svg" with { type: "text" };
import iconStreamStats from "@assets/svg/stream-stats.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 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 = { export const BxIcon = {
STREAM_SETTINGS: iconStreamSettings, STREAM_SETTINGS: iconStreamSettings,
@ -28,5 +32,10 @@ export const BxIcon = {
REMOTE_PLAY: iconRemotePlay, 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"/>', // 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; } as const;

View File

@ -438,6 +438,9 @@ class XcloudInterceptor {
overrides.inputConfiguration = overrides.inputConfiguration || {}; overrides.inputConfiguration = overrides.inputConfiguration || {};
overrides.inputConfiguration.enableVibration = true; overrides.inputConfiguration.enableVibration = true;
overrides.videoConfiguration = overrides.videoConfiguration || {};
overrides.videoConfiguration.setCodecPreferences = true;
// Enable touch controller // Enable touch controller
if (TouchController.isEnabled()) { if (TouchController.isEnabled()) {
overrides.inputConfiguration.enableTouchInput = true; overrides.inputConfiguration.enableTouchInput = true;
@ -570,7 +573,9 @@ export function interceptHttpRequests() {
const newCustomList = customList.map(item => ({ id: item })); const newCustomList = customList.map(item => ({ id: item }));
obj.push(...newCustomList); obj.push(...newCustomList);
} catch (e) {} } catch (e) {
console.log(e);
}
} }
response.json = () => Promise.resolve(obj); response.json = () => Promise.resolve(obj);

View File

@ -46,7 +46,6 @@ export enum PrefKey {
MKB_ABSOLUTE_MOUSE = 'mkb_absolute_mouse', MKB_ABSOLUTE_MOUSE = 'mkb_absolute_mouse',
MKB_DEFAULT_PRESET_ID = 'mkb_default_preset_id', MKB_DEFAULT_PRESET_ID = 'mkb_default_preset_id',
SCREENSHOT_BUTTON_POSITION = 'screenshot_button_position',
SCREENSHOT_APPLY_FILTERS = 'screenshot_apply_filters', SCREENSHOT_APPLY_FILTERS = 'screenshot_apply_filters',
BLOCK_TRACKING = 'block_tracking', BLOCK_TRACKING = 'block_tracking',
@ -227,15 +226,6 @@ export class Preferences {
default: false, 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]: { [PrefKey.SCREENSHOT_APPLY_FILTERS]: {
label: t('screenshot-apply-filters'), label: t('screenshot-apply-filters'),
default: false, default: false,

View File

@ -23,7 +23,7 @@ const Texts = {
"Activate", "Activate",
"Activo", "Activo",
, ,
, "Abilita",
"設定する", "設定する",
"활성화", "활성화",
"Aktywuj", "Aktywuj",
@ -40,7 +40,7 @@ const Texts = {
"Activated", "Activated",
"Activado", "Activado",
, ,
, "In uso",
"設定中", "設定中",
"활성화 됨", "활성화 됨",
"Aktywowane", "Aktywowane",
@ -57,7 +57,7 @@ const Texts = {
"Active", "Active",
"Activo", "Activo",
, ,
, "Attivo",
"有効", "有効",
"활성화", "활성화",
"Aktywny", "Aktywny",
@ -91,7 +91,7 @@ const Texts = {
"Apply", "Apply",
"Aplicar", "Aplicar",
, ,
, "Applica",
"適用", "適用",
, ,
"Zastosuj", "Zastosuj",
@ -303,7 +303,7 @@ const Texts = {
"Яркость", "Яркость",
"Aydınlık", "Aydınlık",
"Яскравість", "Яскравість",
ộ sáng", ộ sáng",
"亮度", "亮度",
], ],
"browser-unsupported-feature": [ "browser-unsupported-feature": [
@ -414,7 +414,7 @@ const Texts = {
"Clear", "Clear",
"Borrar", "Borrar",
, ,
, "Pulisci",
"消去", "消去",
"비우기", "비우기",
"Wyczyść", "Wyczyść",
@ -448,7 +448,7 @@ const Texts = {
"Combine audio & video streams", "Combine audio & video streams",
"Combinar flujos de audio y vídeo", "Combinar flujos de audio y vídeo",
, ,
, "Combinare i flussi audio e video",
"音声を映像ストリーミングと統合", "音声を映像ストリーミングと統合",
, ,
"Połącz strumienie audio i wideo", "Połącz strumienie audio i wideo",
@ -465,7 +465,7 @@ const Texts = {
"May fix the laggy audio problem", "May fix the laggy audio problem",
"Puede arreglar el problema de audio con retraso", "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", "Może rozwiązać problem z zacinającym dźwiękiem",
@ -499,7 +499,7 @@ const Texts = {
"Do you want to delete this preset?", "Do you want to delete this preset?",
"¿Desea eliminar este preajuste?", "¿Desea eliminar este preajuste?",
"Voulez-vous supprimer ce préréglage?", "Voulez-vous supprimer ce préréglage?",
, "Vuoi eliminare questo profilo?",
"このプリセットを削除しますか?", "このプリセットを削除しますか?",
"이 프리셋을 삭제하시겠습니까?", "이 프리셋을 삭제하시겠습니까?",
"Czy na pewno chcesz usunąć ten szablon?", "Czy na pewno chcesz usunąć ten szablon?",
@ -533,7 +533,7 @@ const Texts = {
"Connected", "Connected",
"Conectado", "Conectado",
, ,
, "Connesso",
"接続済み", "接続済み",
, ,
"Połączony", "Połączony",
@ -618,7 +618,7 @@ const Texts = {
"Controller vibration", "Controller vibration",
"Vibración del mando", "Vibración del mando",
, ,
, "Vibrazione del controller",
"コントローラーの振動", "コントローラーの振動",
"컨트롤러 진동", "컨트롤러 진동",
"Wibracje kontrolera", "Wibracje kontrolera",
@ -635,7 +635,7 @@ const Texts = {
"Copy", "Copy",
"Copiar", "Copiar",
, ,
, "Duplica",
"コピー", "コピー",
"복사", "복사",
"Kopiuj", "Kopiuj",
@ -669,7 +669,7 @@ const Texts = {
"Deadzone counterweight", "Deadzone counterweight",
"Contrapeso de la zona muerta", "Contrapeso de la zona muerta",
, ,
, "Compensazione della zona morta",
"デッドゾーンのカウンターウエイト", "デッドゾーンのカウンターウエイト",
, ,
"Przeciwwaga martwej strefy", "Przeciwwaga martwej strefy",
@ -737,7 +737,7 @@ const Texts = {
"Device vibration", "Device vibration",
"Vibración del dispositivo", "Vibración del dispositivo",
, ,
, "Vibrazione del dispositivo",
"デバイスの振動", "デバイスの振動",
"기기 진동", "기기 진동",
"Wibracje urządzenia", "Wibracje urządzenia",
@ -754,7 +754,7 @@ const Texts = {
"On when not using gamepad", "On when not using gamepad",
"Activado cuando no se utiliza el mando", "Activado cuando no se utiliza el mando",
, ,
, "Abilita quando non si usa un gamepad",
"ゲームパッド未使用時にオン", "ゲームパッド未使用時にオン",
"게임패드를 사용하지 않을 때", "게임패드를 사용하지 않을 때",
"Włączone, gdy nie używasz kontrolera", "Włączone, gdy nie używasz kontrolera",
@ -839,7 +839,7 @@ const Texts = {
"Disabled", "Disabled",
"Desactivado", "Desactivado",
, ,
, "Disattivato",
"無効", "無効",
"비활성화됨", "비활성화됨",
"Wyłączony", "Wyłączony",
@ -856,7 +856,7 @@ const Texts = {
"Disconnected", "Disconnected",
"Desconectado", "Desconectado",
, ,
, "Disconnesso",
"切断", "切断",
, ,
"Rozłączony", "Rozłączony",
@ -907,7 +907,7 @@ const Texts = {
"Enable local co-op support", "Enable local co-op support",
"Habilitar soporte co-op local", "Habilitar soporte co-op local",
, ,
, "Abilita supporto cooperativo locale",
"ローカルマルチプレイのサポートを有効化", "ローカルマルチプレイのサポートを有効化",
, ,
"Włącz lokalny co-op", "Włącz lokalny co-op",
@ -924,7 +924,7 @@ const Texts = {
"Only works if the game doesn't require a different profile", "Only works if the game doesn't require a different profile",
"Solo funciona si el juego no requiere un perfil diferente", "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", "Działa tylko wtedy, gdy gra nie wymaga innego profilu",
@ -1026,7 +1026,7 @@ const Texts = {
"Enabled", "Enabled",
"Activado", "Activado",
, ,
, "Attivato",
"有効", "有効",
"활성화됨", "활성화됨",
"Włączony", "Włączony",
@ -1043,7 +1043,7 @@ const Texts = {
"Experimental", "Experimental",
"Experimental", "Experimental",
, ,
, "Sperimentale",
"実験的機能", "実験的機能",
, ,
"Eksperymentalne", "Eksperymentalne",
@ -1060,7 +1060,7 @@ const Texts = {
"Export", "Export",
"Exportar", "Exportar",
, ,
, "Esporta",
"エクスポート(書出し)", "エクスポート(書出し)",
"내보내기", "내보내기",
"Eksportuj", "Eksportuj",
@ -1094,7 +1094,7 @@ const Texts = {
"Allows playing STW mode on mobile", "Allows playing STW mode on mobile",
"Permitir jugar al modo STW en el móvil", "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", "Zezwól na granie w tryb STW na urządzeniu mobilnym",
@ -1111,7 +1111,7 @@ const Texts = {
"Fortnite: force console version", "Fortnite: force console version",
"Fortnite: forzar versión de consola", "Fortnite: forzar versión de consola",
, ,
"Fortnite: Foza la versione console", "Fortnite: forza la versione console",
"Fortnite: 強制的にコンソール版を起動する", "Fortnite: 強制的にコンソール版を起動する",
, ,
"Fortnite: wymuś wersję konsolową", "Fortnite: wymuś wersję konsolową",
@ -1145,7 +1145,7 @@ const Texts = {
"Help", "Help",
"Ayuda", "Ayuda",
, ,
, "Guida",
"ヘルプ", "ヘルプ",
, ,
"Pomoc", "Pomoc",
@ -1179,7 +1179,7 @@ const Texts = {
"Hide web page's scrollbar", "Hide web page's scrollbar",
"Oculta la barra de desplazamiento de la página", "Oculta la barra de desplazamiento de la página",
, ,
, "Nascondi la barra di scorrimento della pagina web",
"Webページのスクロールバーを隠す", "Webページのスクロールバーを隠す",
, ,
"Ukryj pasek przewijania strony", "Ukryj pasek przewijania strony",
@ -1207,13 +1207,30 @@ const Texts = {
"Ẩn biểu tượng của menu Hệ thống", "Ẩ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": [ "horizontal-sensitivity": [
"Horizontale Empfindlichkeit", "Horizontale Empfindlichkeit",
"Sensitifitas horizontal", "Sensitifitas horizontal",
"Horizontal sensitivity", "Horizontal sensitivity",
"Sensibilidad horizontal", "Sensibilidad horizontal",
, ,
, "Sensibilità orizzontale",
"左右方向の感度", "左右方向の感度",
, ,
"Czułość pozioma", "Czułość pozioma",
@ -1230,7 +1247,7 @@ const Texts = {
"Import", "Import",
"Importar", "Importar",
, ,
, "Importa",
"インポート(読込み)", "インポート(読込み)",
"가져오기", "가져오기",
"Importuj", "Importuj",
@ -1247,7 +1264,7 @@ const Texts = {
"Install Better xCloud app for Android", "Install Better xCloud app for Android",
"Instale la aplicación Better xCloud para Android", "Instale la aplicación Better xCloud para Android",
, ,
, "Installa l'applicazione Better xCloud per Android",
"Android用のBetter xCloudをインストール", "Android用のBetter xCloudをインストール",
, ,
"Zainstaluj aplikację Better xCloud na Androida", "Zainstaluj aplikację Better xCloud na Androida",
@ -1264,7 +1281,7 @@ const Texts = {
"Keyboard shortcuts", "Keyboard shortcuts",
"Atajos del teclado", "Atajos del teclado",
, ,
, "Scorciatoie da tastiera",
"キーボードショートカット", "キーボードショートカット",
, ,
"Skróty klawiszowe", "Skróty klawiszowe",
@ -1332,7 +1349,7 @@ const Texts = {
"Left stick", "Left stick",
"Joystick izquierdo", "Joystick izquierdo",
, ,
, "Levetta sinistra",
"左スティック", "左スティック",
"왼쪽 스틱", "왼쪽 스틱",
"Lewy drążek analogowy", "Lewy drążek analogowy",
@ -1366,7 +1383,7 @@ const Texts = {
"Local co-op", "Local co-op",
"Co-op local", "Co-op local",
, ,
, "Cooperativa locale",
"ローカルマルチプレイ", "ローカルマルチプレイ",
, ,
"Lokalna kooperacja", "Lokalna kooperacja",
@ -1383,7 +1400,7 @@ const Texts = {
"Map mouse to", "Map mouse to",
"Mapear ratón a", "Mapear ratón a",
, ,
, "Usa il mouse come",
"マウスの割り当て", "マウスの割り当て",
, ,
"Przypisz myszkę do", "Przypisz myszkę do",
@ -1553,7 +1570,7 @@ const Texts = {
"Name", "Name",
"Nombre", "Nombre",
, ,
, "Nome",
"名前", "名前",
"이름", "이름",
"Nazwa", "Nazwa",
@ -1570,7 +1587,7 @@ const Texts = {
"New", "New",
"Nuevo", "Nuevo",
, ,
, "Nuovo",
"新しい", "新しい",
"새로 만들기", "새로 만들기",
"Nowy", "Nowy",
@ -1808,7 +1825,7 @@ const Texts = {
"Preset", "Preset",
"Preajuste", "Preajuste",
, ,
, "Profilo",
"プリセット", "プリセット",
"프리셋", "프리셋",
"Szablon", "Szablon",
@ -1825,7 +1842,7 @@ const Texts = {
"Press Esc to cancel", "Press Esc to cancel",
"Presione Esc para cancelar", "Presione Esc para cancelar",
, ,
, "Premi Esc per annullare",
"Escを押してキャンセル", "Escを押してキャンセル",
"ESC를 눌러 취소", "ESC를 눌러 취소",
"Naciśnij Esc, aby anulować", "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) => `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) => `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) => `${e.key} 키를 눌러 마우스와 키보드 기능을 활성화 하십시오`, (e: any) => `${e.key} 키를 눌러 마우스와 키보드 기능을 활성화 하십시오`,
(e: any) => `Naciśnij ${e.key}, aby przełączyć funkcję myszy i klawiatury`, (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...", "Press a key or do a mouse click to bind...",
"Presione una tecla o haga un clic del ratón para enlazar...", "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ć...", "Naciśnij klawisz lub kliknij myszą, aby przypisać...",
@ -1876,7 +1893,7 @@ const Texts = {
"Preset's name:", "Preset's name:",
"Nombre del preajuste:", "Nombre del preajuste:",
, ,
, "Nome del profilo:",
"プリセット名:", "プリセット名:",
"프리셋 이름:", "프리셋 이름:",
"Nazwa szablonu:", "Nazwa szablonu:",
@ -1961,7 +1978,7 @@ const Texts = {
"Rename", "Rename",
"Renombrar", "Renombrar",
, ,
, "Rinomina",
"名前変更", "名前変更",
"이름 바꾸기", "이름 바꾸기",
"Zmień nazwę", "Zmień nazwę",
@ -1978,7 +1995,7 @@ const Texts = {
"Right-click on a key to unbind it", "Right-click on a key to unbind it",
"Clic derecho en una tecla para desvincularla", "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", "Kliknij prawym przyciskiem myszy na klawisz, aby anulować przypisanie",
@ -1995,7 +2012,7 @@ const Texts = {
"Right stick", "Right stick",
"Joystick derecho", "Joystick derecho",
, ,
, "Levetta destra",
"右スティック", "右スティック",
"오른쪽 스틱", "오른쪽 스틱",
"Prawy drążek analogowy", "Prawy drążek analogowy",
@ -2114,7 +2131,7 @@ const Texts = {
"Save", "Save",
"Guardar", "Guardar",
, ,
, "Conferma",
"保存", "保存",
"저장", "저장",
"Zapisz", "Zapisz",
@ -2131,7 +2148,7 @@ const Texts = {
"Applies video filters to screenshots", "Applies video filters to screenshots",
"Aplica filtros de vídeo a las capturas de pantalla", "Aplica filtros de vídeo a las capturas de pantalla",
, ,
, "Applica filtri video agli screenshot",
"スクリーンショットにビデオフィルターを適用", "スクリーンショットにビデオフィルターを適用",
, ,
"Stosuje filtry wideo do zrzutów ekranu", "Stosuje filtry wideo do zrzutów ekranu",
@ -2165,7 +2182,7 @@ const Texts = {
"Separate Touch controller & Controller #1", "Separate Touch controller & Controller #1",
"Separar controlador táctil y controlador #1", "Separar controlador táctil y controlador #1",
, ,
, "Controller su schermo e Controller #1 separati",
"タッチコントローラーとコントローラー#1を分ける", "タッチコントローラーとコントローラー#1を分ける",
, ,
"Oddziel Kontroler dotykowy i Kontroler #1", "Oddziel Kontroler dotykowy i Kontroler #1",
@ -2182,7 +2199,7 @@ const Texts = {
"Touch controller is Player 1, Controller #1 is Player 2", "Touch controller is Player 1, Controller #1 is Player 2",
"El controlador táctil es Jugador 1, Controlador #1 es Jugador 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に割り当てられます", "タッチコントローラーがプレイヤー1、コントローラー#1がプレイヤー2に割り当てられます",
, ,
"Kontroler dotykowy to Gracz 1, Kontroler #1 to Gracz 2", "Kontroler dotykowy to Gracz 1, Kontroler #1 to Gracz 2",
@ -2250,7 +2267,7 @@ const Texts = {
"Shortcut keys", "Shortcut keys",
"Teclas de atajo", "Teclas de atajo",
, ,
, "Tasti di scelta rapida",
"ショートカットキー", "ショートカットキー",
, ,
"Skróty klawiszowe", "Skróty klawiszowe",
@ -2295,6 +2312,23 @@ const Texts = {
"Hiển thị thông số khi vào game", "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": [ "show-wait-time": [
"Geschätzte Wartezeit anzeigen", "Geschätzte Wartezeit anzeigen",
"Tampilkan waktu antrian", "Tampilkan waktu antrian",
@ -2556,7 +2590,7 @@ const Texts = {
"Stick decay minimum", "Stick decay minimum",
"Disminuir mínimamente el analógico", "Disminuir mínimamente el analógico",
, ,
, "Tempo minimo di rilascio dello stick",
"スティックの減衰の最小値", "スティックの減衰の最小値",
, ,
"Minimalne opóźnienie drążka", "Minimalne opóźnienie drążka",
@ -2573,7 +2607,7 @@ const Texts = {
"Stick decay strength", "Stick decay strength",
"Intensidad de decaimiento del analógico", "Intensidad de decaimiento del analógico",
, ,
, "Velocità di rilascio dello stick",
"スティックの減衰の強さ", "スティックの減衰の強さ",
, ,
"Siła opóźnienia drążka", "Siła opóźnienia drążka",
@ -2624,7 +2658,7 @@ const Texts = {
"Support Better xCloud", "Support Better xCloud",
"Apoyar a Better xCloud", "Apoyar a Better xCloud",
, ,
, "Sostieni Better xCloud",
"Better xCloudをサポート", "Better xCloudをサポート",
, ,
"Wesprzyj Better xCloud", "Wesprzyj Better xCloud",
@ -2652,6 +2686,23 @@ const Texts = {
"Hoán đổi nút", "Hoán đổi nút",
"交换按钮", "交换按钮",
], ],
"take-screenshot": [
"Screenshot aufnehmen",
,
"Take screenshot",
"Capturar pantalla",
,
,
"スクリーンショットを撮影",
,
,
,
"Сделать снимок экрана",
,
"Зробити знімок екрану",
"Lưu ảnh màn hình",
,
],
"target-resolution": [ "target-resolution": [
"Festgelegte Auflösung", "Festgelegte Auflösung",
"Resolusi", "Resolusi",
@ -2709,7 +2760,7 @@ const Texts = {
"Off when controller found", "Off when controller found",
"Desactivar cuando se encuentra el controlador", "Desactivar cuando se encuentra el controlador",
, ,
, "Disabilitata quando un controllor viene rilevato",
"コントローラー接続時に無効化", "コントローラー接続時に無効化",
, ,
"Wyłącz, gdy kontroler zostanie znaleziony", "Wyłącz, gdy kontroler zostanie znaleziony",
@ -2756,11 +2807,11 @@ const Texts = {
], ],
"tc-default-opacity": [ "tc-default-opacity": [
"Standard Deckkraft", "Standard Deckkraft",
, "Opasitas bawaan",
"Default opacity", "Default opacity",
"Opacidad por defecto", "Opacidad por defecto",
, ,
, "Opacità predefinita",
"既定の透過度", "既定の透過度",
, ,
"Domyślna przezroczystość", "Domyślna przezroczystość",
@ -2894,14 +2945,14 @@ const Texts = {
(e: any) => `Touch-Steuerungslayout von ${e.name}`, (e: any) => `Touch-Steuerungslayout von ${e.name}`,
, ,
(e: any) => `Touch control layout by ${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) => `タッチ操作レイアウト作成者: ${e.name}`,
, ,
(e: any) => `Układ sterowania dotykowego stworzony przez ${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} kişisinin dokunmatik kontrolcü tuş şeması`,
(e: any) => `Розташування сенсорного керування від ${e.name}`, (e: any) => `Розташування сенсорного керування від ${e.name}`,
(e: any) => `Bố cục điều khiển cảm ứng tạo bởi ${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", "Use mouse's absolute position",
"Usar la posición absoluta del ratón", "Usar la posición absoluta del ratón",
, ,
, "Usa la posizione assoluta del mouse",
"マウスの絶対座標を使用", "マウスの絶対座標を使用",
"마우스 절대위치 사용", "마우스 절대위치 사용",
"Użyj pozycji bezwzględnej myszy", "Użyj pozycji bezwzględnej myszy",
@ -3066,7 +3117,7 @@ const Texts = {
"Vibration intensity", "Vibration intensity",
"Intensidad de la vibración", "Intensidad de la vibración",
, ,
, "Intensità della vibrazione",
"振動の強さ", "振動の強さ",
"진동 세기", "진동 세기",
"Siła wibracji", "Siła wibracji",
@ -3083,7 +3134,7 @@ const Texts = {
"Vibration", "Vibration",
"Vibración", "Vibración",
, ,
, "Vibrazione",
"振動", "振動",
, ,
"Wibracje", "Wibracje",