Compare commits

..

11 Commits

Author SHA1 Message Date
4f3430c43c Bump version to 5.9.3 2024-10-29 20:35:15 +07:00
15c6d3c74b Update dists 2024-10-29 20:24:04 +07:00
b170b95145 Fix bugs in NumberStepper 2024-10-29 20:20:34 +07:00
4217b89194 Reduce the amount of event listeners in NumberStepper 2024-10-29 20:11:54 +07:00
38211168e9 Reduce width of controller-friendly select box if it has <optgroup> 2024-10-29 16:56:34 +07:00
392dc2cf86 Categorize servers by continents 2024-10-29 16:51:29 +07:00
67de264aa9 Revert "Use gl.texSubImage2D()"
This reverts commit 3e2c1bb2a4.
2024-10-27 10:20:11 +07:00
3e2c1bb2a4 Use gl.texSubImage2D() 2024-10-27 09:36:34 +07:00
5653914d19 Move WebGL2's drawFrame() function to animate() function 2024-10-26 21:53:03 +07:00
4a8f66f2a1 Upgrade bun 2024-10-25 08:59:05 +07:00
70f43ba8f2 Add emoji flag for SwedenCentral server 2024-10-25 07:33:35 +07:00
13 changed files with 412 additions and 258 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -1,7 +1,7 @@
// ==UserScript==
// @name Better xCloud (Lite)
// @namespace https://github.com/redphx
// @version 5.9.2
// @version 5.9.3
// @description Improve Xbox Cloud Gaming (xCloud) experience
// @author redphx
// @license MIT
@ -105,7 +105,7 @@ class UserAgent {
});
}
}
var SCRIPT_VERSION = "5.9.2", SCRIPT_VARIANT = "lite", AppInterface = window.AppInterface;
var SCRIPT_VERSION = "5.9.3", SCRIPT_VARIANT = "lite", AppInterface = window.AppInterface;
UserAgent.init();
var userAgent = window.navigator.userAgent.toLowerCase(), isTv = userAgent.includes("smart-tv") || userAgent.includes("smarttv") || /\baft.*\b/.test(userAgent), isVr = window.navigator.userAgent.includes("VR") && window.navigator.userAgent.includes("OculusBrowser"), browserHasTouchSupport = "ontouchstart" in window || navigator.maxTouchPoints > 0, userAgentHasTouchSupport = !isTv && !isVr && browserHasTouchSupport, supportMkb = AppInterface || !userAgent.match(/(android|iphone|ipad)/), STATES = {
supportedRegion: !0,
@ -300,6 +300,11 @@ var SUPPORTED_LANGUAGES = {
"confirm-reload-stream": "Do you want to refresh the stream?",
connected: "Connected",
"console-connect": "Connect",
"continent-asia": "Asia",
"continent-australia": "Australia",
"continent-europe": "Europe",
"continent-north-america": "North America",
"continent-south-america": "South America",
contrast: "Contrast",
controller: "Controller",
"controller-friendly-ui": "Controller-friendly UI",
@ -752,14 +757,16 @@ class SettingElement {
}
static #renderNumberStepper(key, setting, value, onChange, options = {}) {
options = options || {}, options.suffix = options.suffix || "", options.disabled = !!options.disabled, options.hideSlider = !!options.hideSlider;
let $text, $btnDec, $btnInc, $range = null, controlValue = value, MIN = options.reverse ? -setting.max : setting.min, MAX = options.reverse ? -setting.min : setting.max, STEPS = Math.max(setting.steps || 1, 1), renderTextValue = (value2) => {
let $text, $btnDec, $btnInc, $range = null, controlValue = value, MIN = options.reverse ? -setting.max : setting.min, MAX = options.reverse ? -setting.min : setting.max, STEPS = Math.max(setting.steps || 1, 1), intervalId, isHolding = !1, clearIntervalId = () => {
intervalId && clearInterval(intervalId), intervalId = null;
}, renderTextValue = (value2) => {
value2 = parseInt(value2);
let textContent = null;
if (options.customTextValue) textContent = options.customTextValue(value2);
if (textContent === null) textContent = value2.toString() + options.suffix;
return textContent;
}, updateButtonsVisibility = () => {
$btnDec.classList.toggle("bx-inactive", controlValue === MIN), $btnInc.classList.toggle("bx-inactive", controlValue === MAX);
if ($btnDec.classList.toggle("bx-inactive", controlValue === MIN), $btnInc.classList.toggle("bx-inactive", controlValue === MAX), controlValue === MIN || controlValue === MAX) clearIntervalId();
}, $wrapper = CE("div", { class: "bx-number-stepper", id: `bx_setting_${key}` }, $btnDec = CE("button", {
"data-type": "dec",
type: "button",
@ -773,7 +780,7 @@ class SettingElement {
}, "+"));
if (options.disabled) return $btnInc.disabled = !0, $btnInc.classList.add("bx-inactive"), $btnDec.disabled = !0, $btnDec.classList.add("bx-inactive"), $wrapper.disabled = !0, $wrapper;
if ($range = CE("input", {
id: `bx_setting_${key}`,
id: `bx_inp_setting_${key}`,
type: "range",
min: MIN,
max: MAX,
@ -800,30 +807,29 @@ class SettingElement {
$wrapper.appendChild($markers);
}
updateButtonsVisibility();
let interval, isHolding = !1, onClick = (e) => {
if (isHolding) {
e.preventDefault(), isHolding = !1;
return;
}
let $btn = e.target, value2 = parseInt(controlValue);
let buttonPressed = (e, $btn) => {
let value2 = parseInt(controlValue);
if ($btn.dataset.type === "dec") value2 = Math.max(MIN, value2 - STEPS);
else value2 = Math.min(MAX, value2 + STEPS);
controlValue = value2, updateButtonsVisibility(), $text.textContent = renderTextValue(value2), $range && ($range.value = value2.toString()), isHolding = !1, !e.ignoreOnChange && onChange && onChange(e, value2);
}, onMouseDown = (e) => {
e.preventDefault(), isHolding = !0;
let args = arguments;
interval && clearInterval(interval), interval = window.setInterval(() => {
e.target && BxEvent.dispatch(e.target, "click", {
arguments: args
});
}, 200);
}, onMouseUp = (e) => {
e.preventDefault(), interval && clearInterval(interval), isHolding = !1;
controlValue = value2, updateButtonsVisibility(), $text.textContent = renderTextValue(value2), $range && ($range.value = value2.toString()), onChange && onChange(e, value2);
}, onClick = (e) => {
if (e.preventDefault(), isHolding) return;
let $btn = e.target.closest("button");
$btn && buttonPressed(e, $btn), clearIntervalId(), isHolding = !1;
}, onPointerDown = (e) => {
clearIntervalId();
let $btn = e.target.closest("button");
if (!$btn) return;
isHolding = !0, e.preventDefault(), intervalId = window.setInterval((e2) => {
buttonPressed(e2, $btn);
}, 200), window.addEventListener("pointerup", onPointerUp, { once: !0 }), window.addEventListener("pointercancel", onPointerUp, { once: !0 });
}, onPointerUp = (e) => {
clearIntervalId(), isHolding = !1;
}, onContextMenu = (e) => e.preventDefault();
return $wrapper.setValue = (value2) => {
$text.textContent = renderTextValue(value2), $range.value = options.reverse ? -value2 : value2;
}, $btnDec.addEventListener("click", onClick), $btnDec.addEventListener("pointerdown", onMouseDown), $btnDec.addEventListener("pointerup", onMouseUp), $btnDec.addEventListener("contextmenu", onContextMenu), $btnInc.addEventListener("click", onClick), $btnInc.addEventListener("pointerdown", onMouseDown), $btnInc.addEventListener("pointerup", onMouseUp), $btnInc.addEventListener("contextmenu", onContextMenu), setNearby($wrapper, {
focus: $range || $btnInc
}, $wrapper.addEventListener("click", onClick), $wrapper.addEventListener("pointerdown", onPointerDown), $wrapper.addEventListener("contextmenu", onContextMenu), setNearby($wrapper, {
focus: options.hideSlider ? $btnInc : $range
}), $wrapper;
}
static #METHOD_MAP = {
@ -2886,6 +2892,7 @@ class NavigationDialogManager {
if (!width) return;
if ($select.multiple) $label = $parent.querySelector(".bx-select-value"), width += 20;
else $label = $parent.querySelector("div");
if ($select.querySelector("optgroup")) width -= 15;
$label.style.minWidth = width + "px", $parent.dataset.calculated = "true";
}
}
@ -2901,7 +2908,7 @@ class NavigationDialogManager {
else if (keyCode === "ArrowLeft" || keyCode === "ArrowRight") {
if (!($target instanceof HTMLInputElement && ($target.type === "text" || $target.type === "range"))) handled = !0, this.focusDirection(keyCode === "ArrowLeft" ? 4 : 2);
} else if (keyCode === "Enter" || keyCode === "NumpadEnter" || keyCode === "Space") {
if (!($target instanceof HTMLInputElement && $target.type === "text")) handled = !0, $target.dispatchEvent(new MouseEvent("click"));
if (!($target instanceof HTMLInputElement && $target.type === "text")) handled = !0, $target.dispatchEvent(new MouseEvent("click", { bubbles: !0 }));
} else if (keyCode === "Escape") handled = !0, this.hide();
if (handled) event.preventDefault(), event.stopPropagation();
break;
@ -2955,7 +2962,7 @@ class NavigationDialogManager {
}
if (this.gamepadLastStates[gamepad.index] = null, lastKeyPressed) return;
if (releasedButton === 0) {
document.activeElement && document.activeElement.dispatchEvent(new MouseEvent("click"));
document.activeElement && document.activeElement.dispatchEvent(new MouseEvent("click", { bubbles: !0 }));
return;
} else if (releasedButton === 1) {
this.hide();
@ -4099,26 +4106,52 @@ class SettingsNavigationDialog extends NavigationDialog {
this.$btnReload.classList.add("bx-danger"), this.$noteGlobalReload.classList.add("bx-gone"), this.$btnGlobalReload.classList.remove("bx-gone"), this.$btnGlobalReload.classList.add("bx-danger");
}
renderServerSetting(setting) {
let selectedValue, $control = CE("select", {
let selectedValue = getPref("server_region"), continents = {
"america-north": {
label: t("continent-north-america")
},
"america-south": {
label: t("continent-south-america")
},
asia: {
label: t("continent-asia")
},
australia: {
label: t("continent-australia")
},
europe: {
label: t("continent-europe")
},
other: {
label: t("other")
}
}, $control = CE("select", {
id: `bx_setting_${setting.pref}`,
title: setting.label,
tabindex: 0
});
$control.name = $control.id, $control.addEventListener("input", (e) => {
setPref(setting.pref, e.target.value), this.onGlobalSettingChanged(e);
}), selectedValue = getPref("server_region"), setting.options = {};
}), setting.options = {};
for (let regionName in STATES.serverRegions) {
let region = STATES.serverRegions[regionName], value = regionName, label = `${region.shortName} - ${regionName}`;
if (region.isDefault) {
if (label += ` (${t("default")})`, value = "default", selectedValue === regionName) selectedValue = "default";
}
setting.options[value] = label;
let $option = CE("option", { value }, label), continent = continents[region.contintent];
if (!continent.children) continent.children = [];
continent.children.push($option);
}
for (let value in setting.options) {
let label = setting.options[value], $option = CE("option", { value }, label);
$control.appendChild($option);
let fragment = document.createDocumentFragment(), key;
for (key in continents) {
let continent = continents[key];
if (!continent.children) continue;
fragment.appendChild(CE("optgroup", {
label: continent.label
}, ...continent.children));
}
return $control.disabled = Object.keys(STATES.serverRegions).length === 0, $control.value = selectedValue, $control;
return $control.appendChild(fragment), $control.disabled = Object.keys(STATES.serverRegions).length === 0, $control.value = selectedValue, $control;
}
renderSettingRow(settingTab, $tabContent, settingTabContent, setting) {
if (typeof setting === "string") setting = {
@ -5035,21 +5068,22 @@ class StreamBadges {
}
}
class XcloudInterceptor {
static SERVER_EMOJIS = {
AustraliaEast: "🇦🇺",
AustraliaSouthEast: "🇦🇺",
BrazilSouth: "🇧🇷",
EastUS: "🇺🇸",
EastUS2: "🇺🇸",
JapanEast: "🇯🇵",
KoreaCentral: "🇰🇷",
MexicoCentral: "🇲🇽",
NorthCentralUs: "🇺🇸",
SouthCentralUS: "🇺🇸",
UKSouth: "🇬🇧",
WestEurope: "🇪🇺",
WestUS: "🇺🇸",
WestUS2: "🇺🇸"
static SERVER_EXTRA_INFO = {
EastUS: ["🇺🇸", "america-north"],
EastUS2: ["🇺🇸", "america-north"],
NorthCentralUs: ["🇺🇸", "america-north"],
SouthCentralUS: ["🇺🇸", "america-north"],
WestUS: ["🇺🇸", "america-north"],
WestUS2: ["🇺🇸", "america-north"],
MexicoCentral: ["🇲🇽", "america-north"],
BrazilSouth: ["🇧🇷", "america-south"],
JapanEast: ["🇯🇵", "asia"],
KoreaCentral: ["🇰🇷", "asia"],
AustraliaEast: ["🇦🇺", "australia"],
AustraliaSouthEast: ["🇦🇺", "australia"],
SwedenCentral: ["🇸🇪", "europe"],
UKSouth: ["🇬🇧", "europe"],
WestEurope: ["🇪🇺", "europe"]
};
static async handleLogin(request, init) {
let bypassServer = getPref("server_bypass_restriction");
@ -5061,14 +5095,13 @@ class XcloudInterceptor {
if (response.status !== 200) return BxEvent.dispatch(window, BxEvent.XCLOUD_SERVERS_UNAVAILABLE), response;
let obj = await response.clone().json();
RemotePlayManager.getInstance().xcloudToken = obj.gsToken;
let serverRegex = /\/\/(\w+)\./, serverEmojis = XcloudInterceptor.SERVER_EMOJIS;
for (let region of obj.offeringSettings.regions) {
let serverRegex = /\/\/(\w+)\./, serverExtra = XcloudInterceptor.SERVER_EXTRA_INFO, region;
for (region of obj.offeringSettings.regions) {
let { name: regionName, name: shortName } = region;
if (region.isDefault) STATES.selectedRegion = Object.assign({}, region);
let match = serverRegex.exec(region.baseUri);
if (match) {
if (shortName = match[1], serverEmojis[regionName]) shortName = serverEmojis[regionName] + " " + shortName;
}
if (match) if (shortName = match[1], serverExtra[regionName]) shortName = serverExtra[regionName][0] + " " + shortName, region.contintent = serverExtra[regionName][1];
else region.contintent = "other";
region.shortName = shortName.toUpperCase(), STATES.serverRegions[region.name] = Object.assign({}, region);
}
BxEvent.dispatch(window, BxEvent.XCLOUD_SERVERS_READY);
@ -5426,28 +5459,32 @@ class WebGL2Player {
let gl = this.gl, program = this.program;
gl.uniform2f(gl.getUniformLocation(program, "iResolution"), this.$canvas.width, this.$canvas.height), gl.uniform1i(gl.getUniformLocation(program, "filterId"), this.options.filterId), gl.uniform1f(gl.getUniformLocation(program, "sharpenFactor"), this.options.sharpenFactor), gl.uniform1f(gl.getUniformLocation(program, "brightness"), this.options.brightness), gl.uniform1f(gl.getUniformLocation(program, "contrast"), this.options.contrast), gl.uniform1f(gl.getUniformLocation(program, "saturation"), this.options.saturation);
}
drawFrame(force = !1) {
if (!force) {
if (this.targetFps === 0) return;
if (this.targetFps < 60) {
let currentTime = performance.now();
if (currentTime - this.lastFrameTime < this.frameInterval) return;
this.lastFrameTime = currentTime;
}
}
forceDrawFrame() {
let gl = this.gl;
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, this.$video), gl.drawArrays(gl.TRIANGLES, 0, 6);
}
setupRendering() {
let animate;
let frameCallback;
if ("requestVideoFrameCallback" in HTMLVideoElement.prototype) {
let $video = this.$video;
animate = () => {
if (!this.stopped) this.drawFrame(), this.animFrameId = $video.requestVideoFrameCallback(animate);
}, this.animFrameId = $video.requestVideoFrameCallback(animate);
} else animate = () => {
if (!this.stopped) this.drawFrame(), this.animFrameId = requestAnimationFrame(animate);
}, this.animFrameId = requestAnimationFrame(animate);
frameCallback = $video.requestVideoFrameCallback.bind($video);
} else frameCallback = requestAnimationFrame;
let animate = () => {
if (this.stopped) return;
let draw = !0;
if (this.targetFps === 0) draw = !1;
else if (this.targetFps < 60) {
let currentTime = performance.now();
if (currentTime - this.lastFrameTime < this.frameInterval) draw = !1;
else this.lastFrameTime = currentTime;
}
if (draw) {
let gl = this.gl;
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, this.$video), gl.drawArrays(gl.TRIANGLES, 0, 6);
}
this.animFrameId = frameCallback(animate);
};
this.animFrameId = frameCallback(animate);
}
setupShaders() {
BxLogger.info(this.LOG_TAG, "Setting up", getPref("video_power_preference"));

View File

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

View File

@ -1,7 +1,7 @@
// ==UserScript==
// @name Better xCloud
// @namespace https://github.com/redphx
// @version 5.9.2
// @version 5.9.3
// @description Improve Xbox Cloud Gaming (xCloud) experience
// @author redphx
// @license MIT
@ -107,7 +107,7 @@ class UserAgent {
});
}
}
var SCRIPT_VERSION = "5.9.2", SCRIPT_VARIANT = "full", AppInterface = window.AppInterface;
var SCRIPT_VERSION = "5.9.3", SCRIPT_VARIANT = "full", AppInterface = window.AppInterface;
UserAgent.init();
var userAgent = window.navigator.userAgent.toLowerCase(), isTv = userAgent.includes("smart-tv") || userAgent.includes("smarttv") || /\baft.*\b/.test(userAgent), isVr = window.navigator.userAgent.includes("VR") && window.navigator.userAgent.includes("OculusBrowser"), browserHasTouchSupport = "ontouchstart" in window || navigator.maxTouchPoints > 0, userAgentHasTouchSupport = !isTv && !isVr && browserHasTouchSupport, supportMkb = AppInterface || !userAgent.match(/(android|iphone|ipad)/), STATES = {
supportedRegion: !0,
@ -323,6 +323,11 @@ var SUPPORTED_LANGUAGES = {
"confirm-reload-stream": "Do you want to refresh the stream?",
connected: "Connected",
"console-connect": "Connect",
"continent-asia": "Asia",
"continent-australia": "Australia",
"continent-europe": "Europe",
"continent-north-america": "North America",
"continent-south-america": "South America",
contrast: "Contrast",
controller: "Controller",
"controller-friendly-ui": "Controller-friendly UI",
@ -775,14 +780,16 @@ class SettingElement {
}
static #renderNumberStepper(key, setting, value, onChange, options = {}) {
options = options || {}, options.suffix = options.suffix || "", options.disabled = !!options.disabled, options.hideSlider = !!options.hideSlider;
let $text, $btnDec, $btnInc, $range = null, controlValue = value, MIN = options.reverse ? -setting.max : setting.min, MAX = options.reverse ? -setting.min : setting.max, STEPS = Math.max(setting.steps || 1, 1), renderTextValue = (value2) => {
let $text, $btnDec, $btnInc, $range = null, controlValue = value, MIN = options.reverse ? -setting.max : setting.min, MAX = options.reverse ? -setting.min : setting.max, STEPS = Math.max(setting.steps || 1, 1), intervalId, isHolding = !1, clearIntervalId = () => {
intervalId && clearInterval(intervalId), intervalId = null;
}, renderTextValue = (value2) => {
value2 = parseInt(value2);
let textContent = null;
if (options.customTextValue) textContent = options.customTextValue(value2);
if (textContent === null) textContent = value2.toString() + options.suffix;
return textContent;
}, updateButtonsVisibility = () => {
$btnDec.classList.toggle("bx-inactive", controlValue === MIN), $btnInc.classList.toggle("bx-inactive", controlValue === MAX);
if ($btnDec.classList.toggle("bx-inactive", controlValue === MIN), $btnInc.classList.toggle("bx-inactive", controlValue === MAX), controlValue === MIN || controlValue === MAX) clearIntervalId();
}, $wrapper = CE("div", { class: "bx-number-stepper", id: `bx_setting_${key}` }, $btnDec = CE("button", {
"data-type": "dec",
type: "button",
@ -796,7 +803,7 @@ class SettingElement {
}, "+"));
if (options.disabled) return $btnInc.disabled = !0, $btnInc.classList.add("bx-inactive"), $btnDec.disabled = !0, $btnDec.classList.add("bx-inactive"), $wrapper.disabled = !0, $wrapper;
if ($range = CE("input", {
id: `bx_setting_${key}`,
id: `bx_inp_setting_${key}`,
type: "range",
min: MIN,
max: MAX,
@ -823,30 +830,29 @@ class SettingElement {
$wrapper.appendChild($markers);
}
updateButtonsVisibility();
let interval, isHolding = !1, onClick = (e) => {
if (isHolding) {
e.preventDefault(), isHolding = !1;
return;
}
let $btn = e.target, value2 = parseInt(controlValue);
let buttonPressed = (e, $btn) => {
let value2 = parseInt(controlValue);
if ($btn.dataset.type === "dec") value2 = Math.max(MIN, value2 - STEPS);
else value2 = Math.min(MAX, value2 + STEPS);
controlValue = value2, updateButtonsVisibility(), $text.textContent = renderTextValue(value2), $range && ($range.value = value2.toString()), isHolding = !1, !e.ignoreOnChange && onChange && onChange(e, value2);
}, onMouseDown = (e) => {
e.preventDefault(), isHolding = !0;
let args = arguments;
interval && clearInterval(interval), interval = window.setInterval(() => {
e.target && BxEvent.dispatch(e.target, "click", {
arguments: args
});
}, 200);
}, onMouseUp = (e) => {
e.preventDefault(), interval && clearInterval(interval), isHolding = !1;
controlValue = value2, updateButtonsVisibility(), $text.textContent = renderTextValue(value2), $range && ($range.value = value2.toString()), onChange && onChange(e, value2);
}, onClick = (e) => {
if (e.preventDefault(), isHolding) return;
let $btn = e.target.closest("button");
$btn && buttonPressed(e, $btn), clearIntervalId(), isHolding = !1;
}, onPointerDown = (e) => {
clearIntervalId();
let $btn = e.target.closest("button");
if (!$btn) return;
isHolding = !0, e.preventDefault(), intervalId = window.setInterval((e2) => {
buttonPressed(e2, $btn);
}, 200), window.addEventListener("pointerup", onPointerUp, { once: !0 }), window.addEventListener("pointercancel", onPointerUp, { once: !0 });
}, onPointerUp = (e) => {
clearIntervalId(), isHolding = !1;
}, onContextMenu = (e) => e.preventDefault();
return $wrapper.setValue = (value2) => {
$text.textContent = renderTextValue(value2), $range.value = options.reverse ? -value2 : value2;
}, $btnDec.addEventListener("click", onClick), $btnDec.addEventListener("pointerdown", onMouseDown), $btnDec.addEventListener("pointerup", onMouseUp), $btnDec.addEventListener("contextmenu", onContextMenu), $btnInc.addEventListener("click", onClick), $btnInc.addEventListener("pointerdown", onMouseDown), $btnInc.addEventListener("pointerup", onMouseUp), $btnInc.addEventListener("contextmenu", onContextMenu), setNearby($wrapper, {
focus: $range || $btnInc
}, $wrapper.addEventListener("click", onClick), $wrapper.addEventListener("pointerdown", onPointerDown), $wrapper.addEventListener("contextmenu", onContextMenu), setNearby($wrapper, {
focus: options.hideSlider ? $btnInc : $range
}), $wrapper;
}
static #METHOD_MAP = {
@ -1842,7 +1848,7 @@ class ScreenshotManager {
if (!$player || !$player.isConnected) return;
$player.parentElement.addEventListener("animationend", this.onAnimationEnd, { once: !0 }), $player.parentElement.classList.add("bx-taking-screenshot");
let canvasContext = this.canvasContext;
if ($player instanceof HTMLCanvasElement) streamPlayer.getWebGL2Player().drawFrame(!0);
if ($player instanceof HTMLCanvasElement) streamPlayer.getWebGL2Player().forceDrawFrame();
if (canvasContext.drawImage($player, 0, 0, $canvas.width, $canvas.height), AppInterface) {
let data = $canvas.toDataURL("image/png").split(";base64,")[1];
AppInterface.saveScreenshot(currentStream.titleSlug, data), canvasContext.clearRect(0, 0, $canvas.width, $canvas.height), callback && callback();
@ -3167,6 +3173,7 @@ class NavigationDialogManager {
if (!width) return;
if ($select.multiple) $label = $parent.querySelector(".bx-select-value"), width += 20;
else $label = $parent.querySelector("div");
if ($select.querySelector("optgroup")) width -= 15;
$label.style.minWidth = width + "px", $parent.dataset.calculated = "true";
}
}
@ -3182,7 +3189,7 @@ class NavigationDialogManager {
else if (keyCode === "ArrowLeft" || keyCode === "ArrowRight") {
if (!($target instanceof HTMLInputElement && ($target.type === "text" || $target.type === "range"))) handled = !0, this.focusDirection(keyCode === "ArrowLeft" ? 4 : 2);
} else if (keyCode === "Enter" || keyCode === "NumpadEnter" || keyCode === "Space") {
if (!($target instanceof HTMLInputElement && $target.type === "text")) handled = !0, $target.dispatchEvent(new MouseEvent("click"));
if (!($target instanceof HTMLInputElement && $target.type === "text")) handled = !0, $target.dispatchEvent(new MouseEvent("click", { bubbles: !0 }));
} else if (keyCode === "Escape") handled = !0, this.hide();
if (handled) event.preventDefault(), event.stopPropagation();
break;
@ -3236,7 +3243,7 @@ class NavigationDialogManager {
}
if (this.gamepadLastStates[gamepad.index] = null, lastKeyPressed) return;
if (releasedButton === 0) {
document.activeElement && document.activeElement.dispatchEvent(new MouseEvent("click"));
document.activeElement && document.activeElement.dispatchEvent(new MouseEvent("click", { bubbles: !0 }));
return;
} else if (releasedButton === 1) {
this.hide();
@ -5243,26 +5250,52 @@ class SettingsNavigationDialog extends NavigationDialog {
PatcherCache.clear(), this.$btnReload.classList.add("bx-danger"), this.$noteGlobalReload.classList.add("bx-gone"), this.$btnGlobalReload.classList.remove("bx-gone"), this.$btnGlobalReload.classList.add("bx-danger");
}
renderServerSetting(setting) {
let selectedValue, $control = CE("select", {
let selectedValue = getPref("server_region"), continents = {
"america-north": {
label: t("continent-north-america")
},
"america-south": {
label: t("continent-south-america")
},
asia: {
label: t("continent-asia")
},
australia: {
label: t("continent-australia")
},
europe: {
label: t("continent-europe")
},
other: {
label: t("other")
}
}, $control = CE("select", {
id: `bx_setting_${setting.pref}`,
title: setting.label,
tabindex: 0
});
$control.name = $control.id, $control.addEventListener("input", (e) => {
setPref(setting.pref, e.target.value), this.onGlobalSettingChanged(e);
}), selectedValue = getPref("server_region"), setting.options = {};
}), setting.options = {};
for (let regionName in STATES.serverRegions) {
let region = STATES.serverRegions[regionName], value = regionName, label = `${region.shortName} - ${regionName}`;
if (region.isDefault) {
if (label += ` (${t("default")})`, value = "default", selectedValue === regionName) selectedValue = "default";
}
setting.options[value] = label;
let $option = CE("option", { value }, label), continent = continents[region.contintent];
if (!continent.children) continent.children = [];
continent.children.push($option);
}
for (let value in setting.options) {
let label = setting.options[value], $option = CE("option", { value }, label);
$control.appendChild($option);
let fragment = document.createDocumentFragment(), key;
for (key in continents) {
let continent = continents[key];
if (!continent.children) continue;
fragment.appendChild(CE("optgroup", {
label: continent.label
}, ...continent.children));
}
return $control.disabled = Object.keys(STATES.serverRegions).length === 0, $control.value = selectedValue, $control;
return $control.appendChild(fragment), $control.disabled = Object.keys(STATES.serverRegions).length === 0, $control.value = selectedValue, $control;
}
renderSettingRow(settingTab, $tabContent, settingTabContent, setting) {
if (typeof setting === "string") setting = {
@ -6612,21 +6645,22 @@ class StreamBadges {
static setupEvents() {}
}
class XcloudInterceptor {
static SERVER_EMOJIS = {
AustraliaEast: "🇦🇺",
AustraliaSouthEast: "🇦🇺",
BrazilSouth: "🇧🇷",
EastUS: "🇺🇸",
EastUS2: "🇺🇸",
JapanEast: "🇯🇵",
KoreaCentral: "🇰🇷",
MexicoCentral: "🇲🇽",
NorthCentralUs: "🇺🇸",
SouthCentralUS: "🇺🇸",
UKSouth: "🇬🇧",
WestEurope: "🇪🇺",
WestUS: "🇺🇸",
WestUS2: "🇺🇸"
static SERVER_EXTRA_INFO = {
EastUS: ["🇺🇸", "america-north"],
EastUS2: ["🇺🇸", "america-north"],
NorthCentralUs: ["🇺🇸", "america-north"],
SouthCentralUS: ["🇺🇸", "america-north"],
WestUS: ["🇺🇸", "america-north"],
WestUS2: ["🇺🇸", "america-north"],
MexicoCentral: ["🇲🇽", "america-north"],
BrazilSouth: ["🇧🇷", "america-south"],
JapanEast: ["🇯🇵", "asia"],
KoreaCentral: ["🇰🇷", "asia"],
AustraliaEast: ["🇦🇺", "australia"],
AustraliaSouthEast: ["🇦🇺", "australia"],
SwedenCentral: ["🇸🇪", "europe"],
UKSouth: ["🇬🇧", "europe"],
WestEurope: ["🇪🇺", "europe"]
};
static async handleLogin(request, init) {
let bypassServer = getPref("server_bypass_restriction");
@ -6638,14 +6672,13 @@ class XcloudInterceptor {
if (response.status !== 200) return BxEvent.dispatch(window, BxEvent.XCLOUD_SERVERS_UNAVAILABLE), response;
let obj = await response.clone().json();
RemotePlayManager.getInstance().xcloudToken = obj.gsToken;
let serverRegex = /\/\/(\w+)\./, serverEmojis = XcloudInterceptor.SERVER_EMOJIS;
for (let region of obj.offeringSettings.regions) {
let serverRegex = /\/\/(\w+)\./, serverExtra = XcloudInterceptor.SERVER_EXTRA_INFO, region;
for (region of obj.offeringSettings.regions) {
let { name: regionName, name: shortName } = region;
if (region.isDefault) STATES.selectedRegion = Object.assign({}, region);
let match = serverRegex.exec(region.baseUri);
if (match) {
if (shortName = match[1], serverEmojis[regionName]) shortName = serverEmojis[regionName] + " " + shortName;
}
if (match) if (shortName = match[1], serverExtra[regionName]) shortName = serverExtra[regionName][0] + " " + shortName, region.contintent = serverExtra[regionName][1];
else region.contintent = "other";
region.shortName = shortName.toUpperCase(), STATES.serverRegions[region.name] = Object.assign({}, region);
}
BxEvent.dispatch(window, BxEvent.XCLOUD_SERVERS_READY);
@ -7042,28 +7075,32 @@ class WebGL2Player {
let gl = this.gl, program = this.program;
gl.uniform2f(gl.getUniformLocation(program, "iResolution"), this.$canvas.width, this.$canvas.height), gl.uniform1i(gl.getUniformLocation(program, "filterId"), this.options.filterId), gl.uniform1f(gl.getUniformLocation(program, "sharpenFactor"), this.options.sharpenFactor), gl.uniform1f(gl.getUniformLocation(program, "brightness"), this.options.brightness), gl.uniform1f(gl.getUniformLocation(program, "contrast"), this.options.contrast), gl.uniform1f(gl.getUniformLocation(program, "saturation"), this.options.saturation);
}
drawFrame(force = !1) {
if (!force) {
if (this.targetFps === 0) return;
if (this.targetFps < 60) {
let currentTime = performance.now();
if (currentTime - this.lastFrameTime < this.frameInterval) return;
this.lastFrameTime = currentTime;
}
}
forceDrawFrame() {
let gl = this.gl;
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, this.$video), gl.drawArrays(gl.TRIANGLES, 0, 6);
}
setupRendering() {
let animate;
let frameCallback;
if ("requestVideoFrameCallback" in HTMLVideoElement.prototype) {
let $video = this.$video;
animate = () => {
if (!this.stopped) this.drawFrame(), this.animFrameId = $video.requestVideoFrameCallback(animate);
}, this.animFrameId = $video.requestVideoFrameCallback(animate);
} else animate = () => {
if (!this.stopped) this.drawFrame(), this.animFrameId = requestAnimationFrame(animate);
}, this.animFrameId = requestAnimationFrame(animate);
frameCallback = $video.requestVideoFrameCallback.bind($video);
} else frameCallback = requestAnimationFrame;
let animate = () => {
if (this.stopped) return;
let draw = !0;
if (this.targetFps === 0) draw = !1;
else if (this.targetFps < 60) {
let currentTime = performance.now();
if (currentTime - this.lastFrameTime < this.frameInterval) draw = !1;
else this.lastFrameTime = currentTime;
}
if (draw) {
let gl = this.gl;
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, this.$video), gl.drawArrays(gl.TRIANGLES, 0, 6);
}
this.animFrameId = frameCallback(animate);
};
this.animFrameId = frameCallback(animate);
}
setupShaders() {
BxLogger.info(this.LOG_TAG, "Setting up", getPref("video_power_preference"));

View File

@ -10,8 +10,8 @@
"build": "build.ts"
},
"devDependencies": {
"@types/bun": "^1.1.11",
"@types/node": "^22.7.8",
"@types/bun": "^1.1.12",
"@types/node": "^22.7.9",
"@types/stylus": "^0.48.43",
"eslint": "^9.13.0",
"eslint-plugin-compat": "^6.0.1",

View File

@ -94,52 +94,52 @@ export class WebGL2Player {
gl.uniform1f(gl.getUniformLocation(program, 'saturation'), this.options.saturation);
}
drawFrame(force=false) {
if (!force) {
// Don't draw when FPS is 0
if (this.targetFps === 0) {
return;
}
// Limit FPS
if (this.targetFps < 60) {
const currentTime = performance.now();
const timeSinceLastFrame = currentTime - this.lastFrameTime;
if (timeSinceLastFrame < this.frameInterval) {
return;
}
this.lastFrameTime = currentTime;
}
}
forceDrawFrame() {
const gl = this.gl!;
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, this.$video);
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
private setupRendering() {
let animate: any;
let frameCallback: any;
if ('requestVideoFrameCallback' in HTMLVideoElement.prototype) {
const $video = this.$video;
animate = () => {
if (!this.stopped) {
this.drawFrame();
this.animFrameId = $video.requestVideoFrameCallback(animate);
}
}
this.animFrameId = $video.requestVideoFrameCallback(animate);
frameCallback = $video.requestVideoFrameCallback.bind($video);
} else {
animate = () => {
if (!this.stopped) {
this.drawFrame();
this.animFrameId = requestAnimationFrame(animate);
frameCallback = requestAnimationFrame;
}
let animate = () => {
if (this.stopped) {
return;
}
let draw = true;
// Don't draw when FPS is 0
if (this.targetFps === 0) {
draw = false;
} else if (this.targetFps < 60) {
// Limit FPS
const currentTime = performance.now();
const timeSinceLastFrame = currentTime - this.lastFrameTime;
if (timeSinceLastFrame < this.frameInterval) {
draw = false;
} else {
this.lastFrameTime = currentTime;
}
}
this.animFrameId = requestAnimationFrame(animate);
if (draw) {
const gl = this.gl!;
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, this.$video);
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
this.animFrameId = frameCallback(animate);
}
this.animFrameId = frameCallback(animate);
}
private setupShaders() {

View File

@ -203,6 +203,11 @@ export class NavigationDialogManager {
$label = $parent.querySelector<HTMLElement>('div')!;
}
// Reduce width if it has <optgroup>
if ($select.querySelector('optgroup')) {
width -= 15;
}
// Set min-width
$label.style.minWidth = width + 'px';
$parent.dataset.calculated = 'true';
@ -234,7 +239,7 @@ export class NavigationDialogManager {
} else if (keyCode === 'Enter' || keyCode === 'NumpadEnter' || keyCode === 'Space') {
if (!($target instanceof HTMLInputElement && $target.type === 'text')) {
handled = true;
$target.dispatchEvent(new MouseEvent('click'));
$target.dispatchEvent(new MouseEvent('click', {bubbles: true}));
}
} else if (keyCode === 'Escape') {
handled = true;
@ -361,7 +366,7 @@ export class NavigationDialogManager {
}
if (releasedButton === GamepadKey.A) {
document.activeElement && document.activeElement.dispatchEvent(new MouseEvent('click'));
document.activeElement && document.activeElement.dispatchEvent(new MouseEvent('click', {bubbles: true}));
return;
} else if (releasedButton === GamepadKey.B) {
this.hide();

View File

@ -1044,13 +1044,37 @@ export class SettingsNavigationDialog extends NavigationDialog {
}
private renderServerSetting(setting: SettingTabContentItem): HTMLElement {
let selectedValue;
let selectedValue =getPref(PrefKey.SERVER_REGION);
const continents: Record<ServerContinent, {
label: string,
children?: HTMLOptionElement[],
}> = {
'america-north': {
label: t('continent-north-america'),
},
'america-south': {
label: t('continent-south-america'),
},
'asia': {
label: t('continent-asia'),
},
'australia': {
label: t('continent-australia'),
},
'europe': {
label: t('continent-europe'),
},
'other': {
label: t('other'),
},
};
const $control = CE<HTMLSelectElement>('select', {
id: `bx_setting_${setting.pref}`,
title: setting.label,
tabindex: 0,
});
id: `bx_setting_${setting.pref}`,
title: setting.label,
tabindex: 0,
});
$control.name = $control.id;
$control.addEventListener('input', (e: Event) => {
@ -1058,8 +1082,6 @@ export class SettingsNavigationDialog extends NavigationDialog {
this.onGlobalSettingChanged(e);
});
selectedValue = getPref(PrefKey.SERVER_REGION);
setting.options = {};
for (const regionName in STATES.serverRegions) {
const region = STATES.serverRegions[regionName];
@ -1076,15 +1098,29 @@ export class SettingsNavigationDialog extends NavigationDialog {
}
setting.options[value] = label;
const $option = CE<HTMLOptionElement>('option', {value: value}, label);
const continent = continents[region.contintent];
if (!continent.children) {
continent.children = [];
}
continent.children.push($option);
}
for (const value in setting.options) {
const label = setting.options[value];
const fragment = document.createDocumentFragment();
let key: keyof typeof continents;
for (key in continents) {
const continent = continents[key];
if (!continent.children) {
continue;
}
const $option = CE('option', {value: value}, label);
$control.appendChild($option);
fragment.appendChild(CE('optgroup', {
label: continent.label,
}, ...continent.children));
}
$control.appendChild(fragment);
$control.disabled = Object.keys(STATES.serverRegions).length === 0;
// Select preferred region

16
src/types/index.d.ts vendored
View File

@ -21,14 +21,24 @@ interface Window {
interface NavigatorBattery extends Navigator {
getBattery: () => Promise<{
charging: boolean,
level: float,
charging: boolean;
level: float;
}>,
}
type ServerContinent = 'america-north' | 'america-south' | 'asia' | 'australia' | 'europe' | 'other';
type ServerRegion = {
baseUri: string;
isDefault: boolean;
name: string;
shortName: string;
contintent: ServerContinent;
};
type BxStates = {
supportedRegion: boolean;
serverRegions: any;
serverRegions: Record<string, ServerRegion>;
selectedRegion: any;
gsToken: string;
isSignedIn: boolean;

View File

@ -65,7 +65,7 @@ export class ScreenshotManager {
const canvasContext = this.canvasContext;
if ($player instanceof HTMLCanvasElement) {
streamPlayer.getWebGL2Player().drawFrame(true);
streamPlayer.getWebGL2Player().forceDrawFrame();
}
canvasContext.drawImage($player, 0, 0, $canvas.width, $canvas.height);

View File

@ -25,9 +25,9 @@ export interface BxSelectSettingElement extends HTMLSelectElement, BxBaseSetting
export class SettingElement {
static #renderOptions(key: string, setting: PreferenceSetting, currentValue: any, onChange: any): BxSelectSettingElement {
const $control = CE<BxSelectSettingElement>('select', {
// title: setting.label,
tabindex: 0,
});
// title: setting.label,
tabindex: 0,
});
let $parent: HTMLElement;
if (setting.optionsGroup) {
@ -64,10 +64,10 @@ export class SettingElement {
static #renderMultipleOptions(key: string, setting: PreferenceSetting, currentValue: any, onChange: any, params: MultipleOptionsParams={}): BxSelectSettingElement {
const $control = CE<BxSelectSettingElement>('select', {
// title: setting.label,
multiple: true,
tabindex: 0,
});
// title: setting.label,
multiple: true,
tabindex: 0,
});
if (params && params.size) {
$control.setAttribute('size', params.size.toString());
@ -164,6 +164,14 @@ export class SettingElement {
const MAX = options.reverse ? -setting.min! : setting.max!;
const STEPS = Math.max(setting.steps || 1, 1);
let intervalId: number | null;
let isHolding = false;
const clearIntervalId = () => {
intervalId && clearInterval(intervalId);
intervalId = null;
}
const renderTextValue = (value: any) => {
value = parseInt(value as string);
@ -182,6 +190,10 @@ export class SettingElement {
const updateButtonsVisibility = () => {
$btnDec.classList.toggle('bx-inactive', controlValue === MIN);
$btnInc.classList.toggle('bx-inactive', controlValue === MAX);
if (controlValue === MIN || controlValue === MAX) {
clearIntervalId();
}
}
const $wrapper = CE<BxHtmlSettingElement>('div', {'class': 'bx-number-stepper', id: `bx_setting_${key}`},
@ -212,7 +224,7 @@ export class SettingElement {
}
$range = CE<HTMLInputElement>('input', {
id: `bx_setting_${key}`,
id: `bx_inp_setting_${key}`,
type: 'range',
min: MIN,
max: MAX,
@ -273,18 +285,7 @@ export class SettingElement {
updateButtonsVisibility();
let interval: number;
let isHolding = false;
const onClick = (e: Event) => {
if (isHolding) {
e.preventDefault();
isHolding = false;
return;
}
const $btn = e.target as HTMLElement;
const buttonPressed = (e: Event, $btn: HTMLElement) => {
let value = parseInt(controlValue);
const btnType = $btn.dataset.type;
@ -300,27 +301,43 @@ export class SettingElement {
$text.textContent = renderTextValue(value);
$range && ($range.value = value.toString());
isHolding = false;
!(e as any).ignoreOnChange && onChange && onChange(e, value);
}
const onMouseDown = (e: PointerEvent) => {
e.preventDefault();
isHolding = true;
const args = arguments;
interval && clearInterval(interval);
interval = window.setInterval(() => {
e.target && BxEvent.dispatch(e.target as HTMLElement, 'click', {
arguments: args,
});
}, 200);
onChange && onChange(e, value);
};
const onMouseUp = (e: PointerEvent) => {
const onClick = (e: Event) => {
e.preventDefault();
if (isHolding) {
return;
}
const $btn = (e.target as HTMLElement).closest('button') as HTMLElement;
$btn && buttonPressed(e, $btn);
clearIntervalId();
isHolding = false;
};
const onPointerDown = (e: PointerEvent) => {
clearIntervalId();
const $btn = (e.target as HTMLElement).closest('button') as HTMLElement;
if (!$btn) {
return;
}
isHolding = true;
e.preventDefault();
interval && clearInterval(interval);
intervalId = window.setInterval((e: Event) => {
buttonPressed(e, $btn);
}, 200);
window.addEventListener('pointerup', onPointerUp, {once: true});
window.addEventListener('pointercancel', onPointerUp, {once: true});
};
const onPointerUp = (e: PointerEvent) => {
clearIntervalId();
isHolding = false;
};
@ -332,18 +349,11 @@ export class SettingElement {
$range.value = options.reverse ? -value : value;
};
$btnDec.addEventListener('click', onClick);
$btnDec.addEventListener('pointerdown', onMouseDown);
$btnDec.addEventListener('pointerup', onMouseUp);
$btnDec.addEventListener('contextmenu', onContextMenu);
$btnInc.addEventListener('click', onClick);
$btnInc.addEventListener('pointerdown', onMouseDown);
$btnInc.addEventListener('pointerup', onMouseUp);
$btnInc.addEventListener('contextmenu', onContextMenu);
$wrapper.addEventListener('click', onClick);
$wrapper.addEventListener('pointerdown', onPointerDown);
$wrapper.addEventListener('contextmenu', onContextMenu);
setNearby($wrapper, {
focus: $range || $btnInc,
focus: options.hideSlider ? $btnInc : $range,
})
return $wrapper;

View File

@ -67,6 +67,11 @@ const Texts = {
"confirm-reload-stream": "Do you want to refresh the stream?",
"connected": "Connected",
"console-connect": "Connect",
"continent-asia": "Asia",
"continent-australia": "Australia",
"continent-europe": "Europe",
"continent-north-america": "North America",
"continent-south-america": "South America",
"contrast": "Contrast",
"controller": "Controller",
"controller-friendly-ui": "Controller-friendly UI",

View File

@ -14,21 +14,31 @@ import { PrefKey } from "@/enums/pref-keys";
import { getPref, StreamResolution, StreamTouchController } from "./settings-storages/global-settings-storage";
export class XcloudInterceptor {
private static readonly SERVER_EMOJIS = {
AustraliaEast: '🇦🇺',
AustraliaSouthEast: '🇦🇺',
BrazilSouth: '🇧🇷',
EastUS: '🇺🇸',
EastUS2: '🇺🇸',
JapanEast: '🇯🇵',
KoreaCentral: '🇰🇷',
MexicoCentral: '🇲🇽',
NorthCentralUs: '🇺🇸',
SouthCentralUS: '🇺🇸',
UKSouth: '🇬🇧',
WestEurope: '🇪🇺',
WestUS: '🇺🇸',
WestUS2: '🇺🇸',
private static readonly SERVER_EXTRA_INFO: Record<string, [string, ServerContinent]> = {
// North America
EastUS: ['🇺🇸', 'america-north'],
EastUS2: ['🇺🇸', 'america-north'],
NorthCentralUs: ['🇺🇸', 'america-north'],
SouthCentralUS: ['🇺🇸', 'america-north'],
WestUS: ['🇺🇸', 'america-north'],
WestUS2: ['🇺🇸', 'america-north'],
MexicoCentral: ['🇲🇽', 'america-north'],
// South America
BrazilSouth: ['🇧🇷', 'america-south'],
// Asia
JapanEast: ['🇯🇵', 'asia'],
KoreaCentral: ['🇰🇷', 'asia'],
// Australia
AustraliaEast: ['🇦🇺', 'australia'],
AustraliaSouthEast: ['🇦🇺', 'australia'],
// Europe
SwedenCentral: ['🇸🇪', 'europe'],
UKSouth: ['🇬🇧', 'europe'],
WestEurope: ['🇪🇺', 'europe'],
};
private static async handleLogin(request: RequestInfo | URL, init?: RequestInit) {
@ -52,10 +62,11 @@ export class XcloudInterceptor {
// Get server list
const serverRegex = /\/\/(\w+)\./;
const serverEmojis = XcloudInterceptor.SERVER_EMOJIS;
const serverExtra = XcloudInterceptor.SERVER_EXTRA_INFO;
for (let region of obj.offeringSettings.regions) {
const regionName = region.name as keyof typeof serverEmojis;
let region: ServerRegion;
for (region of obj.offeringSettings.regions) {
const regionName = region.name as keyof typeof serverExtra;
let shortName = region.name;
if (region.isDefault) {
@ -65,8 +76,11 @@ export class XcloudInterceptor {
let match = serverRegex.exec(region.baseUri);
if (match) {
shortName = match[1];
if (serverEmojis[regionName]) {
shortName = serverEmojis[regionName] + ' ' + shortName;
if (serverExtra[regionName]) {
shortName = serverExtra[regionName][0] + ' ' + shortName;
region.contintent = serverExtra[regionName][1];
} else {
region.contintent = 'other';
}
}