mirror of
https://github.com/redphx/better-xcloud.git
synced 2025-06-06 07:37:19 +02:00
Integrate TrueAchievements
This commit is contained in:
parent
4b02fec8ac
commit
a6e358479a
@ -1,11 +1,19 @@
|
||||
.bx-guide-home-buttons[data-is-playing="true"] {
|
||||
button[data-state='normal'] {
|
||||
display: none;
|
||||
.bx-guide-home-buttons {
|
||||
> div {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.bx-guide-home-buttons[data-is-playing="false"] {
|
||||
button[data-state='playing'] {
|
||||
display: none;
|
||||
&[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;
|
||||
}
|
||||
|
||||
.bx-normal-link {
|
||||
text-transform: none !important;
|
||||
text-align: left !important;
|
||||
font-weight: 400 !important;
|
||||
font-family: var(--bx-normal-font) !important;
|
||||
}
|
||||
|
||||
select[multiple] {
|
||||
overflow: auto;
|
||||
}
|
||||
|
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 |
76
src/index.ts
76
src/index.ts
@ -14,7 +14,7 @@ import { Toast } from "@utils/toast";
|
||||
import { LoadingScreen } from "@modules/loading-screen";
|
||||
import { MouseCursorHider } from "@modules/mkb/mouse-cursor-hider";
|
||||
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 { RemotePlay } from "@modules/remote-play";
|
||||
import { onHistoryChanged, patchHistoryMethod } from "@utils/history";
|
||||
@ -39,6 +39,8 @@ import { compressCss } from "@macros/build" with {type: "macro"};
|
||||
import { SettingsNavigationDialog } from "./modules/ui/dialog/settings-dialog";
|
||||
import { StreamUiHandler } from "./modules/stream/stream-ui";
|
||||
import { UserAgent } from "./utils/user-agent";
|
||||
import { XboxApi } from "./utils/xbox-api";
|
||||
import { TrueAchievements } from "./utils/true-achievements";
|
||||
|
||||
|
||||
// Handle login page
|
||||
@ -198,15 +200,10 @@ window.addEventListener(BxEvent.XCLOUD_SERVERS_READY, e => {
|
||||
|
||||
window.addEventListener(BxEvent.STREAM_LOADING, e => {
|
||||
// Get title ID for screenshot's name
|
||||
if (window.location.pathname.includes('/launch/')) {
|
||||
const matches = /\/launch\/(?<title_id>[^\/]+)\/(?<product_id>\w+)/.exec(window.location.pathname);
|
||||
if (matches?.groups) {
|
||||
STATES.currentStream.titleId = matches.groups.title_id;
|
||||
STATES.currentStream.productId = matches.groups.product_id;
|
||||
}
|
||||
if (window.location.pathname.includes('/launch/') && STATES.currentStream.titleInfo) {
|
||||
STATES.currentStream.titleSlug = productTitleToSlug(STATES.currentStream.titleInfo.product.title);
|
||||
} else {
|
||||
STATES.currentStream.titleId = 'remote-play';
|
||||
STATES.currentStream.productId = '';
|
||||
STATES.currentStream.titleSlug = 'remote-play';
|
||||
}
|
||||
});
|
||||
|
||||
@ -252,6 +249,38 @@ window.addEventListener(BxEvent.XCLOUD_RENDERING_COMPONENT, e => {
|
||||
}
|
||||
});
|
||||
|
||||
// Detect game change
|
||||
window.addEventListener(BxEvent.DATA_CHANNEL_CREATED, e => {
|
||||
const dataChannel = (e as any).dataChannel;
|
||||
if (!dataChannel || dataChannel.label !== 'message') {
|
||||
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) {
|
||||
return;
|
||||
@ -288,7 +317,7 @@ window.addEventListener(BxEvent.CAPTURE_SCREENSHOT, e => {
|
||||
|
||||
|
||||
function observeRootDialog($root: HTMLElement) {
|
||||
let currentShown = false;
|
||||
let beingShown = false;
|
||||
|
||||
const observer = new MutationObserver(mutationList => {
|
||||
for (const mutation of mutationList) {
|
||||
@ -296,21 +325,28 @@ function observeRootDialog($root: HTMLElement) {
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log('added', mutation.addedNodes);
|
||||
if (mutation.addedNodes.length === 1) {
|
||||
const $addedElm = mutation.addedNodes[0];
|
||||
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
|
||||
if (document.querySelector('#gamepass-dialog-root div[class*=GuideDialog]')) {
|
||||
// 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++);
|
||||
// Achievement Details page
|
||||
const $achievDetailPage = $addedElm.querySelector('div[class*=AchievementDetailPage]');
|
||||
if ($achievDetailPage) {
|
||||
TrueAchievements.injectAchievementDetailPage($achievDetailPage as HTMLElement);
|
||||
} else {
|
||||
// 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});
|
||||
if (index === 0) {
|
||||
BxEvent.dispatch(window, BxEvent.XCLOUD_GUIDE_MENU_SHOWN, {where: GuideMenuTab.HOME});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -319,8 +355,8 @@ function observeRootDialog($root: HTMLElement) {
|
||||
}
|
||||
|
||||
const shown = ($root.firstElementChild && $root.firstElementChild.childElementCount > 0) || false;
|
||||
if (shown !== currentShown) {
|
||||
currentShown = shown;
|
||||
if (shown !== beingShown) {
|
||||
beingShown = shown;
|
||||
BxEvent.dispatch(window, shown ? BxEvent.XCLOUD_DIALOG_SHOWN : BxEvent.XCLOUD_DIALOG_DISMISSED);
|
||||
}
|
||||
}
|
||||
|
@ -15,11 +15,11 @@ export class MicrophoneAction extends BaseGameBarAction {
|
||||
super();
|
||||
|
||||
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);
|
||||
this.$content.setAttribute('data-enabled', enabled.toString());
|
||||
};
|
||||
const enabled = MicrophoneShortcut.toggle(false);
|
||||
this.$content.setAttribute('data-enabled', enabled.toString());
|
||||
};
|
||||
|
||||
const $btnDefault = createButton({
|
||||
style: ButtonStyle.GHOST,
|
||||
|
@ -12,16 +12,16 @@ export class ScreenshotAction extends BaseGameBarAction {
|
||||
super();
|
||||
|
||||
const onClick = (e: Event) => {
|
||||
BxEvent.dispatch(window, BxEvent.GAME_BAR_ACTION_ACTIVATED);
|
||||
Screenshot.takeScreenshot();
|
||||
};
|
||||
BxEvent.dispatch(window, BxEvent.GAME_BAR_ACTION_ACTIVATED);
|
||||
Screenshot.takeScreenshot();
|
||||
};
|
||||
|
||||
this.$content = createButton({
|
||||
style: ButtonStyle.GHOST,
|
||||
icon: BxIcon.SCREENSHOT,
|
||||
title: t('take-screenshot'),
|
||||
onClick: onClick,
|
||||
});
|
||||
style: ButtonStyle.GHOST,
|
||||
icon: BxIcon.SCREENSHOT,
|
||||
title: t('take-screenshot'),
|
||||
onClick: onClick,
|
||||
});
|
||||
}
|
||||
|
||||
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,6 +8,7 @@ import { STATES } from "@utils/global";
|
||||
import { MicrophoneAction } from "./action-microphone";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref, StreamTouchController } from "@/utils/settings-storages/global-settings-storage";
|
||||
import { TrueAchievementsAction } from "./action-true-achievements";
|
||||
|
||||
|
||||
export class GameBar {
|
||||
@ -42,6 +43,7 @@ export class GameBar {
|
||||
this.actions = [
|
||||
new ScreenshotAction(),
|
||||
...(STATES.userAgent.capabilities.touch && (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) !== StreamTouchController.OFF) ? [new TouchControlAction()] : []),
|
||||
new TrueAchievementsAction(),
|
||||
new MicrophoneAction(),
|
||||
];
|
||||
|
||||
@ -92,7 +94,7 @@ export class GameBar {
|
||||
|
||||
// Toggle Game bar
|
||||
const mode = (e as any).mode;
|
||||
mode !== 'None' ? this.disable() : this.enable();
|
||||
mode !== 'none' ? this.disable() : this.enable();
|
||||
}).bind(this));
|
||||
}
|
||||
|
||||
|
@ -459,7 +459,7 @@ export class EmulatedMkbHandler extends MkbHandler {
|
||||
}
|
||||
|
||||
const mode = (e as any).mode;
|
||||
if (mode === 'None') {
|
||||
if (mode === 'none') {
|
||||
this.#$message.classList.remove('bx-offscreen');
|
||||
} else {
|
||||
this.#$message.classList.add('bx-offscreen');
|
||||
|
@ -69,7 +69,7 @@ export class NativeMkbHandler extends MkbHandler {
|
||||
}
|
||||
|
||||
const mode = (e as any).mode;
|
||||
if (mode === 'None') {
|
||||
if (mode === 'none') {
|
||||
this.#$message.classList.remove('bx-offscreen');
|
||||
} else {
|
||||
this.#$message.classList.add('bx-offscreen');
|
||||
|
@ -465,7 +465,7 @@ e.guideUI = null;
|
||||
}
|
||||
|
||||
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);
|
||||
return str;
|
||||
|
@ -3,6 +3,8 @@ import { AppInterface, STATES } from "@/utils/global";
|
||||
import { createButton, ButtonStyle, CE } from "@/utils/html";
|
||||
import { t } from "@/utils/translation";
|
||||
import { SettingsNavigationDialog } from "./dialog/settings-dialog";
|
||||
import { TrueAchievements } from "@/utils/true-achievements";
|
||||
import { BxIcon } from "@/utils/bx-icon";
|
||||
|
||||
export enum GuideMenuTab {
|
||||
HOME = 'home',
|
||||
@ -24,31 +26,19 @@ export class GuideMenu {
|
||||
},
|
||||
}),
|
||||
|
||||
appSettings: createButton({
|
||||
label: t('app-settings'),
|
||||
style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE,
|
||||
onClick: e => {
|
||||
// Close all xCloud's dialogs
|
||||
window.BX_EXPOSED.dialogRoutes.closeAll();
|
||||
|
||||
AppInterface.openAppSettings && AppInterface.openAppSettings();
|
||||
},
|
||||
}),
|
||||
|
||||
closeApp: createButton({
|
||||
label: t('close-app'),
|
||||
icon: BxIcon.POWER,
|
||||
title: t('close-app'),
|
||||
style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE | ButtonStyle.DANGER,
|
||||
onClick: e => {
|
||||
AppInterface.closeApp();
|
||||
},
|
||||
attributes: {
|
||||
'data-state': 'normal',
|
||||
},
|
||||
}),
|
||||
|
||||
reloadPage: createButton({
|
||||
label: t('reload-page'),
|
||||
style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE,
|
||||
icon: BxIcon.REFRESH,
|
||||
title: t('reload-page'),
|
||||
style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE | ButtonStyle.GHOST,
|
||||
onClick: e => {
|
||||
if (STATES.isPlaying) {
|
||||
confirm(t('confirm-reload-stream')) && window.location.reload();
|
||||
@ -62,15 +52,30 @@ export class GuideMenu {
|
||||
}),
|
||||
|
||||
backToHome: createButton({
|
||||
label: t('back-to-home'),
|
||||
style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE,
|
||||
icon: BxIcon.HOME,
|
||||
title: t('back-to-home'),
|
||||
style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE | ButtonStyle.GHOST,
|
||||
onClick: e => {
|
||||
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',
|
||||
},
|
||||
}),
|
||||
|
||||
trueAchievements: createButton({
|
||||
label: t('true-achievements'),
|
||||
style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE,
|
||||
onClick: e => {
|
||||
TrueAchievements.open(false);
|
||||
|
||||
// Close all xCloud's dialogs
|
||||
window.BX_EXPOSED.dialogRoutes.closeAll();
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
static #$renderedButtons: HTMLElement;
|
||||
@ -86,13 +91,28 @@ export class GuideMenu {
|
||||
|
||||
const buttons = [
|
||||
GuideMenu.#BUTTONS.scriptSettings,
|
||||
GuideMenu.#BUTTONS.reloadPage,
|
||||
GuideMenu.#BUTTONS.backToHome,
|
||||
AppInterface && GuideMenu.#BUTTONS.closeApp,
|
||||
GuideMenu.#BUTTONS.trueAchievements,
|
||||
[
|
||||
GuideMenu.#BUTTONS.backToHome,
|
||||
GuideMenu.#BUTTONS.reloadPage,
|
||||
AppInterface && GuideMenu.#BUTTONS.closeApp,
|
||||
],
|
||||
];
|
||||
|
||||
for (const $button of buttons) {
|
||||
$button && $div.appendChild($button);
|
||||
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;
|
||||
|
@ -11,7 +11,7 @@ export function localRedirect(path: string) {
|
||||
|
||||
const $anchor = CE<HTMLAnchorElement>('a', {
|
||||
href: url,
|
||||
class: 'bx-hidden bx-offscreen'
|
||||
class: 'bx-hidden bx-offscreen',
|
||||
}, '');
|
||||
$anchor.addEventListener('click', e => {
|
||||
// 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<{
|
||||
titleId: string;
|
||||
productId: string;
|
||||
titleSlug: string;
|
||||
titleInfo: XcloudTitleInfo;
|
||||
xboxTitleId: number;
|
||||
|
||||
streamPlayer: StreamPlayer | null;
|
||||
|
||||
@ -65,6 +65,7 @@ type BxStates = {
|
||||
config: {
|
||||
serverId: string;
|
||||
};
|
||||
titleId?: string;
|
||||
}>;
|
||||
|
||||
pointerServerPort: number;
|
||||
@ -75,6 +76,7 @@ type XcloudTitleInfo = {
|
||||
|
||||
details: {
|
||||
productId: string;
|
||||
xboxTitleId: number;
|
||||
supportedInputTypes: InputType[];
|
||||
supportedTabs: any[];
|
||||
hasNativeTouchSupport: boolean;
|
||||
@ -84,6 +86,7 @@ type XcloudTitleInfo = {
|
||||
};
|
||||
|
||||
product: {
|
||||
title: string;
|
||||
heroImageUrl: string;
|
||||
titledHeroImageUrl: string;
|
||||
tileImageUrl: string;
|
||||
@ -118,3 +121,42 @@ type MkbMouseWheel = {
|
||||
vertical: 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,5 @@
|
||||
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 iconCommand from "@assets/svg/command.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 iconNativeMkb from "@assets/svg/native-mkb.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 iconRefresh from "@assets/svg/refresh.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 = {
|
||||
BETTER_XCLOUD: iconBetterXcloud,
|
||||
TRUE_ACHIEVEMENTS: iconTrueAchievements,
|
||||
STREAM_SETTINGS: iconStreamSettings,
|
||||
STREAM_STATS: iconStreamStats,
|
||||
CLOSE: iconClose,
|
||||
@ -50,6 +53,7 @@ export const BxIcon = {
|
||||
COPY: iconCopy,
|
||||
TRASH: iconTrash,
|
||||
CURSOR_TEXT: iconCursorText,
|
||||
POWER: iconPower,
|
||||
QUESTION: iconQuestion,
|
||||
REFRESH: iconRefresh,
|
||||
VIRTUAL_CONTROLLER: iconVirtualController,
|
||||
|
@ -14,6 +14,7 @@ export enum ButtonStyle {
|
||||
TALL = 256,
|
||||
CIRCULAR = 512,
|
||||
NORMAL_CASE = 1024,
|
||||
NORMAL_LINK = 2048,
|
||||
}
|
||||
|
||||
const ButtonStyleClass = {
|
||||
@ -28,6 +29,7 @@ const ButtonStyleClass = {
|
||||
[ButtonStyle.TALL]: 'bx-tall',
|
||||
[ButtonStyle.CIRCULAR]: 'bx-circular',
|
||||
[ButtonStyle.NORMAL_CASE]: 'bx-normal-case',
|
||||
[ButtonStyle.NORMAL_LINK]: 'bx-normal-link',
|
||||
}
|
||||
|
||||
type BxButton = {
|
||||
|
@ -71,7 +71,7 @@ export class Screenshot {
|
||||
// Get data URL and pass to parent app
|
||||
if (AppInterface) {
|
||||
const data = $canvas.toDataURL('image/png').split(';base64,')[1];
|
||||
AppInterface.saveScreenshot(currentStream.titleId, data);
|
||||
AppInterface.saveScreenshot(currentStream.titleSlug, data);
|
||||
|
||||
// Free screenshot from memory
|
||||
canvasContext.clearRect(0, 0, $canvas.width, $canvas.height);
|
||||
@ -84,7 +84,7 @@ export class Screenshot {
|
||||
// Download screenshot
|
||||
const now = +new Date;
|
||||
const $anchor = CE<HTMLAnchorElement>('a', {
|
||||
'download': `${currentStream.titleId}-${now}.png`,
|
||||
'download': `${currentStream.titleSlug}-${now}.png`,
|
||||
'href': URL.createObjectURL(blob!),
|
||||
});
|
||||
$anchor.click();
|
||||
|
@ -321,6 +321,7 @@ const Texts = {
|
||||
],
|
||||
"touch-controller": "Touch controller",
|
||||
"transparent-background": "Transparent background",
|
||||
"true-achievements": "TrueAchievements",
|
||||
"ui": "UI",
|
||||
"unexpected-behavior": "May cause unexpected behavior",
|
||||
"united-states": "United States",
|
||||
|
81
src/utils/true-achievements.ts
Normal file
81
src/utils/true-achievements.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import { BX_FLAGS } from "./bx-flags";
|
||||
import { AppInterface, STATES } from "./global";
|
||||
import { ButtonStyle, CE, createButton, getReactProps } from "./html";
|
||||
import { t } from "./translation";
|
||||
|
||||
export class TrueAchievements {
|
||||
private static $taLink = createButton({
|
||||
label: t('true-achievements'),
|
||||
url: 'https://www.trueachievements.com',
|
||||
style: ButtonStyle.FOCUSABLE | ButtonStyle.GHOST | ButtonStyle.FULL_WIDTH | ButtonStyle.NORMAL_LINK,
|
||||
onClick: e => {
|
||||
e.preventDefault();
|
||||
|
||||
const dataset = TrueAchievements.$taLink.dataset;
|
||||
TrueAchievements.open(true, dataset.xboxTitleId, dataset.id);
|
||||
},
|
||||
}) as HTMLAnchorElement;
|
||||
|
||||
private static $hiddenLink = CE<HTMLAnchorElement>('a', {
|
||||
target: '_blank',
|
||||
});
|
||||
|
||||
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.$taLink.dataset.xboxTitleId = xboxTitleId;
|
||||
TrueAchievements.$taLink.dataset.id = id;
|
||||
|
||||
TrueAchievements.$taLink.href = `https://www.trueachievements.com/deeplink/${xboxTitleId}/${id}`;
|
||||
$parent.appendChild(TrueAchievements.$taLink);
|
||||
}
|
||||
} catch (e) {};
|
||||
}
|
||||
|
||||
static open(override: boolean, xboxTitleId?: number | string, id?: number | string) {
|
||||
if (!xboxTitleId) {
|
||||
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) {
|
||||
url += `/deeplink/${xboxTitleId}/${id}`;
|
||||
} else {
|
||||
url += `/deeplink/${xboxTitleId}`;
|
||||
}
|
||||
}
|
||||
|
||||
TrueAchievements.$hiddenLink.href = url;
|
||||
TrueAchievements.$hiddenLink.click();
|
||||
}
|
||||
}
|
@ -110,3 +110,12 @@ export async function copyToClipboard(text: string, showToast=true): Promise<boo
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function productTitleToSlug(title: string): string {
|
||||
return title.replace(/[;,/?:@&=+_`~$%#^*()!^\u2122\xae\xa9]/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;
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user