mirror of
https://github.com/redphx/better-xcloud.git
synced 2025-06-06 23:57:19 +02:00
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
This commit is contained in:
parent
b66ca192b2
commit
b2e932cc4c
91
src/assets/css/game-bar.styl
Normal file
91
src/assets/css/game-bar.styl
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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';
|
||||
|
6
src/assets/svg/camera.svg
Normal file
6
src/assets/svg/camera.svg
Normal 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 |
5
src/assets/svg/caret-right.svg
Normal file
5
src/assets/svg/caret-right.svg
Normal 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 |
9
src/assets/svg/touch-control-disable.svg
Normal file
9
src/assets/svg/touch-control-disable.svg
Normal 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 |
6
src/assets/svg/touch-control-enable.svg
Normal file
6
src/assets/svg/touch-control-enable.svg
Normal 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 |
41
src/index.ts
41
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();
|
||||
|
6
src/modules/game-bar/action-base.ts
Normal file
6
src/modules/game-bar/action-base.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export abstract class BaseGameBarAction {
|
||||
constructor() {}
|
||||
reset() {}
|
||||
|
||||
abstract render(): HTMLElement;
|
||||
}
|
78
src/modules/game-bar/action-screenshot.ts
Normal file
78
src/modules/game-bar/action-screenshot.ts
Normal 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');
|
||||
}
|
||||
}
|
51
src/modules/game-bar/action-touch-control.ts
Normal file
51
src/modules/game-bar/action-touch-control.ts
Normal 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');
|
||||
}
|
||||
}
|
116
src/modules/game-bar/game-bar.ts
Normal file
116
src/modules/game-bar/game-bar.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
|
@ -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",
|
||||
|
Loading…
x
Reference in New Issue
Block a user