Compare commits

..

37 Commits

Author SHA1 Message Date
df2af43c64 Bump version to 4.1.1 2024-05-04 17:01:57 +07:00
fca3bee6dd Update dist 2024-05-04 16:20:38 +07:00
9bf8a2ef66 Add a standalone Refresh stream button (#315) 2024-05-04 16:20:12 +07:00
b1df189c7d Support "styles" param in touch control 2024-05-04 15:33:19 +07:00
d91fdb798e Update dist 2024-05-04 15:15:15 +07:00
a291443d43 Update dist 2024-05-04 15:01:49 +07:00
8a7be5d523 Add ability to use normal website's layout on TV 2024-05-04 15:00:27 +07:00
7588f37472 Add "Smart TV" + "Meta Quest VR" User-Agent profiles 2024-05-04 14:52:54 +07:00
a597d52585 Update dist 2024-05-04 14:34:19 +07:00
f945a3adde Move PatcherCache.init() to patchFunctionBind() 2024-05-04 14:34:06 +07:00
438afe086a Update dist 2024-05-04 11:23:19 +07:00
f6ee79770c Clear PatcherCache when changing settings 2024-05-04 11:22:42 +07:00
f36c77e727 Fix touch control not showing when using "caches" as variable name 2024-05-04 11:21:18 +07:00
176e86c9bb Bump version to 4.1.0 2024-05-03 18:24:52 +07:00
ddb8628e57 Update dist 2024-05-03 18:09:43 +07:00
f144fac81e Fix not clearing "appliedPatches" correctly 2024-05-03 18:09:31 +07:00
07a4034cc1 Fix screenshot button now working after switching game 2024-05-03 18:05:59 +07:00
d30efb2bed Update dist 2024-05-03 17:29:14 +07:00
3670946da4 Refactor BxIcon 2024-05-03 17:27:13 +07:00
3d3a013a5c Disable achievement toast in Remote Play 2024-05-03 17:17:00 +07:00
db1ce23b53 Refactor Dialog 2024-05-03 17:06:10 +07:00
bcd61833b2 Fix touch controller not working in Remote Play 2024-05-03 17:05:58 +07:00
ba0ccf5213 Fix disabling social features also disables notifications 2024-05-03 17:05:30 +07:00
6ff81971b0 Don't refresh the page when signature has changed 2024-05-03 08:55:55 +07:00
ea57e04d4f Add PatcherCache class 2024-05-03 07:36:03 +07:00
43e6f3083e Update dist 2024-05-02 17:54:24 +07:00
e5ab7c93f9 Fix imports 2024-05-02 17:53:17 +07:00
06c6b8c5af Simplify import paths again 2024-05-01 22:14:28 +07:00
0114108bdf Simplify import paths 2024-05-01 22:01:39 +07:00
006e21f477 Replace DualEnum with normal enum 2024-05-01 21:36:17 +07:00
7883949b94 Update dist 2024-05-01 18:57:58 +07:00
17afd364da Refactor icons 2024-05-01 18:57:50 +07:00
29a1fa9f10 Update IPv6's priority value 2024-05-01 18:31:53 +07:00
594c9d3f2e Update dist 2024-04-30 20:29:09 +07:00
edc8991a6a Create BxLogger to show colored logs 2024-04-30 18:52:56 +07:00
26c318fb8d Format code 2024-04-30 18:24:58 +07:00
9f0097fd8c Handle xHome's /play request 2024-04-30 18:18:02 +07:00
56 changed files with 1596 additions and 1249 deletions

View File

@ -1,5 +1,5 @@
// ==UserScript==
// @name Better xCloud
// @namespace https://github.com/redphx
// @version 4.0.1
// @version 4.1.1
// ==/UserScript==

File diff suppressed because it is too large Load Diff

View File

@ -7,3 +7,11 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
background-color: #2d2d2d !important;
color: #000 !important;
}
.bx-stream-refresh-button {
top: calc(env(safe-area-inset-top, 0px) + 10px + 50px) !important;
}
body[data-media-type=tv] .bx-stream-refresh-button {
top: calc(var(--gds-focus-borderSize) + 80px) !important;
}

View File

@ -0,0 +1,3 @@
<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'>
<path d='M19.193 12.807h3.193m-13.836 0h4.257'/><path d='M10.678 10.678v4.257'/><path d='M13.061 19.193l-5.602 6.359c-.698.698-1.646 1.09-2.633 1.09-2.044 0-3.725-1.682-3.725-3.725a3.73 3.73 0 0 1 .056-.646l2.177-11.194a6.94 6.94 0 0 1 6.799-5.721h11.722c3.795 0 6.918 3.123 6.918 6.918s-3.123 6.918-6.918 6.918h-8.793z'/><path d='M18.939 19.193l5.602 6.359c.698.698 1.646 1.09 2.633 1.09 2.044 0 3.725-1.682 3.725-3.725a3.73 3.73 0 0 0-.056-.646l-2.177-11.194'/>
</svg>

After

Width:  |  Height:  |  Size: 646 B

3
src/assets/svg/copy.svg Normal file
View File

@ -0,0 +1,3 @@
<svg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='#fff' fill-rule='evenodd' stroke-linecap='round' stroke-linejoin='round' stroke-width='4' viewBox='0 0 32 32'>
<path d='M1.498 6.772h23.73v23.73H1.498zm5.274-5.274h23.73v23.73'/>
</svg>

After

Width:  |  Height:  |  Size: 250 B

View File

@ -0,0 +1,3 @@
<svg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='#fff' fill-rule='evenodd' stroke-linecap='round' stroke-linejoin='round' stroke-width='4' viewBox='0 0 32 32'>
<path d='M16 7.3a5.83 5.83 0 0 1 5.8-5.8h2.9m0 29h-2.9a5.83 5.83 0 0 1-5.8-5.8'/><path d='M7.3 30.5h2.9a5.83 5.83 0 0 0 5.8-5.8V7.3a5.83 5.83 0 0 0-5.8-5.8H7.3'/><path d='M11.65 16h8.7'/>
</svg>

After

Width:  |  Height:  |  Size: 370 B

View File

@ -0,0 +1,3 @@
<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'>
<path d='M1.238 21.119c0 1.928 1.565 3.493 3.493 3.493H27.27c1.928 0 3.493-1.565 3.493-3.493V5.961c0-1.928-1.565-3.493-3.493-3.493H4.731c-1.928 0-3.493 1.565-3.493 3.493v15.158zm19.683 8.413H11.08'/>
</svg>

After

Width:  |  Height:  |  Size: 382 B

View File

@ -0,0 +1,3 @@
<svg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='#fff' fill-rule='evenodd' stroke-linecap='round' stroke-linejoin='round' stroke-width='4' viewBox='0 0 32 32'>
<g transform='matrix(1.10403 0 0 1.10403 -4.17656 -.560429)' fill='none' stroke='#fff'><g stroke-width='1.755'><path d='M24.49 16.255l.01-8.612A6.15 6.15 0 0 0 18.357 1.5h-5.714A6.15 6.15 0 0 0 6.5 7.643v13.715a6.15 6.15 0 0 0 6.143 6.143h5.714'/><path d='M15.5 12.501v-6'/></g><circle cx='48' cy='48' r='15' stroke-width='7.02' transform='matrix(.142357 0 0 .142357 17.667421 16.541885)'/><path d='M24.61 27.545h-.214l-1.711.955c-.666-.224-1.284-.572-1.821-1.025l-.006-1.922-.107-.182-1.701-.969c-.134-.678-.134-1.375 0-2.053l1.7-.966.107-.182.009-1.922c.537-.454 1.154-.803 1.82-1.029l1.708.955h.214l1.708-.955c.666.224 1.284.572 1.821 1.025l.006 1.922.107.182 1.7.968c.134.678.134 1.375 0 2.053l-1.7.966-.107.182-.009 1.922c-.536.455-1.154.804-1.819 1.029l-1.706-.955z' stroke-width='.999'/></g>
</svg>

After

Width:  |  Height:  |  Size: 981 B

3
src/assets/svg/mouse.svg Normal file
View File

@ -0,0 +1,3 @@
<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'>
<path d='M26.256 8.185c0-3.863-3.137-7-7-7h-6.512c-3.863 0-7 3.137-7 7v15.629c0 3.863 3.137 7 7 7h6.512c3.863 0 7-3.137 7-7V8.185z'/><path d='M16 13.721V6.883'/>
</svg>

After

Width:  |  Height:  |  Size: 344 B

3
src/assets/svg/new.svg Normal file
View File

@ -0,0 +1,3 @@
<svg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='#fff' fill-rule='evenodd' stroke-linecap='round' stroke-linejoin='round' stroke-width='4' viewBox='0 0 32 32'>
<path d='M26.875 30.5H5.125c-.663 0-1.208-.545-1.208-1.208V2.708c0-.663.545-1.208 1.208-1.208h14.5l8.458 8.458v19.333c0 .663-.545 1.208-1.208 1.208z'/><path d='M19.625 1.5v8.458h8.458m-15.708 9.667h7.25'/><path d='M16 16v7.25'/>
</svg>

After

Width:  |  Height:  |  Size: 411 B

View File

@ -0,0 +1,3 @@
<svg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='#fff' fill-rule='evenodd' stroke-linecap='round' stroke-linejoin='round' stroke-width='4' viewBox='0 0 32 32'>
<g transform='matrix(.256867 0 0 .256867 -16.878964 -18.049342)'><circle cx='128' cy='180' r='12' fill='#fff'/><path d='M128 144v-8c17.67 0 32-12.54 32-28s-14.33-28-32-28-32 12.54-32 28v4' fill='none' stroke='#fff' stroke-width='16'/></g>
</svg>

After

Width:  |  Height:  |  Size: 421 B

View File

@ -0,0 +1,3 @@
<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'>
<path d="M23.247 12.377h7.247V5.13"/><path d="M23.911 25.663a13.29 13.29 0 0 1-9.119 3.623C7.504 29.286 1.506 23.289 1.506 16S7.504 2.713 14.792 2.713a13.29 13.29 0 0 1 9.395 3.891l6.307 5.772"/>
</svg>

After

Width:  |  Height:  |  Size: 378 B

View File

@ -0,0 +1,3 @@
<svg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='#fff' fill-rule='evenodd' stroke-linecap='round' stroke-linejoin='round' stroke-width='4' viewBox='0 0 32 32'>
<g transform='matrix(.492308 0 0 .581818 -14.7692 -11.6364)'><clipPath id='A'><path d='M30 20h65v55H30z'/></clipPath><g clip-path='url(#A)'><g transform='matrix(.395211 0 0 .334409 11.913 7.01124)'><g transform='matrix(.555556 0 0 .555556 57.8889 -20.2417)' fill='none' stroke='#fff' stroke-width='13.88'><path d='M200 140.564c-42.045-33.285-101.955-33.285-144 0M168 165c-23.783-17.3-56.217-17.3-80 0'/></g><g transform='matrix(-.555556 0 0 -.555556 200.111 262.393)'><g transform='matrix(1 0 0 1 0 11.5642)'><path d='M200 129c-17.342-13.728-37.723-21.795-58.636-24.198C111.574 101.378 80.703 109.444 56 129' fill='none' stroke='#fff' stroke-width='13.88'/></g><path d='M168 165c-23.783-17.3-56.217-17.3-80 0' fill='none' stroke='#fff' stroke-width='13.88'/></g><g transform='matrix(.75 0 0 .75 32 32)'><path d='M24 72h208v93.881H24z' fill='none' stroke='#fff' stroke-linejoin='miter' stroke-width='9.485'/><circle cx='188' cy='128' r='12' stroke-width='10' transform='matrix(.708333 0 0 .708333 71.8333 12.8333)'/><path d='M24.358 103.5h110' fill='none' stroke='#fff' stroke-linecap='butt' stroke-width='10.282'/></g></g></g></g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,3 @@
<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(.142357 0 0 .142357 -2.22021 -2.22164)' fill='none' stroke='#fff' stroke-width='16'><circle cx='128' cy='128' r='40'/><path d='M130.05 206.11h-4L94 224c-12.477-4.197-24.049-10.711-34.11-19.2l-.12-36c-.71-1.12-1.38-2.25-2-3.41L25.9 147.24a99.16 99.16 0 0 1 0-38.46l31.84-18.1c.65-1.15 1.32-2.29 2-3.41l.16-36C69.951 42.757 81.521 36.218 94 32l32 17.89h4L162 32c12.477 4.197 24.049 10.711 34.11 19.2l.12 36c.71 1.12 1.38 2.25 2 3.41l31.85 18.14a99.16 99.16 0 0 1 0 38.46l-31.84 18.1c-.65 1.15-1.32 2.29-2 3.41l-.16 36A104.59 104.59 0 0 1 162 224l-31.95-17.89z'/></g>
</svg>

