Compare commits

...

14 Commits

Author SHA1 Message Date
redphx
ecd2d4af1d Bump version to 3.0.2 2024-01-08 17:22:37 +07:00
redphx
c0a14d59a1 Update translations 2024-01-08 16:58:44 +07:00
redphx
73b1142c1e Update styling of the Remote Play button 2024-01-08 16:56:57 +07:00
redphx
1b7fe7f7d0 Set UI_LOADING_SCREEN_WAIT_TIME default value to "true" 2024-01-08 08:54:54 +07:00
redphx
41da54a27c Add server name to the loading screen 2024-01-08 08:54:06 +07:00
redphx
649bb0452d Merge branch 'main' of https://github.com/redphx/better-xcloud 2024-01-08 08:53:28 +07:00
redphx
9c0949930f Add Phosphor Icons license 2024-01-08 08:06:23 +07:00
redphx
2ecb40e6ee Add a help button to the Remote Play dialog 2024-01-08 07:59:26 +07:00
redphx
3490ce47bb Update styling of the Remote Play button 2024-01-07 22:43:13 +07:00
redphx
bb912ae1b4 Replace Icon.BOOK with Icon.QUESTION 2024-01-07 22:34:05 +07:00
redphx
b3e10ce721 Update the help buttons in Stream Settings 2024-01-07 18:50:40 +07:00
redphx
7e6800b3ef In Settings, replace the "Remote Play" button with the "Help" button 2024-01-07 18:39:46 +07:00
redphx
32422e5a62 Catch exception in clearDbLogs() 2024-01-07 17:05:54 +07:00
redphx
75d4e6f65c Add a "Remote Play" button to the "Jump back in" list 2024-01-07 16:59:42 +07:00
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;
try {
const objectStore = db.transaction(table, 'readwrite').objectStore(table);
const objectStoreRequest = objectStore.clear();
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');