Compare commits

..

14 Commits

3 changed files with 177 additions and 70 deletions

View File

@ -1,6 +1,7 @@
MIT License
Copyright (c) 2023 redphx
Copyright (c) 2020 Phosphor Icons
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@ -19,3 +20,5 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---

View File

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

View File

@ -1,7 +1,7 @@
// ==UserScript==
// @name Better xCloud
// @namespace https://github.com/redphx
// @version 3.0.1
// @version 3.0.2
// @description Improve Xbox Cloud Gaming (xCloud) experience
// @author redphx
// @license MIT
@ -13,7 +13,7 @@
// ==/UserScript==
'use strict';
const SCRIPT_VERSION = '3.0.1';
const SCRIPT_VERSION = '3.0.2';
const SCRIPT_HOME = 'https://github.com/redphx/better-xcloud';
const ENABLE_XCLOUD_LOGGER = false;
@ -39,6 +39,14 @@ window.NATIVE_MKB_TITLES = [
console.log(`[Better xCloud] readyState: ${document.readyState}`);
const BxEvent = {
JUMP_BACK_IN_READY: 'bx-jump-back-in-ready',
POPSTATE: 'bx-popstate',
STREAM_STARTING: 'bx-stream-starting',
STREAM_STARTED: 'bx-stream-started',
STREAM_STOPPED: 'bx-stream-stopped',
};
// Quickly create a tree of elements without having to use innerHTML
function createElement(elmName, props = {}) {
@ -82,6 +90,7 @@ function createElement(elmName, props = {}) {
}
const CE = createElement;
window.BX_CE = CE;
const CTN = document.createTextNode.bind(document);
@ -103,7 +112,7 @@ const createSvgIcon = (icon, strokeWidth=2) => {
const createButton = options => {
const $btn = CE('button', {'class': 'bx-button'});
const $btn = CE(options.url ? 'a' : 'button', {'class': 'bx-button'});
options.isPrimary && $btn.classList.add('bx-primary');
options.isDanger && $btn.classList.add('bx-danger');
@ -113,6 +122,11 @@ const createButton = options => {
options.title && $btn.setAttribute('title', options.title);
options.onClick && $btn.addEventListener('click', options.onClick);
if (options.url) {
$btn.href = options.url;
$btn.target = '_blank';
}
return $btn;
}
@ -677,7 +691,7 @@ const Translations = {
"es-ES": "Contrapeso de la zona muerta",
"ja-JP": "デッドゾーンのカウンターウエイト",
"pt-BR": "Contador da Zona Morta",
"ru-RU": "Противовес мертвой зоны",
"ru-RU": "Противодействие мертвой зоне игры",
"tr-TR": "Ölü alan denge ağırlığı",
"uk-UA": "Противага Deadzone",
"vi-VN": "Đối trọng vùng chết",
@ -999,6 +1013,14 @@ const Translations = {
"vi-VN": "Đang lấy danh sách các console...",
"zh-CN": "正在获取控制台列表...",
},
"help": {
"de-DE": "Hilfe",
"en-US": "Help",
"ja-JP": "ヘルプ",
"pt-BR": "Ajuda",
"ru-RU": "Справка",
"vi-VN": "Trợ giúp",
},
"hide-idle-cursor": {
"de-DE": "Mauszeiger bei Inaktivität ausblenden",
"en-US": "Hide mouse cursor on idle",
@ -1145,21 +1167,6 @@ const Translations = {
"uk-UA": "Прив'язати мишу до",
"vi-VN": "Gán chuột với",
},
"max-bitrate": {
"de-DE": "Max. Bitrate",
"en-US": "Max bitrate",
"es-ES": "Tasa de bits máxima",
"it-IT": "Bitrate massimo",
"ja-JP": "最大ビットレート",
"ko-KR": "최대 비트레이트",
"pl-PL": "Maksymalny bitrate",
"pt-BR": "Taxa máxima dos bits",
"ru-RU": "Максимальный битрейт",
"tr-TR": "Maksimum bithızı",
"uk-UA": "Максимальний бітрейт",
"vi-VN": "Bitrate tối đa",
"zh-CN": "最大比特率",
},
"may-not-work-properly": {
"de-DE": "Funktioniert evtl. nicht fehlerfrei!",
"en-US": "May not work properly!",
@ -2119,6 +2126,7 @@ const Translations = {
"en-US": "Stick decay minimum",
"ja-JP": "スティックの減衰の最小値",
"pt-BR": "Mínimo decaimento do analógico",
"ru-RU": "Минимальная перезарядка стика",
"tr-TR": "Çubuğun ortalanma süresi minimumu",
"vi-VN": "Độ suy giảm tối thiểu của cần điều khiển",
},
@ -2127,6 +2135,7 @@ const Translations = {
"en-US": "Stick decay strength",
"ja-JP": "スティックの減衰の強さ",
"pt-BR": "Força de decaimento do analógico",
"ru-RU": "Скорость перезарядки стика",
"tr-TR": "Çubuğun ortalanma gücü",
"vi-VN": "Sức mạnh độ suy giảm của cần điều khiển",
},
@ -2717,7 +2726,9 @@ const Icon = {
COPY: '<path d="M1.498 6.772h23.73v23.73H1.498zm5.274-5.274h23.73v23.73"/>',
TRASH: '<path d="M29.5 6.182h-27m9.818 7.363v9.818m7.364-9.818v9.818"/><path d="M27.045 6.182V29.5c0 .673-.554 1.227-1.227 1.227H6.182c-.673 0-1.227-.554-1.227-1.227V6.182m17.181 0V3.727a2.47 2.47 0 0 0-2.455-2.455h-7.364a2.47 2.47 0 0 0-2.455 2.455v2.455"/>',
CURSOR_TEXT: '<path d="M16 7.3a5.83 5.83 0 0 1 5.8-5.8h2.9m0 29h-2.9a5.83 5.83 0 0 1-5.8-5.8"/><path d="M7.3 30.5h2.9a5.83 5.83 0 0 0 5.8-5.8V7.3a5.83 5.83 0 0 0-5.8-5.8H7.3"/><path d="M11.65 16h8.7"/>',
INFO: '<g transform="matrix(.153399 0 0 .153398 -3.63501 -3.635009)"><g fill="none" stroke="#fff" stroke-width="16"><circle cx="128" cy="128" r="96"/><path d="M120 120c4.389 0 8 3.611 8 8v40c0 4.389 3.611 8 8 8"/></g><circle cx="124" cy="84" r="12" stroke-width="6"/></g>',
QUESTION: '<g transform="matrix(.256867 0 0 .256867 -16.878964 -18.049342)"><circle cx="128" cy="180" r="12" fill="#fff"/><path d="M128 144v-8c17.67 0 32-12.54 32-28s-14.33-28-32-28-32 12.54-32 28v4" fill="none" stroke="#fff" stroke-width="16"/></g>',
REMOTE_PLAY: '<g transform="matrix(.492308 0 0 .581818 -14.7692 -11.6364)"><clipPath id="A"><path d="M30 20h65v55H30z"/></clipPath><g clip-path="url(#A)"><g transform="matrix(.395211 0 0 .334409 11.913 7.01124)"><g transform="matrix(.555556 0 0 .555556 57.8889 -20.2417)" fill="none" stroke="#fff" stroke-width="13.88"><path d="M200 140.564c-42.045-33.285-101.955-33.285-144 0M168 165c-23.783-17.3-56.217-17.3-80 0"/></g><g transform="matrix(-.555556 0 0 -.555556 200.111 262.393)"><g transform="matrix(1 0 0 1 0 11.5642)"><path d="M200 129c-17.342-13.728-37.723-21.795-58.636-24.198C111.574 101.378 80.703 109.444 56 129" fill="none" stroke="#fff" stroke-width="13.88"/></g><path d="M168 165c-23.783-17.3-56.217-17.3-80 0" fill="none" stroke="#fff" stroke-width="13.88"/></g><g transform="matrix(.75 0 0 .75 32 32)"><path d="M24 72h208v93.881H24z" fill="none" stroke="#fff" stroke-linejoin="miter" stroke-width="9.485"/><circle cx="188" cy="128" r="12" stroke-width="10" transform="matrix(.708333 0 0 .708333 71.8333 12.8333)"/><path d="M24.358 103.5h110" fill="none" stroke="#fff" stroke-linecap="butt" stroke-width="10.282"/></g></g></g></g>',
SCREENSHOT_B64: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDMyIDMyIiBmaWxsPSIjZmZmIj48cGF0aCBkPSJNMjguMzA4IDUuMDM4aC00LjI2NWwtMi4wOTctMy4xNDVhMS4yMyAxLjIzIDAgMCAwLTEuMDIzLS41NDhoLTkuODQ2YTEuMjMgMS4yMyAwIDAgMC0xLjAyMy41NDhMNy45NTYgNS4wMzhIMy42OTJBMy43MSAzLjcxIDAgMCAwIDAgOC43MzF2MTcuMjMxYTMuNzEgMy43MSAwIDAgMCAzLjY5MiAzLjY5MmgyNC42MTVBMy43MSAzLjcxIDAgMCAwIDMyIDI1Ljk2MlY4LjczMWEzLjcxIDMuNzEgMCAwIDAtMy42OTItMy42OTJ6bS02Ljc2OSAxMS42OTJjMCAzLjAzOS0yLjUgNS41MzgtNS41MzggNS41MzhzLTUuNTM4LTIuNS01LjUzOC01LjUzOCAyLjUtNS41MzggNS41MzgtNS41MzggNS41MzggMi41IDUuNTM4IDUuNTM4eiIvPjwvc3ZnPgo=',
};
@ -2731,6 +2742,7 @@ class Dialog {
content,
hideCloseButton,
onClose,
helpUrl,
} = options;
// Create dialog overlay
@ -2747,7 +2759,9 @@ class Dialog {
let $close;
this.onClose = onClose;
this.$dialog = CE('div', {'class': `bx-dialog ${className || ''} bx-gone`},
this.$title = CE('b', {}, title),
this.$title = CE('h2', {}, CE('b', {}, title),
helpUrl && createButton({icon: Icon.QUESTION, isGhost: true, title: __('help'), url: helpUrl}),
),
this.$content = CE('div', {'class': 'bx-dialog-content'}, content),
!hideCloseButton && ($close = CE('button', {}, __('close'))),
);
@ -2767,7 +2781,7 @@ class Dialog {
show(newOptions) {
if (newOptions && newOptions.title) {
this.$title.textContent = newOptions.title;
this.$title.querySelector('b').textContent = newOptions.title;
this.$title.classList.remove('bx-gone');
}
@ -2859,6 +2873,7 @@ class RemotePlay {
RemotePlay.#dialog = new Dialog({
title: __('remote-play'),
content: RemotePlay.#$content,
helpUrl: 'https://better-xcloud.github.io/remote-play/',
});
RemotePlay.#getXhomeToken(() => {
@ -3181,10 +3196,12 @@ class LoadingScreen {
let $waitTimeBox = LoadingScreen.#$waitTimeBox;
if (!$waitTimeBox) {
$waitTimeBox = CE('div', {'class': 'bx-wait-time-box'},
CE('label', {}, __('server')),
CE('span', {}, getPreferredServerRegion()),
CE('label', {}, __('wait-time-estimated')),
$estimated = CE('span', {'class': 'bx-wait-time-estimated'}),
$estimated = CE('span', {}),
CE('label', {}, __('wait-time-countdown')),
$countDown = CE('span', {'class': 'bx-wait-time-countdown'}),
$countDown = CE('span', {}),
);
document.documentElement.appendChild($waitTimeBox);
@ -4766,7 +4783,7 @@ class MkbRemapper {
// Update state of Activate button
const activated = PREFS.get(Preferences.MKB_DEFAULT_PRESET_ID) === this.#STATE.currentPresetId;
this.#$.activateButton.disabled = activated;
this.#$.activateButton.textContent = activated ? __('activated') : __('activate');
this.#$.activateButton.querySelector('span').textContent = activated ? __('activated') : __('activate');
}
#refresh() {
@ -4811,7 +4828,7 @@ class MkbRemapper {
// Update state of Activate button
const activated = defaultPresetId === this.#STATE.currentPresetId;
this.#$.activateButton.disabled = activated;
this.#$.activateButton.textContent = activated ? __('activated') : __('activate');
this.#$.activateButton.querySelector('span').textContent = activated ? __('activated') : __('activate');
!this.#STATE.isEditing && this.#switchPreset(this.#STATE.currentPresetId);
});
@ -6203,7 +6220,7 @@ class Preferences {
'default': true,
},
[Preferences.UI_LOADING_SCREEN_WAIT_TIME]: {
'default': false,
'default': true,
},
[Preferences.UI_LOADING_SCREEN_ROCKET]: {
'default': 'show',
@ -6609,6 +6626,57 @@ class Patcher {
return funcStr.replace(text, `connectMode:window.BX_REMOTE_PLAY_CONFIG?"xhome-connect":"cloud-connect",remotePlayServerId:(window.BX_REMOTE_PLAY_CONFIG&&window.BX_REMOTE_PLAY_CONFIG.serverId)||''`);
},
// Add a "Remote Play" button to the "Jump back in" list
remotePlayPatchHomeJumpBackIn: PREFS.get(Preferences.REMOTE_PLAY_ENABLED) && function(funcStr) {
const index = funcStr.indexOf('MruTitlesGamesRow-module__childContainer');
if (index === -1) {
return false;
}
const startIndex = funcStr.indexOf('return(', index);
const newCode = `
setTimeout(() => { window.dispatchEvent(new Event('${BxEvent.JUMP_BACK_IN_READY}')); }, 1000);
`;
// Add event listener
window.addEventListener(BxEvent.JUMP_BACK_IN_READY, e => {
const $list = document.querySelector('ol[class^=ItemRow-module__list]');
if (!$list) {
return;
}
if ($list.querySelector('.bx-jump-in-li')) {
return;
}
const $li = $list.firstElementChild.cloneNode(true);
$li.classList.add('bx-jump-in-li');
$li.addEventListener('click', e => {
RemotePlay.showDialog();
});
const $button = $li.querySelector('button');
$button.removeAttribute('id');
$button.removeAttribute('aria-labelledby');
$button.setAttribute('aria-label', __('remote-play'));
// Remove card's info
const $cardInfo = $button.querySelector('div[class^=BaseItem]');
$cardInfo.parentElement.removeChild($cardInfo);
const $images = $button.querySelector('div[class^=WrappedResponsiveImage]');
$images.classList.add('bx-remote-play-icon-wrapper');
$images.innerHTML = '';
$images.appendChild(createSvgIcon(Icon.REMOTE_PLAY), 2);
$list.insertBefore($li, $list.firstElementChild);
});
funcStr = funcStr.substring(0, startIndex) + newCode + funcStr.substring(startIndex);
return funcStr;
},
// Disable trackEvent() function
disableTrackEvent: PREFS.get(Preferences.BLOCK_TRACKING) && function(funcStr) {
const text = 'this.trackEvent=';
@ -6772,13 +6840,15 @@ if (window.BX_VIBRATION_INTENSITY && window.BX_VIBRATION_INTENSITY < 1) {
};
static #PATCH_ORDERS = [
['disableStreamGate'],
[
'disableAiTrack',
'disableTelemetry',
],
['disableStreamGate'],
['remotePlayPatchHomeJumpBackIn'],
['tvLayout'],
['enableXcloudLogger'],
@ -7035,7 +7105,6 @@ function addCss() {
--bx-danger-button-hover-color: #e61d1d;
--bx-danger-button-disabled-color: #a26c6c;
--bx-toast-z-index: 9999;
--bx-dialog-z-index: 9101;
--bx-dialog-overlay-z-index: 9100;
@ -7061,6 +7130,39 @@ div[class^=HUDButton-module__hiddenContainer] ~ div:not([class^=HUDButton-module
left: -9999px;
}
.bx-jump-in-li {
display: inline-block;
}
.bx-jump-in-li button {
width: 60px;
box-shadow: none !important;
}
.bx-jump-in-li button:not(:focus) .bx-remote-play-icon-wrapper {
background: transparent;
}
.bx-remote-play-icon-wrapper, .bx-jump-in-li button:hover .bx-remote-play-icon-wrapper {
background: #7b7b7b1a;
}
.bx-remote-play-icon-wrapper svg {
padding: 10px;
height: 100%;
filter: drop-shadow(0px 1px 1px rgba(0, 0, 0, 0.5));
opacity: 0.7;
}
.bx-jump-in-li button:hover .bx-remote-play-icon-wrapper svg, .bx-jump-in-li button:focus .bx-remote-play-icon-wrapper svg {
opacity: 1;
}
a.bx-button {
display: inline-block;
}
.bx-button {
background-color: var(--bx-default-button-color);
user-select: none;
@ -7130,9 +7232,10 @@ div[class^=HUDButton-module__hiddenContainer] ~ div:not([class^=HUDButton-module
.bx-button span {
display: inline-block;
height: 32px;
height: 30px;
line-height: 32px;
vertical-align: middle;
color: #fff;
}
.bx-settings-button {
@ -7561,17 +7664,22 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
outline: none !important;
}
.bx-dialog > b {
.bx-dialog h2 {
display: flex;
margin-bottom: 12px;
}
.bx-dialog h2 b {
flex: 1;
color: #fff;
display: block;
font-family: var(--bx-title-font);
font-size: 26px;
font-weight: 400;
line-height: 32px;
margin-bottom: 12px;
}
.bx-dialog.bx-binding-dialog > b {
.bx-dialog.bx-binding-dialog h2 b {
font-family: var(--bx-promptfont-font) !important;
}
@ -7722,20 +7830,23 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
}
.bx-quick-settings-tab-contents h2 {
margin-bottom: 8px;
display: flex;
align-item: center;
}
.bx-quick-settings-tab-contents h2 span {
display: inline-block;
font-size: 28px;
font-weight: bold;
margin-bottom: 8px;
text-transform: uppercase;
text-align: left;
display: flex;
flex: 1;
height: 32px;
line-height: 32px;
}
.bx-quick-settings-tab-contents h2 a {
display: flex;
width: 16px;
height: 16px;
margin-left: 8px;
align-selft: flex-start;
}
.bx-quick-settings-tab-contents input[type="range"] {
@ -8034,15 +8145,16 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
margin: 0;
}
.bx-wait-time-estimated, .bx-wait-time-countdown {
.bx-wait-time-box span {
display: block;
font-family: var(--bx-monospaced-font);
text-align: right;
font-size: 16px;
margin-bottom: 10px;
}
.bx-wait-time-estimated {
margin-bottom: 10px;
.bx-wait-time-box span:last-of-type {
margin-bottom: 0;
}
/* REMOTE PLAY */
@ -8348,12 +8460,14 @@ function clearDbLogs(dbName, table) {
request.onsuccess = e => {
const db = e.target.result;
const objectStore = db.transaction(table, 'readwrite').objectStore(table);
const objectStoreRequest = objectStore.clear();
try {
const objectStore = db.transaction(table, 'readwrite').objectStore(table);
const objectStoreRequest = objectStore.clear();
objectStoreRequest.onsuccess = function(event) {
console.log(`[Better xCloud] Cleared ${dbName}.${table}`);
};
objectStoreRequest.onsuccess = function(event) {
console.log(`[Better xCloud] Cleared ${dbName}.${table}`);
};
} catch (e) {}
}
}
@ -8786,7 +8900,6 @@ function injectSettingsButton($parent) {
const PREF_PREFERRED_REGION = getPreferredServerRegion();
const PREF_LATEST_VERSION = PREFS.get(Preferences.LATEST_VERSION);
const PREF_REMOTE_PLAY_ENABLED = PREFS.get(Preferences.REMOTE_PLAY_ENABLED);
// Setup Settings button
const $button = CE('button', {'class': 'bx-settings-button'}, PREF_PREFERRED_REGION);
@ -8810,7 +8923,7 @@ function injectSettingsButton($parent) {
});
let $updateAvailable;
let $remotePlayBtn;
const $wrapper = CE('div', {'class': 'bx-settings-wrapper'},
CE('div', {'class': 'bx-settings-title-wrapper'},
CE('a', {
@ -8818,7 +8931,7 @@ function injectSettingsButton($parent) {
'href': SCRIPT_HOME,
'target': '_blank',
}, 'Better xCloud ' + SCRIPT_VERSION),
$remotePlayBtn = CE('button', {'class': 'bx-primary-button bx-no-margin'}, __('remote-play')),
createButton({icon: Icon.QUESTION, label: __('help'), url: 'https://better-xcloud.github.io/features/'}),
)
);
$updateAvailable = CE('a', {
@ -8827,17 +8940,6 @@ function injectSettingsButton($parent) {
'target': '_blank',
});
if (PREF_REMOTE_PLAY_ENABLED) {
$remotePlayBtn.addEventListener('click', e => {
RemotePlay.showDialog();
// Hide Settings
$container.classList.add('bx-gone');
});
} else {
$remotePlayBtn.classList.add('bx-gone');
}
$wrapper.appendChild($updateAvailable);
// Show new version indicator
@ -9682,8 +9784,8 @@ function setupQuickSettingsBar() {
for (const settingGroup of settingTab.items) {
$group.appendChild(CE('h2', {},
settingGroup.label,
settingGroup.help_url && CE('a', {href: settingGroup.help_url, target: '_blank'}, createSvgIcon(Icon.INFO, 4)),
CE('span', {}, settingGroup.label),
settingGroup.help_url && createButton({icon: Icon.QUESTION, isGhost: true, url: settingGroup.help_url, title: __('help')}),
));
if (settingGroup.note) {
if (typeof settingGroup.note === 'string') {
@ -9806,9 +9908,10 @@ function setupScreenshotButton() {
function patchHistoryMethod(type) {
var orig = window.history[type];
const orig = window.history[type];
return function(...args) {
const event = new Event('xcloud_popstate');
const event = new Event(BxEvent.POPSTATE);
event.arguments = args;
window.dispatchEvent(event);
@ -10025,9 +10128,10 @@ function setupBxUi() {
// Hide Settings UI when navigate to another page
window.addEventListener('xcloud_popstate', onHistoryChanged);
window.addEventListener(BxEvent.POPSTATE, onHistoryChanged);
window.addEventListener('popstate', onHistoryChanged);
// Make pushState/replaceState methods dispatch "xcloud_popstate" event
// Make pushState/replaceState methods dispatch BxEvent.POPSTATE event
window.history.pushState = patchHistoryMethod('pushState');
window.history.replaceState = patchHistoryMethod('replaceState');