After

Width:  |  Height:  |  Size: 768 B

View File

@ -0,0 +1,3 @@
<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'>
<path d='M1.181 24.55v-3.259c0-8.19 6.576-14.952 14.767-14.98H16c8.13 0 14.819 6.69 14.819 14.819v3.42c0 .625-.515 1.14-1.14 1.14H2.321c-.625 0-1.14-.515-1.14-1.14z'/><path d='M16 6.311v4.56M12.58 25.69l9.12-12.54m4.559 5.7h4.386m-29.266 0H5.74'/>
</svg>

After

Width:  |  Height:  |  Size: 430 B

3
src/assets/svg/trash.svg Normal file
View File

@ -0,0 +1,3 @@
<svg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='#fff' fill-rule='evenodd' stroke-linecap='round' stroke-linejoin='round' stroke-width='4' viewBox='0 0 32 32'>
<path d='M29.5 6.182h-27m9.818 7.363v9.818m7.364-9.818v9.818'/><path d='M27.045 6.182V29.5c0 .673-.554 1.227-1.227 1.227H6.182c-.673 0-1.227-.554-1.227-1.227V6.182m17.181 0V3.727a2.47 2.47 0 0 0-2.455-2.455h-7.364a2.47 2.47 0 0 0-2.455 2.455v2.455'/>
</svg>

After

Width:  |  Height:  |  Size: 433 B

View File

