Compare commits
37 Commits
Author | SHA1 | Date | |
---|---|---|---|
df2af43c64 | |||
fca3bee6dd | |||
9bf8a2ef66 | |||
b1df189c7d | |||
d91fdb798e | |||
a291443d43 | |||
8a7be5d523 | |||
7588f37472 | |||
a597d52585 | |||
f945a3adde | |||
438afe086a | |||
f6ee79770c | |||
f36c77e727 | |||
176e86c9bb | |||
ddb8628e57 | |||
f144fac81e | |||
07a4034cc1 | |||
d30efb2bed | |||
3670946da4 | |||
3d3a013a5c | |||
db1ce23b53 | |||
bcd61833b2 | |||
ba0ccf5213 | |||
6ff81971b0 | |||
ea57e04d4f | |||
43e6f3083e | |||
e5ab7c93f9 | |||
06c6b8c5af | |||
0114108bdf | |||
006e21f477 | |||
7883949b94 | |||
17afd364da | |||
29a1fa9f10 | |||
594c9d3f2e | |||
edc8991a6a | |||
26c318fb8d | |||
9f0097fd8c |
2
dist/better-xcloud.meta.js
vendored
@ -1,5 +1,5 @@
|
||||
// ==UserScript==
|
||||
// @name Better xCloud
|
||||
// @namespace https://github.com/redphx
|
||||
// @version 4.0.1
|
||||
// @version 4.1.1
|
||||
// ==/UserScript==
|
||||
|
1486
dist/better-xcloud.user.js
vendored
@ -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;
|
||||
}
|
||||
|
3
src/assets/svg/controller.svg
Normal 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
@ -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 |
3
src/assets/svg/cursor-text.svg
Normal 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 |
3
src/assets/svg/display.svg
Normal 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 |
3
src/assets/svg/mouse-settings.svg
Normal 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
@ -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
@ -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 |
3
src/assets/svg/question.svg
Normal 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 |
3
src/assets/svg/refresh.svg
Normal 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 |
3
src/assets/svg/remote-play.svg
Normal 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 |
3
src/assets/svg/stream-settings.svg
Normal 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 |
3
src/assets/svg/stream-stats.svg
Normal 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
@ -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 |
65
src/index.ts
@ -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();
|
||||
|
||||
|
@ -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')
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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();
|
||||
}
|
||||
});
|
||||
|
@ -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 {
|
||||
|
@ -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 => {
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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'),
|
||||
|
@ -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!;
|
||||
|
@ -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',
|
||||
|
@ -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(':');
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -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();
|
||||
|
@ -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 => {
|
||||
|
@ -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'),
|
||||
|
@ -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,
|
||||
|
3
src/types/index.d.ts
vendored
@ -67,3 +67,6 @@ type XcloudTitleInfo = {
|
||||
tileImageUrl: string;
|
||||
};
|
||||
};
|
||||
|
||||
declare module "*.svg";
|
||||
declare module "*.styl";
|
||||
|
2
src/types/mkb.d.ts
vendored
@ -1,4 +1,4 @@
|
||||
import { MkbPresetKey } from "../modules/mkb/definitions";
|
||||
import { MkbPresetKey } from "@modules/mkb/definitions";
|
||||
|
||||
type GamepadKeyNameType = {[index: string | number]: string[]};
|
||||
|
||||
|
@ -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',
|
||||
|
@ -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
@ -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
@ -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;
|
@ -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() {
|
||||
|
@ -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)) {
|
||||
|
@ -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];
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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'),
|
||||
},
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { CE } from "./html";
|
||||
import { CE } from "@utils/html";
|
||||
|
||||
type ToastOptions = {
|
||||
instant?: boolean;
|
||||
|
@ -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',
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
|