mirror of
https://github.com/redphx/better-xcloud.git
synced 2025-07-01 20:01:44 +02:00
Compare commits
33 Commits
Author | SHA1 | Date | |
---|---|---|---|
15b7869e5d | |||
2ed4e23c87 | |||
e952bf07c8 | |||
8d44dab04d | |||
6a792548fa | |||
29f6413306 | |||
53d67616c3 | |||
03ad02bd4d | |||
110106aa97 | |||
7310700dbb | |||
5a0ef88237 | |||
a6e358479a | |||
4b02fec8ac | |||
93e3f1fa49 | |||
ae9a1a68d4 | |||
adf6b05c10 | |||
e0489d30bb | |||
9f46eca956 | |||
4888c399f0 | |||
e372db8dd9 | |||
5ba4a669e6 | |||
26b28564cc | |||
ad0be634d2 | |||
6f460302cf | |||
24f0cf18d9 | |||
2df8274233 | |||
a095370ab8 | |||
339447d29c | |||
efe0caf02f | |||
6daabea288 | |||
772a642283 | |||
675fc8431c | |||
9a97053662 |
13
build.ts
13
build.ts
@ -35,6 +35,13 @@ const postProcess = (str: string): string => {
|
|||||||
// Add ADDITIONAL CODE block
|
// Add ADDITIONAL CODE block
|
||||||
str = str.replace('var DEFAULT_FLAGS', '\n/* ADDITIONAL CODE */\n\nvar DEFAULT_FLAGS');
|
str = str.replace('var DEFAULT_FLAGS', '\n/* ADDITIONAL CODE */\n\nvar DEFAULT_FLAGS');
|
||||||
|
|
||||||
|
// Minify SVG
|
||||||
|
str = str.replaceAll(/= "(<svg.*)";/g, function(match) {
|
||||||
|
match = match.replaceAll(/\\n*\s*/g, '');
|
||||||
|
return match;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
assert(str.includes('/* ADDITIONAL CODE */'));
|
assert(str.includes('/* ADDITIONAL CODE */'));
|
||||||
assert(str.includes('window.BX_EXPOSED = BxExposed'));
|
assert(str.includes('window.BX_EXPOSED = BxExposed'));
|
||||||
assert(str.includes('window.BxEvent = BxEvent'));
|
assert(str.includes('window.BxEvent = BxEvent'));
|
||||||
@ -85,8 +92,10 @@ const build = async (target: BuildTarget, version: string, config: any={}) => {
|
|||||||
// Save to script
|
// Save to script
|
||||||
await Bun.write(path, scriptHeader + result);
|
await Bun.write(path, scriptHeader + result);
|
||||||
|
|
||||||
// Create meta file
|
// Create meta file (don't build if it's beta version)
|
||||||
await Bun.write(outDir + '/' + outputMetaName, txtMetaHeader.replace('[[VERSION]]', version));
|
if (!version.includes('beta')) {
|
||||||
|
await Bun.write(outDir + '/' + outputMetaName, txtMetaHeader.replace('[[VERSION]]', version));
|
||||||
|
}
|
||||||
|
|
||||||
// Check with ESLint
|
// Check with ESLint
|
||||||
const eslint = new ESLint();
|
const eslint = new ESLint();
|
||||||
|
2
dist/better-xcloud.meta.js
vendored
2
dist/better-xcloud.meta.js
vendored
@ -1,5 +1,5 @@
|
|||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name Better xCloud
|
// @name Better xCloud
|
||||||
// @namespace https://github.com/redphx
|
// @namespace https://github.com/redphx
|
||||||
// @version 5.6.0
|
// @version 5.7.0
|
||||||
// ==/UserScript==
|
// ==/UserScript==
|
||||||
|
559
dist/better-xcloud.user.js
vendored
559
dist/better-xcloud.user.js
vendored
File diff suppressed because one or more lines are too long
@ -9,14 +9,14 @@
|
|||||||
"build": "build.ts"
|
"build": "build.ts"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "^1.1.6",
|
"@types/bun": "^1.1.8",
|
||||||
"@types/node": "^20.16.1",
|
"@types/node": "^22.5.2",
|
||||||
"@types/stylus": "^0.48.42",
|
"@types/stylus": "^0.48.42",
|
||||||
"eslint": "^9.9.0",
|
"eslint": "^9.9.1",
|
||||||
"eslint-plugin-compat": "^6.0.0",
|
"eslint-plugin-compat": "^6.0.0",
|
||||||
"stylus": "^0.63.0"
|
"stylus": "^0.63.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "^5.5.2"
|
"typescript": "^5.5.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
19
src/assets/css/guide-menu.styl
Normal file
19
src/assets/css/guide-menu.styl
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
.bx-guide-home-buttons {
|
||||||
|
> div {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-is-playing="true"] {
|
||||||
|
button[data-state='normal'] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-is-playing="false"] {
|
||||||
|
button[data-state='playing'] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -133,6 +133,13 @@ div[class^=HUDButton-module__hiddenContainer] ~ div:not([class^=HUDButton-module
|
|||||||
text-transform: none !important;
|
text-transform: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bx-normal-link {
|
||||||
|
text-transform: none !important;
|
||||||
|
text-align: left !important;
|
||||||
|
font-weight: 400 !important;
|
||||||
|
font-family: var(--bx-normal-font) !important;
|
||||||
|
}
|
||||||
|
|
||||||
select[multiple] {
|
select[multiple] {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
@ -168,16 +175,11 @@ div[class*=SupportedInputsBadge] {
|
|||||||
left: 0;
|
left: 0;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
background: #0000008c;
|
background: #0000008c;
|
||||||
display: none;
|
display: flex;
|
||||||
border-radius: 0 0 4px 0;
|
border-radius: 4px 0 4px 0;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
|
|
||||||
a[class^=BaseItem-module__container]:focus &,
|
|
||||||
button[class^=BaseItem-module__container]:focus & {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
width: 14px;
|
width: 14px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
@ -190,6 +192,7 @@ div[class*=SupportedInputsBadge] {
|
|||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
margin-left: 2px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,11 +60,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: #5dc21e;
|
color: #1c9d1c;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
&:hover, &:focus {
|
&:hover, &:focus {
|
||||||
color: #128112;
|
color: #5dc21e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -470,9 +470,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.bx-suggest-link {
|
.bx-suggest-link {
|
||||||
font-size: 12px;
|
font-size: 14px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-top: 10px;
|
margin-top: 4px;
|
||||||
|
padding: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bx-suggest-row {
|
.bx-suggest-row {
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
@import 'loading-screen.styl';
|
@import 'loading-screen.styl';
|
||||||
@import 'remote-play.styl';
|
@import 'remote-play.styl';
|
||||||
@import 'web-components.styl';
|
@import 'web-components.styl';
|
||||||
|
@import 'guide-menu.styl';
|
||||||
|
|
||||||
@import 'stream.styl';
|
@import 'stream.styl';
|
||||||
@import 'number-stepper.styl';
|
@import 'number-stepper.styl';
|
||||||
|
3
src/assets/svg/power.svg
Normal file
3
src/assets/svg/power.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='M16 2.445v12.91m7.746-11.619C27.631 6.27 30.2 10.37 30.2 15.355c0 7.79-6.41 14.2-14.2 14.2s-14.2-6.41-14.2-14.2c0-4.985 2.569-9.085 6.454-11.619'/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 339 B |
3
src/assets/svg/true-achievements.svg
Normal file
3
src/assets/svg/true-achievements.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns='http://www.w3.org/2000/svg' fill='#fff' stroke='nons' fill-rule='evenodd' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 32 32'>
|
||||||
|
<path d='M2.497 14.127c.781-6.01 5.542-10.849 11.551-11.708V0C6.634.858.858 6.712 0 14.127h2.497zM17.952 2.419V0C25.366.858 31.142 6.712 32 14.127h-2.497c-.781-6.01-5.542-10.849-11.551-11.708zM2.497 17.873c.781 6.01 5.542 10.849 11.551 11.708V32C6.634 31.142.858 25.288 0 17.873h2.497zm27.006 0H32C31.142 25.288 25.366 31.142 17.952 32v-2.419c6.009-.859 10.77-5.698 11.551-11.708zm-19.2-4.527h2.028a.702.702 0 1 0 0-1.404h-2.107a1.37 1.37 0 0 1-1.326-1.327V9.21a.7.7 0 0 0-.703-.703c-.387 0-.703.316-.703.7v1.408c.079 1.483 1.25 2.731 2.811 2.731zm2.809 7.337h-2.888a1.37 1.37 0 0 1-1.326-1.327v-4.917c0-.387-.316-.703-.7-.703a.7.7 0 0 0-.706.703v4.917a2.77 2.77 0 0 0 2.732 2.732h2.81c.387 0 .702-.316.702-.7.078-.393-.234-.705-.624-.705zM25.6 19.2a.7.7 0 0 0-.702-.702c-.387 0-.703.316-.703.699v.081c0 .702-.546 1.326-1.248 1.326H19.98c-.702-.078-1.248-.624-1.248-1.326v-.312c0-.78.624-1.327 1.326-1.327h2.811a2.77 2.77 0 0 0 2.731-2.732v-.312a2.68 2.68 0 0 0-2.576-2.732h-4.76a.702.702 0 1 0 0 1.405h4.526a1.37 1.37 0 0 1 1.327 1.327v.234c0 .781-.624 1.327-1.327 1.327h-2.81a2.77 2.77 0 0 0-2.731 2.732v.312a2.77 2.77 0 0 0 2.731 2.732h2.967a2.74 2.74 0 0 0 2.575-2.732s.078.078.078 0z'/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
138
src/index.ts
138
src/index.ts
@ -14,7 +14,7 @@ import { Toast } from "@utils/toast";
|
|||||||
import { LoadingScreen } from "@modules/loading-screen";
|
import { LoadingScreen } from "@modules/loading-screen";
|
||||||
import { MouseCursorHider } from "@modules/mkb/mouse-cursor-hider";
|
import { MouseCursorHider } from "@modules/mkb/mouse-cursor-hider";
|
||||||
import { TouchController } from "@modules/touch-controller";
|
import { TouchController } from "@modules/touch-controller";
|
||||||
import { checkForUpdate, disablePwa } from "@utils/utils";
|
import { checkForUpdate, disablePwa, productTitleToSlug } from "@utils/utils";
|
||||||
import { Patcher } from "@modules/patcher";
|
import { Patcher } from "@modules/patcher";
|
||||||
import { RemotePlay } from "@modules/remote-play";
|
import { RemotePlay } from "@modules/remote-play";
|
||||||
import { onHistoryChanged, patchHistoryMethod } from "@utils/history";
|
import { onHistoryChanged, patchHistoryMethod } from "@utils/history";
|
||||||
@ -26,7 +26,7 @@ import { BxLogger } from "@utils/bx-logger";
|
|||||||
import { GameBar } from "./modules/game-bar/game-bar";
|
import { GameBar } from "./modules/game-bar/game-bar";
|
||||||
import { Screenshot } from "./utils/screenshot";
|
import { Screenshot } from "./utils/screenshot";
|
||||||
import { NativeMkbHandler } from "./modules/mkb/native-mkb-handler";
|
import { NativeMkbHandler } from "./modules/mkb/native-mkb-handler";
|
||||||
import { GuideMenu, GuideMenuTab } from "./modules/ui/guide-menu";
|
import { GuideMenu } from "./modules/ui/guide-menu";
|
||||||
import { updateVideoPlayer } from "./modules/stream/stream-settings-utils";
|
import { updateVideoPlayer } from "./modules/stream/stream-settings-utils";
|
||||||
import { UiSection } from "./enums/ui-sections";
|
import { UiSection } from "./enums/ui-sections";
|
||||||
import { HeaderSection } from "./modules/ui/header";
|
import { HeaderSection } from "./modules/ui/header";
|
||||||
@ -38,6 +38,8 @@ import { getPref, StreamTouchController } from "./utils/settings-storages/global
|
|||||||
import { compressCss } from "@macros/build" with {type: "macro"};
|
import { compressCss } from "@macros/build" with {type: "macro"};
|
||||||
import { SettingsNavigationDialog } from "./modules/ui/dialog/settings-dialog";
|
import { SettingsNavigationDialog } from "./modules/ui/dialog/settings-dialog";
|
||||||
import { StreamUiHandler } from "./modules/stream/stream-ui";
|
import { StreamUiHandler } from "./modules/stream/stream-ui";
|
||||||
|
import { UserAgent } from "./utils/user-agent";
|
||||||
|
import { XboxApi } from "./utils/xbox-api";
|
||||||
|
|
||||||
|
|
||||||
// Handle login page
|
// Handle login page
|
||||||
@ -70,26 +72,65 @@ if (BX_FLAGS.SafariWorkaround && document.readyState !== 'loading') {
|
|||||||
.bx-reload-overlay {
|
.bx-reload-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
background: #000000cc;
|
background: #000000cc;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
width: 100%;
|
|
||||||
line-height: 100vh;
|
|
||||||
color: #fff;
|
color: #fff;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-family: "Segoe UI", Arial, Helvetica, sans-serif;
|
font-family: "Segoe UI", Arial, Helvetica, sans-serif;
|
||||||
font-size: 1.3rem;
|
font-size: 1.3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bx-reload-overlay *:focus {
|
||||||
|
outline: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bx-reload-overlay > div {
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bx-reload-overlay a {
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
background: #107c10;
|
||||||
|
color: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
const isSafari = UserAgent.isSafari();
|
||||||
|
let $secondaryAction: HTMLElement;
|
||||||
|
if (isSafari) {
|
||||||
|
$secondaryAction = CE('p', {}, t('settings-reloading'));
|
||||||
|
} else {
|
||||||
|
$secondaryAction = CE('a', {
|
||||||
|
href: 'https://better-xcloud.github.io/troubleshooting',
|
||||||
|
target: '_blank',
|
||||||
|
}, '🤓 ' + t('how-to-fix'));
|
||||||
|
}
|
||||||
|
|
||||||
const $fragment = document.createDocumentFragment();
|
const $fragment = document.createDocumentFragment();
|
||||||
$fragment.appendChild(CE('style', {}, css));
|
$fragment.appendChild(CE('style', {}, css));
|
||||||
$fragment.appendChild(CE('div', {'class': 'bx-reload-overlay'}, t('safari-failed-message')));
|
$fragment.appendChild(CE('div',{
|
||||||
|
class: 'bx-reload-overlay',
|
||||||
|
},
|
||||||
|
CE('div', {},
|
||||||
|
CE('p', {}, t('load-failed-message')),
|
||||||
|
$secondaryAction,
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
document.documentElement.appendChild($fragment);
|
document.documentElement.appendChild($fragment);
|
||||||
|
|
||||||
// Reload the page
|
// Reload the page if using Safari
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
window.location.reload(true);
|
isSafari && window.location.reload(true);
|
||||||
|
|
||||||
// Stop processing the script
|
// Stop processing the script
|
||||||
throw new Error('[Better xCloud] Executing workaround for Safari');
|
throw new Error('[Better xCloud] Executing workaround for Safari');
|
||||||
@ -158,15 +199,10 @@ window.addEventListener(BxEvent.XCLOUD_SERVERS_READY, e => {
|
|||||||
|
|
||||||
window.addEventListener(BxEvent.STREAM_LOADING, e => {
|
window.addEventListener(BxEvent.STREAM_LOADING, e => {
|
||||||
// Get title ID for screenshot's name
|
// Get title ID for screenshot's name
|
||||||
if (window.location.pathname.includes('/launch/')) {
|
if (window.location.pathname.includes('/launch/') && STATES.currentStream.titleInfo) {
|
||||||
const matches = /\/launch\/(?<title_id>[^\/]+)\/(?<product_id>\w+)/.exec(window.location.pathname);
|
STATES.currentStream.titleSlug = productTitleToSlug(STATES.currentStream.titleInfo.product.title);
|
||||||
if (matches?.groups) {
|
|
||||||
STATES.currentStream.titleId = matches.groups.title_id;
|
|
||||||
STATES.currentStream.productId = matches.groups.product_id;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
STATES.currentStream.titleId = 'remote-play';
|
STATES.currentStream.titleSlug = 'remote-play';
|
||||||
STATES.currentStream.productId = '';
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -208,15 +244,50 @@ window.addEventListener(BxEvent.STREAM_ERROR_PAGE, e => {
|
|||||||
window.addEventListener(BxEvent.XCLOUD_RENDERING_COMPONENT, e => {
|
window.addEventListener(BxEvent.XCLOUD_RENDERING_COMPONENT, e => {
|
||||||
const component = (e as any).component;
|
const component = (e as any).component;
|
||||||
if (component === 'product-details') {
|
if (component === 'product-details') {
|
||||||
ProductDetailsPage.injectShortcutButton();
|
ProductDetailsPage.injectButtons();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function unload() {
|
// Detect game change
|
||||||
if (!STATES.isPlaying) {
|
window.addEventListener(BxEvent.DATA_CHANNEL_CREATED, e => {
|
||||||
|
const dataChannel = (e as any).dataChannel;
|
||||||
|
if (!dataChannel || dataChannel.label !== 'message') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dataChannel.addEventListener('message', async (msg: MessageEvent) => {
|
||||||
|
if (msg.origin === 'better-xcloud' || typeof msg.data !== 'string') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get xboxTitleId from message
|
||||||
|
if (msg.data.includes('/titleinfo')) {
|
||||||
|
const json = JSON.parse(JSON.parse(msg.data).content);
|
||||||
|
const xboxTitleId = parseInt(json.titleid, 16);
|
||||||
|
STATES.currentStream.xboxTitleId = xboxTitleId;
|
||||||
|
|
||||||
|
// Get titleSlug for Remote Play
|
||||||
|
if (STATES.remotePlay.isPlaying) {
|
||||||
|
STATES.currentStream.titleSlug = 'remote-play';
|
||||||
|
if (json.focused) {
|
||||||
|
const productTitle = await XboxApi.getProductTitle(xboxTitleId);
|
||||||
|
if (productTitle) {
|
||||||
|
STATES.currentStream.titleSlug = productTitleToSlug(productTitle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function unload() {
|
||||||
|
if (!STATES.isPlaying && !Object.keys(STATES.currentStream).length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
STATES.isPlaying = false;
|
||||||
|
STATES.currentStream = {};
|
||||||
|
|
||||||
// Stop MKB listeners
|
// Stop MKB listeners
|
||||||
EmulatedMkbHandler.getInstance().destroy();
|
EmulatedMkbHandler.getInstance().destroy();
|
||||||
NativeMkbHandler.getInstance().destroy();
|
NativeMkbHandler.getInstance().destroy();
|
||||||
@ -224,8 +295,6 @@ function unload() {
|
|||||||
// Destroy StreamPlayer
|
// Destroy StreamPlayer
|
||||||
STATES.currentStream.streamPlayer?.destroy();
|
STATES.currentStream.streamPlayer?.destroy();
|
||||||
|
|
||||||
STATES.isPlaying = false;
|
|
||||||
STATES.currentStream = {};
|
|
||||||
window.BX_EXPOSED.shouldShowSensorControls = false;
|
window.BX_EXPOSED.shouldShowSensorControls = false;
|
||||||
window.BX_EXPOSED.stopTakRendering = false;
|
window.BX_EXPOSED.stopTakRendering = false;
|
||||||
|
|
||||||
@ -248,7 +317,7 @@ window.addEventListener(BxEvent.CAPTURE_SCREENSHOT, e => {
|
|||||||
|
|
||||||
|
|
||||||
function observeRootDialog($root: HTMLElement) {
|
function observeRootDialog($root: HTMLElement) {
|
||||||
let currentShown = false;
|
let beingShown = false;
|
||||||
|
|
||||||
const observer = new MutationObserver(mutationList => {
|
const observer = new MutationObserver(mutationList => {
|
||||||
for (const mutation of mutationList) {
|
for (const mutation of mutationList) {
|
||||||
@ -256,31 +325,20 @@ function observeRootDialog($root: HTMLElement) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BX_FLAGS.Debug && BxLogger.warning('RootDialog', 'added', mutation.addedNodes);
|
||||||
if (mutation.addedNodes.length === 1) {
|
if (mutation.addedNodes.length === 1) {
|
||||||
const $addedElm = mutation.addedNodes[0];
|
const $addedElm = mutation.addedNodes[0];
|
||||||
if ($addedElm instanceof HTMLElement && $addedElm.className) {
|
if ($addedElm instanceof HTMLElement && $addedElm.className) {
|
||||||
if ($addedElm.className.startsWith('NavigationAnimation') || $addedElm.className.startsWith('DialogRoutes') || $addedElm.className.startsWith('Dialog-module__container')) {
|
// Make sure it's Guide dialog
|
||||||
// Make sure it's Guide dialog
|
if ($root.querySelector('div[class*=GuideDialog]')) {
|
||||||
if (document.querySelector('#gamepass-dialog-root div[class*=GuideDialog]')) {
|
GuideMenu.observe($addedElm);
|
||||||
// Find navigation bar
|
|
||||||
const $selectedTab = $addedElm.querySelector('div[class^=NavigationMenu] button[aria-selected=true');
|
|
||||||
if ($selectedTab) {
|
|
||||||
let $elm: Element | null = $selectedTab;
|
|
||||||
let index;
|
|
||||||
for (index = 0; ($elm = $elm?.previousElementSibling); index++);
|
|
||||||
|
|
||||||
if (index === 0) {
|
|
||||||
BxEvent.dispatch(window, BxEvent.XCLOUD_GUIDE_MENU_SHOWN, {where: GuideMenuTab.HOME});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const shown = ($root.firstElementChild && $root.firstElementChild.childElementCount > 0) || false;
|
const shown = !!($root.firstElementChild && $root.firstElementChild.childElementCount > 0);
|
||||||
if (shown !== currentShown) {
|
if (shown !== beingShown) {
|
||||||
currentShown = shown;
|
beingShown = shown;
|
||||||
BxEvent.dispatch(window, shown ? BxEvent.XCLOUD_DIALOG_SHOWN : BxEvent.XCLOUD_DIALOG_DISMISSED);
|
BxEvent.dispatch(window, shown ? BxEvent.XCLOUD_DIALOG_SHOWN : BxEvent.XCLOUD_DIALOG_DISMISSED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -339,7 +397,7 @@ function main() {
|
|||||||
(getPref(PrefKey.GAME_BAR_POSITION) !== 'off') && GameBar.getInstance();
|
(getPref(PrefKey.GAME_BAR_POSITION) !== 'off') && GameBar.getInstance();
|
||||||
Screenshot.setup();
|
Screenshot.setup();
|
||||||
|
|
||||||
GuideMenu.observe();
|
GuideMenu.addEventListeners();
|
||||||
StreamBadges.setupEvents();
|
StreamBadges.setupEvents();
|
||||||
StreamStats.setupEvents();
|
StreamStats.setupEvents();
|
||||||
EmulatedMkbHandler.setupEvents();
|
EmulatedMkbHandler.setupEvents();
|
||||||
|
@ -14,6 +14,6 @@ export const renderStylus = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export const compressCss = async (css: string) => {
|
export const compressCss = (css: string) => {
|
||||||
return await (stylus(css, {}).set('compress', true)).render();
|
return (stylus(css, {}).set('compress', true)).render();
|
||||||
};
|
};
|
||||||
|
@ -15,11 +15,11 @@ export class MicrophoneAction extends BaseGameBarAction {
|
|||||||
super();
|
super();
|
||||||
|
|
||||||
const onClick = (e: Event) => {
|
const onClick = (e: Event) => {
|
||||||
BxEvent.dispatch(window, BxEvent.GAME_BAR_ACTION_ACTIVATED);
|
BxEvent.dispatch(window, BxEvent.GAME_BAR_ACTION_ACTIVATED);
|
||||||
|
|
||||||
const enabled = MicrophoneShortcut.toggle(false);
|
const enabled = MicrophoneShortcut.toggle(false);
|
||||||
this.$content.setAttribute('data-enabled', enabled.toString());
|
this.$content.setAttribute('data-enabled', enabled.toString());
|
||||||
};
|
};
|
||||||
|
|
||||||
const $btnDefault = createButton({
|
const $btnDefault = createButton({
|
||||||
style: ButtonStyle.GHOST,
|
style: ButtonStyle.GHOST,
|
||||||
|
@ -12,16 +12,16 @@ export class ScreenshotAction extends BaseGameBarAction {
|
|||||||
super();
|
super();
|
||||||
|
|
||||||
const onClick = (e: Event) => {
|
const onClick = (e: Event) => {
|
||||||
BxEvent.dispatch(window, BxEvent.GAME_BAR_ACTION_ACTIVATED);
|
BxEvent.dispatch(window, BxEvent.GAME_BAR_ACTION_ACTIVATED);
|
||||||
Screenshot.takeScreenshot();
|
Screenshot.takeScreenshot();
|
||||||
};
|
};
|
||||||
|
|
||||||
this.$content = createButton({
|
this.$content = createButton({
|
||||||
style: ButtonStyle.GHOST,
|
style: ButtonStyle.GHOST,
|
||||||
icon: BxIcon.SCREENSHOT,
|
icon: BxIcon.SCREENSHOT,
|
||||||
title: t('take-screenshot'),
|
title: t('take-screenshot'),
|
||||||
onClick: onClick,
|
onClick: onClick,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): HTMLElement {
|
render(): HTMLElement {
|
||||||
|
30
src/modules/game-bar/action-true-achievements.ts
Normal file
30
src/modules/game-bar/action-true-achievements.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { BxEvent } from "@/utils/bx-event";
|
||||||
|
import { BxIcon } from "@/utils/bx-icon";
|
||||||
|
import { createButton, ButtonStyle } from "@/utils/html";
|
||||||
|
import { t } from "@/utils/translation";
|
||||||
|
import { BaseGameBarAction } from "./action-base";
|
||||||
|
import { TrueAchievements } from "@/utils/true-achievements";
|
||||||
|
|
||||||
|
export class TrueAchievementsAction extends BaseGameBarAction {
|
||||||
|
$content: HTMLElement;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
const onClick = (e: Event) => {
|
||||||
|
BxEvent.dispatch(window, BxEvent.GAME_BAR_ACTION_ACTIVATED);
|
||||||
|
TrueAchievements.open(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.$content = createButton({
|
||||||
|
style: ButtonStyle.GHOST,
|
||||||
|
icon: BxIcon.TRUE_ACHIEVEMENTS,
|
||||||
|
title: t('true-achievements'),
|
||||||
|
onClick: onClick,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): HTMLElement {
|
||||||
|
return this.$content;
|
||||||
|
}
|
||||||
|
}
|
@ -8,11 +8,11 @@ import { STATES } from "@utils/global";
|
|||||||
import { MicrophoneAction } from "./action-microphone";
|
import { MicrophoneAction } from "./action-microphone";
|
||||||
import { PrefKey } from "@/enums/pref-keys";
|
import { PrefKey } from "@/enums/pref-keys";
|
||||||
import { getPref, StreamTouchController } from "@/utils/settings-storages/global-settings-storage";
|
import { getPref, StreamTouchController } from "@/utils/settings-storages/global-settings-storage";
|
||||||
|
import { TrueAchievementsAction } from "./action-true-achievements";
|
||||||
|
|
||||||
|
|
||||||
export class GameBar {
|
export class GameBar {
|
||||||
private static instance: GameBar;
|
private static instance: GameBar;
|
||||||
|
|
||||||
public static getInstance(): GameBar {
|
public static getInstance(): GameBar {
|
||||||
if (!GameBar.instance) {
|
if (!GameBar.instance) {
|
||||||
GameBar.instance = new GameBar();
|
GameBar.instance = new GameBar();
|
||||||
@ -43,6 +43,7 @@ export class GameBar {
|
|||||||
this.actions = [
|
this.actions = [
|
||||||
new ScreenshotAction(),
|
new ScreenshotAction(),
|
||||||
...(STATES.userAgent.capabilities.touch && (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) !== StreamTouchController.OFF) ? [new TouchControlAction()] : []),
|
...(STATES.userAgent.capabilities.touch && (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) !== StreamTouchController.OFF) ? [new TouchControlAction()] : []),
|
||||||
|
new TrueAchievementsAction(),
|
||||||
new MicrophoneAction(),
|
new MicrophoneAction(),
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -93,7 +94,7 @@ export class GameBar {
|
|||||||
|
|
||||||
// Toggle Game bar
|
// Toggle Game bar
|
||||||
const mode = (e as any).mode;
|
const mode = (e as any).mode;
|
||||||
mode !== 'None' ? this.disable() : this.enable();
|
mode !== 'none' ? this.disable() : this.enable();
|
||||||
}).bind(this));
|
}).bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,7 +126,7 @@ export class GameBar {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$container.classList.remove('bx-offscreen', 'bx-hide');
|
this.$container.classList.remove('bx-offscreen', 'bx-hide' , 'bx-gone');
|
||||||
this.$container.classList.add('bx-show');
|
this.$container.classList.add('bx-show');
|
||||||
|
|
||||||
this.beginHideTimeout();
|
this.beginHideTimeout();
|
||||||
|
@ -4,6 +4,7 @@ import { t } from "@utils/translation";
|
|||||||
import { STATES } from "@utils/global";
|
import { STATES } from "@utils/global";
|
||||||
import { PrefKey } from "@/enums/pref-keys";
|
import { PrefKey } from "@/enums/pref-keys";
|
||||||
import { getPref } from "@/utils/settings-storages/global-settings-storage";
|
import { getPref } from "@/utils/settings-storages/global-settings-storage";
|
||||||
|
import { compressCss } from "@macros/build" with {type: "macro"};
|
||||||
|
|
||||||
export class LoadingScreen {
|
export class LoadingScreen {
|
||||||
static #$bgStyle: HTMLElement;
|
static #$bgStyle: HTMLElement;
|
||||||
@ -43,7 +44,7 @@ export class LoadingScreen {
|
|||||||
static #hideRocket() {
|
static #hideRocket() {
|
||||||
let $bgStyle = LoadingScreen.#$bgStyle;
|
let $bgStyle = LoadingScreen.#$bgStyle;
|
||||||
|
|
||||||
const css = `
|
const css = compressCss(`
|
||||||
#game-stream div[class*=RocketAnimation-module__container] > svg {
|
#game-stream div[class*=RocketAnimation-module__container] > svg {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@ -51,8 +52,8 @@ export class LoadingScreen {
|
|||||||
#game-stream video[class*=RocketAnimationVideo-module__video] {
|
#game-stream video[class*=RocketAnimationVideo-module__video] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
`;
|
`);
|
||||||
$bgStyle.textContent += css;
|
$bgStyle.textContent! += css;
|
||||||
}
|
}
|
||||||
|
|
||||||
static #setBackground(imageUrl: string) {
|
static #setBackground(imageUrl: string) {
|
||||||
@ -62,9 +63,8 @@ export class LoadingScreen {
|
|||||||
// Limit max width to reduce image size
|
// Limit max width to reduce image size
|
||||||
imageUrl = imageUrl + '?w=1920';
|
imageUrl = imageUrl + '?w=1920';
|
||||||
|
|
||||||
const css = `
|
const css = compressCss(`
|
||||||
#game-stream {
|
#game-stream {
|
||||||
background-image: linear-gradient(#00000033, #000000e6), url(${imageUrl}) !important;
|
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
background-position: center center !important;
|
background-position: center center !important;
|
||||||
background-repeat: no-repeat !important;
|
background-repeat: no-repeat !important;
|
||||||
@ -74,16 +74,16 @@ export class LoadingScreen {
|
|||||||
#game-stream rect[width="800"] {
|
#game-stream rect[width="800"] {
|
||||||
transition: opacity 0.3s ease-in-out !important;
|
transition: opacity 0.3s ease-in-out !important;
|
||||||
}
|
}
|
||||||
`;
|
`) + `#game-stream {background-image: linear-gradient(#00000033, #000000e6), url(${imageUrl}) !important;}`;
|
||||||
$bgStyle.textContent += css;
|
$bgStyle.textContent! += css;
|
||||||
|
|
||||||
const bg = new Image();
|
const bg = new Image();
|
||||||
bg.onload = e => {
|
bg.onload = e => {
|
||||||
$bgStyle.textContent += `
|
$bgStyle.textContent += compressCss(`
|
||||||
#game-stream rect[width="800"] {
|
#game-stream rect[width="800"] {
|
||||||
opacity: 0 !important;
|
opacity: 0 !important;
|
||||||
}
|
}
|
||||||
`;
|
`);
|
||||||
};
|
};
|
||||||
bg.src = imageUrl;
|
bg.src = imageUrl;
|
||||||
}
|
}
|
||||||
@ -150,18 +150,18 @@ export class LoadingScreen {
|
|||||||
if (getPref(PrefKey.UI_LOADING_SCREEN_GAME_ART) && LoadingScreen.#$bgStyle) {
|
if (getPref(PrefKey.UI_LOADING_SCREEN_GAME_ART) && LoadingScreen.#$bgStyle) {
|
||||||
const $rocketBg = document.querySelector('#game-stream rect[width="800"]');
|
const $rocketBg = document.querySelector('#game-stream rect[width="800"]');
|
||||||
$rocketBg && $rocketBg.addEventListener('transitionend', e => {
|
$rocketBg && $rocketBg.addEventListener('transitionend', e => {
|
||||||
LoadingScreen.#$bgStyle.textContent += `
|
LoadingScreen.#$bgStyle.textContent += compressCss(`
|
||||||
#game-stream {
|
#game-stream {
|
||||||
background: #000 !important;
|
background: #000 !important;
|
||||||
}
|
}
|
||||||
`;
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
LoadingScreen.#$bgStyle.textContent += `
|
LoadingScreen.#$bgStyle.textContent += compressCss(`
|
||||||
#game-stream rect[width="800"] {
|
#game-stream rect[width="800"] {
|
||||||
opacity: 1 !important;
|
opacity: 1 !important;
|
||||||
}
|
}
|
||||||
`;
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(LoadingScreen.reset, 2000);
|
setTimeout(LoadingScreen.reset, 2000);
|
||||||
|
@ -459,7 +459,7 @@ export class EmulatedMkbHandler extends MkbHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mode = (e as any).mode;
|
const mode = (e as any).mode;
|
||||||
if (mode === 'None') {
|
if (mode === 'none') {
|
||||||
this.#$message.classList.remove('bx-offscreen');
|
this.#$message.classList.remove('bx-offscreen');
|
||||||
} else {
|
} else {
|
||||||
this.#$message.classList.add('bx-offscreen');
|
this.#$message.classList.add('bx-offscreen');
|
||||||
|
@ -69,7 +69,7 @@ export class NativeMkbHandler extends MkbHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mode = (e as any).mode;
|
const mode = (e as any).mode;
|
||||||
if (mode === 'None') {
|
if (mode === 'none') {
|
||||||
this.#$message.classList.remove('bx-offscreen');
|
this.#$message.classList.remove('bx-offscreen');
|
||||||
} else {
|
} else {
|
||||||
this.#$message.classList.add('bx-offscreen');
|
this.#$message.classList.add('bx-offscreen');
|
||||||
|
@ -465,7 +465,7 @@ e.guideUI = null;
|
|||||||
}
|
}
|
||||||
|
|
||||||
const newCode = `
|
const newCode = `
|
||||||
BxEvent.dispatch(window, BxEvent.XCLOUD_POLLING_MODE_CHANGED, {mode: e});
|
BxEvent.dispatch(window, BxEvent.XCLOUD_POLLING_MODE_CHANGED, {mode: e.toLowerCase()});
|
||||||
`;
|
`;
|
||||||
str = str.replace(text, text + newCode);
|
str = str.replace(text, text + newCode);
|
||||||
return str;
|
return str;
|
||||||
|
@ -913,7 +913,7 @@ export class SettingsNavigationDialog extends NavigationDialog {
|
|||||||
href: 'https://better-xcloud.github.io/guide/android-webview-tweaks/',
|
href: 'https://better-xcloud.github.io/guide/android-webview-tweaks/',
|
||||||
target: '_blank',
|
target: '_blank',
|
||||||
tabindex: 0,
|
tabindex: 0,
|
||||||
}, t('how-to-improve-app-performance')),
|
}, '🤓 ' + t('how-to-improve-app-performance')),
|
||||||
|
|
||||||
BX_FLAGS.DeviceInfo.deviceType.includes('android') && !hasRecommendedSettings && CE('a', {
|
BX_FLAGS.DeviceInfo.deviceType.includes('android') && !hasRecommendedSettings && CE('a', {
|
||||||
class: 'bx-suggest-link bx-focusable',
|
class: 'bx-suggest-link bx-focusable',
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { BxEvent } from "@/utils/bx-event";
|
import { BxEvent } from "@/utils/bx-event";
|
||||||
import { BxIcon } from "@/utils/bx-icon";
|
import { BxIcon } from "@/utils/bx-icon";
|
||||||
import { CE, createSvgIcon, getReactProps } from "@/utils/html";
|
import { CE, createSvgIcon, getReactProps, isElementVisible } from "@/utils/html";
|
||||||
import { XcloudApi } from "@/utils/xcloud-api";
|
import { XcloudApi } from "@/utils/xcloud-api";
|
||||||
|
|
||||||
export class GameTile {
|
export class GameTile {
|
||||||
@ -23,6 +23,11 @@ export class GameTile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async #showWaitTime($elm: HTMLElement, productId: string) {
|
static async #showWaitTime($elm: HTMLElement, productId: string) {
|
||||||
|
if (($elm as any).hasWaitTime) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
($elm as any).hasWaitTime = true;
|
||||||
|
|
||||||
let totalWaitTime;
|
let totalWaitTime;
|
||||||
|
|
||||||
const api = XcloudApi.getInstance();
|
const api = XcloudApi.getInstance();
|
||||||
@ -34,7 +39,7 @@ export class GameTile {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof totalWaitTime === 'number' && $elm.isConnected) {
|
if (typeof totalWaitTime === 'number' && isElementVisible($elm)) {
|
||||||
const $div = CE('div', {'class': 'bx-game-tile-wait-time'},
|
const $div = CE('div', {'class': 'bx-game-tile-wait-time'},
|
||||||
createSvgIcon(BxIcon.PLAYTIME),
|
createSvgIcon(BxIcon.PLAYTIME),
|
||||||
CE('span', {}, GameTile.#secondsToHms(totalWaitTime)),
|
CE('span', {}, GameTile.#secondsToHms(totalWaitTime)),
|
||||||
@ -43,45 +48,61 @@ export class GameTile {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static requestWaitTime($elm: HTMLElement, productId: string) {
|
static #requestWaitTime($elm: HTMLElement, productId: string) {
|
||||||
GameTile.#timeout && clearTimeout(GameTile.#timeout);
|
GameTile.#timeout && clearTimeout(GameTile.#timeout);
|
||||||
GameTile.#timeout = window.setTimeout(async () => {
|
GameTile.#timeout = window.setTimeout(async () => {
|
||||||
if (!($elm as any).hasWaitTime) {
|
GameTile.#showWaitTime($elm, productId);
|
||||||
($elm as any).hasWaitTime = true;
|
}, 500);
|
||||||
GameTile.#showWaitTime($elm, productId);
|
}
|
||||||
|
|
||||||
|
static #findProductId($elm: HTMLElement): string | null {
|
||||||
|
let productId = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (($elm.tagName === 'BUTTON' && $elm.className.includes('MruGameCard')) || (($elm.tagName === 'A' && $elm.className.includes('GameCard')))) {
|
||||||
|
let props = getReactProps($elm.parentElement!);
|
||||||
|
|
||||||
|
// When context menu is enabled
|
||||||
|
if (Array.isArray(props.children)) {
|
||||||
|
productId = props.children[0].props.productId;
|
||||||
|
} else {
|
||||||
|
productId = props.children.props.productId;
|
||||||
|
}
|
||||||
|
} else if ($elm.tagName === 'A' && $elm.className.includes('GameItem')) {
|
||||||
|
let props = getReactProps($elm.parentElement!);
|
||||||
|
props = props.children.props;
|
||||||
|
if (props.location !== 'NonStreamableGameItem') {
|
||||||
|
if ('productId' in props) {
|
||||||
|
productId = props.productId;
|
||||||
|
} else {
|
||||||
|
// Search page
|
||||||
|
productId = props.children.props.productId;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, 1000);
|
} catch (e) {}
|
||||||
|
|
||||||
|
return productId;
|
||||||
}
|
}
|
||||||
|
|
||||||
static setup() {
|
static setup() {
|
||||||
window.addEventListener(BxEvent.NAVIGATION_FOCUS_CHANGED, e => {
|
window.addEventListener(BxEvent.NAVIGATION_FOCUS_CHANGED, e => {
|
||||||
let productId;
|
|
||||||
const $elm = (e as any).element;
|
const $elm = (e as any).element;
|
||||||
try {
|
const className = $elm.className || '';
|
||||||
if (($elm.tagName === 'BUTTON' && $elm.className.includes('MruGameCard')) || (($elm.tagName === 'A' && $elm.className.includes('GameCard')))) {
|
if (className.includes('MruGameCard')) {
|
||||||
let props = getReactProps($elm.parentElement);
|
// Show the wait time of every games in the "Jump back in" section all at once
|
||||||
|
const $ol = $elm.closest('ol');
|
||||||
// When context menu is enabled
|
if ($ol && !($ol as any).hasWaitTime) {
|
||||||
if (Array.isArray(props.children)) {
|
($ol as any).hasWaitTime = true;
|
||||||
productId = props.children[0].props.productId;
|
$ol.querySelectorAll('button[class*=MruGameCard]').forEach(($elm: HTMLElement) => {
|
||||||
} else {
|
const productId = GameTile.#findProductId($elm);
|
||||||
productId = props.children.props.productId;
|
productId && GameTile.#showWaitTime($elm, productId);
|
||||||
}
|
});
|
||||||
} else if ($elm.tagName === 'A' && $elm.className.includes('GameItem')) {
|
|
||||||
let props = getReactProps($elm.parentElement);
|
|
||||||
props = props.children.props;
|
|
||||||
if (props.location !== 'NonStreamableGameItem') {
|
|
||||||
if ('productId' in props) {
|
|
||||||
productId = props.productId;
|
|
||||||
} else {
|
|
||||||
// Search page
|
|
||||||
productId = props.children.props.productId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} else {
|
||||||
|
const productId = GameTile.#findProductId($elm);
|
||||||
productId && GameTile.requestWaitTime($elm, productId);
|
productId && GameTile.#requestWaitTime($elm, productId);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,8 @@ import { AppInterface, STATES } from "@/utils/global";
|
|||||||
import { createButton, ButtonStyle, CE } from "@/utils/html";
|
import { createButton, ButtonStyle, CE } from "@/utils/html";
|
||||||
import { t } from "@/utils/translation";
|
import { t } from "@/utils/translation";
|
||||||
import { SettingsNavigationDialog } from "./dialog/settings-dialog";
|
import { SettingsNavigationDialog } from "./dialog/settings-dialog";
|
||||||
|
import { TrueAchievements } from "@/utils/true-achievements";
|
||||||
|
import { BxIcon } from "@/utils/bx-icon";
|
||||||
|
|
||||||
export enum GuideMenuTab {
|
export enum GuideMenuTab {
|
||||||
HOME = 'home',
|
HOME = 'home',
|
||||||
@ -24,27 +26,22 @@ export class GuideMenu {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
appSettings: createButton({
|
closeApp: AppInterface && createButton({
|
||||||
label: t('app-settings'),
|
icon: BxIcon.POWER,
|
||||||
style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE,
|
title: t('close-app'),
|
||||||
onClick: e => {
|
|
||||||
// Close all xCloud's dialogs
|
|
||||||
window.BX_EXPOSED.dialogRoutes.closeAll();
|
|
||||||
|
|
||||||
AppInterface.openAppSettings && AppInterface.openAppSettings();
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
|
|
||||||
closeApp: createButton({
|
|
||||||
label: t('close-app'),
|
|
||||||
style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE | ButtonStyle.DANGER,
|
style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE | ButtonStyle.DANGER,
|
||||||
onClick: e => {
|
onClick: e => {
|
||||||
AppInterface.closeApp();
|
AppInterface.closeApp();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
attributes: {
|
||||||
|
'data-state': 'normal',
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
reloadPage: createButton({
|
reloadPage: createButton({
|
||||||
label: t('reload-page'),
|
icon: BxIcon.REFRESH,
|
||||||
|
title: t('reload-page'),
|
||||||
style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE,
|
style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE,
|
||||||
onClick: e => {
|
onClick: e => {
|
||||||
if (STATES.isPlaying) {
|
if (STATES.isPlaying) {
|
||||||
@ -59,74 +56,87 @@ export class GuideMenu {
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
backToHome: createButton({
|
backToHome: createButton({
|
||||||
label: t('back-to-home'),
|
icon: BxIcon.HOME,
|
||||||
|
title: t('back-to-home'),
|
||||||
style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE,
|
style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE,
|
||||||
onClick: e => {
|
onClick: e => {
|
||||||
confirm(t('back-to-home-confirm')) && (window.location.href = window.location.href.substring(0, 31));
|
confirm(t('back-to-home-confirm')) && (window.location.href = window.location.href.substring(0, 31));
|
||||||
|
|
||||||
|
// Close all xCloud's dialogs
|
||||||
|
window.BX_EXPOSED.dialogRoutes.closeAll();
|
||||||
|
},
|
||||||
|
attributes: {
|
||||||
|
'data-state': 'playing',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
static #renderButtons(buttons: HTMLElement[]) {
|
static #$renderedButtons: HTMLElement;
|
||||||
const $div = CE('div', {});
|
|
||||||
|
|
||||||
for (const $button of buttons) {
|
static #renderButtons() {
|
||||||
$div.appendChild($button);
|
if (GuideMenu.#$renderedButtons) {
|
||||||
|
return GuideMenu.#$renderedButtons;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const $div = CE('div', {
|
||||||
|
class: 'bx-guide-home-buttons',
|
||||||
|
});
|
||||||
|
|
||||||
|
const buttons = [
|
||||||
|
GuideMenu.#BUTTONS.scriptSettings,
|
||||||
|
[
|
||||||
|
TrueAchievements.$button,
|
||||||
|
GuideMenu.#BUTTONS.backToHome,
|
||||||
|
GuideMenu.#BUTTONS.reloadPage,
|
||||||
|
GuideMenu.#BUTTONS.closeApp,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const $button of buttons) {
|
||||||
|
if (!$button) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($button instanceof HTMLElement) {
|
||||||
|
$div.appendChild($button);
|
||||||
|
} else if (Array.isArray($button)) {
|
||||||
|
const $wrapper = CE('div', {});
|
||||||
|
for (const $child of $button) {
|
||||||
|
$child && $wrapper.appendChild($child);
|
||||||
|
}
|
||||||
|
$div.appendChild($wrapper);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GuideMenu.#$renderedButtons = $div;
|
||||||
return $div;
|
return $div;
|
||||||
}
|
}
|
||||||
|
|
||||||
static #injectHome($root: HTMLElement) {
|
static #injectHome($root: HTMLElement, isPlaying = false) {
|
||||||
// Find the last divider
|
// Find the element to add buttons to
|
||||||
const $dividers = $root.querySelectorAll('div[class*=Divider-module__divider]');
|
let $target: HTMLElement | null = null;
|
||||||
if (!$dividers) {
|
if (isPlaying) {
|
||||||
return;
|
// Quit button
|
||||||
|
$target = $root.querySelector('a[class*=QuitGameButton]');
|
||||||
|
|
||||||
|
// Hide xCloud's Home button
|
||||||
|
const $btnXcloudHome = $root.querySelector('div[class^=HomeButtonWithDivider]') as HTMLElement;
|
||||||
|
$btnXcloudHome && ($btnXcloudHome.style.display = 'none');
|
||||||
|
} else {
|
||||||
|
// Last divider
|
||||||
|
const $dividers = $root.querySelectorAll('div[class*=Divider-module__divider]');
|
||||||
|
if ($dividers) {
|
||||||
|
$target = $dividers[$dividers.length - 1] as HTMLElement;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const buttons: HTMLElement[] = [];
|
if (!$target) {
|
||||||
|
return false;
|
||||||
// "Better xCloud" button
|
|
||||||
buttons.push(GuideMenu.#BUTTONS.scriptSettings);
|
|
||||||
|
|
||||||
// "App settings" button
|
|
||||||
AppInterface && buttons.push(GuideMenu.#BUTTONS.appSettings);
|
|
||||||
|
|
||||||
// "Reload page" button
|
|
||||||
buttons.push(GuideMenu.#BUTTONS.reloadPage);
|
|
||||||
|
|
||||||
// "Close app" buttons
|
|
||||||
AppInterface && buttons.push(GuideMenu.#BUTTONS.closeApp);
|
|
||||||
|
|
||||||
const $buttons = GuideMenu.#renderButtons(buttons);
|
|
||||||
|
|
||||||
const $lastDivider = $dividers[$dividers.length - 1];
|
|
||||||
$lastDivider.insertAdjacentElement('afterend', $buttons);
|
|
||||||
}
|
|
||||||
|
|
||||||
static #injectHomePlaying($root: HTMLElement) {
|
|
||||||
const $btnQuit = $root.querySelector('a[class*=QuitGameButton]');
|
|
||||||
if (!$btnQuit) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const buttons: HTMLElement[] = [];
|
const $buttons = GuideMenu.#renderButtons();
|
||||||
|
$buttons.dataset.isPlaying = isPlaying.toString();
|
||||||
buttons.push(GuideMenu.#BUTTONS.scriptSettings);
|
$target.insertAdjacentElement('afterend', $buttons);
|
||||||
AppInterface && buttons.push(GuideMenu.#BUTTONS.appSettings);
|
|
||||||
|
|
||||||
// Reload page
|
|
||||||
buttons.push(GuideMenu.#BUTTONS.reloadPage);
|
|
||||||
|
|
||||||
// Back to home
|
|
||||||
buttons.push(GuideMenu.#BUTTONS.backToHome);
|
|
||||||
|
|
||||||
const $buttons = GuideMenu.#renderButtons(buttons);
|
|
||||||
$btnQuit.insertAdjacentElement('afterend', $buttons);
|
|
||||||
|
|
||||||
// Hide xCloud's Home button
|
|
||||||
const $btnXcloudHome = $root.querySelector('div[class^=HomeButtonWithDivider]') as HTMLElement;
|
|
||||||
$btnXcloudHome && ($btnXcloudHome.style.display = 'none');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async #onShown(e: Event) {
|
static async #onShown(e: Event) {
|
||||||
@ -134,17 +144,39 @@ export class GuideMenu {
|
|||||||
|
|
||||||
if (where === GuideMenuTab.HOME) {
|
if (where === GuideMenuTab.HOME) {
|
||||||
const $root = document.querySelector('#gamepass-dialog-root div[role=dialog] div[role=tabpanel] div[class*=HomeLandingPage]') as HTMLElement;
|
const $root = document.querySelector('#gamepass-dialog-root div[role=dialog] div[role=tabpanel] div[class*=HomeLandingPage]') as HTMLElement;
|
||||||
if ($root) {
|
$root && GuideMenu.#injectHome($root, STATES.isPlaying);
|
||||||
if (STATES.isPlaying) {
|
|
||||||
GuideMenu.#injectHomePlaying($root);
|
|
||||||
} else {
|
|
||||||
GuideMenu.#injectHome($root);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static observe() {
|
static addEventListeners() {
|
||||||
window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, GuideMenu.#onShown);
|
window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, GuideMenu.#onShown);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static observe($addedElm: HTMLElement) {
|
||||||
|
const className = $addedElm.className;
|
||||||
|
if (!className.startsWith('NavigationAnimation') &&
|
||||||
|
!className.startsWith('DialogRoutes') &&
|
||||||
|
!className.startsWith('Dialog-module__container')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Achievement Details page
|
||||||
|
const $achievDetailPage = $addedElm.querySelector('div[class*=AchievementDetailPage]');
|
||||||
|
if ($achievDetailPage) {
|
||||||
|
TrueAchievements.injectAchievementDetailPage($achievDetailPage as HTMLElement);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find navigation bar
|
||||||
|
const $selectedTab = $addedElm.querySelector('div[class^=NavigationMenu] button[aria-selected=true');
|
||||||
|
if ($selectedTab) {
|
||||||
|
let $elm: Element | null = $selectedTab;
|
||||||
|
let index;
|
||||||
|
for (index = 0; ($elm = $elm?.previousElementSibling); index++);
|
||||||
|
|
||||||
|
if (index === 0) {
|
||||||
|
BxEvent.dispatch(window, BxEvent.XCLOUD_GUIDE_MENU_SHOWN, {where: GuideMenuTab.HOME});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,30 +5,60 @@ import { ButtonStyle, createButton } from "@/utils/html";
|
|||||||
import { t } from "@/utils/translation";
|
import { t } from "@/utils/translation";
|
||||||
|
|
||||||
export class ProductDetailsPage {
|
export class ProductDetailsPage {
|
||||||
private static $btnShortcut = createButton({
|
private static $btnShortcut = AppInterface && createButton({
|
||||||
classes: ['bx-button-shortcut'],
|
classes: ['bx-button-shortcut'],
|
||||||
icon: BxIcon.CREATE_SHORTCUT,
|
icon: BxIcon.CREATE_SHORTCUT,
|
||||||
label: t('create-shortcut'),
|
label: t('create-shortcut'),
|
||||||
style: ButtonStyle.FOCUSABLE,
|
style: ButtonStyle.FOCUSABLE,
|
||||||
tabIndex: 0,
|
tabIndex: 0,
|
||||||
onClick: e => {
|
onClick: e => {
|
||||||
AppInterface && AppInterface.createShortcut(window.location.pathname.substring(6));
|
AppInterface.createShortcut(window.location.pathname.substring(6));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
private static shortcutTimeoutId: number | null = null;
|
private static $btnWallpaper = AppInterface && createButton({
|
||||||
|
classes: ['bx-button-shortcut'],
|
||||||
|
icon: BxIcon.DOWNLOAD,
|
||||||
|
label: t('wallpaper'),
|
||||||
|
style: ButtonStyle.FOCUSABLE,
|
||||||
|
tabIndex: 0,
|
||||||
|
onClick: async e => {
|
||||||
|
try {
|
||||||
|
const matches = /\/games\/(?<titleSlug>[^\/]+)\/(?<productId>\w+)/.exec(window.location.pathname);
|
||||||
|
if (!matches?.groups) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
static injectShortcutButton() {
|
const titleSlug = matches.groups.titleSlug.replaceAll('\%' + '7C', '-');
|
||||||
if (!AppInterface || BX_FLAGS.DeviceInfo.deviceType !== 'android') {
|
const productId = matches.groups.productId;
|
||||||
|
AppInterface.downloadWallpapers(titleSlug, productId);
|
||||||
|
} catch (e) {}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
private static injectTimeoutId: number | null = null;
|
||||||
|
|
||||||
|
static injectButtons() {
|
||||||
|
if (!AppInterface) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ProductDetailsPage.shortcutTimeoutId && clearTimeout(ProductDetailsPage.shortcutTimeoutId);
|
ProductDetailsPage.injectTimeoutId && clearTimeout(ProductDetailsPage.injectTimeoutId);
|
||||||
ProductDetailsPage.shortcutTimeoutId = window.setTimeout(() => {
|
ProductDetailsPage.injectTimeoutId = window.setTimeout(() => {
|
||||||
// Find action buttons container
|
// Find action buttons container
|
||||||
const $container = document.querySelector('div[class*=ActionButtons-module__container]');
|
const $container = document.querySelector('div[class*=ActionButtons-module__container]');
|
||||||
if ($container) {
|
if ($container && $container.parentElement) {
|
||||||
$container.parentElement?.appendChild(ProductDetailsPage.$btnShortcut);
|
const fragment = document.createDocumentFragment();
|
||||||
|
|
||||||
|
// Shortcut button
|
||||||
|
if (BX_FLAGS.DeviceInfo.deviceType === 'android') {
|
||||||
|
fragment.appendChild(ProductDetailsPage.$btnShortcut);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wallpaper button
|
||||||
|
fragment.appendChild(ProductDetailsPage.$btnWallpaper);
|
||||||
|
|
||||||
|
$container.parentElement.appendChild(fragment);
|
||||||
}
|
}
|
||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ export function localRedirect(path: string) {
|
|||||||
|
|
||||||
const $anchor = CE<HTMLAnchorElement>('a', {
|
const $anchor = CE<HTMLAnchorElement>('a', {
|
||||||
href: url,
|
href: url,
|
||||||
class: 'bx-hidden bx-offscreen'
|
class: 'bx-hidden bx-offscreen',
|
||||||
}, '');
|
}, '');
|
||||||
$anchor.addEventListener('click', e => {
|
$anchor.addEventListener('click', e => {
|
||||||
// Remove element after clicking on it
|
// Remove element after clicking on it
|
||||||
|
46
src/types/index.d.ts
vendored
46
src/types/index.d.ts
vendored
@ -48,9 +48,9 @@ type BxStates = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
currentStream: Partial<{
|
currentStream: Partial<{
|
||||||
titleId: string;
|
titleSlug: string;
|
||||||
productId: string;
|
|
||||||
titleInfo: XcloudTitleInfo;
|
titleInfo: XcloudTitleInfo;
|
||||||
|
xboxTitleId: number;
|
||||||
|
|
||||||
streamPlayer: StreamPlayer | null;
|
streamPlayer: StreamPlayer | null;
|
||||||
|
|
||||||
@ -65,6 +65,7 @@ type BxStates = {
|
|||||||
config: {
|
config: {
|
||||||
serverId: string;
|
serverId: string;
|
||||||
};
|
};
|
||||||
|
titleId?: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
pointerServerPort: number;
|
pointerServerPort: number;
|
||||||
@ -75,6 +76,7 @@ type XcloudTitleInfo = {
|
|||||||
|
|
||||||
details: {
|
details: {
|
||||||
productId: string;
|
productId: string;
|
||||||
|
xboxTitleId: number;
|
||||||
supportedInputTypes: InputType[];
|
supportedInputTypes: InputType[];
|
||||||
supportedTabs: any[];
|
supportedTabs: any[];
|
||||||
hasNativeTouchSupport: boolean;
|
hasNativeTouchSupport: boolean;
|
||||||
@ -84,6 +86,7 @@ type XcloudTitleInfo = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
product: {
|
product: {
|
||||||
|
title: string;
|
||||||
heroImageUrl: string;
|
heroImageUrl: string;
|
||||||
titledHeroImageUrl: string;
|
titledHeroImageUrl: string;
|
||||||
tileImageUrl: string;
|
tileImageUrl: string;
|
||||||
@ -118,3 +121,42 @@ type MkbMouseWheel = {
|
|||||||
vertical: number;
|
vertical: number;
|
||||||
horizontal: number;
|
horizontal: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type XboxAchievement = {
|
||||||
|
version: number;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
gamerscore: number;
|
||||||
|
isSecret: boolean;
|
||||||
|
isUnlocked: boolean;
|
||||||
|
description: {
|
||||||
|
locked: string;
|
||||||
|
unlocked: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
imageUrl: string,
|
||||||
|
requirements: Array<{
|
||||||
|
current: number;
|
||||||
|
target: number;
|
||||||
|
percentComplete: number;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
percentComplete: 0,
|
||||||
|
rarity: {
|
||||||
|
currentCategory: string;
|
||||||
|
currentPercentage: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
rewards: Array<{
|
||||||
|
value: number;
|
||||||
|
valueType: string;
|
||||||
|
type: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
title: {
|
||||||
|
id: string;
|
||||||
|
scid: string;
|
||||||
|
productId: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import { AppInterface } from "@utils/global";
|
import { AppInterface } from "@utils/global";
|
||||||
|
import { BxLogger } from "./bx-logger";
|
||||||
|
import { BX_FLAGS } from "./bx-flags";
|
||||||
|
|
||||||
|
|
||||||
export namespace BxEvent {
|
export namespace BxEvent {
|
||||||
@ -51,7 +53,7 @@ export namespace BxEvent {
|
|||||||
|
|
||||||
export const XCLOUD_POLLING_MODE_CHANGED = 'bx-xcloud-polling-mode-changed';
|
export const XCLOUD_POLLING_MODE_CHANGED = 'bx-xcloud-polling-mode-changed';
|
||||||
|
|
||||||
export const XCLOUD_RENDERING_COMPONENT = 'bx-xcloud-rendering-page';
|
export const XCLOUD_RENDERING_COMPONENT = 'bx-xcloud-rendering-component';
|
||||||
|
|
||||||
export const XCLOUD_ROUTER_HISTORY_READY = 'bx-xcloud-router-history-ready';
|
export const XCLOUD_ROUTER_HISTORY_READY = 'bx-xcloud-router-history-ready';
|
||||||
|
|
||||||
@ -75,6 +77,8 @@ export namespace BxEvent {
|
|||||||
|
|
||||||
target.dispatchEvent(event);
|
target.dispatchEvent(event);
|
||||||
AppInterface && AppInterface.onEvent(eventName);
|
AppInterface && AppInterface.onEvent(eventName);
|
||||||
|
|
||||||
|
BX_FLAGS.Debug && BxLogger.warning('BxEvent', 'dispatch', eventName, data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { BxLogger } from "./bx-logger";
|
import { BxLogger } from "./bx-logger";
|
||||||
|
|
||||||
type BxFlags = {
|
type BxFlags = {
|
||||||
|
Debug: boolean;
|
||||||
|
|
||||||
CheckForUpdate: boolean;
|
CheckForUpdate: boolean;
|
||||||
EnableXcloudLogging: boolean;
|
EnableXcloudLogging: boolean;
|
||||||
SafariWorkaround: boolean;
|
SafariWorkaround: boolean;
|
||||||
@ -20,6 +22,8 @@ type BxFlags = {
|
|||||||
|
|
||||||
// Setup flags
|
// Setup flags
|
||||||
const DEFAULT_FLAGS: BxFlags = {
|
const DEFAULT_FLAGS: BxFlags = {
|
||||||
|
Debug: false,
|
||||||
|
|
||||||
CheckForUpdate: true,
|
CheckForUpdate: true,
|
||||||
EnableXcloudLogging: false,
|
EnableXcloudLogging: false,
|
||||||
SafariWorkaround: true,
|
SafariWorkaround: true,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import iconBetterXcloud from "@assets/svg/better-xcloud.svg" with { type: "text" };
|
import iconBetterXcloud from "@assets/svg/better-xcloud.svg" with { type: "text" };
|
||||||
|
import iconTrueAchievements from "@assets/svg/true-achievements.svg" with { type: "text" };
|
||||||
import iconClose from "@assets/svg/close.svg" with { type: "text" };
|
import iconClose from "@assets/svg/close.svg" with { type: "text" };
|
||||||
import iconCommand from "@assets/svg/command.svg" with { type: "text" };
|
import iconCommand from "@assets/svg/command.svg" with { type: "text" };
|
||||||
import iconController from "@assets/svg/controller.svg" with { type: "text" };
|
import iconController from "@assets/svg/controller.svg" with { type: "text" };
|
||||||
@ -9,6 +10,7 @@ import iconDisplay from "@assets/svg/display.svg" with { type: "text" };
|
|||||||
import iconHome from "@assets/svg/home.svg" with { type: "text" };
|
import iconHome from "@assets/svg/home.svg" with { type: "text" };
|
||||||
import iconNativeMkb from "@assets/svg/native-mkb.svg" with { type: "text" };
|
import iconNativeMkb from "@assets/svg/native-mkb.svg" with { type: "text" };
|
||||||
import iconNew from "@assets/svg/new.svg" with { type: "text" };
|
import iconNew from "@assets/svg/new.svg" with { type: "text" };
|
||||||
|
import iconPower from "@assets/svg/power.svg" with { type: "text" };
|
||||||
import iconQuestion from "@assets/svg/question.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 iconRefresh from "@assets/svg/refresh.svg" with { type: "text" };
|
||||||
import iconRemotePlay from "@assets/svg/remote-play.svg" with { type: "text" };
|
import iconRemotePlay from "@assets/svg/remote-play.svg" with { type: "text" };
|
||||||
@ -37,6 +39,7 @@ import iconUpload from "@assets/svg/upload.svg" with { type: "text" };
|
|||||||
|
|
||||||
export const BxIcon = {
|
export const BxIcon = {
|
||||||
BETTER_XCLOUD: iconBetterXcloud,
|
BETTER_XCLOUD: iconBetterXcloud,
|
||||||
|
TRUE_ACHIEVEMENTS: iconTrueAchievements,
|
||||||
STREAM_SETTINGS: iconStreamSettings,
|
STREAM_SETTINGS: iconStreamSettings,
|
||||||
STREAM_STATS: iconStreamStats,
|
STREAM_STATS: iconStreamStats,
|
||||||
CLOSE: iconClose,
|
CLOSE: iconClose,
|
||||||
@ -50,6 +53,7 @@ export const BxIcon = {
|
|||||||
COPY: iconCopy,
|
COPY: iconCopy,
|
||||||
TRASH: iconTrash,
|
TRASH: iconTrash,
|
||||||
CURSOR_TEXT: iconCursorText,
|
CURSOR_TEXT: iconCursorText,
|
||||||
|
POWER: iconPower,
|
||||||
QUESTION: iconQuestion,
|
QUESTION: iconQuestion,
|
||||||
REFRESH: iconRefresh,
|
REFRESH: iconRefresh,
|
||||||
VIRTUAL_CONTROLLER: iconVirtualController,
|
VIRTUAL_CONTROLLER: iconVirtualController,
|
||||||
|
@ -14,6 +14,7 @@ export enum ButtonStyle {
|
|||||||
TALL = 256,
|
TALL = 256,
|
||||||
CIRCULAR = 512,
|
CIRCULAR = 512,
|
||||||
NORMAL_CASE = 1024,
|
NORMAL_CASE = 1024,
|
||||||
|
NORMAL_LINK = 2048,
|
||||||
}
|
}
|
||||||
|
|
||||||
const ButtonStyleClass = {
|
const ButtonStyleClass = {
|
||||||
@ -28,6 +29,7 @@ const ButtonStyleClass = {
|
|||||||
[ButtonStyle.TALL]: 'bx-tall',
|
[ButtonStyle.TALL]: 'bx-tall',
|
||||||
[ButtonStyle.CIRCULAR]: 'bx-circular',
|
[ButtonStyle.CIRCULAR]: 'bx-circular',
|
||||||
[ButtonStyle.NORMAL_CASE]: 'bx-normal-case',
|
[ButtonStyle.NORMAL_CASE]: 'bx-normal-case',
|
||||||
|
[ButtonStyle.NORMAL_LINK]: 'bx-normal-link',
|
||||||
}
|
}
|
||||||
|
|
||||||
type BxButton = {
|
type BxButton = {
|
||||||
|
@ -71,7 +71,7 @@ export class Screenshot {
|
|||||||
// Get data URL and pass to parent app
|
// Get data URL and pass to parent app
|
||||||
if (AppInterface) {
|
if (AppInterface) {
|
||||||
const data = $canvas.toDataURL('image/png').split(';base64,')[1];
|
const data = $canvas.toDataURL('image/png').split(';base64,')[1];
|
||||||
AppInterface.saveScreenshot(currentStream.titleId, data);
|
AppInterface.saveScreenshot(currentStream.titleSlug, data);
|
||||||
|
|
||||||
// Free screenshot from memory
|
// Free screenshot from memory
|
||||||
canvasContext.clearRect(0, 0, $canvas.width, $canvas.height);
|
canvasContext.clearRect(0, 0, $canvas.width, $canvas.height);
|
||||||
@ -84,7 +84,7 @@ export class Screenshot {
|
|||||||
// Download screenshot
|
// Download screenshot
|
||||||
const now = +new Date;
|
const now = +new Date;
|
||||||
const $anchor = CE<HTMLAnchorElement>('a', {
|
const $anchor = CE<HTMLAnchorElement>('a', {
|
||||||
'download': `${currentStream.titleId}-${now}.png`,
|
'download': `${currentStream.titleSlug}-${now}.png`,
|
||||||
'href': URL.createObjectURL(blob!),
|
'href': URL.createObjectURL(blob!),
|
||||||
});
|
});
|
||||||
$anchor.click();
|
$anchor.click();
|
||||||
|
@ -140,6 +140,10 @@ export class SettingElement {
|
|||||||
!(e as any).ignoreOnChange && onChange(e, (e.target as HTMLInputElement).checked);
|
!(e as any).ignoreOnChange && onChange(e, (e.target as HTMLInputElement).checked);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
($control as any).setValue = (value: boolean) => {
|
||||||
|
$control.checked = !!value;
|
||||||
|
};
|
||||||
|
|
||||||
return $control;
|
return $control;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ import type { PrefKey } from "@/enums/pref-keys";
|
|||||||
import type { NumberStepperParams, SettingDefinitions } from "@/types/setting-definition";
|
import type { NumberStepperParams, SettingDefinitions } from "@/types/setting-definition";
|
||||||
import { BxEvent } from "../bx-event";
|
import { BxEvent } from "../bx-event";
|
||||||
import { SettingElementType } from "../setting-element";
|
import { SettingElementType } from "../setting-element";
|
||||||
|
import { t } from "../translation";
|
||||||
|
|
||||||
export class BaseSettingsStore {
|
export class BaseSettingsStore {
|
||||||
private storage: Storage;
|
private storage: Storage;
|
||||||
@ -145,6 +146,8 @@ export class BaseSettingsStore {
|
|||||||
if (value in options) {
|
if (value in options) {
|
||||||
return options[value];
|
return options[value];
|
||||||
}
|
}
|
||||||
|
} else if (typeof value === 'boolean') {
|
||||||
|
return value ? t('on') : t('off')
|
||||||
}
|
}
|
||||||
|
|
||||||
return value.toString();
|
return value.toString();
|
||||||
|
@ -113,6 +113,7 @@ const Texts = {
|
|||||||
"fortnite-force-console-version": "Fortnite: force console version",
|
"fortnite-force-console-version": "Fortnite: force console version",
|
||||||
"game-bar": "Game Bar",
|
"game-bar": "Game Bar",
|
||||||
"getting-consoles-list": "Getting the list of consoles...",
|
"getting-consoles-list": "Getting the list of consoles...",
|
||||||
|
"guide": "Guide",
|
||||||
"help": "Help",
|
"help": "Help",
|
||||||
"hide": "Hide",
|
"hide": "Hide",
|
||||||
"hide-idle-cursor": "Hide mouse cursor on idle",
|
"hide-idle-cursor": "Hide mouse cursor on idle",
|
||||||
@ -125,6 +126,7 @@ const Texts = {
|
|||||||
"highest-quality-note": "Your device may not be powerful enough to use these settings",
|
"highest-quality-note": "Your device may not be powerful enough to use these settings",
|
||||||
"horizontal-scroll-sensitivity": "Horizontal scroll sensitivity",
|
"horizontal-scroll-sensitivity": "Horizontal scroll sensitivity",
|
||||||
"horizontal-sensitivity": "Horizontal sensitivity",
|
"horizontal-sensitivity": "Horizontal sensitivity",
|
||||||
|
"how-to-fix": "How to fix",
|
||||||
"how-to-improve-app-performance": "How to improve app's performance",
|
"how-to-improve-app-performance": "How to improve app's performance",
|
||||||
"ignore": "Ignore",
|
"ignore": "Ignore",
|
||||||
"import": "Import",
|
"import": "Import",
|
||||||
@ -137,6 +139,7 @@ const Texts = {
|
|||||||
"large": "Large",
|
"large": "Large",
|
||||||
"layout": "Layout",
|
"layout": "Layout",
|
||||||
"left-stick": "Left stick",
|
"left-stick": "Left stick",
|
||||||
|
"load-failed-message": "Failed to run Better xCloud",
|
||||||
"loading-screen": "Loading screen",
|
"loading-screen": "Loading screen",
|
||||||
"local-co-op": "Local co-op",
|
"local-co-op": "Local co-op",
|
||||||
"low-power": "Low power",
|
"low-power": "Low power",
|
||||||
@ -198,22 +201,22 @@ const Texts = {
|
|||||||
(e: any) => `Recommended settings for ${e.device}`,
|
(e: any) => `Recommended settings for ${e.device}`,
|
||||||
,
|
,
|
||||||
,
|
,
|
||||||
,
|
(e: any) => `Empfohlene Einstellungen für ${e.device}`,
|
||||||
,
|
,
|
||||||
(e: any) => `Ajustes recomendados para ${e.device}`,
|
(e: any) => `Ajustes recomendados para ${e.device}`,
|
||||||
,
|
(e: any) => `Paramètres recommandés pour ${e.device}`,
|
||||||
(e: any) => `Configurazioni consigliate per ${e.device}`,
|
(e: any) => `Configurazioni consigliate per ${e.device}`,
|
||||||
,
|
(e: any) => `${e.device} の推奨設定`,
|
||||||
(e: any) => `다음 기기에서 권장되는 설정: ${e.device}`,
|
(e: any) => `다음 기기에서 권장되는 설정: ${e.device}`,
|
||||||
|
(e: any) => `Zalecane ustawienia dla ${e.device}`,
|
||||||
,
|
,
|
||||||
|
(e: any) => `Рекомендуемые настройки для ${e.device}`,
|
||||||
,
|
,
|
||||||
,
|
(e: any) => `${e.device} için önerilen ayarlar`,
|
||||||
,
|
|
||||||
,
|
|
||||||
(e: any) => `Рекомендовані налаштування для ${e.device}`,
|
(e: any) => `Рекомендовані налаштування для ${e.device}`,
|
||||||
(e: any) => `Cấu hình được đề xuất cho ${e.device}`,
|
(e: any) => `Cấu hình được đề xuất cho ${e.device}`,
|
||||||
,
|
(e: any) => `${e.device} 的推荐设置`,
|
||||||
,
|
(e: any) => `${e.device} 推薦的設定`,
|
||||||
],
|
],
|
||||||
"reduce-animations": "Reduce UI animations",
|
"reduce-animations": "Reduce UI animations",
|
||||||
"region": "Region",
|
"region": "Region",
|
||||||
@ -228,7 +231,6 @@ const Texts = {
|
|||||||
"rocket-always-show": "Always show",
|
"rocket-always-show": "Always show",
|
||||||
"rocket-animation": "Rocket animation",
|
"rocket-animation": "Rocket animation",
|
||||||
"rocket-hide-queue": "Hide when queuing",
|
"rocket-hide-queue": "Hide when queuing",
|
||||||
"safari-failed-message": "Failed to run Better xCloud. Retrying, please wait...",
|
|
||||||
"saturation": "Saturation",
|
"saturation": "Saturation",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"screen": "Screen",
|
"screen": "Screen",
|
||||||
@ -319,6 +321,7 @@ const Texts = {
|
|||||||
],
|
],
|
||||||
"touch-controller": "Touch controller",
|
"touch-controller": "Touch controller",
|
||||||
"transparent-background": "Transparent background",
|
"transparent-background": "Transparent background",
|
||||||
|
"true-achievements": "TrueAchievements",
|
||||||
"ui": "UI",
|
"ui": "UI",
|
||||||
"unexpected-behavior": "May cause unexpected behavior",
|
"unexpected-behavior": "May cause unexpected behavior",
|
||||||
"united-states": "United States",
|
"united-states": "United States",
|
||||||
|
96
src/utils/true-achievements.ts
Normal file
96
src/utils/true-achievements.ts
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import { BxIcon } from "./bx-icon";
|
||||||
|
import { AppInterface, STATES } from "./global";
|
||||||
|
import { ButtonStyle, CE, createButton, getReactProps } from "./html";
|
||||||
|
import { t } from "./translation";
|
||||||
|
|
||||||
|
export class TrueAchievements {
|
||||||
|
private static $link = createButton({
|
||||||
|
label: t('true-achievements'),
|
||||||
|
url: '#',
|
||||||
|
icon: BxIcon.TRUE_ACHIEVEMENTS,
|
||||||
|
style: ButtonStyle.FOCUSABLE | ButtonStyle.GHOST | ButtonStyle.FULL_WIDTH | ButtonStyle.NORMAL_LINK,
|
||||||
|
onClick: TrueAchievements.onClick,
|
||||||
|
}) as HTMLAnchorElement;
|
||||||
|
|
||||||
|
static $button = createButton({
|
||||||
|
title: t('true-achievements'),
|
||||||
|
icon: BxIcon.TRUE_ACHIEVEMENTS,
|
||||||
|
style: ButtonStyle.FOCUSABLE | ButtonStyle.FULL_WIDTH,
|
||||||
|
onClick: TrueAchievements.onClick,
|
||||||
|
}) as HTMLAnchorElement;
|
||||||
|
|
||||||
|
private static onClick(e: Event) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const dataset = TrueAchievements.$link.dataset;
|
||||||
|
TrueAchievements.open(true, dataset.xboxTitleId, dataset.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static $hiddenLink = CE<HTMLAnchorElement>('a', {
|
||||||
|
target: '_blank',
|
||||||
|
});
|
||||||
|
|
||||||
|
private static updateLinks(xboxTitleId?: string, id?: string) {
|
||||||
|
TrueAchievements.$link.dataset.xboxTitleId = xboxTitleId;
|
||||||
|
TrueAchievements.$link.dataset.id = id;
|
||||||
|
|
||||||
|
TrueAchievements.$button.dataset.xboxTitleId = xboxTitleId;
|
||||||
|
TrueAchievements.$button.dataset.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
static injectAchievementDetailPage($parent: HTMLElement) {
|
||||||
|
const props = getReactProps($parent);
|
||||||
|
if (!props) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Achievement list
|
||||||
|
const achievementList: XboxAchievement[] = props.children.props.data.data;
|
||||||
|
|
||||||
|
// Get current achievement name
|
||||||
|
const $header = $parent.querySelector('div[class*=AchievementDetailHeader]') as HTMLElement;
|
||||||
|
const achievementName = getReactProps($header).children[0].props.achievementName;
|
||||||
|
|
||||||
|
// Find achievement based on name
|
||||||
|
let id: string | undefined;
|
||||||
|
let xboxTitleId: string | undefined;
|
||||||
|
for (const achiev of achievementList) {
|
||||||
|
if (achiev.name === achievementName) {
|
||||||
|
id = achiev.id;
|
||||||
|
xboxTitleId = achiev.title.id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Found achievement -> add TrueAchievements button
|
||||||
|
if (id) {
|
||||||
|
TrueAchievements.updateLinks(xboxTitleId, id);
|
||||||
|
$parent.appendChild(TrueAchievements.$link);
|
||||||
|
}
|
||||||
|
} catch (e) {};
|
||||||
|
}
|
||||||
|
|
||||||
|
static open(override: boolean, xboxTitleId?: number | string, id?: number | string) {
|
||||||
|
if (!xboxTitleId || xboxTitleId === 'undefined') {
|
||||||
|
xboxTitleId = STATES.currentStream.xboxTitleId || STATES.currentStream.titleInfo?.details.xboxTitleId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AppInterface && AppInterface.openTrueAchievementsLink) {
|
||||||
|
AppInterface.openTrueAchievementsLink(override, xboxTitleId?.toString(), id?.toString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = 'https://www.trueachievements.com';
|
||||||
|
if (xboxTitleId) {
|
||||||
|
if (id && id !== 'undefined') {
|
||||||
|
url += `/deeplink/${xboxTitleId}/${id}`;
|
||||||
|
} else {
|
||||||
|
url += `/deeplink/${xboxTitleId}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TrueAchievements.$hiddenLink.href = url;
|
||||||
|
TrueAchievements.$hiddenLink.click();
|
||||||
|
}
|
||||||
|
}
|
@ -110,3 +110,13 @@ export async function copyToClipboard(text: string, showToast=true): Promise<boo
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function productTitleToSlug(title: string): string {
|
||||||
|
return title.replace(/[;,/?:@&=+_`~$%#^*()!^\u2122\xae\xa9]/g, '')
|
||||||
|
.replace(/\|/g, '-')
|
||||||
|
.replace(/ {2,}/g, ' ')
|
||||||
|
.trim()
|
||||||
|
.substr(0, 50)
|
||||||
|
.replace(/ /g, '-')
|
||||||
|
.toLowerCase();
|
||||||
|
}
|
||||||
|
25
src/utils/xbox-api.ts
Normal file
25
src/utils/xbox-api.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { NATIVE_FETCH } from "./bx-flags"
|
||||||
|
|
||||||
|
export class XboxApi {
|
||||||
|
private static CACHED_TITLES: Record<string, string> = {};
|
||||||
|
|
||||||
|
static async getProductTitle(xboxTitleId: number | string): Promise<string | null> {
|
||||||
|
xboxTitleId = xboxTitleId.toString();
|
||||||
|
if (XboxApi.CACHED_TITLES[xboxTitleId]) {
|
||||||
|
return XboxApi.CACHED_TITLES[xboxTitleId];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = `https://displaycatalog.mp.microsoft.com/v7.0/products/lookup?market=US&languages=en&value=${xboxTitleId}&alternateId=XboxTitleId&fieldsTemplate=browse`;
|
||||||
|
const resp = await NATIVE_FETCH(url);
|
||||||
|
const json = await resp.json();
|
||||||
|
|
||||||
|
const productTitle = json['Products'][0]['LocalizedProperties'][0]['ProductTitle'];
|
||||||
|
XboxApi.CACHED_TITLES[xboxTitleId] = productTitle;
|
||||||
|
|
||||||
|
return productTitle;
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user