@ -1,31 +1,32 @@
import "./utils/global";
import { BxEvent } from "./utils/bx-event";
import { BX_FLAGS } from "./utils/bx-flags";
import { BxExposed } from "./utils/bx-exposed";
import { t } from "./utils/translation";
import { interceptHttpRequests } from "./utils/network";
import { CE } from "./utils/html";
import { showGamepadToast } from "./utils/gamepad";
import { MkbHandler } from "./modules/mkb/mkb-handler";
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 { PrefKey, getPref } from "./utils/preferences";
import { LoadingScreen } from "./modules/loading-screen";
import { MouseCursorHider } from "./modules/mkb/mouse-cursor-hider";
import { TouchController } from "./modules/touch-controller";
import { watchHeader } from "./modules/ui/header";
import { checkForUpdate, disablePwa } from "./utils/utils";
import { Patcher } from "./modules/patcher";
import { RemotePlay } from "./modules/remote-play";
import { onHistoryChanged, patchHistoryMethod } from "./utils/history";
import { VibrationManager } from "./modules/vibration-manager";
import { PreloadedState } from "./utils/titles-info";
import { patchAudioContext, patchRtcCodecs, patchRtcPeerConnection, patchVideoApi } from "./utils/monkey-patches";
import { STATES } from "./utils/global";
import { injectStreamMenuButtons } from "./modules/stream/stream-ui";
import "@utils/global";
import { BxEvent } from "@utils/bx-event";
import { BX_FLAGS } from "@utils/bx-flags";
import { BxExposed } from "@utils/bx-exposed";
import { t } from "@utils/translation";
import { interceptHttpRequests } from "@utils/network";
import { CE } from "@utils/html";
import { showGamepadToast } from "@utils/gamepad";
import { MkbHandler } from "@modules/mkb/mkb-handler";
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 { PrefKey, getPref } from "@utils/preferences";
import { LoadingScreen } from "@modules/loading-screen";
import { MouseCursorHider } from "@modules/mkb/mouse-cursor-hider";
import { TouchController } from "@modules/touch-controller";
import { watchHeader } from "@modules/ui/header";
import { checkForUpdate, disablePwa } from "@utils/utils";
import { Patcher } from "@modules/patcher";
import { RemotePlay } from "@modules/remote-play";
import { onHistoryChanged, patchHistoryMethod } from "@utils/history";
import { VibrationManager } from "@modules/vibration-manager";
import { PreloadedState } from "@utils/titles-info";
import { patchAudioContext, patchRtcCodecs, patchRtcPeerConnection, patchVideoApi } from "@utils/monkey-patches";
import { STATES } from "@utils/global";
import { injectStreamMenuButtons } from "@modules/stream/stream-ui";
import { BxLogger } from "@utils/bx-logger";
// Handle login page
if (window.location.pathname.includes('/auth/msa')) {
@ -40,7 +41,7 @@ if (window.location.pathname.includes('/auth/msa')) {
throw new Error('[Better xCloud] Refreshing the page after logging in');
}
console.log(`[Better xCloud] readyState: ${document.readyState}`);
BxLogger.info('readyState', document.readyState);
if (BX_FLAGS.SafariWorkaround && document.readyState !== 'loading') {
// Stop loading
@ -161,6 +162,7 @@ window.addEventListener(BxEvent.STREAM_PLAYING, e => {
// 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') {
@ -169,6 +171,9 @@ window.addEventListener(BxEvent.STREAM_PLAYING, e => {
$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 => {
@ -231,7 +236,7 @@ function main() {
StreamStats.setupEvents();
MkbHandler.setupEvents();
Patcher.initialize();
Patcher.init();
disablePwa();

View File

@ -1,7 +1,6 @@
import stylus from 'stylus';
// @ts-ignore
import cssStr from "../assets/css/styles.styl" with { type: "text" };
import cssStr from "@assets/css/styles.styl" with { type: "text" };
const generatedCss = await (stylus(cssStr, {})
.set('filename', 'styles.css')

View File

@ -1,20 +1,21 @@
import { t } from "../utils/translation";
import { CE, createButton, ButtonStyle, Icon } from "../utils/html";
import { t } from "@utils/translation";
import { CE, createButton, ButtonStyle } from "@utils/html";
import { BxIcon } from "@utils/bx-icon";
type DialogOptions = {
title?: string;
className?: string;
content?: string | HTMLElement;
hideCloseButton?: boolean;
onClose?: string;
helpUrl?: string;
}
type DialogOptions = Partial<{
title: string;
className: string;
content: string | HTMLElement;
hideCloseButton: boolean;
onClose: string;
helpUrl: string;
}>;
export class Dialog {
$dialog?: HTMLElement;
$title?: HTMLElement;
$content?: HTMLElement;
$overlay?: Element | null;
$dialog: HTMLElement;
$title: HTMLElement;
$content: HTMLElement;
$overlay: HTMLElement;
onClose: any;
@ -29,14 +30,17 @@ export class Dialog {
} = options;
// Create dialog overlay
this.$overlay = document.querySelector('.bx-dialog-overlay');
if (!this.$overlay) {
const $overlay = document.querySelector('.bx-dialog-overlay') as HTMLElement;
if (!$overlay) {
this.$overlay = CE('div', {'class': 'bx-dialog-overlay bx-gone'});
// Disable right click
this.$overlay!.addEventListener('contextmenu', e => e.preventDefault());
this.$overlay.addEventListener('contextmenu', e => e.preventDefault());
document.documentElement.appendChild(this.$overlay!);
document.documentElement.appendChild(this.$overlay);
} else {
this.$overlay = $overlay;
}
let $close;
@ -44,7 +48,7 @@ export class Dialog {
this.$dialog = CE('div', {'class': `bx-dialog ${className || ''} bx-gone`},
this.$title = CE('h2', {}, CE('b', {}, title),
helpUrl && createButton({
icon: Icon.QUESTION,
icon: BxIcon.QUESTION,
style: ButtonStyle.GHOST,
title: t('help'),
url: helpUrl,
@ -72,19 +76,19 @@ export class Dialog {
document.activeElement && (document.activeElement as HTMLElement).blur();
if (newOptions && newOptions.title) {
this.$title!.querySelector('b')!.textContent = newOptions.title;
this.$title!.classList.remove('bx-gone');
this.$title.querySelector('b')!.textContent = newOptions.title;
this.$title.classList.remove('bx-gone');
}
this.$dialog!.classList.remove('bx-gone');
this.$overlay!.classList.remove('bx-gone');
this.$dialog.classList.remove('bx-gone');
this.$overlay.classList.remove('bx-gone');
document.body.classList.add('bx-no-scroll');
}
hide(e?: any) {
this.$dialog!.classList.add('bx-gone');
this.$overlay!.classList.add('bx-gone');
this.$dialog.classList.add('bx-gone');
this.$overlay.classList.add('bx-gone');
document.body.classList.remove('bx-no-scroll');
@ -92,7 +96,7 @@ export class Dialog {
}
toggle() {
this.$dialog!.classList.toggle('bx-gone');
this.$overlay!.classList.toggle('bx-gone');
this.$dialog.classList.toggle('bx-gone');
this.$overlay.classList.toggle('bx-gone');
}
}

View File

@ -1,8 +1,8 @@
import { CE } from "../utils/html";
import { getPreferredServerRegion } from "../utils/region";
import { PrefKey, getPref } from "../utils/preferences";
import { t } from "../utils/translation";
import { STATES } from "../utils/global";
import { CE } from "@utils/html";
import { getPreferredServerRegion } from "@utils/region";
import { PrefKey, getPref } from "@utils/preferences";
import { t } from "@utils/translation";
import { STATES } from "@utils/global";
export class LoadingScreen {
static #$bgStyle: HTMLElement;

View File

@ -1,32 +1,34 @@
import type { GamepadKeyNameType } from "../../types/mkb";
import type { GamepadKeyNameType } from "@/types/mkb";
export const GamepadKey: DualEnum = {};
GamepadKey[GamepadKey.A = 0] = 'A';
GamepadKey[GamepadKey.B = 1] = 'B';
GamepadKey[GamepadKey.X = 2] = 'X';
GamepadKey[GamepadKey.Y = 3] = 'Y';
GamepadKey[GamepadKey.LB = 4] = 'LB';
GamepadKey[GamepadKey.RB = 5] = 'RB';
GamepadKey[GamepadKey.LT = 6] = 'LT';
GamepadKey[GamepadKey.RT = 7] = 'RT';
GamepadKey[GamepadKey.SELECT = 8] = 'SELECT';
GamepadKey[GamepadKey.START = 9] = 'START';
GamepadKey[GamepadKey.L3 = 10] = 'L3';
GamepadKey[GamepadKey.R3 = 11] = 'R3';
GamepadKey[GamepadKey.UP = 12] = 'UP';
GamepadKey[GamepadKey.DOWN = 13] = 'DOWN';
GamepadKey[GamepadKey.LEFT = 14] = 'LEFT';
GamepadKey[GamepadKey.RIGHT = 15] = 'RIGHT';
GamepadKey[GamepadKey.HOME = 16] = 'HOME';
export enum GamepadKey {
A = 0,
B = 1,
X = 2,
Y = 3,
LB = 4,
RB = 5,
LT = 6,
RT = 7,
SELECT = 8,
START = 9,
L3 = 10,
R3 = 11,
UP = 12,
DOWN = 13,
LEFT = 14,
RIGHT = 15,
HOME = 16,
GamepadKey[GamepadKey.LS_UP = 100] = 'LS_UP';
GamepadKey[GamepadKey.LS_DOWN = 101] = 'LS_DOWN';
GamepadKey[GamepadKey.LS_LEFT = 102] = 'LS_LEFT';
GamepadKey[GamepadKey.LS_RIGHT = 103] = 'LS_RIGHT';
GamepadKey[GamepadKey.RS_UP = 200] = 'RS_UP';
GamepadKey[GamepadKey.RS_DOWN = 201] = 'RS_DOWN';
GamepadKey[GamepadKey.RS_LEFT = 202] = 'RS_LEFT';
GamepadKey[GamepadKey.RS_RIGHT = 203] = 'RS_RIGHT';
LS_UP = 100,
LS_DOWN = 101,
LS_LEFT = 102,
LS_RIGHT = 103,
RS_UP = 200,
RS_DOWN = 201,
RS_LEFT = 202,
RS_RIGHT = 203,
};
export const GamepadKeyName: GamepadKeyNameType = {
@ -74,10 +76,11 @@ export enum MouseButtonCode {
MIDDLE_CLICK = 'Mouse1',
};
export const MouseMapTo: DualEnum = {};
MouseMapTo[MouseMapTo.OFF = 0] = 'OFF';
MouseMapTo[MouseMapTo.LS = 1] = 'LS';
MouseMapTo[MouseMapTo.RS = 2] = 'RS';
export enum MouseMapTo {
OFF = 0,
LS = 1,
RS = 2,
}
export enum WheelCode {

View File

@ -1,16 +1,20 @@
import { MkbPreset } from "./mkb-preset";
import { GamepadKey, MkbPresetKey, GamepadStick, MouseMapTo } from "./definitions";
import { createButton, Icon, ButtonStyle, CE } from "../../utils/html";
import { BxEvent } from "../../utils/bx-event";
import { PrefKey, getPref } from "../../utils/preferences";
import { Toast } from "../../utils/toast";
import { t } from "../../utils/translation";
import { LocalDb } from "../../utils/local-db";
import { createButton, ButtonStyle, CE } from "@utils/html";
import { BxEvent } from "@utils/bx-event";
import { PrefKey, getPref } from "@utils/preferences";
import { Toast } from "@utils/toast";
import { t } from "@utils/translation";
import { LocalDb } from "@utils/local-db";
import { KeyHelper } from "./key-helper";
import type { MkbStoredPreset } from "../../types/mkb";
import { showStreamSettings } from "../stream/stream-ui";
import { STATES } from "../../utils/global";
import { UserAgent } from "../../utils/user-agent";
import type { MkbStoredPreset } from "@/types/mkb";
import { showStreamSettings } from "@modules/stream/stream-ui";
import { STATES } from "@utils/global";
import { UserAgent } from "@utils/user-agent";
import { BxLogger } from "@utils/bx-logger";
import { BxIcon } from "@utils/bx-icon";
const LOG_TAG = 'MkbHandler';
/*
This class uses some code from Yuzu emulator to handle mouse's movements
@ -61,11 +65,11 @@ export class MkbHandler {
#$message?: HTMLElement;
#STICK_MAP: {[index: keyof typeof GamepadKey]: (number | number[])[]};
#LEFT_STICK_X: number[] = [];
#LEFT_STICK_Y: number[] = [];
#RIGHT_STICK_X: number[] = [];
#RIGHT_STICK_Y: number[] = [];
#STICK_MAP: {[key in GamepadKey]?: [GamepadKey[], number, number]};
#LEFT_STICK_X: GamepadKey[] = [];
#LEFT_STICK_Y: GamepadKey[] = [];
#RIGHT_STICK_X: GamepadKey[] = [];
#RIGHT_STICK_Y: GamepadKey[] = [];
constructor() {
this.#STICK_MAP = {
@ -125,11 +129,11 @@ export class MkbHandler {
gamepad.timestamp = performance.now();
}
#pressButton = (buttonIndex: number, pressed: boolean) => {
#pressButton = (buttonIndex: GamepadKey, pressed: boolean) => {
const virtualGamepad = this.#getVirtualGamepad();
if (buttonIndex >= 100) {
let [valueArr, axisIndex] = this.#STICK_MAP[buttonIndex];
let [valueArr, axisIndex] = this.#STICK_MAP[buttonIndex]!;
valueArr = valueArr as number[];
axisIndex = axisIndex as number;
@ -145,7 +149,7 @@ export class MkbHandler {
let value;
if (valueArr.length) {
// Get value of the last key of the axis
value = this.#STICK_MAP[valueArr[valueArr.length - 1]][2] as number;
value = this.#STICK_MAP[valueArr[valueArr.length - 1]]![2] as number;
} else {
value = 0;
}
@ -378,7 +382,7 @@ export class MkbHandler {
this.#$message = CE('div', {'class': 'bx-mkb-pointer-lock-msg bx-gone'},
createButton({
icon: Icon.MOUSE_SETTINGS,
icon: BxIcon.MOUSE_SETTINGS,
style: ButtonStyle.PRIMARY,
onClick: e => {
e.preventDefault();
@ -472,7 +476,7 @@ export class MkbHandler {
getPref(PrefKey.MKB_ENABLED) && !UserAgent.isMobile() && window.addEventListener(BxEvent.STREAM_PLAYING, () => {
// Enable MKB
if (!STATES.currentStream.titleInfo?.details.hasMkbSupport) {
console.log('Emulate MKB');
BxLogger.info(LOG_TAG, 'Emulate MKB');
MkbHandler.INSTANCE.init();
}
});

View File

@ -1,9 +1,9 @@
import { t } from "../../utils/translation";
import { SettingElementType } from "../../utils/settings";
import { t } from "@utils/translation";
import { SettingElementType } from "@utils/settings";
import { GamepadKey, MouseButtonCode, MouseMapTo, MkbPresetKey } from "./definitions";
import { MkbHandler } from "./mkb-handler";
import type { MkbPresetData, MkbConvertedPresetData } from "../../types/mkb";
import type { PreferenceSettings } from "../../types/preferences";
import type { MkbPresetData, MkbConvertedPresetData } from "@/types/mkb";
import type { PreferenceSettings } from "@/types/preferences";
export class MkbPreset {

View File

@ -1,16 +1,16 @@
import { GamepadKey } from "./definitions";
import { CE, createButton, ButtonStyle } from "../../utils/html";
import { t } from "../../utils/translation";
import { Dialog } from "../dialog";
import { getPref, setPref, PrefKey } from "../../utils/preferences";
import { CE, createButton, ButtonStyle } from "@utils/html";
import { t } from "@utils/translation";
import { Dialog } from "@modules/dialog";
import { getPref, setPref, PrefKey } from "@utils/preferences";
import { MkbPresetKey, GamepadKeyName } from "./definitions";
import { KeyHelper } from "./key-helper";
import { MkbPreset } from "./mkb-preset";
import { MkbHandler } from "./mkb-handler";
import { LocalDb } from "../../utils/local-db";
import { Icon } from "../../utils/html";
import { SettingElement } from "../../utils/settings";
import type { MkbPresetData, MkbStoredPresets } from "../../types/mkb";
import { LocalDb } from "@utils/local-db";
import { BxIcon } from "@utils/bx-icon";
import { SettingElement } from "@utils/settings";
import type { MkbPresetData, MkbStoredPresets } from "@/types/mkb";
type MkbRemapperElements = {
@ -340,7 +340,7 @@ export class MkbRemapper {
// Rename button
createButton({
title: t('rename'),
icon: Icon.CURSOR_TEXT,
icon: BxIcon.CURSOR_TEXT,
onClick: e => {
const preset = this.#getCurrentPreset();
@ -357,7 +357,7 @@ export class MkbRemapper {
// New button
createButton({
icon: Icon.NEW,
icon: BxIcon.NEW,
title: t('new'),
onClick: e => {
let newName = promptNewName('');
@ -375,7 +375,7 @@ export class MkbRemapper {
// Copy button
createButton({
icon: Icon.COPY,
icon: BxIcon.COPY,
title: t('copy'),
onClick: e => {
const preset = this.#getCurrentPreset();
@ -395,7 +395,7 @@ export class MkbRemapper {
// Delete button
createButton({
icon: Icon.TRASH,
icon: BxIcon.TRASH,
style: ButtonStyle.DANGER,
title: t('delete'),
onClick: e => {

View File

@ -1,7 +1,15 @@
import { STATES } from "../utils/global";
import { BX_FLAGS } from "../utils/bx-flags";
import { getPref, PrefKey } from "../utils/preferences";
import { VibrationManager } from "./vibration-manager";
import { SCRIPT_VERSION, STATES } from "@utils/global";
import { BX_FLAGS } from "@utils/bx-flags";
import { getPref, PrefKey } from "@utils/preferences";
import { VibrationManager } from "@modules/vibration-manager";
import { BxLogger } from "@utils/bx-logger";
import { hashCode } from "@/utils/utils";
type PatchArray = (keyof typeof PATCHES)[];
const ENDING_CHUNKS_PATCH_NAME = 'loadingEndingChunks';
const LOG_TAG = 'Patcher';
const PATCHES = {
// Disable ApplicationInsights.track() function
@ -36,15 +44,15 @@ const PATCHES = {
}
const newCode = [
'this.trackEvent',
'this.trackPageView',
'this.trackHttpCompleted',
'this.trackHttpFailed',
'this.trackError',
'this.trackErrorLike',
'this.onTrackEvent',
'()=>{}',
].join('=');
'this.trackEvent',
'this.trackPageView',
'this.trackHttpCompleted',
'this.trackHttpFailed',
'this.trackError',
'this.trackErrorLike',
'this.onTrackEvent',
'()=>{}',
].join('=');
return str.replace(text, newCode + ';' + text);
},
@ -59,14 +67,15 @@ const PATCHES = {
return str.replace(text, text + 'return;');
},
// Set TV layout
tvLayout(str: string) {
// Set custom website layout
websiteLayout(str: string) {
const text = '?"tv":"default"';
if (!str.includes(text)) {
return false;
}
return str.replace(text, '?"tv":"tv"');
const layout = getPref(PrefKey.UI_LAYOUT) === 'tv' ? 'tv' : 'default';
return str.replace(text, `?"${layout}":"${layout}"`);
},
// Replace "/direct-connect" with "/play"
@ -107,14 +116,20 @@ const PATCHES = {
return str.replace(text, `connectMode:window.BX_REMOTE_PLAY_CONFIG?"xhome-connect":"cloud-connect",remotePlayServerId:(window.BX_REMOTE_PLAY_CONFIG&&window.BX_REMOTE_PLAY_CONFIG.serverId)||''`);
},
// Fix the Guide/Nexus button not working in Remote Play
remotePlayGuideWorkaround(str: string) {
const text = 'nexusButtonHandler:this.featureGates.EnableClientGuideInStream';
// Disable achievement toast in Remote Play
remotePlayDisableAchievementToast(str: string) {
const text = '.AchievementUnlock:{';
if (!str.includes(text)) {
return false;
}
return str.replace(text, `nexusButtonHandler: !window.BX_REMOTE_PLAY_CONFIG && this.featureGates.EnableClientGuideInStream`);
const newCode = `
if (!!window.BX_REMOTE_PLAY_CONFIG) {
return;
}
`;
return str.replace(text, text + newCode);
},
// Disable trackEvent() function
@ -177,13 +192,13 @@ const PATCHES = {
const newCode = `
if (!window.BX_ENABLE_CONTROLLER_VIBRATION) {
return void(0);
return void(0);
}
if (window.BX_VIBRATION_INTENSITY && window.BX_VIBRATION_INTENSITY < 1) {
e.leftMotorPercent = e.leftMotorPercent * window.BX_VIBRATION_INTENSITY;
e.rightMotorPercent = e.rightMotorPercent * window.BX_VIBRATION_INTENSITY;
e.leftTriggerMotorPercent = e.leftTriggerMotorPercent * window.BX_VIBRATION_INTENSITY;
e.rightTriggerMotorPercent = e.rightTriggerMotorPercent * window.BX_VIBRATION_INTENSITY;
e.leftMotorPercent = e.leftMotorPercent * window.BX_VIBRATION_INTENSITY;
e.rightMotorPercent = e.rightMotorPercent * window.BX_VIBRATION_INTENSITY;
e.leftTriggerMotorPercent = e.leftTriggerMotorPercent * window.BX_VIBRATION_INTENSITY;
e.rightTriggerMotorPercent = e.rightTriggerMotorPercent * window.BX_VIBRATION_INTENSITY;
}
`;
@ -238,14 +253,13 @@ e.rightTriggerMotorPercent = e.rightTriggerMotorPercent * window.BX_VIBRATION_IN
// Add patches that are only needed when start playing
loadingEndingChunks(str: string) {
const text = 'Symbol("ChatSocketPlugin")';
const text = '"FamilySagaManager"';
if (!str.includes(text)) {
return false;
}
console.log('[Better xCloud] Remaining patches:', PATCH_ORDERS);
BxLogger.info(LOG_TAG, 'Remaining patches:', PATCH_ORDERS);
PATCH_ORDERS = PATCH_ORDERS.concat(PLAYING_PATCH_ORDERS);
Patcher.cleanupPatches();
return str;
},
@ -293,9 +307,9 @@ if (match) {
const gamepadIndexVar = match[0];
onGamepadInputStr = onGamepadInputStr.replace('this.gamepadStates.get(', \`this.gamepadStates.get(\${gamepadIndexVar},\`);
eval(\`this.onGamepadInput = function \${onGamepadInputStr}\`);
console.log('[Better xCloud] ✅ Successfully patched local co-op support');
BxLogger.info('supportLocalCoOp', '✅ Successfully patched local co-op support');
} else {
console.log('[Better xCloud] ❌ Unable to patch local co-op support');
BxLogger.error('supportLocalCoOp', '❌ Unable to patch local co-op support');
}
`;
@ -323,11 +337,34 @@ if (match) {
return false;
}
const newCode = `
const titleInfo = window.BX_EXPOSED.getTitleInfo();
if (!titleInfo.details.hasTouchSupport && !titleInfo.details.hasFakeTouchSupport) {
let remotePlayCode = '';
if (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) !== 'off' && getPref(PrefKey.STREAM_TOUCH_CONTROLLER_AUTO_OFF)) {
remotePlayCode = `
const gamepads = window.navigator.getGamepads();
let gamepadFound = false;
for (let gamepad of gamepads) {
if (gamepad && gamepad.connected) {
gamepadFound = true;
break;
}
}
if (gamepadFound) {
return;
}
`;
}
const newCode = `
if (!!window.BX_REMOTE_PLAY_CONFIG) {
${remotePlayCode}
} else {
const titleInfo = window.BX_EXPOSED.getTitleInfo();
if (titleInfo && !titleInfo.details.hasTouchSupport && !titleInfo.details.hasFakeTouchSupport) {
return;
}
}
`;
str = str.replace(text, newCode + text);
@ -389,7 +426,7 @@ window.BX_EXPOSED.onPollingModeChanged && window.BX_EXPOSED.onPollingModeChanged
const newCode = `
${titleInfoVar} = window.BX_EXPOSED.modifyTitleInfo(${titleInfoVar});
console.log(${titleInfoVar});
BxLogger.info('patchXcloudTitleInfo', ${titleInfoVar});
`;
str = str.substring(0, backetIndex + 1) + newCode + str.substring(backetIndex + 1);
return str;
@ -415,7 +452,7 @@ Object.assign(${configsVar}.inputConfiguration, {
enableKeyboardInput: false,
enableAbsoluteMouse: false,
});
console.log(${configsVar});
BxLogger.info('patchRemotePlayMkb', ${configsVar});
`;
str = str.substring(0, backetIndex + 1) + newCode + str.substring(backetIndex + 1);
@ -424,78 +461,74 @@ console.log(${configsVar});
},
};
let PATCH_ORDERS = [
getPref(PrefKey.BLOCK_TRACKING) && [
let PATCH_ORDERS: PatchArray = [
'disableStreamGate',
'overrideSettings',
'broadcastPollingMode',
getPref(PrefKey.UI_LAYOUT) !== 'default' && 'websiteLayout',
getPref(PrefKey.LOCAL_CO_OP_ENABLED) && 'supportLocalCoOp',
getPref(PrefKey.GAME_FORTNITE_FORCE_CONSOLE) && 'forceFortniteConsole',
...(getPref(PrefKey.BLOCK_TRACKING) ? [
'disableAiTrack',
'disableTelemetry',
],
['disableStreamGate'],
['broadcastPollingMode'],
getPref(PrefKey.UI_LAYOUT) === 'tv' && ['tvLayout'],
BX_FLAGS.EnableXcloudLogging && [
'enableConsoleLogging',
'enableXcloudLogger',
],
getPref(PrefKey.LOCAL_CO_OP_ENABLED) && ['supportLocalCoOp'],
getPref(PrefKey.BLOCK_TRACKING) && [
'blockWebRtcStatsCollector',
'disableIndexDbLogging',
],
getPref(PrefKey.BLOCK_TRACKING) && [
'disableTelemetryProvider',
'disableTrackEvent',
],
] : []),
getPref(PrefKey.REMOTE_PLAY_ENABLED) && ['remotePlayKeepAlive'],
getPref(PrefKey.REMOTE_PLAY_ENABLED) && ['remotePlayDirectConnectUrl'],
[
'overrideSettings',
],
getPref(PrefKey.REMOTE_PLAY_ENABLED) && STATES.hasTouchSupport && ['patchUpdateInputConfigurationAsync'],
getPref(PrefKey.GAME_FORTNITE_FORCE_CONSOLE) && ['forceFortniteConsole'],
];
...(getPref(PrefKey.REMOTE_PLAY_ENABLED) ? [
'remotePlayKeepAlive',
'remotePlayDirectConnectUrl',
'remotePlayDisableAchievementToast',
STATES.hasTouchSupport && 'patchUpdateInputConfigurationAsync',
] : []),
...(BX_FLAGS.EnableXcloudLogging ? [
'enableConsoleLogging',
'enableXcloudLogger',
] : []),
].filter(item => !!item);
// Only when playing
const PLAYING_PATCH_ORDERS = [
['patchXcloudTitleInfo'],
getPref(PrefKey.REMOTE_PLAY_ENABLED) && ['patchRemotePlayMkb'],
let PLAYING_PATCH_ORDERS: PatchArray = [
'patchXcloudTitleInfo',
'disableGamepadDisconnectedScreen',
'patchStreamHud',
'playVibration',
getPref(PrefKey.REMOTE_PLAY_ENABLED) && ['remotePlayConnectMode'],
getPref(PrefKey.REMOTE_PLAY_ENABLED) && ['remotePlayGuideWorkaround'],
STATES.hasTouchSupport && getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === 'all' && 'exposeTouchLayoutManager',
STATES.hasTouchSupport && (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === 'off' || getPref(PrefKey.STREAM_TOUCH_CONTROLLER_AUTO_OFF)) && 'disableTakRenderer',
['patchStreamHud'],
BX_FLAGS.EnableXcloudLogging && 'enableConsoleLogging',
['playVibration'],
STATES.hasTouchSupport && getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === 'all' && ['exposeTouchLayoutManager'],
STATES.hasTouchSupport && (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === 'off' || getPref(PrefKey.STREAM_TOUCH_CONTROLLER_AUTO_OFF)) && ['disableTakRenderer'],
getPref(PrefKey.BLOCK_TRACKING) && 'blockGamepadStatsCollector',
BX_FLAGS.EnableXcloudLogging && ['enableConsoleLogging'],
getPref(PrefKey.STREAM_COMBINE_SOURCES) && 'streamCombineSources',
getPref(PrefKey.BLOCK_TRACKING) && ['blockGamepadStatsCollector'],
...(getPref(PrefKey.REMOTE_PLAY_ENABLED) ? [
'patchRemotePlayMkb',
'remotePlayConnectMode',
] : []),
].filter(item => !!item);
[
'disableGamepadDisconnectedScreen',
],
getPref(PrefKey.STREAM_COMBINE_SOURCES) && ['streamCombineSources'],
];
const ALL_PATCHES = [...PATCH_ORDERS, ...PLAYING_PATCH_ORDERS];
export class Patcher {
static #patchFunctionBind() {
const nativeBind = Function.prototype.bind;
Function.prototype.bind = function() {
let valid = false;
// Looking for these criteria:
// - Variable name <= 2 characters
// - Has 2 params:
// - The first one is null
// - The second one is either 0 or a function
if (this.name.length <= 2 && arguments.length === 2 && arguments[0] === null) {
if (arguments[1] === 0 || (typeof arguments[1] === 'function')) {
valid = true;
@ -507,18 +540,15 @@ export class Patcher {
return nativeBind.apply(this, arguments);
}
PatcherCache.init();
if (typeof arguments[1] === 'function') {
console.log('[Better xCloud] Restored Function.prototype.bind()');
BxLogger.info(LOG_TAG, 'Restored Function.prototype.bind()');
Function.prototype.bind = nativeBind;
}
const orgFunc = this;
const newFunc = (a: any, item: any) => {
if (Patcher.length() === 0) {
orgFunc(a, item);
return;
}
Patcher.patch(item);
orgFunc(a, item);
}
@ -528,98 +558,189 @@ export class Patcher {
};
}
static length() { return PATCH_ORDERS.length; };
static patch(item: any) {
static patch(item: [[number], { [key: string]: () => {} }]) {
// !!! Use "caches" as variable name will break touch controller???
// console.log('patch', '-----');
let appliedPatches;
let patchesToCheck: PatchArray;
let appliedPatches: PatchArray;
const patchesMap: { [key: string]: PatchArray } = {};
for (let id in item[1]) {
if (PATCH_ORDERS.length <= 0) {
return;
}
appliedPatches = [];
const func = item[1][id];
let str = func.toString();
for (let groupIndex = 0; groupIndex < PATCH_ORDERS.length; groupIndex++) {
const group = PATCH_ORDERS[groupIndex];
let modified = false;
for (let patchIndex = 0; patchIndex < group.length; patchIndex++) {
const patchName = group[patchIndex] as keyof typeof PATCHES;
if (appliedPatches.indexOf(patchName) > -1) {
continue;
}
const patchedstr = PATCHES[patchName].call(null, str);
if (!patchedstr) {
// Only stop if the first patch is failed
if (patchIndex === 0) {
break;
} else {
continue;
}
}
modified = true;
str = patchedstr;
console.log(`[Better xCloud] Applied "${patchName}" patch`);
appliedPatches.push(patchName);
// Remove patch from group
group.splice(patchIndex, 1);
patchIndex--;
}
// Apply patched functions
if (modified) {
item[1][id] = eval(str);
}
// Remove empty group
if (!group.length) {
PATCH_ORDERS.splice(groupIndex, 1);
groupIndex--;
}
const cachedPatches = PatcherCache.getPatches(id);
if (cachedPatches) {
patchesToCheck = cachedPatches.slice(0);
patchesToCheck.push(...PATCH_ORDERS);
} else {
patchesToCheck = PATCH_ORDERS.slice(0);
}
}
}
// Remove disabled patches
static cleanupPatches() {
for (let groupIndex = PATCH_ORDERS.length - 1; groupIndex >= 0; groupIndex--) {
const group = PATCH_ORDERS[groupIndex];
if (group === false) {
PATCH_ORDERS.splice(groupIndex, 1);
// Empty patch list
if (!patchesToCheck.length) {
continue;
}
for (let patchIndex = group.length - 1; patchIndex >= 0; patchIndex--) {
const patchName = group[patchIndex] as keyof typeof PATCHES;
if (!PATCHES[patchName]) {
// Remove disabled patch
group.splice(patchIndex, 1);
const func = item[1][id];
let str = func.toString();
let modified = false;
for (let patchIndex = 0; patchIndex < patchesToCheck.length; patchIndex++) {
const patchName = patchesToCheck[patchIndex];
if (appliedPatches.indexOf(patchName) > -1) {
continue;
}
if (!PATCHES[patchName]) {
continue;
}
// Check function against patch
const patchedStr = PATCHES[patchName].call(null, str);
// Not patched
if (!patchedStr) {
continue;
}
modified = true;
str = patchedStr;
BxLogger.info(LOG_TAG, `Applied "${patchName}" patch`);
appliedPatches.push(patchName);
// Remove patch
patchesToCheck.splice(patchIndex, 1);
patchIndex--;
PATCH_ORDERS = PATCH_ORDERS.filter(item => item != patchName);
}
// Remove empty group
if (!group.length) {
PATCH_ORDERS.splice(groupIndex, 1);
// Apply patched functions
if (modified) {
item[1][id] = eval(str);
}
// Save to cache
if (appliedPatches.length) {
patchesMap[id] = appliedPatches;
}
}
if (Object.keys(patchesMap).length) {
PatcherCache.saveToCache(patchesMap);
}
}
static initialize() {
if (window.location.pathname.includes('/play/')) {
PATCH_ORDERS = PATCH_ORDERS.concat(PLAYING_PATCH_ORDERS);
} else {
PATCH_ORDERS.push(['loadingEndingChunks']);
}
Patcher.cleanupPatches();
static init() {
Patcher.#patchFunctionBind();
}
}
export class PatcherCache {
static #KEY_CACHE = 'better_xcloud_patches_cache';
static #KEY_SIGNATURE = 'better_xcloud_patches_cache_signature';
static #CACHE: any;
static #isInitialized = false;
/**
* Get patch's signature
*/
static #getSignature(): number {
const scriptVersion = SCRIPT_VERSION;
const webVersion = (document.querySelector('meta[name=gamepass-app-version]') as HTMLMetaElement)?.content;
const patches = JSON.stringify(ALL_PATCHES);
// Calculate signature
const sig = hashCode(scriptVersion + webVersion + patches)
return sig;
}
static clear() {
// Clear cache
window.localStorage.removeItem(PatcherCache.#KEY_CACHE);
PatcherCache.#CACHE = {};
}
static checkSignature() {
const storedSig = window.localStorage.getItem(PatcherCache.#KEY_SIGNATURE) || 0;
const currentSig = PatcherCache.#getSignature();
if (currentSig !== parseInt(storedSig as string)) {
// Save new signature
BxLogger.warning(LOG_TAG, 'Signature changed');
window.localStorage.setItem(PatcherCache.#KEY_SIGNATURE, currentSig.toString());
PatcherCache.clear();
} else {
BxLogger.info(LOG_TAG, 'Signature unchanged');
}
}
static #cleanupPatches(patches: PatchArray): PatchArray {
return patches.filter(item => {
for (const id in PatcherCache.#CACHE) {
const cached = PatcherCache.#CACHE[id];
if (cached.includes(item)) {
return false;
}
}
return true;
});
}
static getPatches(id: string): PatchArray {
return PatcherCache.#CACHE[id];
}
static saveToCache(subCache: { [key: string]: PatchArray }) {
for (const id in subCache) {
const patchNames = subCache[id];
let data = PatcherCache.#CACHE[id];
if (!data) {
PatcherCache.#CACHE[id] = patchNames;
} else {
for (const patchName of patchNames) {
if (!data.includes(patchName)) {
data.push(patchName);
}
}
}
}
// Save to storage
window.localStorage.setItem(PatcherCache.#KEY_CACHE, JSON.stringify(PatcherCache.#CACHE));
}
static init() {
if (PatcherCache.#isInitialized) {
return;
}
PatcherCache.#isInitialized = true;
PatcherCache.checkSignature();
// Read cache from storage
PatcherCache.#CACHE = JSON.parse(window.localStorage.getItem(PatcherCache.#KEY_CACHE) || '{}');
BxLogger.info(LOG_TAG, PatcherCache.#CACHE);
if (window.location.pathname.includes('/play/')) {
PATCH_ORDERS.push(...PLAYING_PATCH_ORDERS);
} else {
PATCH_ORDERS.push(ENDING_CHUNKS_PATCH_NAME);
}
// Remove cached patches from PATCH_ORDERS & PLAYING_PATCH_ORDERS
PATCH_ORDERS = PatcherCache.#cleanupPatches(PATCH_ORDERS);
PLAYING_PATCH_ORDERS = PatcherCache.#cleanupPatches(PLAYING_PATCH_ORDERS);
BxLogger.info(LOG_TAG, PATCH_ORDERS.slice(0));
BxLogger.info(LOG_TAG, PLAYING_PATCH_ORDERS.slice(0));
}
}

View File

@ -1,10 +1,14 @@
import { STATES, AppInterface } from "../utils/global";
import { CE, createButton, ButtonStyle, Icon } from "../utils/html";
import { Toast } from "../utils/toast";
import { BxEvent } from "../utils/bx-event";
import { getPref, PrefKey, setPref } from "../utils/preferences";
import { t } from "../utils/translation";
import { localRedirect } from "./ui/ui";
import { STATES, AppInterface } from "@utils/global";
import { CE, createButton, ButtonStyle } from "@utils/html";
import { BxIcon } from "@utils/bx-icon";
import { Toast } from "@utils/toast";
import { BxEvent } from "@utils/bx-event";
import { getPref, PrefKey, setPref } from "@utils/preferences";
import { t } from "@utils/translation";
import { localRedirect } from "@modules/ui/ui";
import { BxLogger } from "@utils/bx-logger";
const LOG_TAG = 'RemotePlay';
enum RemotePlayConsoleState {
ON = 'On',
@ -92,7 +96,7 @@ export class RemotePlay {
RemotePlay.#$content = CE('div', {}, t('getting-consoles-list'));
RemotePlay.#getXhomeToken(() => {
RemotePlay.#getConsolesList(() => {
console.log(RemotePlay.#CONSOLES);
BxLogger.info(LOG_TAG, 'Consoles', RemotePlay.#CONSOLES);
RemotePlay.#renderConsoles();
BxEvent.dispatch(window, BxEvent.REMOTE_PLAY_READY);
});
@ -180,7 +184,7 @@ export class RemotePlay {
// Add Help button
$fragment.appendChild(createButton({
icon: Icon.QUESTION,
icon: BxIcon.QUESTION,
style: ButtonStyle.GHOST | ButtonStyle.FOCUSABLE,
url: 'https://better-xcloud.github.io/remote-play',
label: t('help'),

View File

@ -1,5 +1,5 @@
import { STATES, AppInterface } from "../utils/global";
import { CE } from "../utils/html";
import { STATES, AppInterface } from "@utils/global";
import { CE } from "@utils/html";
export function takeScreenshot(callback: any) {
const currentStream = STATES.currentStream!;

View File

@ -1,7 +1,7 @@
import { t } from "../../utils/translation";
import { BxEvent } from "../../utils/bx-event";
import { CE } from "../../utils/html";
import { STATES } from "../../utils/global";
import { t } from "@utils/translation";
import { BxEvent } from "@utils/bx-event";
import { CE } from "@utils/html";
import { STATES } from "@utils/global";
enum StreamBadge {
PLAYTIME = 'playtime',

View File

@ -1,10 +1,11 @@
import { PrefKey } from "../../utils/preferences"
import { BxEvent } from "../../utils/bx-event"
import { getPref } from "../../utils/preferences"
import { PrefKey } from "@utils/preferences"
import { BxEvent } from "@utils/bx-event"
import { getPref } from "@utils/preferences"
import { StreamBadges } from "./stream-badges"
import { CE } from "../../utils/html"
import { t } from "../../utils/translation"
import { STATES } from "../../utils/global"
import { CE } from "@utils/html"
import { t } from "@utils/translation"
import { STATES } from "@utils/global"
import { BxLogger } from "@utils/bx-logger"
export enum StreamStat {
PING = 'ping',
@ -274,7 +275,7 @@ export class StreamStats {
// Get server type
if (candidateId) {
console.log('candidate', candidateId, allCandidates);
BxLogger.info('candidate', candidateId, allCandidates);
StreamBadges.ipv6 = allCandidates[candidateId].includes(':');
}

View File

@ -1,72 +1,14 @@
import { STATES } from "../../utils/global";
import { Icon } from "../../utils/html";
import { BxEvent } from "../../utils/bx-event";
import { PrefKey, getPref } from "../../utils/preferences";
import { t } from "../../utils/translation";
import { StreamBadges } from "./stream-badges";
import { StreamStats } from "./stream-stats";
import { STATES } from "@utils/global.ts";
import { createSvgIcon } from "@utils/html.ts";
import { BxIcon } from "@utils/bx-icon";
import { BxEvent } from "@utils/bx-event.ts";
import { PrefKey, getPref } from "@utils/preferences.ts";
import { t } from "@utils/translation.ts";
import { StreamBadges } from "./stream-badges.ts";
import { StreamStats } from "./stream-stats.ts";
class MouseHoldEvent {
#isHolding = false;
#timeout?: number | null;
#$elm;
#callback;
#duration;
#onMouseDown(e: MouseEvent | TouchEvent) {
const _this = this;
this.#isHolding = false;
this.#timeout && clearTimeout(this.#timeout);
this.#timeout = window.setTimeout(() => {
_this.#isHolding = true;
_this.#callback();
}, this.#duration);
};
#onMouseUp(e: MouseEvent | TouchEvent) {
this.#timeout && clearTimeout(this.#timeout);
this.#timeout = null;
if (this.#isHolding) {
e.preventDefault();
e.stopPropagation();
}
this.#isHolding = false;
};
#addEventListeners = () => {
this.#$elm.addEventListener('mousedown', this.#onMouseDown.bind(this));
this.#$elm.addEventListener('click', this.#onMouseUp.bind(this));
this.#$elm.addEventListener('touchstart', this.#onMouseDown.bind(this));
this.#$elm.addEventListener('touchend', this.#onMouseUp.bind(this));
}
/*
#clearEventLiseners = () => {
this.#$elm.removeEventListener('mousedown', this.#onMouseDown);
this.#$elm.removeEventListener('click', this.#onMouseUp);
this.#$elm.removeEventListener('touchstart', this.#onMouseDown);
this.#$elm.removeEventListener('touchend', this.#onMouseUp);
}
*/
constructor($elm: HTMLElement, callback: any, duration=1000) {
this.#$elm = $elm;
this.#callback = callback;
this.#duration = duration;
this.#addEventListeners();
// $elm.clearMouseHoldEventListeners = this.#clearEventLiseners;
}
}
function cloneStreamHudButton($orgButton: HTMLElement, label: string, svgIcon: Icon) {
function cloneStreamHudButton($orgButton: HTMLElement, label: string, svgIcon: typeof BxIcon) {
const $container = $orgButton.cloneNode(true) as HTMLElement;
let timeout: number | null;
@ -101,25 +43,13 @@ function cloneStreamHudButton($orgButton: HTMLElement, label: string, svgIcon: I
const $button = $container.querySelector('button')!;
$button.setAttribute('title', label);
const $svg = $button.querySelector('svg')!;
$svg.innerHTML = svgIcon;
const $orgSvg = $button.querySelector('svg')!;
const $svg = createSvgIcon(svgIcon);
$svg.style.fill = 'none';
$svg.setAttribute('class', $orgSvg.getAttribute('class') || '');
$svg.ariaHidden = 'true';
const attrs = {
'fill': 'none',
'stroke': '#fff',
'fill-rule': 'evenodd',
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
'stroke-width': '2',
'viewBox': '0 0 32 32'
};
let attr: keyof typeof attrs;
for (attr in attrs) {
$svg.setAttribute(attr, attrs[attr]);
}
$orgSvg.replaceWith($svg);
return $container;
}
@ -203,25 +133,39 @@ export function injectStreamMenuButtons() {
}
// Render badges
if ($elm.className.startsWith('StreamMenu')) {
if ($elm.className.startsWith('StreamMenu-module__container')) {
BxEvent.dispatch(window, BxEvent.STREAM_MENU_SHOWN);
// Hide Quick bar when closing HUD
const $btnCloseHud = document.querySelector('button[class*=StreamMenu-module__backButton]');
if (!$btnCloseHud) {
return;
}
// Hide Quick bar when closing HUD
$btnCloseHud && $btnCloseHud.addEventListener('click', e => {
$quickBar.classList.add('bx-gone');
});
// Get "Quit game" button
const $btnQuit = $elm.querySelector('div[class^=StreamMenu] > div > button:last-child') as HTMLElement;
// Hold "Quit game" button to refresh the stream
new MouseHoldEvent($btnQuit, () => {
// Create Refresh button from the Close button
const $btnRefresh = $btnCloseHud.cloneNode(true) as HTMLElement;
// Refresh SVG
const $svgRefresh = createSvgIcon(BxIcon.REFRESH);
// Copy classes
$svgRefresh.setAttribute('class', $btnRefresh.firstElementChild!.getAttribute('class') || '');
$svgRefresh.style.fill = 'none';
$btnRefresh.classList.add('bx-stream-refresh-button');
// Remove icon
$btnRefresh.removeChild($btnRefresh.firstElementChild!);
// Add Refresh icon
$btnRefresh.appendChild($svgRefresh);
// Add "click" event listener
$btnRefresh.addEventListener('click', e => {
confirm(t('confirm-reload-stream')) && window.location.reload();
}, 1000);
});
// Add to website
$btnCloseHud.insertAdjacentElement('afterend', $btnRefresh);
// Render stream badges
const $menu = document.querySelector('div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module]');
@ -261,7 +205,7 @@ export function injectStreamMenuButtons() {
// Create Stream Settings button
if (!$btnStreamSettings) {
$btnStreamSettings = cloneStreamHudButton($orgButton, t('menu-stream-settings'), Icon.STREAM_SETTINGS);
$btnStreamSettings = cloneStreamHudButton($orgButton, t('menu-stream-settings'), BxIcon.STREAM_SETTINGS);
$btnStreamSettings.addEventListener('click', e => {
hideGripHandle();
e.preventDefault();
@ -279,7 +223,7 @@ export function injectStreamMenuButtons() {
// Create Stream Stats button
if (!$btnStreamStats) {
$btnStreamStats = cloneStreamHudButton($orgButton, t('menu-stream-stats'), Icon.STREAM_STATS);
$btnStreamStats = cloneStreamHudButton($orgButton, t('menu-stream-stats'), BxIcon.STREAM_STATS);
$btnStreamStats.addEventListener('click', e => {
hideGripHandle();
e.preventDefault();

View File

@ -1,11 +1,14 @@
import { STATES } from "../utils/global";
import { CE } from "../utils/html";
import { Toast } from "../utils/toast";
import { BxEvent } from "../utils/bx-event";
import { BX_FLAGS } from "../utils/bx-flags";
import { getPref, PrefKey } from "../utils/preferences";
import { t } from "../utils/translation";
import { NATIVE_FETCH } from "../utils/network";
import { STATES } from "@utils/global";
import { CE } from "@utils/html";
import { Toast } from "@utils/toast";
import { BxEvent } from "@utils/bx-event";
import { BX_FLAGS } from "@utils/bx-flags";
import { getPref, PrefKey } from "@utils/preferences";
import { t } from "@utils/translation";
import { NATIVE_FETCH } from "@utils/network";
import { BxLogger } from "@utils/bx-logger";
const LOG_TAG = 'TouchController';
export class TouchController {
static readonly #EVENT_SHOW_DEFAULT_CONTROLLER = new MessageEvent('message', {
@ -175,9 +178,7 @@ export class TouchController {
layout: {
id: 'System.Standard',
displayName: 'System',
layoutFile: {
content: layout.content,
},
layoutFile: layout,
}
});
}, delay);
@ -185,7 +186,7 @@ export class TouchController {
static setup() {
// Function for testing touch control
window.BX_EXPOSED.test_touch_control = (content: any) => {
window.BX_EXPOSED.test_touch_control = (layout: any) => {
const { touch_layout_manager } = window.BX_EXPOSED;
touch_layout_manager && touch_layout_manager.changeLayoutForScope({
@ -195,9 +196,7 @@ export class TouchController {
layout: {
id: 'System.Standard',
displayName: 'Custom',
layoutFile: {
content: content,
},
layoutFile: layout,
},
});
};
@ -296,7 +295,7 @@ export class TouchController {
STATES.currentStream.xboxTitleId = parseInt(json.titleid, 16).toString();
}
} catch (e) {
console.log(e);
BxLogger.error(LOG_TAG, 'Load custom layout', e);
}
});
});

View File

@ -1,9 +1,11 @@
import { STATES, AppInterface, SCRIPT_HOME, SCRIPT_VERSION } from "../../utils/global";
import { CE, createButton, Icon, ButtonStyle } from "../../utils/html";
import { getPreferredServerRegion } from "../../utils/region";
import { UserAgent, UserAgentProfile } from "../../utils/user-agent";
import { getPref, Preferences, PrefKey, setPref, toPrefElement } from "../../utils/preferences";
import { t, refreshCurrentLocale } from "../../utils/translation";
import { STATES, AppInterface, SCRIPT_HOME, SCRIPT_VERSION } from "@utils/global";
import { CE, createButton, ButtonStyle } from "@utils/html";
import { BxIcon } from "@utils/bx-icon";
import { getPreferredServerRegion } from "@utils/region";
import { UserAgent, UserAgentProfile } from "@utils/user-agent";
import { getPref, Preferences, PrefKey, setPref, toPrefElement } from "@utils/preferences";
import { t, refreshCurrentLocale } from "@utils/translation";
import { PatcherCache } from "../patcher";
const SETTINGS_UI = {
'Better xCloud': {
@ -120,7 +122,7 @@ export function setupSettingsUi() {
'href': SCRIPT_HOME,
'target': '_blank',
}, 'Better xCloud ' + SCRIPT_VERSION),
createButton({icon: Icon.QUESTION, label: t('help'), url: 'https://better-xcloud.github.io/features/'}),
createButton({icon: BxIcon.QUESTION, label: t('help'), url: 'https://better-xcloud.github.io/features/'}),
)
);
$updateAvailable = CE('a', {
@ -158,6 +160,9 @@ export function setupSettingsUi() {
$reloadBtnWrapper.classList.remove('bx-gone');
// Clear PatcherCache;
PatcherCache.clear();
if ((e.target as HTMLElement).id === 'bx_setting_' + PrefKey.BETTER_XCLOUD_LOCALE) {
// Update locale
refreshCurrentLocale();

View File

@ -1,9 +1,10 @@
import { SCRIPT_VERSION } from "../../utils/global";
import { createButton, Icon, ButtonStyle } from "../../utils/html";
import { getPreferredServerRegion } from "../../utils/region";
import { PrefKey, getPref } from "../../utils/preferences";
import { RemotePlay } from "../remote-play";
import { t } from "../../utils/translation";
import { SCRIPT_VERSION } from "@utils/global";
import { createButton, ButtonStyle } from "@utils/html";
import { BxIcon } from "@utils/bx-icon";
import { getPreferredServerRegion } from "@utils/region";
import { PrefKey, getPref } from "@utils/preferences";
import { RemotePlay } from "@modules/remote-play";
import { t } from "@utils/translation";
import { setupSettingsUi } from "./global-settings";
@ -21,7 +22,7 @@ function injectSettingsButton($parent?: HTMLElement) {
if (getPref(PrefKey.REMOTE_PLAY_ENABLED)) {
const $remotePlayBtn = createButton({
classes: ['bx-header-remote-play-button'],
icon: Icon.REMOTE_PLAY,
icon: BxIcon.REMOTE_PLAY,
title: t('remote-play'),
style: ButtonStyle.GHOST | ButtonStyle.FOCUSABLE,
onClick: e => {

View File

@ -1,14 +1,15 @@
import { STATES } from "../../utils/global";
import { Icon, CE, createButton, ButtonStyle } from "../../utils/html";
import { UserAgent } from "../../utils/user-agent";
import { BxEvent } from "../../utils/bx-event";
import { MkbRemapper } from "../mkb/mkb-remapper";
import { getPref, PrefKey, toPrefElement } from "../../utils/preferences";
import { setupScreenshotButton } from "../screenshot";
import { StreamStats } from "../stream/stream-stats";
import { TouchController } from "../touch-controller";
import { t } from "../../utils/translation";
import { VibrationManager } from "../vibration-manager";
import { STATES } from "@utils/global";
import { CE, createButton, ButtonStyle, createSvgIcon } from "@utils/html";
import { BxIcon } from "@utils/bx-icon";
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";
export function localRedirect(path: string) {
@ -70,7 +71,7 @@ function setupQuickSettingsBar() {
const SETTINGS_UI = [
getPref(PrefKey.MKB_ENABLED) && {
icon: Icon.MOUSE,
icon: BxIcon.MOUSE,
group: 'mkb',
items: [
{
@ -83,7 +84,7 @@ function setupQuickSettingsBar() {
},
{
icon: Icon.DISPLAY,
icon: BxIcon.DISPLAY,
group: 'stream',
items: [
{
@ -145,7 +146,7 @@ function setupQuickSettingsBar() {
},
{
icon: Icon.CONTROLLER,
icon: BxIcon.CONTROLLER,
group: 'controller',
items: [
{
@ -232,7 +233,7 @@ function setupQuickSettingsBar() {
},
{
icon: Icon.STREAM_STATS,
icon: BxIcon.STREAM_STATS,
group: 'stats',
items: [
{
@ -300,18 +301,7 @@ function setupQuickSettingsBar() {
continue;
}
const $svg = CE('svg', {
'xmlns': 'http://www.w3.org/2000/svg',
'data-group': settingTab.group,
'fill': 'none',
'stroke': '#fff',
'fill-rule': 'evenodd',
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
'stroke-width': 2,
});
$svg.innerHTML = settingTab.icon;
$svg.setAttribute('viewBox', '0 0 32 32');
const $svg = createSvgIcon(settingTab.icon);
$svg.addEventListener('click', e => {
// Switch tab
for (const $child of Array.from($settings.children)) {
@ -342,7 +332,7 @@ function setupQuickSettingsBar() {
$group.appendChild(CE('h2', {},
CE('span', {}, settingGroup.label),
settingGroup.help_url && createButton({
icon: Icon.QUESTION,
icon: BxIcon.QUESTION,
style: ButtonStyle.GHOST,
url: settingGroup.help_url,
title: t('help'),

View File

@ -1,6 +1,6 @@
import { AppInterface } from "../utils/global";
import { BxEvent } from "../utils/bx-event";
import { PrefKey, getPref } from "../utils/preferences";
import { AppInterface } from "@utils/global";
import { BxEvent } from "@utils/bx-event";
import { PrefKey, getPref } from "@utils/preferences";
const VIBRATION_DATA_MAP = {
'gamepadIndex': 8,

View File

@ -67,3 +67,6 @@ type XcloudTitleInfo = {
tileImageUrl: string;
};
};
declare module "*.svg";
declare module "*.styl";

2
src/types/mkb.d.ts vendored
View File

@ -1,4 +1,4 @@
import { MkbPresetKey } from "../modules/mkb/definitions";
import { MkbPresetKey } from "@modules/mkb/definitions";
type GamepadKeyNameType = {[index: string | number]: string[]};

View File

@ -1,4 +1,4 @@
import { AppInterface } from "./global";
import { AppInterface } from "@utils/global";
export enum BxEvent {
JUMP_BACK_IN_READY = 'bx-jump-back-in-ready',

View File

@ -1,7 +1,7 @@
import { BxEvent } from "./bx-event";
import { STATES } from "./global";
import { getPref, PrefKey } from "./preferences";
import { UserAgent } from "./user-agent";
import { BxEvent } from "@utils/bx-event";
import { STATES } from "@utils/global";
import { getPref, PrefKey } from "@utils/preferences";
import { UserAgent } from "@utils/user-agent";
enum InputType {
CONTROLLER = 'Controller',
@ -73,9 +73,9 @@ export const BxExposed = {
// Pre-check supported input types
titleInfo.details.hasMkbSupport = supportedInputTypes.includes(InputType.MKB);
titleInfo.details.hasTouchSupport = supportedInputTypes.includes(InputType.NATIVE_TOUCH) &&
!supportedInputTypes.includes(InputType.CUSTOM_TOUCH_OVERLAY) &&
!supportedInputTypes.includes(InputType.GENERIC_TOUCH);
titleInfo.details.hasTouchSupport = supportedInputTypes.includes(InputType.NATIVE_TOUCH) ||
supportedInputTypes.includes(InputType.CUSTOM_TOUCH_OVERLAY) ||
supportedInputTypes.includes(InputType.GENERIC_TOUCH);
if (!titleInfo.details.hasTouchSupport && touchControllerAvailability === 'all') {
// Add generic touch support for non touch-supported games

32
src/utils/bx-icon.ts Normal file
View File

@ -0,0 +1,32 @@
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" };
import iconDisplay from "@assets/svg/display.svg" with { type: "text" };
import iconMouseSettings from "@assets/svg/mouse-settings.svg" with { type: "text" };
import iconMouse from "@assets/svg/mouse.svg" with { type: "text" };
import iconNew from "@assets/svg/new.svg" with { type: "text" };
import iconQuestion from "@assets/svg/question.svg" with { type: "text" };
import iconRefresh from "@assets/svg/refresh.svg" with { type: "text" };
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" };
export const BxIcon = {
STREAM_SETTINGS: iconStreamSettings,
STREAM_STATS: iconStreamStats,
CONTROLLER: iconController,
DISPLAY: iconDisplay,
MOUSE: iconMouse,
MOUSE_SETTINGS: iconMouseSettings,
NEW: iconNew,
COPY: iconCopy,
TRASH: iconTrash,
CURSOR_TEXT: iconCursorText,
QUESTION: iconQuestion,
REFRESH: iconRefresh,
REMOTE_PLAY: iconRemotePlay,
// 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;

27
src/utils/bx-logger.ts Normal file
View File

@ -0,0 +1,27 @@
enum TextColor {
INFO = '#008746',
WARNING = '#c1a404',
ERROR = '#c10404',
}
export class BxLogger {
static #PREFIX = '[BxC]';
static info(tag: string, ...args: any[]) {
BxLogger.#log(TextColor.INFO, tag, ...args);
}
static warning(tag: string, ...args: any[]) {
BxLogger.#log(TextColor.WARNING, tag, ...args);
}
static error(tag: string, ...args: any[]) {
BxLogger.#log(TextColor.ERROR, tag, ...args);
}
static #log(color: TextColor, tag: string, ...args: any) {
console.log('%c' + BxLogger.#PREFIX, 'color:' + color + ';font-weight:bold;', tag, '-', ...args);
}
}
(window as any).BxLogger = BxLogger;

View File

@ -1,6 +1,6 @@
import { CE } from "./html";
import { PrefKey, getPref } from "./preferences";
import { renderStylus } from "../macros/build" with {type: "macro"};
import { CE } from "@utils/html";
import { PrefKey, getPref } from "@utils/preferences";
import { renderStylus } from "@macros/build" with {type: "macro"};
export function addCss() {

View File

@ -1,7 +1,8 @@
import { MkbHandler } from "../modules/mkb/mkb-handler";
import { PrefKey, getPref } from "./preferences";
import { t } from "./translation";
import { Toast } from "./toast";
import { MkbHandler } from "@modules/mkb/mkb-handler";
import { PrefKey, getPref } from "@utils/preferences";
import { t } from "@utils/translation";
import { Toast } from "@utils/toast";
import { BxLogger } from "@utils/bx-logger";
// Show a toast when connecting/disconecting controller
export function showGamepadToast(gamepad: Gamepad) {
@ -10,7 +11,7 @@ export function showGamepadToast(gamepad: Gamepad) {
return;
}
console.log(gamepad);
BxLogger.info('Gamepad', gamepad);
let text = '🎮';
if (getPref(PrefKey.LOCAL_CO_OP_ENABLED)) {

View File

@ -1,7 +1,7 @@
import { BxEvent } from "./bx-event";
import { LoadingScreen } from "../modules/loading-screen";
import { RemotePlay } from "../modules/remote-play";
import { checkHeader } from "../modules/ui/header";
import { BxEvent } from "@utils/bx-event";
import { LoadingScreen } from "@modules/loading-screen";
import { RemotePlay } from "@modules/remote-play";
import { checkHeader } from "@modules/ui/header";
export function patchHistoryMethod(type: 'pushState' | 'replaceState') {
const orig = window.history[type];

View File

@ -1,8 +1,10 @@
import type { BxIcon } from "@utils/bx-icon";
type BxButton = {
style?: number | string;
url?: string;
classes?: string[];
icon?: string;
icon?: typeof BxIcon;
label?: string;
title?: string;
disabled?: boolean;
@ -52,39 +54,11 @@ function createElement<T=HTMLElement>(elmName: string, props: {[index: string]:
export const CE = createElement;
// Credit: https://phosphoricons.com
export enum Icon {
STREAM_SETTINGS = '<g transform="matrix(.142357 0 0 .142357 -2.22021 -2.22164)" fill="none" stroke="#fff" stroke-width="16"><circle cx="128" cy="128" r="40"/><path d="M130.05 206.11h-4L94 224c-12.477-4.197-24.049-10.711-34.11-19.2l-.12-36c-.71-1.12-1.38-2.25-2-3.41L25.9 147.24a99.16 99.16 0 0 1 0-38.46l31.84-18.1c.65-1.15 1.32-2.29 2-3.41l.16-36C69.951 42.757 81.521 36.218 94 32l32 17.89h4L162 32c12.477 4.197 24.049 10.711 34.11 19.2l.12 36c.71 1.12 1.38 2.25 2 3.41l31.85 18.14a99.16 99.16 0 0 1 0 38.46l-31.84 18.1c-.65 1.15-1.32 2.29-2 3.41l-.16 36A104.59 104.59 0 0 1 162 224l-31.95-17.89z"/></g>',
STREAM_STATS = '<path d="M1.181 24.55v-3.259c0-8.19 6.576-14.952 14.767-14.98H16c8.13 0 14.819 6.69 14.819 14.819v3.42c0 .625-.515 1.14-1.14 1.14H2.321c-.625 0-1.14-.515-1.14-1.14z"/><path d="M16 6.311v4.56M12.58 25.69l9.12-12.54m4.559 5.7h4.386m-29.266 0H5.74"/>',
CONTROLLER = '<path d="M19.193 12.807h3.193m-13.836 0h4.257"/><path d="M10.678 10.678v4.257"/><path d="M13.061 19.193l-5.602 6.359c-.698.698-1.646 1.09-2.633 1.09-2.044 0-3.725-1.682-3.725-3.725a3.73 3.73 0 0 1 .056-.646l2.177-11.194a6.94 6.94 0 0 1 6.799-5.721h11.722c3.795 0 6.918 3.123 6.918 6.918s-3.123 6.918-6.918 6.918h-8.793z"/><path d="M18.939 19.193l5.602 6.359c.698.698 1.646 1.09 2.633 1.09 2.044 0 3.725-1.682 3.725-3.725a3.73 3.73 0 0 0-.056-.646l-2.177-11.194"/>',
DISPLAY = '<path d="M1.238 21.119c0 1.928 1.565 3.493 3.493 3.493H27.27c1.928 0 3.493-1.565 3.493-3.493V5.961c0-1.928-1.565-3.493-3.493-3.493H4.731c-1.928 0-3.493 1.565-3.493 3.493v15.158zm19.683 8.413H11.08"/>',
MOUSE = '<path d="M26.256 8.185c0-3.863-3.137-7-7-7h-6.512c-3.863 0-7 3.137-7 7v15.629c0 3.863 3.137 7 7 7h6.512c3.863 0 7-3.137 7-7V8.185z"/><path d="M16 13.721V6.883"/>',
MOUSE_SETTINGS = '<g transform="matrix(1.10403 0 0 1.10403 -4.17656 -.560429)" fill="none" stroke="#fff"><g stroke-width="1.755"><path d="M24.49 16.255l.01-8.612A6.15 6.15 0 0 0 18.357 1.5h-5.714A6.15 6.15 0 0 0 6.5 7.643v13.715a6.15 6.15 0 0 0 6.143 6.143h5.714"/><path d="M15.5 12.501v-6"/></g><circle cx="48" cy="48" r="15" stroke-width="7.02" transform="matrix(.142357 0 0 .142357 17.667421 16.541885)"/><path d="M24.61 27.545h-.214l-1.711.955c-.666-.224-1.284-.572-1.821-1.025l-.006-1.922-.107-.182-1.701-.969c-.134-.678-.134-1.375 0-2.053l1.7-.966.107-.182.009-1.922c.537-.454 1.154-.803 1.82-1.029l1.708.955h.214l1.708-.955c.666.224 1.284.572 1.821 1.025l.006 1.922.107.182 1.7.968c.134.678.134 1.375 0 2.053l-1.7.966-.107.182-.009 1.922c-.536.455-1.154.804-1.819 1.029l-1.706-.955z" stroke-width=".999"/></g>',
NEW = '<path d="M26.875 30.5H5.125c-.663 0-1.208-.545-1.208-1.208V2.708c0-.663.545-1.208 1.208-1.208h14.5l8.458 8.458v19.333c0 .663-.545 1.208-1.208 1.208z"/><path d="M19.625 1.5v8.458h8.458m-15.708 9.667h7.25"/><path d="M16 16v7.25"/>',
COPY = '<path d="M1.498 6.772h23.73v23.73H1.498zm5.274-5.274h23.73v23.73"/>',
TRASH = '<path d="M29.5 6.182h-27m9.818 7.363v9.818m7.364-9.818v9.818"/><path d="M27.045 6.182V29.5c0 .673-.554 1.227-1.227 1.227H6.182c-.673 0-1.227-.554-1.227-1.227V6.182m17.181 0V3.727a2.47 2.47 0 0 0-2.455-2.455h-7.364a2.47 2.47 0 0 0-2.455 2.455v2.455"/>',
CURSOR_TEXT = '<path d="M16 7.3a5.83 5.83 0 0 1 5.8-5.8h2.9m0 29h-2.9a5.83 5.83 0 0 1-5.8-5.8"/><path d="M7.3 30.5h2.9a5.83 5.83 0 0 0 5.8-5.8V7.3a5.83 5.83 0 0 0-5.8-5.8H7.3"/><path d="M11.65 16h8.7"/>',
QUESTION = '<g transform="matrix(.256867 0 0 .256867 -16.878964 -18.049342)"><circle cx="128" cy="180" r="12" fill="#fff"/><path d="M128 144v-8c17.67 0 32-12.54 32-28s-14.33-28-32-28-32 12.54-32 28v4" fill="none" stroke="#fff" stroke-width="16"/></g>',
const svgParser = (svg: string) => new DOMParser().parseFromString(svg, 'image/svg+xml').documentElement;
REMOTE_PLAY = '<g transform="matrix(.492308 0 0 .581818 -14.7692 -11.6364)"><clipPath id="A"><path d="M30 20h65v55H30z"/></clipPath><g clip-path="url(#A)"><g transform="matrix(.395211 0 0 .334409 11.913 7.01124)"><g transform="matrix(.555556 0 0 .555556 57.8889 -20.2417)" fill="none" stroke="#fff" stroke-width="13.88"><path d="M200 140.564c-42.045-33.285-101.955-33.285-144 0M168 165c-23.783-17.3-56.217-17.3-80 0"/></g><g transform="matrix(-.555556 0 0 -.555556 200.111 262.393)"><g transform="matrix(1 0 0 1 0 11.5642)"><path d="M200 129c-17.342-13.728-37.723-21.795-58.636-24.198C111.574 101.378 80.703 109.444 56 129" fill="none" stroke="#fff" stroke-width="13.88"/></g><path d="M168 165c-23.783-17.3-56.217-17.3-80 0" fill="none" stroke="#fff" stroke-width="13.88"/></g><g transform="matrix(.75 0 0 .75 32 32)"><path d="M24 72h208v93.881H24z" fill="none" stroke="#fff" stroke-linejoin="miter" stroke-width="9.485"/><circle cx="188" cy="128" r="12" stroke-width="10" transform="matrix(.708333 0 0 .708333 71.8333 12.8333)"/><path d="M24.358 103.5h110" fill="none" stroke="#fff" stroke-linecap="butt" stroke-width="10.282"/></g></g></g></g>',
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"/>',
};
export const createSvgIcon = (icon: string, strokeWidth=2) => {
const $svg = CE('svg', {
'xmlns': 'http://www.w3.org/2000/svg',
'fill': 'none',
'stroke': '#fff',
'fill-rule': 'evenodd',
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
'stroke-width': strokeWidth,
});
$svg.innerHTML = icon;
$svg.setAttribute('viewBox', '0 0 32 32');
return $svg;
};
export const createSvgIcon = (icon: typeof BxIcon) => {
return svgParser(icon.toString());
}
export const ButtonStyle: DualEnum = {};
ButtonStyle[ButtonStyle.PRIMARY = 1] = 'bx-primary';
@ -113,7 +87,7 @@ export const createButton = <T=HTMLButtonElement>(options: BxButton): T => {
options.classes && $btn.classList.add(...options.classes);
options.icon && $btn.appendChild(createSvgIcon(options.icon, 4));
options.icon && $btn.appendChild(createSvgIcon(options.icon));
options.label && $btn.appendChild(CE('span', {}, options.label));
options.title && $btn.setAttribute('title', options.title);
options.disabled && (($btn as HTMLButtonElement).disabled = true);

View File

@ -1,7 +1,7 @@
import { MkbPreset } from "../modules/mkb/mkb-preset";
import { PrefKey, setPref } from "./preferences";
import { t } from "./translation";
import type { MkbStoredPreset, MkbStoredPresets } from "../types/mkb";
import { MkbPreset } from "@modules/mkb/mkb-preset";
import { PrefKey, setPref } from "@utils/preferences";
import { t } from "@utils/translation";
import type { MkbStoredPreset, MkbStoredPresets } from "@/types/mkb";
export class LocalDb {
static #instance: LocalDb;

View File

@ -1,7 +1,8 @@
import { BxEvent } from "./bx-event";
import { getPref, PrefKey } from "./preferences";
import { STATES } from "./global";
import { UserAgent } from "./user-agent";
import { BxEvent } from "@utils/bx-event";
import { getPref, PrefKey } from "@utils/preferences";
import { STATES } from "@utils/global";
import { UserAgent } from "@utils/user-agent";
import { BxLogger } from "@utils/bx-logger";
export function patchVideoApi() {
const PREF_SKIP_SPLASH_VIDEO = getPref(PrefKey.SKIP_SPLASH_VIDEO);
@ -77,7 +78,7 @@ export function patchRtcCodecs() {
nativeSetCodecPreferences.apply(this, [newCodecs]);
} catch (e) {
// Didn't work -> use default codecs
console.log(e);
BxLogger.error('setCodecPreferences', e);
nativeSetCodecPreferences.apply(this, [codecs]);
}
}
@ -106,7 +107,8 @@ export function patchRtcPeerConnection() {
if (conn.connectionState === 'connecting') {
STATES.currentStream.audioGainNode = null;
}
console.log('connectionState', conn.connectionState);
BxLogger.info('connectionstatechange', conn.connectionState);
});
return conn;
}

View File

@ -1,12 +1,12 @@
import { BxEvent } from "./bx-event";
import { BX_FLAGS } from "./bx-flags";
import { LoadingScreen } from "../modules/loading-screen";
import { PrefKey, getPref } from "./preferences";
import { RemotePlay } from "../modules/remote-play";
import { StreamBadges } from "../modules/stream/stream-badges";
import { TouchController } from "../modules/touch-controller";
import { STATES } from "./global";
import { getPreferredServerRegion } from "./region";
import { BxEvent } from "@utils/bx-event";
import { BX_FLAGS } from "@utils/bx-flags";
import { LoadingScreen } from "@modules/loading-screen";
import { PrefKey, getPref } from "@utils/preferences";
import { RemotePlay } from "@modules/remote-play";
import { StreamBadges } from "@modules/stream/stream-badges";
import { TouchController } from "@modules/touch-controller";
import { STATES } from "@utils/global";
import { getPreferredServerRegion } from "@utils/region";
export const NATIVE_FETCH = window.fetch;
@ -78,7 +78,7 @@ function updateIceCandidates(candidates: any, options: any) {
lst.forEach(item => {
item.foundation = foundation;
item.priority = (foundation == 1) ? 10000 : 1;
item.priority = (foundation == 1) ? 2130706431 : 1;
newCandidates.push(newCandidate(`a=candidate:${item.foundation} 1 UDP ${item.priority} ${item.ip} ${item.port} ${item.the_rest}`));
++foundation;
@ -226,6 +226,19 @@ class XhomeInterceptor {
return NATIVE_FETCH(request);
}
static async #handlePlay(request: RequestInfo | URL) {
const clone = (request as Request).clone();
const body = await clone.json();
// body.settings.useIceConnection = true;
const newRequest = new Request(request, {
body: JSON.stringify(body),
});
return NATIVE_FETCH(newRequest);
}
static async handle(request: Request) {
TouchController.disable();
@ -267,6 +280,8 @@ class XhomeInterceptor {
// Get console IP
if (url.includes('/configuration')) {
return XhomeInterceptor.#handleConfiguration(request);
} else if (url.endsWith('/sessions/home/play')) {
return XhomeInterceptor.#handlePlay(request);
} else if (url.includes('inputconfigs')) {
return XhomeInterceptor.#handleInputConfigs(request, opts);
} else if (url.includes('/login/user')) {
@ -473,7 +488,7 @@ export function interceptHttpRequests() {
'https://arc.msn.com',
'https://browser.events.data.microsoft.com',
'https://dc.services.visualstudio.com',
// 'https://2c06dea3f26c40c69b8456d319791fd0@o427368.ingest.sentry.io',
'https://2c06dea3f26c40c69b8456d319791fd0@o427368.ingest.sentry.io',
]);
}
@ -481,7 +496,8 @@ export function interceptHttpRequests() {
BLOCKED_URLS = BLOCKED_URLS.concat([
'https://peoplehub.xboxlive.com/users/me/people/social',
'https://peoplehub.xboxlive.com/users/me/people/recommendations',
'https://notificationinbox.xboxlive.com',
'https://xblmessaging.xboxlive.com/network/xbox/users/me/inbox',
// 'https://notificationinbox.xboxlive.com',
// 'https://accounts.xboxlive.com/family/memberXuid',
]);
}
@ -510,7 +526,7 @@ export function interceptHttpRequests() {
return nativeXhrSend.apply(this, arguments);
};
window.fetch = async (request: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
(window as any).BX_FETCH = window.fetch = async (request: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
let url = (typeof request === 'string') ? request : (request as Request).url;
// Check blocked URLs

View File

@ -1,10 +1,10 @@
import { CE } from "./html";
import { SUPPORTED_LANGUAGES, t } from "./translation";
import { SettingElement, SettingElementType } from "./settings";
import { UserAgentProfile } from "./user-agent";
import { StreamStat } from "../modules/stream/stream-stats";
import type { PreferenceSettings } from "../types/preferences";
import { STATES } from "./global";
import { CE } from "@utils/html";
import { SUPPORTED_LANGUAGES, t } from "@utils/translation";
import { SettingElement, SettingElementType } from "@utils/settings";
import { UserAgentProfile } from "@utils/user-agent";
import { StreamStat } from "@modules/stream/stream-stats";
import type { PreferenceSettings } from "@/types/preferences";
import { STATES } from "@utils/global";
export enum PrefKey {
LAST_UPDATE_CHECK = 'version_last_check',
@ -417,6 +417,7 @@ export class Preferences {
default: 'default',
options: {
default: t('default'),
normal: t('normal'),
tv: t('smart-tv'),
},
},
@ -441,7 +442,9 @@ export class Preferences {
[UserAgentProfile.DEFAULT]: t('default'),
[UserAgentProfile.EDGE_WINDOWS]: 'Edge + Windows',
[UserAgentProfile.SAFARI_MACOS]: 'Safari + macOS',
[UserAgentProfile.SMARTTV]: 'Smart TV',
[UserAgentProfile.SMARTTV_TIZEN]: 'Samsung Smart TV',
[UserAgentProfile.VR_OCULUS]: 'Meta Quest VR',
[UserAgentProfile.KIWI_V123]: 'Kiwi Browser v123',
[UserAgentProfile.CUSTOM]: t('custom'),
},

View File

@ -1,5 +1,5 @@
import { getPref, PrefKey } from "./preferences";
import { STATES } from "./global";
import { getPref, PrefKey } from "@utils/preferences";
import { STATES } from "@utils/global";
export function getPreferredServerRegion(shortName = false) {

View File

@ -1,5 +1,5 @@
import type { PreferenceSetting } from "../types/preferences";
import { CE } from "./html";
import type { PreferenceSetting } from "@/types/preferences";
import { CE } from "@utils/html";
type MultipleOptionsParams = {
size?: number;

View File

@ -1,5 +1,5 @@
import { STATES } from "./global";
import { UserAgent } from "./user-agent";
import { STATES } from "@utils/global";
import { UserAgent } from "@utils/user-agent";
export class PreloadedState {

View File

@ -1,4 +1,4 @@
import { CE } from "./html";
import { CE } from "@utils/html";
type ToastOptions = {
instant?: boolean;

View File

@ -1,9 +1,11 @@
import { PrefKey, getPref } from "./preferences";
import { PrefKey, getPref } from "@utils/preferences";
export enum UserAgentProfile {
EDGE_WINDOWS = 'edge-windows',
SAFARI_MACOS = 'safari-macos',
SMARTTV = 'smarttv',
SMARTTV_TIZEN = 'smarttv-tizen',
VR_OCULUS = 'vr-oculus',
KIWI_V123 = 'kiwi-v123',
DEFAULT = 'default',
CUSTOM = 'custom',
@ -26,7 +28,9 @@ export class UserAgent {
static #USER_AGENTS = {
[UserAgentProfile.EDGE_WINDOWS]: EDGE_USER_AGENT,
[UserAgentProfile.SAFARI_MACOS]: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5.2 Safari/605.1.1',
[UserAgentProfile.SMARTTV]: window.navigator.userAgent + ' SmartTV',
[UserAgentProfile.SMARTTV_TIZEN]: 'Mozilla/5.0 (SMART-TV; LINUX; Tizen 7.0) AppleWebKit/537.36 (KHTML, like Gecko) 94.0.4606.31/7.0 TV Safari/537.36',
[UserAgentProfile.VR_OCULUS]: window.navigator.userAgent + ' OculusBrowser VR',
[UserAgentProfile.KIWI_V123]: 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.118 Mobile Safari/537.36',
}

View File

@ -1,7 +1,10 @@
import { PrefKey, getPref, setPref } from "./preferences";
import { SCRIPT_VERSION } from "./global";
import { UserAgent } from "./user-agent";
import { PrefKey, getPref, setPref } from "@utils/preferences";
import { SCRIPT_VERSION } from "@utils/global";
import { UserAgent } from "@utils/user-agent";
/**
* Check for update
*/
export function checkForUpdate() {
const CHECK_INTERVAL_SECONDS = 2 * 3600; // check every 2 hours
@ -25,6 +28,9 @@ export function checkForUpdate() {
}
/**
* Disable PWA requirement on Safari
*/
export function disablePwa() {
const userAgent = ((window.navigator as any).orgUserAgent || window.navigator.userAgent || '').toLowerCase();
if (!userAgent) {
@ -39,3 +45,19 @@ export function disablePwa() {
});
}
}
/**
* Calculate hash code from a string
* @see http://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/
*/
export function hashCode(str: string): number {
let hash = 0;
for (let i = 0, len = str.length; i < len; i++) {
const chr = str.charCodeAt(i);
hash = (hash << 5) - hash + chr;
hash |= 0; // Convert to 32-bit integer
}
return hash;
}

View File

@ -7,15 +7,23 @@
"dist"
],
"compilerOptions": {
"baseUrl": "./src",
"paths": {
"@/*": ["./*"],
"@assets/*": ["./assets/*"],
"@macros/*": ["./macros/*"],
"@modules/*": ["./modules/*"],
"@utils/*": ["./utils/*"],
},
// Enable latest features
"lib": ["ESNext", "DOM"],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"allowJs": true,
"allowJs": false,
// Bundler mode
"moduleResolution": "bundler",
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,