Compare commits

...

10 Commits

15 changed files with 335 additions and 53 deletions

View File

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

File diff suppressed because one or more lines are too long

View File

@ -1,8 +1,54 @@
.bx-guide-home-achievements-progress {
display: flex;
gap: 10px;
flex-direction: row;
.bx-button {
margin-bottom: 0 !important;
}
html[data-xds-platform=tv] & {
flex-direction: column;
}
html:not([data-xds-platform=tv]) & {
flex-direction: row;
> button:first-of-type {
flex: 1;
}
> button:last-of-type {
width: 40px;
span {
display: none;
}
}
}
}
.bx-guide-home-buttons { .bx-guide-home-buttons {
> div { > div {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: 12px; gap: 12px;
html[data-xds-platform=tv] & {
flex-direction: column;
button {
margin-bottom: 0 !important;
}
}
html:not([data-xds-platform=tv]) & {
button {
span {
display: none;
}
}
}
} }
&[data-is-playing="true"] { &[data-is-playing="true"] {

View File

@ -0,0 +1,3 @@
<svg xmlns='http://www.w3.org/2000/svg' fill='#fff' stroke='none' fill-rule='evenodd' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 32 32'>
<path d='M5.462 3.4c-.205-.23-.499-.363-.808-.363-.592 0-1.079.488-1.079 1.08a1.08 1.08 0 0 0 .289.736l4.247 4.672H2.504a2.17 2.17 0 0 0-2.16 2.16v8.637a2.17 2.17 0 0 0 2.16 2.16h6.107l9.426 7.33a1.08 1.08 0 0 0 .662.227c.592 0 1.08-.487 1.08-1.079v-6.601l5.679 6.247a1.08 1.08 0 0 0 .808.363c.592 0 1.08-.487 1.08-1.079a1.08 1.08 0 0 0-.29-.736L5.462 3.4zm-2.958 8.285h5.398v8.637H2.504v-8.637zM17.62 26.752l-7.558-5.878V11.67l7.558 8.313v6.769zm5.668-8.607c1.072-1.218 1.072-3.063 0-4.281a1.08 1.08 0 0 1-.293-.74c0-.592.487-1.079 1.079-1.079a1.08 1.08 0 0 1 .834.393 5.42 5.42 0 0 1 0 7.137 1.08 1.08 0 0 1-.81.365c-.593 0-1.08-.488-1.08-1.08 0-.263.096-.517.27-.715zM12.469 7.888c-.147-.19-.228-.423-.228-.663a1.08 1.08 0 0 1 .417-.853l5.379-4.184a1.08 1.08 0 0 1 .662-.227c.593 0 1.08.488 1.08 1.08v10.105c0 .593-.487 1.08-1.08 1.08s-1.079-.487-1.079-1.08V5.255l-3.636 2.834c-.469.362-1.153.273-1.515-.196v-.005zm19.187 8.115a10.79 10.79 0 0 1-2.749 7.199 1.08 1.08 0 0 1-.793.347c-.593 0-1.08-.487-1.08-1.079 0-.26.094-.511.264-.708 2.918-3.262 2.918-8.253 0-11.516-.184-.2-.287-.461-.287-.733 0-.592.487-1.08 1.08-1.08a1.08 1.08 0 0 1 .816.373 10.78 10.78 0 0 1 2.749 7.197z' fill-rule='nonzero'/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -281,13 +281,10 @@ window.addEventListener(BxEvent.DATA_CHANNEL_CREATED, e => {
}); });
function unload() { function unload() {
if (!STATES.isPlaying && !Object.keys(STATES.currentStream).length) { if (!STATES.isPlaying) {
return; return;
} }
STATES.isPlaying = false;
STATES.currentStream = {};
// Stop MKB listeners // Stop MKB listeners
EmulatedMkbHandler.getInstance().destroy(); EmulatedMkbHandler.getInstance().destroy();
NativeMkbHandler.getInstance().destroy(); NativeMkbHandler.getInstance().destroy();
@ -295,6 +292,8 @@ 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;

View File

@ -1,7 +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 { createButton, ButtonStyle, CE } from "@utils/html"; import { createButton, ButtonStyle, CE } from "@utils/html";
import { t } from "@utils/translation";
import { BaseGameBarAction } from "./action-base"; import { BaseGameBarAction } from "./action-base";
import { MicrophoneShortcut, MicrophoneState } from "../shortcuts/shortcut-microphone"; import { MicrophoneShortcut, MicrophoneState } from "../shortcuts/shortcut-microphone";
@ -24,7 +23,6 @@ export class MicrophoneAction extends BaseGameBarAction {
const $btnDefault = createButton({ const $btnDefault = createButton({
style: ButtonStyle.GHOST, style: ButtonStyle.GHOST,
icon: BxIcon.MICROPHONE, icon: BxIcon.MICROPHONE,
title: t('show-touch-controller'),
onClick: onClick, onClick: onClick,
classes: ['bx-activated'], classes: ['bx-activated'],
}); });
@ -32,7 +30,6 @@ export class MicrophoneAction extends BaseGameBarAction {
const $btnMuted = createButton({ const $btnMuted = createButton({
style: ButtonStyle.GHOST, style: ButtonStyle.GHOST,
icon: BxIcon.MICROPHONE_MUTED, icon: BxIcon.MICROPHONE_MUTED,
title: t('hide-touch-controller'),
onClick: onClick, onClick: onClick,
}); });

View File

@ -0,0 +1,54 @@
import { BxEvent } from "@utils/bx-event";
import { BxIcon } from "@utils/bx-icon";
import { createButton, ButtonStyle, CE } from "@utils/html";
import { BaseGameBarAction } from "./action-base";
import { SoundShortcut, SpeakerState } from "../shortcuts/shortcut-sound";
export class SpeakerAction extends BaseGameBarAction {
$content: HTMLElement;
constructor() {
super();
const onClick = (e: Event) => {
BxEvent.dispatch(window, BxEvent.GAME_BAR_ACTION_ACTIVATED);
SoundShortcut.muteUnmute();
};
const $btnEnable = createButton({
style: ButtonStyle.GHOST,
icon: BxIcon.AUDIO,
onClick: onClick,
});
const $btnMuted = createButton({
style: ButtonStyle.GHOST,
icon: BxIcon.SPEAKER_MUTED,
onClick: onClick,
classes: ['bx-activated'],
});
this.$content = CE('div', {},
$btnEnable,
$btnMuted,
);
this.reset();
window.addEventListener(BxEvent.SPEAKER_STATE_CHANGED, e => {
const speakerState = (e as any).speakerState;
const enabled = speakerState === SpeakerState.ENABLED;
this.$content.dataset.enabled = enabled.toString();
});
}
render(): HTMLElement {
return this.$content;
}
reset(): void {
this.$content.dataset.enabled = 'true';
}
}

View File

@ -26,7 +26,6 @@ export class TouchControlAction extends BaseGameBarAction {
icon: BxIcon.TOUCH_CONTROL_ENABLE, icon: BxIcon.TOUCH_CONTROL_ENABLE,
title: t('show-touch-controller'), title: t('show-touch-controller'),
onClick: onClick, onClick: onClick,
classes: ['bx-activated'],
}); });
const $btnDisable = createButton({ const $btnDisable = createButton({
@ -34,6 +33,7 @@ export class TouchControlAction extends BaseGameBarAction {
icon: BxIcon.TOUCH_CONTROL_DISABLE, icon: BxIcon.TOUCH_CONTROL_DISABLE,
title: t('hide-touch-controller'), title: t('hide-touch-controller'),
onClick: onClick, onClick: onClick,
classes: ['bx-activated'],
}); });
this.$content = CE('div', {}, this.$content = CE('div', {},

View File

@ -1,4 +1,4 @@
import { CE, createSvgIcon } from "@utils/html"; import { CE, clearFocus, createSvgIcon } from "@utils/html";
import { ScreenshotAction } from "./action-screenshot"; import { ScreenshotAction } from "./action-screenshot";
import { TouchControlAction } from "./action-touch-control"; import { TouchControlAction } from "./action-touch-control";
import { BxEvent } from "@utils/bx-event"; import { BxEvent } from "@utils/bx-event";
@ -9,6 +9,7 @@ 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"; import { TrueAchievementsAction } from "./action-true-achievements";
import { SpeakerAction } from "./action-speaker";
export class GameBar { export class GameBar {
@ -43,8 +44,9 @@ 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 SpeakerAction(),
new MicrophoneAction(), new MicrophoneAction(),
new TrueAchievementsAction(),
]; ];
// Reverse the action list if Game Bar's position is on the right side // Reverse the action list if Game Bar's position is on the right side
@ -133,6 +135,9 @@ export class GameBar {
} }
hideBar() { hideBar() {
// Stop focusing Game Bar
clearFocus();
if (!this.$container) { if (!this.$container) {
return; return;
} }

View File

@ -4,6 +4,12 @@ import { Toast } from "@utils/toast";
import { ceilToNearest, floorToNearest } from "@/utils/utils"; import { ceilToNearest, floorToNearest } from "@/utils/utils";
import { PrefKey } from "@/enums/pref-keys"; import { PrefKey } from "@/enums/pref-keys";
import { getPref, setPref } from "@/utils/settings-storages/global-settings-storage"; import { getPref, setPref } from "@/utils/settings-storages/global-settings-storage";
import { BxEvent } from "@/utils/bx-event";
export enum SpeakerState {
ENABLED,
MUTED,
}
export class SoundShortcut { export class SoundShortcut {
static adjustGainNodeVolume(amount: number): number { static adjustGainNodeVolume(amount: number): number {
@ -64,6 +70,10 @@ export class SoundShortcut {
SoundShortcut.setGainNodeVolume(targetValue); SoundShortcut.setGainNodeVolume(targetValue);
Toast.show(`${t('stream')} ${t('volume')}`, status, {instant: true}); Toast.show(`${t('stream')} ${t('volume')}`, status, {instant: true});
BxEvent.dispatch(window, BxEvent.SPEAKER_STATE_CHANGED, {
speakerState: targetValue === 0 ? SpeakerState.MUTED : SpeakerState.ENABLED,
})
return; return;
} }
@ -79,6 +89,10 @@ export class SoundShortcut {
const status = $media.muted ? t('muted') : t('unmuted'); const status = $media.muted ? t('muted') : t('unmuted');
Toast.show(`${t('stream')} ${t('volume')}`, status, {instant: true}); Toast.show(`${t('stream')} ${t('volume')}`, status, {instant: true});
BxEvent.dispatch(window, BxEvent.SPEAKER_STATE_CHANGED, {
speakerState: $media.muted ? SpeakerState.MUTED : SpeakerState.ENABLED,
})
} }
} }
} }

View File

@ -28,6 +28,7 @@ export class GuideMenu {
closeApp: AppInterface && createButton({ closeApp: AppInterface && createButton({
icon: BxIcon.POWER, icon: BxIcon.POWER,
label: t('close-app'),
title: t('close-app'), title: t('close-app'),
style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE | ButtonStyle.DANGER, style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE | ButtonStyle.DANGER,
onClick: e => { onClick: e => {
@ -41,6 +42,7 @@ export class GuideMenu {
reloadPage: createButton({ reloadPage: createButton({
icon: BxIcon.REFRESH, icon: BxIcon.REFRESH,
label: t('reload-page'),
title: t('reload-page'), title: t('reload-page'),
style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE, style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE,
onClick: e => { onClick: e => {
@ -57,6 +59,7 @@ export class GuideMenu {
backToHome: createButton({ backToHome: createButton({
icon: BxIcon.HOME, icon: BxIcon.HOME,
label: t('back-to-home'),
title: t('back-to-home'), title: t('back-to-home'),
style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE, style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE,
onClick: e => { onClick: e => {
@ -85,7 +88,6 @@ export class GuideMenu {
const buttons = [ const buttons = [
GuideMenu.#BUTTONS.scriptSettings, GuideMenu.#BUTTONS.scriptSettings,
[ [
TrueAchievements.$button,
GuideMenu.#BUTTONS.backToHome, GuideMenu.#BUTTONS.backToHome,
GuideMenu.#BUTTONS.reloadPage, GuideMenu.#BUTTONS.reloadPage,
GuideMenu.#BUTTONS.closeApp, GuideMenu.#BUTTONS.closeApp,
@ -113,6 +115,11 @@ export class GuideMenu {
} }
static #injectHome($root: HTMLElement, isPlaying = false) { static #injectHome($root: HTMLElement, isPlaying = false) {
const $achievementsProgress = $root.querySelector('button[class*=AchievementsButton-module__progressBarContainer]');
if ($achievementsProgress) {
TrueAchievements.injectAchievementsProgress($achievementsProgress as HTMLElement);
}
// Find the element to add buttons to // Find the element to add buttons to
let $target: HTMLElement | null = null; let $target: HTMLElement | null = null;
if (isPlaying) { if (isPlaying) {
@ -154,6 +161,12 @@ export class GuideMenu {
static observe($addedElm: HTMLElement) { static observe($addedElm: HTMLElement) {
const className = $addedElm.className; const className = $addedElm.className;
if (className.includes('AchievementsButton-module__progressBarContainer')) {
TrueAchievements.injectAchievementsProgress($addedElm);
return;
}
if (!className.startsWith('NavigationAnimation') && if (!className.startsWith('NavigationAnimation') &&
!className.startsWith('DialogRoutes') && !className.startsWith('DialogRoutes') &&
!className.startsWith('Dialog-module__container')) { !className.startsWith('Dialog-module__container')) {

View File

@ -37,6 +37,7 @@ export namespace BxEvent {
export const GAME_BAR_ACTION_ACTIVATED = 'bx-game-bar-action-activated'; export const GAME_BAR_ACTION_ACTIVATED = 'bx-game-bar-action-activated';
export const MICROPHONE_STATE_CHANGED = 'bx-microphone-state-changed'; export const MICROPHONE_STATE_CHANGED = 'bx-microphone-state-changed';
export const SPEAKER_STATE_CHANGED = 'bx-speaker-state-changed';
export const CAPTURE_SCREENSHOT = 'bx-capture-screenshot'; export const CAPTURE_SCREENSHOT = 'bx-capture-screenshot';

View File

@ -14,6 +14,7 @@ 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" };
import iconSpeakerSlash from "@assets/svg/speaker-slash.svg" with { type: "text" };
import iconStreamSettings from "@assets/svg/stream-settings.svg" with { type: "text" }; import iconStreamSettings from "@assets/svg/stream-settings.svg" with { type: "text" };
import iconStreamStats from "@assets/svg/stream-stats.svg" with { type: "text" }; import iconStreamStats from "@assets/svg/stream-stats.svg" with { type: "text" };
import iconTouchControlDisable from "@assets/svg/touch-control-disable.svg" with { type: "text" }; import iconTouchControlDisable from "@assets/svg/touch-control-disable.svg" with { type: "text" };
@ -64,6 +65,7 @@ export const BxIcon = {
CARET_LEFT: iconCaretLeft, CARET_LEFT: iconCaretLeft,
CARET_RIGHT: iconCaretRight, CARET_RIGHT: iconCaretRight,
SCREENSHOT: iconCamera, SCREENSHOT: iconCamera,
SPEAKER_MUTED: iconSpeakerSlash,
TOUCH_CONTROL_ENABLE: iconTouchControlEnable, TOUCH_CONTROL_ENABLE: iconTouchControlEnable,
TOUCH_CONTROL_DISABLE: iconTouchControlDisable, TOUCH_CONTROL_DISABLE: iconTouchControlDisable,

View File

@ -174,3 +174,16 @@ export function removeChildElements($parent: HTMLElement) {
$parent.firstElementChild.remove(); $parent.firstElementChild.remove();
} }
} }
export function clearFocus() {
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
}
export function clearDataSet($elm: HTMLElement) {
Object.keys($elm.dataset).forEach(key => {
delete $elm.dataset[key];
});
}

View File

@ -1,6 +1,6 @@
import { BxIcon } from "./bx-icon"; import { BxIcon } from "./bx-icon";
import { AppInterface, STATES } from "./global"; import { AppInterface, STATES } from "./global";
import { ButtonStyle, CE, createButton, getReactProps } from "./html"; import { ButtonStyle, CE, clearDataSet, createButton, getReactProps } from "./html";
import { t } from "./translation"; import { t } from "./translation";
export class TrueAchievements { export class TrueAchievements {
@ -13,9 +13,10 @@ export class TrueAchievements {
}) as HTMLAnchorElement; }) as HTMLAnchorElement;
static $button = createButton({ static $button = createButton({
label: t('true-achievements'),
title: t('true-achievements'), title: t('true-achievements'),
icon: BxIcon.TRUE_ACHIEVEMENTS, icon: BxIcon.TRUE_ACHIEVEMENTS,
style: ButtonStyle.FOCUSABLE | ButtonStyle.FULL_WIDTH, style: ButtonStyle.FOCUSABLE,
onClick: TrueAchievements.onClick, onClick: TrueAchievements.onClick,
}) as HTMLAnchorElement; }) as HTMLAnchorElement;
@ -24,18 +25,67 @@ export class TrueAchievements {
const dataset = TrueAchievements.$link.dataset; const dataset = TrueAchievements.$link.dataset;
TrueAchievements.open(true, dataset.xboxTitleId, dataset.id); TrueAchievements.open(true, dataset.xboxTitleId, dataset.id);
// Close all xCloud's dialogs
window.BX_EXPOSED.dialogRoutes.closeAll();
} }
private static $hiddenLink = CE<HTMLAnchorElement>('a', { private static $hiddenLink = CE<HTMLAnchorElement>('a', {
target: '_blank', target: '_blank',
}); });
private static updateLinks(xboxTitleId?: string, id?: string) { private static updateIds(xboxTitleId?: string, id?: string) {
TrueAchievements.$link.dataset.xboxTitleId = xboxTitleId; const $link = TrueAchievements.$link;
TrueAchievements.$link.dataset.id = id; const $button = TrueAchievements.$button;
TrueAchievements.$button.dataset.xboxTitleId = xboxTitleId; clearDataSet($link);
TrueAchievements.$button.dataset.id = id; clearDataSet($button);
if (xboxTitleId) {
$link.dataset.xboxTitleId = xboxTitleId;
$button.dataset.xboxTitleId = xboxTitleId;
}
if (id) {
$link.dataset.id = id;
$button.dataset.id = id;
}
}
static injectAchievementsProgress($elm: HTMLElement) {
const $parent = $elm.parentElement!;
// Wrap xCloud's element with our own
const $div = CE('div', {
class: 'bx-guide-home-achievements-progress',
}, $elm);
// Get xboxTitleId of the game
let xboxTitleId: string | number | undefined;
try {
const $container = $parent.closest('div[class*=AchievementsPreview-module__container]') as HTMLElement;
if ($container) {
const props = getReactProps($container);
xboxTitleId = props.children.props.data.data.xboxTitleId;
}
} catch (e) {}
if (!xboxTitleId) {
xboxTitleId = TrueAchievements.getStreamXboxTitleId();
}
if (typeof xboxTitleId !== 'undefined') {
xboxTitleId = xboxTitleId.toString();
}
TrueAchievements.updateIds(xboxTitleId);
if (document.documentElement.dataset.xdsPlatform === 'tv') {
$div.appendChild(TrueAchievements.$link);
} else {
$div.appendChild(TrueAchievements.$button);
}
$parent.appendChild($div);
} }
static injectAchievementDetailPage($parent: HTMLElement) { static injectAchievementDetailPage($parent: HTMLElement) {
@ -65,15 +115,19 @@ export class TrueAchievements {
// Found achievement -> add TrueAchievements button // Found achievement -> add TrueAchievements button
if (id) { if (id) {
TrueAchievements.updateLinks(xboxTitleId, id); TrueAchievements.updateIds(xboxTitleId, id);
$parent.appendChild(TrueAchievements.$link); $parent.appendChild(TrueAchievements.$link);
} }
} catch (e) {}; } catch (e) {};
} }
private static getStreamXboxTitleId() : number | undefined {
return STATES.currentStream.xboxTitleId || STATES.currentStream.titleInfo?.details.xboxTitleId;
}
static open(override: boolean, xboxTitleId?: number | string, id?: number | string) { static open(override: boolean, xboxTitleId?: number | string, id?: number | string) {
if (!xboxTitleId || xboxTitleId === 'undefined') { if (!xboxTitleId || xboxTitleId === 'undefined') {
xboxTitleId = STATES.currentStream.xboxTitleId || STATES.currentStream.titleInfo?.details.xboxTitleId; xboxTitleId = TrueAchievements.getStreamXboxTitleId();
} }
if (AppInterface && AppInterface.openTrueAchievementsLink) { if (AppInterface && AppInterface.openTrueAchievementsLink) {
@ -83,10 +137,10 @@ export class TrueAchievements {
let url = 'https://www.trueachievements.com'; let url = 'https://www.trueachievements.com';
if (xboxTitleId) { if (xboxTitleId) {
if (id && id !== 'undefined') { url += `/deeplink/${xboxTitleId}`;
url += `/deeplink/${xboxTitleId}/${id}`;
} else { if (id) {
url += `/deeplink/${xboxTitleId}`; url += `/${id}`;
} }
} }