Merge Global settings and Stream settings into one dialog
9
build.ts
@ -22,10 +22,19 @@ const postProcess = (str: string): string => {
|
||||
// Replace "globalThis." with "var";
|
||||
str = str.replaceAll('globalThis.', 'var ');
|
||||
|
||||
// Remove enum's inlining comments
|
||||
str = str.replaceAll(/ \/\* [A-Z0-9_]+ \*\//g, '');
|
||||
|
||||
// Remove comments from import
|
||||
str = str.replaceAll(/\/\/ src.*\n/g, '');
|
||||
|
||||
// Add ADDITIONAL CODE block
|
||||
str = str.replace('var DEFAULT_FLAGS', '\n/* ADDITIONAL CODE */\n\nvar DEFAULT_FLAGS');
|
||||
|
||||
assert(str.includes('/* ADDITIONAL CODE */'));
|
||||
assert(str.includes('window.BX_EXPOSED = BxExposed'));
|
||||
assert(str.includes('window.BxEvent = BxEvent'));
|
||||
assert(str.includes('window.BX_FETCH = window.fetch'));
|
||||
|
||||
return str;
|
||||
}
|
||||
|
@ -7,7 +7,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.1.6",
|
||||
"@types/node": "^20.14.10",
|
||||
"@types/node": "^20.14.12",
|
||||
"@types/stylus": "^0.48.42",
|
||||
"stylus": "^0.63.0"
|
||||
},
|
||||
|
@ -1,5 +1,10 @@
|
||||
.bx-button {
|
||||
background-color: var(--bx-default-button-color);
|
||||
--button-rgb: var(--bx-default-button-rgb);
|
||||
--button-hover-rgb: var(--bx-default-button-hover-rgb);
|
||||
--button-active-rgb: var(--bx-default-button-active-rgb);
|
||||
--button-disabled-rgb: var(--bx-default-button-disabled-rgb);
|
||||
|
||||
background-color: unquote('rgb(var(--button-rgb))');
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
color: #fff;
|
||||
@ -14,55 +19,97 @@
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
|
||||
&:not([disabled]):active {
|
||||
background-color: unquote('rgb(var(--button-active-rgb))');
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
&:hover, &.bx-focusable:focus {
|
||||
background-color: var(--bx-default-button-hover-color);
|
||||
&:not([disabled]):not(:active) {
|
||||
&:hover, &.bx-focusable:focus {
|
||||
background-color: unquote('rgb(var(--button-hover-rgb))');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&:disabled {
|
||||
cursor: default;
|
||||
background-color: var(--bx-default-button-disabled-color);
|
||||
background-color: unquote('rgb(var(--button-disabled-rgb))');
|
||||
}
|
||||
|
||||
&.bx-ghost {
|
||||
background-color: transparent;
|
||||
|
||||
&:hover, &.bx-focusable:focus {
|
||||
background-color: var(--bx-default-button-hover-color);
|
||||
&:not([disabled]):not(:active) {
|
||||
&:hover, &.bx-focusable:focus {
|
||||
background-color: unquote('rgb(var(--button-hover-rgb))');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.bx-primary {
|
||||
background-color: var(--bx-primary-button-color);
|
||||
--button-rgb: var(--bx-primary-button-rgb);
|
||||
|
||||
&:hover, &.bx-focusable:focus {
|
||||
background-color: var(--bx-primary-button-hover-color);
|
||||
&:not([disabled]):active {
|
||||
--button-active-rgb: var(--bx-primary-button-active-rgb);
|
||||
}
|
||||
|
||||
&:not([disabled]):not(:active) {
|
||||
&:hover, &.bx-focusable:focus {
|
||||
--button-hover-rgb: var(--bx-primary-button-hover-rgb);
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: var(--bx-primary-button-disabled-color);
|
||||
--button-disabled-rgb: var(--bx-primary-button-disabled-rgb);
|
||||
}
|
||||
}
|
||||
|
||||
&.bx-danger {
|
||||
background-color: var(--bx-danger-button-color);
|
||||
--button-rgb: var(--bx-danger-button-rgb);
|
||||
|
||||
&:hover, &.bx-focusable:focus {
|
||||
background-color: var(--bx-danger-button-hover-color);
|
||||
&:not([disabled]):active {
|
||||
--button-active-rgb: var(--bx-danger-button-active-rgb);
|
||||
}
|
||||
|
||||
&:not([disabled]):not(:active) {
|
||||
&:hover, &.bx-focusable:focus {
|
||||
--button-hover-rgb: var(--bx-danger-button-hover-rgb);
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: var(--bx-danger-button-disabled-color);
|
||||
--button-disabled-rgb: var(--bx-danger-button-disabled-rgb);
|
||||
}
|
||||
}
|
||||
|
||||
&.bx-frosted {
|
||||
--button-alpha: 0.2;
|
||||
background-color: unquote('rgba(var(--button-rgb), var(--button-alpha))');
|
||||
backdrop-filter: blur(4px) brightness(1.5);
|
||||
|
||||
&:not([disabled]):not(:active) {
|
||||
&:hover, &.bx-focusable:focus {
|
||||
background-color: unquote('rgba(var(--button-hover-rgb), var(--button-alpha))');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.bx-drop-shadow {
|
||||
box-shadow: 0 0 4px #00000080;
|
||||
}
|
||||
|
||||
&.bx-tall {
|
||||
height: calc(var(--bx-button-height) * 1.5) !important;
|
||||
}
|
||||
|
||||
&.bx-circular {
|
||||
border-radius: var(--bx-button-height);
|
||||
height: var(--bx-button-height);
|
||||
}
|
||||
|
||||
svg {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
@ -87,20 +134,29 @@
|
||||
|
||||
.bx-focusable {
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
|
||||
&::after {
|
||||
border: 2px solid transparent;
|
||||
border-radius: 4px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
&:focus::after {
|
||||
&:focus-visible::after {
|
||||
offset = -6px;
|
||||
|
||||
content: '';
|
||||
border-color: white;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
top: offset;
|
||||
left: offset;
|
||||
right: offset;
|
||||
bottom: offset;
|
||||
}
|
||||
|
||||
&.bx-circular {
|
||||
&::after {
|
||||
border-radius: var(--bx-button-height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -121,6 +177,7 @@ button.bx-inactive {
|
||||
.bx-button-shortcut {
|
||||
max-width: max-content;
|
||||
margin: 10px 0 0 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media (min-width: 568px) and (max-height: 480px) {
|
||||
|
@ -1,226 +0,0 @@
|
||||
.bx-settings-reload-button {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.bx-settings-container {
|
||||
background-color: #151515;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
color: #fff;
|
||||
font-family: var(--bx-normal-font);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.bx-settings-wrapper a.bx-settings-title:hover {
|
||||
color: #83f73a;
|
||||
}
|
||||
}
|
||||
|
||||
.bx-settings-wrapper {
|
||||
min-width: 450px;
|
||||
max-width: 600px;
|
||||
margin: auto;
|
||||
padding: 12px 6px;
|
||||
|
||||
@media screen and (max-width: 450px) {
|
||||
min-width: unset;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
*:focus {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.bx-top-buttons {
|
||||
.bx-button {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.bx-settings-title-wrapper {
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
a.bx-settings-title {
|
||||
font-family: var(--bx-title-font);
|
||||
font-size: 1.4rem;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
flex: 1;
|
||||
text-transform: none;
|
||||
margin-right: 10px;
|
||||
|
||||
span {
|
||||
color: #5dc21e !important;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
span {
|
||||
color: #83f73a !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a.bx-settings-update {
|
||||
display: block;
|
||||
color: #ff834b;
|
||||
text-decoration: none;
|
||||
margin-bottom: 8px;
|
||||
text-align: center;
|
||||
background: #222;
|
||||
border-radius: 4px;
|
||||
padding: 4px;
|
||||
|
||||
&:hover {
|
||||
@media (hover: hover) {
|
||||
color: #ff9869;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
color: #ff9869;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bx-settings-group-label {
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
|
||||
.bx-settings-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 6px 12px;
|
||||
position: relative;
|
||||
|
||||
label {
|
||||
align-self: center;
|
||||
margin: 0 4px 0;
|
||||
}
|
||||
|
||||
.bx-setting-control {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: right;
|
||||
}
|
||||
|
||||
|
||||
&:hover, &:focus-within {
|
||||
background-color: #242424;
|
||||
}
|
||||
|
||||
input {
|
||||
align-self: center;
|
||||
accent-color: var(--bx-primary-button-color);
|
||||
|
||||
&:focus {
|
||||
accent-color: var(--bx-danger-button-color);
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
&:disabled {
|
||||
-webkit-appearance: none;
|
||||
background: transparent;
|
||||
text-align-last: right;
|
||||
border: none;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
input[type=checkbox] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type=checkbox],
|
||||
select {
|
||||
&:focus {
|
||||
filter: drop-shadow(1px 0 0 #fff) drop-shadow(-1px 0 0 #fff) drop-shadow(0 1px 0 #fff) drop-shadow(0 -1px 0 #fff);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&:has(input:focus), &:has(select:focus), &:has(button:focus) {
|
||||
&::before {
|
||||
content: ' ';
|
||||
border-radius: 4px;
|
||||
border: 2px solid #fff;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bx-settings-group-label b, .bx-settings-row label b {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
font-style: italic;
|
||||
font-weight: normal;
|
||||
color: #828282;
|
||||
}
|
||||
|
||||
.bx-settings-group-label b {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.bx-settings-app-version {
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
color: #747474;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.bx-donation-link {
|
||||
display: block;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
font-size: 14px;
|
||||
margin-top: 10px;
|
||||
color: #5dc21e;
|
||||
|
||||
&:hover {
|
||||
color: #6dd72b;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.bx-settings-custom-user-agent {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.bx-debug-info {
|
||||
button {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin-top: 10px;
|
||||
cursor: copy;
|
||||
color: white;
|
||||
padding: 8px;
|
||||
border: 1px solid #2d2d2d;
|
||||
background: #212121;
|
||||
white-space: break-spaces;
|
||||
|
||||
&:hover {
|
||||
background: #272727;
|
||||
}
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@
|
||||
|
||||
svg {
|
||||
width: 24px;
|
||||
height: 46px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
|
18
src/assets/css/navigation-dialog.styl
Normal file
@ -0,0 +1,18 @@
|
||||
.bx-navigation-dialog {
|
||||
position: absolute;
|
||||
z-index: var(--bx-navigation-dialog-z-index);
|
||||
}
|
||||
|
||||
.bx-navigation-dialog-overlay {
|
||||
position: fixed;
|
||||
background: #0b0b0be3;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: var(--bx-navigation-dialog-overlay-z-index);
|
||||
|
||||
&[data-is-playing="true"] {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
@ -1,3 +1,18 @@
|
||||
button_color(name, normal, hover, active, disabled)
|
||||
prefix = unquote('--bx-' + name + '-button');
|
||||
{prefix + '-color'}: normal;
|
||||
{prefix + '-rgb'}: red(normal), green(normal), blue(normal);
|
||||
|
||||
{prefix + '-hover-color'}: hover;
|
||||
{prefix + '-hover-rgb'}: red(hover), green(hover), blue(hover);
|
||||
|
||||
{prefix + '-active-color'}: active;
|
||||
{prefix + '-active-rgb'}: red(active), green(active), blue(active);
|
||||
|
||||
{prefix + '-disabled-color'}: disabled;
|
||||
{prefix + '-disabled-rgb'}: red(disabled), green(disabled), blue(disabled);
|
||||
|
||||
|
||||
:root {
|
||||
--bx-title-font: Bahnschrift, Arial, Helvetica, sans-serif;
|
||||
--bx-title-font-semibold: Bahnschrift Semibold, Arial, Helvetica, sans-serif;
|
||||
@ -7,27 +22,22 @@
|
||||
|
||||
--bx-button-height: 40px;
|
||||
|
||||
--bx-default-button-color: #2d3036;
|
||||
--bx-default-button-hover-color: #515863;
|
||||
--bx-default-button-disabled-color: #8e8e8e;
|
||||
|
||||
--bx-primary-button-color: #008746;
|
||||
--bx-primary-button-hover-color: #04b358;
|
||||
--bx-primary-button-disabled-color: #448262;
|
||||
|
||||
--bx-danger-button-color: #c10404;
|
||||
--bx-danger-button-hover-color: #e61d1d;
|
||||
--bx-danger-button-disabled-color: #a26c6c;
|
||||
button_color('default', #2d3036, #515863, #222428, #8e8e8e);
|
||||
button_color('primary', #008746, #04b358, #044e2a, #448262);
|
||||
button_color('danger', #c10404, #e61d1d, #a26c6c, #df5656);
|
||||
|
||||
--bx-toast-z-index: 9999;
|
||||
--bx-dialog-z-index: 9101;
|
||||
--bx-dialog-overlay-z-index: 9100;
|
||||
--bx-remote-play-popup-z-index: 9090;
|
||||
--bx-stats-bar-z-index: 9010;
|
||||
--bx-stream-settings-z-index: 9001;
|
||||
--bx-mkb-pointer-lock-msg-z-index: 9000;
|
||||
--bx-stream-settings-overlay-z-index: 8999;
|
||||
--bx-game-bar-z-index: 8888;
|
||||
|
||||
--bx-navigation-dialog-z-index: 8999;
|
||||
--bx-navigation-dialog-overlay-z-index: 8998;
|
||||
|
||||
--bx-remote-play-popup-z-index: 2000;
|
||||
|
||||
--bx-game-bar-z-index: 1000;
|
||||
--bx-wait-time-box-z-index: 100;
|
||||
--bx-screenshot-animation-z-index: 1;
|
||||
}
|
||||
@ -65,6 +75,14 @@ div[class^=HUDButton-module__hiddenContainer] ~ div:not([class^=HUDButton-module
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.bx-hide-scroll-bar {
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.bx-gone {
|
||||
display: none !important;
|
||||
}
|
||||
@ -106,7 +124,11 @@ div[class^=HUDButton-module__hiddenContainer] ~ div:not([class^=HUDButton-module
|
||||
}
|
||||
|
||||
.bx-line-through {
|
||||
text-decoration: line-through
|
||||
text-decoration: line-through !important;
|
||||
}
|
||||
|
||||
.bx-normal-case {
|
||||
text-transform: none !important;
|
||||
}
|
||||
|
||||
select[multiple] {
|
||||
|
@ -1,41 +1,82 @@
|
||||
.bx-stream-settings-dialog {
|
||||
.bx-settings-dialog {
|
||||
display: flex;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: var(--bx-stream-settings-z-index);
|
||||
opacity: 0.98;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.bx-stream-settings-overlay {
|
||||
position: fixed;
|
||||
background: #0b0b0be3;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: var(--bx-stream-settings-overlay-z-index);
|
||||
.bx-focusable {
|
||||
&::after {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&[data-is-playing="true"] {
|
||||
background: transparent;
|
||||
&:focus::after {
|
||||
offset = 0;
|
||||
|
||||
top: offset;
|
||||
left: offset;
|
||||
right: offset;
|
||||
bottom: offset;
|
||||
}
|
||||
}
|
||||
|
||||
.bx-settings-reload-note {
|
||||
font-size: 0.8rem;
|
||||
display: block;
|
||||
padding: 8px;
|
||||
font-style: italic;
|
||||
font-weight: normal;
|
||||
height: var(--bx-button-height);
|
||||
}
|
||||
}
|
||||
|
||||
.bx-stream-settings-tabs {
|
||||
display: flex;
|
||||
.bx-settings-tabs-container {
|
||||
position: fixed;
|
||||
width: 48px;
|
||||
max-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> div:last-of-type {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: end;
|
||||
|
||||
button {
|
||||
flex-shrink: 0;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
margin-top: 8px;
|
||||
height: unset;
|
||||
padding: 8px 10px;
|
||||
|
||||
svg {
|
||||
size = 16px;
|
||||
|
||||
width: size;
|
||||
height: size;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bx-settings-tabs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 0 0 0 8px;
|
||||
box-shadow: 0px 0px 6px #000;
|
||||
overflow: clip;
|
||||
box-shadow: 0 0 6px #000;
|
||||
overflow: overlay;
|
||||
flex: 1;
|
||||
|
||||
svg {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
size = 24px;
|
||||
width: size;
|
||||
height: size;
|
||||
padding: 10px;
|
||||
flex-shrink: 0;
|
||||
box-sizing: content-box;
|
||||
background: #131313;
|
||||
cursor: pointer;
|
||||
@ -55,14 +96,28 @@
|
||||
border-color: #fff;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&[data-group=global] {
|
||||
&[data-need-refresh=true] {
|
||||
background: var(--bx-danger-button-color) !important;
|
||||
|
||||
&:hover {
|
||||
background: var(--bx-danger-button-hover-color) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.bx-stream-settings-tab-contents {
|
||||
.bx-settings-tab-contents {
|
||||
tabsWidth = 48px;
|
||||
|
||||
flex-direction: column;
|
||||
padding: 14px 14px 0;
|
||||
width: 420px;
|
||||
padding: 10px;
|
||||
margin-left: tabsWidth;
|
||||
width: 450px;
|
||||
max-width: calc(100vw - tabsWidth);
|
||||
background: #1a1b1e;
|
||||
color: #fff;
|
||||
font-weight: 400;
|
||||
@ -71,7 +126,6 @@
|
||||
text-align: center;
|
||||
box-shadow: 0px 0px 6px #000;
|
||||
overflow: overlay;
|
||||
margin-left: 56px;
|
||||
z-index: 1;
|
||||
|
||||
> div[data-tab-group=mkb] {
|
||||
@ -81,99 +135,7 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&:focus,
|
||||
*:focus {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
align-item: center;
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
text-align: left;
|
||||
flex: 1;
|
||||
height: var(--bx-button-height);
|
||||
line-height: calc(var(--bx-button-height) + 4px);
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 500px) {
|
||||
.bx-stream-settings-tab-contents {
|
||||
width: calc(100vw - 56px);
|
||||
}
|
||||
}
|
||||
|
||||
.bx-stream-settings-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
border-bottom: 1px solid #40404080;
|
||||
padding: 16px 8px;
|
||||
border-left: 2px solid transparent;
|
||||
|
||||
&:hover, &:focus-within {
|
||||
background-color: #242424;
|
||||
}
|
||||
|
||||
input[type=checkbox],
|
||||
select {
|
||||
&:focus {
|
||||
filter: drop-shadow(1px 0 0 #fff) drop-shadow(-1px 0 0 #fff) drop-shadow(0 1px 0 #fff) drop-shadow(0 -1px 0 #fff);
|
||||
}
|
||||
}
|
||||
|
||||
&:has(input:focus), &:has(select:focus), &:has(button:focus) {
|
||||
border-left-color: white;
|
||||
}
|
||||
|
||||
> label {
|
||||
font-size: 16px;
|
||||
display: block;
|
||||
text-align: left;
|
||||
flex: 1;
|
||||
align-self: center;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
input {
|
||||
accent-color: var(--bx-primary-button-color);
|
||||
|
||||
&:focus {
|
||||
accent-color: var(--bx-danger-button-color);
|
||||
}
|
||||
}
|
||||
|
||||
select:disabled {
|
||||
-webkit-appearance: none;
|
||||
background: transparent;
|
||||
text-align-last: right;
|
||||
border: none;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
select option:disabled {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.bx-stream-settings-dialog-note {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
font-weight: lighter;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.bx-stream-settings-tab-contents {
|
||||
div[data-tab-group="shortcuts"] {
|
||||
> div[data-tab-group=shortcuts] {
|
||||
> div {
|
||||
&[data-has-gamepad=true] {
|
||||
> div:first-of-type {
|
||||
@ -229,10 +191,191 @@
|
||||
|
||||
&:last-of-type {
|
||||
opacity: 0;
|
||||
z-index: calc(var(--bx-stream-settings-z-index) + 1);
|
||||
z-index: calc(var(--bx-settings-z-index) + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:focus,
|
||||
*:focus {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.bx-top-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.bx-button {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 16px 0 8px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
flex: 1;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
.bx-settings-tab-contents {
|
||||
width: calc(100vw - 48px);
|
||||
}
|
||||
}
|
||||
|
||||
.bx-settings-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
border-bottom: 1px solid #2c2c2e;
|
||||
padding: 16px 8px;
|
||||
margin: 0;
|
||||
border-left: 2px solid transparent;
|
||||
|
||||
&:hover, &:focus-within {
|
||||
background-color: #242424;
|
||||
}
|
||||
|
||||
&:not(:has(> input[type=checkbox])) {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
input[type=checkbox],
|
||||
select {
|
||||
&:focus {
|
||||
filter: drop-shadow(1px 0 0 #fff) drop-shadow(-1px 0 0 #fff) drop-shadow(0 1px 0 #fff) drop-shadow(0 -1px 0 #fff);
|
||||
}
|
||||
}
|
||||
|
||||
&:has(input:focus), &:has(select:focus), &:has(button:focus) {
|
||||
border-left-color: white;
|
||||
}
|
||||
|
||||
> span.bx-settings-label {
|
||||
font-size: 14px;
|
||||
display: block;
|
||||
text-align: left;
|
||||
align-self: center;
|
||||
margin-bottom: 0 !important;
|
||||
|
||||
+ * {
|
||||
margin: 0 0 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
accent-color: var(--bx-primary-button-color);
|
||||
|
||||
&:focus {
|
||||
accent-color: var(--bx-danger-button-color);
|
||||
}
|
||||
}
|
||||
|
||||
select:disabled {
|
||||
-webkit-appearance: none;
|
||||
background: transparent;
|
||||
text-align-last: right;
|
||||
border: none;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
select option:disabled {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.bx-settings-dialog-note {
|
||||
display: block;
|
||||
color: #afafb0;
|
||||
font-size: 12px;
|
||||
font-weight: lighter;
|
||||
font-style: italic;
|
||||
|
||||
&:not(:has(a)) {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.bx-settings-custom-user-agent {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.bx-donation-link {
|
||||
display: block;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
font-size: 14px;
|
||||
margin-top: 10px;
|
||||
color: #5dc21e;
|
||||
|
||||
&:hover {
|
||||
color: #6dd72b;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.bx-debug-info {
|
||||
button {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin-top: 10px;
|
||||
cursor: copy;
|
||||
color: white;
|
||||
padding: 8px;
|
||||
border: 1px solid #2d2d2d;
|
||||
background: #212121;
|
||||
white-space: break-spaces;
|
||||
text-align: left;
|
||||
|
||||
&:hover {
|
||||
background: #272727;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bx-settings-app-version {
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
color: #747474;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.bx-note-unsupported {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
font-style: italic;
|
||||
font-weight: normal;
|
||||
color: #828282;
|
||||
}
|
@ -2,8 +2,9 @@
|
||||
|
||||
@import 'button.styl';
|
||||
@import 'header.styl';
|
||||
@import 'global-settings.styl';
|
||||
@import 'dialog.styl';
|
||||
@import 'navigation-dialog.styl';
|
||||
@import 'settings-dialog.styl';
|
||||
@import 'toast.styl';
|
||||
@import 'loading-screen.styl';
|
||||
@import 'remote-play.styl';
|
||||
@ -13,5 +14,4 @@
|
||||
@import 'number-stepper.styl';
|
||||
@import 'game-bar.styl';
|
||||
@import 'stream-stats.styl';
|
||||
@import 'stream-settings.styl';
|
||||
@import 'mkb.styl';
|
||||
|
@ -12,4 +12,4 @@
|
||||
// @updateURL https://raw.githubusercontent.com/redphx/better-xcloud/typescript/dist/better-xcloud.meta.js
|
||||
// @downloadURL https://github.com/redphx/better-xcloud/releases/latest/download/better-xcloud.user.js
|
||||
// ==/UserScript==
|
||||
'use strict';
|
||||
"use strict";
|
||||
|
4
src/assets/svg/better-xcloud.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='#fff' fill-rule='evenodd' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='2' stroke-width='2' viewBox='0 0 32 32'>
|
||||
<path d='M16.001 7.236h-2.328c-.443 0-1.941-.851-2.357-.905-.824-.106-1.684 0-2.489.176a13.04 13.04 0 0 0-3.137 1.14c-.392.275-.677.668-.866 1.104v.03l-3.302 8.963-.015.015c-.288.867-.553 3.75-.5 4.279a4.89 4.89 0 0 0 1.022 2.55c.654.823 3.71 1.364 4.057 1.016l4.462-4.475c.185-.186 1.547-.706 2.01-.706h6.884c.463 0 1.825.52 2.01.706l4.462 4.475c.347.348 3.403-.193 4.057-1.016a4.89 4.89 0 0 0 1.022-2.55c.053-.529-.212-3.412-.5-4.279l-.015-.015-3.302-8.963v-.03c-.189-.436-.474-.829-.866-1.104a13.04 13.04 0 0 0-3.137-1.14c-.805-.176-1.665-.282-2.489-.176-.416.054-1.914.905-2.357.905h-2.328' fill='none' stroke='#fff'/>
|
||||
<path d='M8.172 12.914H6.519c-.235 0-.315.267-.335.452l-.052.578c0 .193.033.384.054.576.023.202.091.511.355.511h1.631l-.001 1.652c0 .234.266.315.452.335l.578.052c.193 0 .384-.033.576-.054.203-.023.511-.091.511-.355V15.03l1.652.001c.234 0 .315-.266.335-.452l.052-.578c-.001-.193-.033-.385-.055-.577-.022-.202-.09-.51-.354-.51h-1.632v-1.652c0-.234-.266-.315-.453-.335l-.577-.052c-.193 0-.385.033-.577.054-.202.023-.51.091-.51.355v1.631m16.546 2.994h-3.487c-.206 0-.413-.043-.604-.121-.177-.072-.339-.183-.476-.316-.149-.144-.259-.315-.341-.504-.156-.361-.172-.788-.032-1.157a1.57 1.57 0 0 1 .459-.641c.106-.089.223-.164.349-.222a1.52 1.52 0 0 1 .423-.123c.167-.024.338-.02.504.012a1.83 1.83 0 0 1 .455-.482 1.62 1.62 0 0 1 .522-.252c.307-.089.651-.09.959-.003a1.75 1.75 0 0 1 1.009.764 1.83 1.83 0 0 1 .251.721c.156 0 .312.031.456.09a1.24 1.24 0 0 1 .372.248c.091.087.165.19.221.302a1.19 1.19 0 0 1-.173 1.299c-.119.132-.276.239-.441.305a1.17 1.17 0 0 1-.426.08z' fill='#fff' stroke='none'/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.8 KiB |
4
src/assets/svg/close.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='#fff' fill-rule='evenodd' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 32 32'>
|
||||
<path d='M29.928,2.072L2.072,29.928'/>
|
||||
<path d='M29.928,29.928L2.072,2.072'/>
|
||||
</svg>
|
After Width: | Height: | Size: 264 B |
@ -1,4 +1,4 @@
|
||||
<svg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='#fff' fill-rule='evenodd' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 32 32'>
|
||||
<path d="M13.253 3.639c0-.758-.615-1.373-1.373-1.373H3.639c-.758 0-1.373.615-1.373 1.373v8.241c0 .758.615 1.373 1.373 1.373h8.241c.758 0 1.373-.615 1.373-1.373V3.639zm0 16.481c0-.758-.615-1.373-1.373-1.373H3.639c-.758 0-1.373.615-1.373 1.373v8.241c0 .758.615 1.373 1.373 1.373h8.241c.758 0 1.373-.615 1.373-1.373V20.12zm16.481 0c0-.758-.615-1.373-1.373-1.373H20.12c-.758 0-1.373.615-1.373 1.373v8.241c0 .758.615 1.373 1.373 1.373h8.241c.758 0 1.373-.615 1.373-1.373V20.12zM19.262 7.76h9.957"/>
|
||||
<path d="M24.24 2.781v9.957"/>
|
||||
<path d='M13.253 3.639c0-.758-.615-1.373-1.373-1.373H3.639c-.758 0-1.373.615-1.373 1.373v8.241c0 .758.615 1.373 1.373 1.373h8.241c.758 0 1.373-.615 1.373-1.373V3.639zm0 16.481c0-.758-.615-1.373-1.373-1.373H3.639c-.758 0-1.373.615-1.373 1.373v8.241c0 .758.615 1.373 1.373 1.373h8.241c.758 0 1.373-.615 1.373-1.373V20.12zm16.481 0c0-.758-.615-1.373-1.373-1.373H20.12c-.758 0-1.373.615-1.373 1.373v8.241c0 .758.615 1.373 1.373 1.373h8.241c.758 0 1.373-.615 1.373-1.373V20.12zM19.262 7.76h9.957'/>
|
||||
<path d='M24.24 2.781v9.957'/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 711 B After Width: | Height: | Size: 711 B |
@ -1,10 +1,10 @@
|
||||
<svg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='#fff' fill-rule='evenodd' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 32 32'>
|
||||
<g stroke-width="2.1">
|
||||
<path d="m15.817 6h-10.604c-2.215 0-4.013 1.798-4.013 4.013v12.213c0 2.215 1.798 4.013 4.013 4.013h11.21"/>
|
||||
<path d="m5.698 20.617h1.124m-1.124-4.517h7.9m-7.881-4.5h7.9m-2.3 9h2.2"/>
|
||||
<g stroke-width='2.1'>
|
||||
<path d='m15.817 6h-10.604c-2.215 0-4.013 1.798-4.013 4.013v12.213c0 2.215 1.798 4.013 4.013 4.013h11.21'/>
|
||||
<path d='m5.698 20.617h1.124m-1.124-4.517h7.9m-7.881-4.5h7.9m-2.3 9h2.2'/>
|
||||
</g>
|
||||
<g stroke-width="2.13">
|
||||
<path d="m30.805 13.1c0-3.919-3.181-7.1-7.1-7.1s-7.1 3.181-7.1 7.1v6.4c0 3.919 3.182 7.1 7.1 7.1s7.1-3.181 7.1-7.1z"/>
|
||||
<path d="m23.705 14.715v-4.753"/>
|
||||
<g stroke-width='2.13'>
|
||||
<path d='m30.805 13.1c0-3.919-3.181-7.1-7.1-7.1s-7.1 3.181-7.1 7.1v6.4c0 3.919 3.182 7.1 7.1 7.1s7.1-3.181 7.1-7.1z'/>
|
||||
<path d='m23.705 14.715v-4.753'/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 619 B After Width: | Height: | Size: 619 B |
@ -1,11 +1,11 @@
|
||||
<svg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='#fff' fill-rule='evenodd' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 32 32'>
|
||||
<g stroke-width="2.06">
|
||||
<path d="M8.417 13.218h4.124"/>
|
||||
<path d="M10.479 11.155v4.125"/>
|
||||
<path d="M12.787 19.404L7.36 25.565a3.61 3.61 0 0 1-2.551 1.056A3.63 3.63 0 0 1 1.2 23.013c0-.21.018-.42.055-.626l2.108-10.845C3.923 8.356 6.714 6.007 9.949 6h5.192"/>
|
||||
<g stroke-width='2.06'>
|
||||
<path d='M8.417 13.218h4.124'/>
|
||||
<path d='M10.479 11.155v4.125'/>
|
||||
<path d='M12.787 19.404L7.36 25.565a3.61 3.61 0 0 1-2.551 1.056A3.63 3.63 0 0 1 1.2 23.013c0-.21.018-.42.055-.626l2.108-10.845C3.923 8.356 6.714 6.007 9.949 6h5.192'/>
|
||||
</g>
|
||||
<g stroke-width="2.11">
|
||||
<path d="M30.8 13.1c0-3.919-3.181-7.1-7.1-7.1s-7.1 3.181-7.1 7.1v6.421c0 3.919 3.181 7.1 7.1 7.1s7.1-3.181 7.1-7.1V13.1z"/>
|
||||
<path d="M23.7 14.724V9.966"/>
|
||||
<g stroke-width='2.11'>
|
||||
<path d='M30.8 13.1c0-3.919-3.181-7.1-7.1-7.1s-7.1 3.181-7.1 7.1v6.421c0 3.919 3.181 7.1 7.1 7.1s7.1-3.181 7.1-7.1V13.1z'/>
|
||||
<path d='M23.7 14.724V9.966'/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 680 B After Width: | Height: | Size: 680 B |
@ -1,8 +1,10 @@
|
||||
import { t } from "@/utils/translation"
|
||||
|
||||
export const BypassServers = {
|
||||
'br': 'Brazil',
|
||||
'jp': 'Japan',
|
||||
'pl': 'Poland',
|
||||
'us': 'United States',
|
||||
'br': t('brazil'),
|
||||
'jp': t('japan'),
|
||||
'pl': t('poland'),
|
||||
'us': t('united-states'),
|
||||
}
|
||||
|
||||
export const BypassServerIps = {
|
||||
|
@ -1,5 +1,6 @@
|
||||
export enum GamePassCloudGallery {
|
||||
ALL = '29a81209-df6f-41fd-a528-2ae6b91f719c',
|
||||
MOST_POPULAR = 'e7590b22-e299-44db-ae22-25c61405454c',
|
||||
NATIVE_MKB = '8fa264dd-124f-4af3-97e8-596fcdf4b486',
|
||||
TOUCH = '9c86f07a-f3e8-45ad-82a0-a1f759597059',
|
||||
}
|
||||
|
101
src/enums/pref-keys.ts
Normal file
@ -0,0 +1,101 @@
|
||||
export enum StorageKey {
|
||||
GLOBAL = 'better_xcloud',
|
||||
}
|
||||
|
||||
export enum PrefKey {
|
||||
LAST_UPDATE_CHECK = 'version_last_check',
|
||||
LATEST_VERSION = 'version_latest',
|
||||
CURRENT_VERSION = 'version_current',
|
||||
|
||||
BETTER_XCLOUD_LOCALE = 'bx_locale',
|
||||
|
||||
SERVER_REGION = 'server_region',
|
||||
SERVER_BYPASS_RESTRICTION = 'server_bypass_restriction',
|
||||
|
||||
PREFER_IPV6_SERVER = 'prefer_ipv6_server',
|
||||
STREAM_TARGET_RESOLUTION = 'stream_target_resolution',
|
||||
STREAM_PREFERRED_LOCALE = 'stream_preferred_locale',
|
||||
STREAM_CODEC_PROFILE = 'stream_codec_profile',
|
||||
|
||||
USER_AGENT_PROFILE = 'user_agent_profile',
|
||||
STREAM_SIMPLIFY_MENU = 'stream_simplify_menu',
|
||||
|
||||
STREAM_COMBINE_SOURCES = 'stream_combine_sources',
|
||||
|
||||
STREAM_TOUCH_CONTROLLER = 'stream_touch_controller',
|
||||
STREAM_TOUCH_CONTROLLER_AUTO_OFF = 'stream_touch_controller_auto_off',
|
||||
STREAM_TOUCH_CONTROLLER_DEFAULT_OPACITY = 'stream_touch_controller_default_opacity',
|
||||
STREAM_TOUCH_CONTROLLER_STYLE_STANDARD = 'stream_touch_controller_style_standard',
|
||||
STREAM_TOUCH_CONTROLLER_STYLE_CUSTOM = 'stream_touch_controller_style_custom',
|
||||
|
||||
STREAM_DISABLE_FEEDBACK_DIALOG = 'stream_disable_feedback_dialog',
|
||||
|
||||
BITRATE_VIDEO_MAX = 'bitrate_video_max',
|
||||
|
||||
GAME_BAR_POSITION = 'game_bar_position',
|
||||
|
||||
LOCAL_CO_OP_ENABLED = 'local_co_op_enabled',
|
||||
// LOCAL_CO_OP_SEPARATE_TOUCH_CONTROLLER = 'local_co_op_separate_touch_controller',
|
||||
|
||||
CONTROLLER_ENABLE_SHORTCUTS = 'controller_enable_shortcuts',
|
||||
CONTROLLER_ENABLE_VIBRATION = 'controller_enable_vibration',
|
||||
CONTROLLER_DEVICE_VIBRATION = 'controller_device_vibration',
|
||||
CONTROLLER_VIBRATION_INTENSITY = 'controller_vibration_intensity',
|
||||
CONTROLLER_SHOW_CONNECTION_STATUS = 'controller_show_connection_status',
|
||||
|
||||
NATIVE_MKB_ENABLED = 'native_mkb_enabled',
|
||||
NATIVE_MKB_SCROLL_HORIZONTAL_SENSITIVITY = 'native_mkb_scroll_x_sensitivity',
|
||||
NATIVE_MKB_SCROLL_VERTICAL_SENSITIVITY = 'native_mkb_scroll_y_sensitivity',
|
||||
|
||||
MKB_ENABLED = 'mkb_enabled',
|
||||
MKB_HIDE_IDLE_CURSOR = 'mkb_hide_idle_cursor',
|
||||
MKB_ABSOLUTE_MOUSE = 'mkb_absolute_mouse',
|
||||
MKB_DEFAULT_PRESET_ID = 'mkb_default_preset_id',
|
||||
|
||||
SCREENSHOT_APPLY_FILTERS = 'screenshot_apply_filters',
|
||||
|
||||
BLOCK_TRACKING = 'block_tracking',
|
||||
BLOCK_SOCIAL_FEATURES = 'block_social_features',
|
||||
SKIP_SPLASH_VIDEO = 'skip_splash_video',
|
||||
HIDE_DOTS_ICON = 'hide_dots_icon',
|
||||
REDUCE_ANIMATIONS = 'reduce_animations',
|
||||
|
||||
UI_LOADING_SCREEN_GAME_ART = 'ui_loading_screen_game_art',
|
||||
UI_LOADING_SCREEN_WAIT_TIME = 'ui_loading_screen_wait_time',
|
||||
UI_LOADING_SCREEN_ROCKET = 'ui_loading_screen_rocket',
|
||||
|
||||
UI_CONTROLLER_FRIENDLY = 'ui_controller_friendly',
|
||||
UI_LAYOUT = 'ui_layout',
|
||||
UI_SCROLLBAR_HIDE = 'ui_scrollbar_hide',
|
||||
UI_HIDE_SECTIONS = 'ui_hide_sections',
|
||||
|
||||
UI_HOME_CONTEXT_MENU_DISABLED = 'ui_home_context_menu_disabled',
|
||||
UI_GAME_CARD_SHOW_WAIT_TIME = 'ui_game_card_show_wait_time',
|
||||
|
||||
VIDEO_PLAYER_TYPE = 'video_player_type',
|
||||
VIDEO_PROCESSING = 'video_processing',
|
||||
VIDEO_POWER_PREFERENCE = 'video_power_preference',
|
||||
VIDEO_SHARPNESS = 'video_sharpness',
|
||||
VIDEO_RATIO = 'video_ratio',
|
||||
VIDEO_BRIGHTNESS = 'video_brightness',
|
||||
VIDEO_CONTRAST = 'video_contrast',
|
||||
VIDEO_SATURATION = 'video_saturation',
|
||||
|
||||
AUDIO_MIC_ON_PLAYING = 'audio_mic_on_playing',
|
||||
AUDIO_ENABLE_VOLUME_CONTROL = 'audio_enable_volume_control',
|
||||
AUDIO_VOLUME = 'audio_volume',
|
||||
|
||||
STATS_ITEMS = 'stats_items',
|
||||
STATS_SHOW_WHEN_PLAYING = 'stats_show_when_playing',
|
||||
STATS_QUICK_GLANCE = 'stats_quick_glance',
|
||||
STATS_POSITION = 'stats_position',
|
||||
STATS_TEXT_SIZE = 'stats_text_size',
|
||||
STATS_TRANSPARENT = 'stats_transparent',
|
||||
STATS_OPACITY = 'stats_opacity',
|
||||
STATS_CONDITIONAL_FORMATTING = 'stats_conditional_formatting',
|
||||
|
||||
REMOTE_PLAY_ENABLED = 'xhome_enabled',
|
||||
REMOTE_PLAY_RESOLUTION = 'xhome_resolution',
|
||||
|
||||
GAME_FORTNITE_FORCE_CONSOLE = 'game_fortnite_force_console',
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
export enum UiSection {
|
||||
NEWS = 'news',
|
||||
ALL_GAMES = 'all-games',
|
||||
FRIENDS = 'friends',
|
||||
MOST_POPULAR = 'most-popular',
|
||||
ALL_GAMES = 'all-games',
|
||||
NATIVE_MKB = 'native-mkb',
|
||||
NEWS = 'news',
|
||||
TOUCH = 'touch',
|
||||
}
|
||||
|
18
src/index.ts
@ -11,8 +11,6 @@ import { StreamBadges } from "@modules/stream/stream-badges";
|
||||
import { StreamStats } from "@modules/stream/stream-stats";
|
||||
import { addCss, preloadFonts } from "@utils/css";
|
||||
import { Toast } from "@utils/toast";
|
||||
import { setupStreamUi } from "@modules/ui/ui";
|
||||
import { PrefKey, getPref } from "@utils/preferences";
|
||||
import { LoadingScreen } from "@modules/loading-screen";
|
||||
import { MouseCursorHider } from "@modules/mkb/mouse-cursor-hider";
|
||||
import { TouchController } from "@modules/touch-controller";
|
||||
@ -30,12 +28,14 @@ import { GameBar } from "./modules/game-bar/game-bar";
|
||||
import { Screenshot } from "./utils/screenshot";
|
||||
import { NativeMkbHandler } from "./modules/mkb/native-mkb-handler";
|
||||
import { GuideMenu, GuideMenuTab } from "./modules/ui/guide-menu";
|
||||
import { StreamSettings } from "./modules/stream/stream-settings";
|
||||
import { updateVideoPlayer } from "./modules/stream/stream-settings-utils";
|
||||
import { UiSection } from "./enums/ui-sections";
|
||||
import { HeaderSection } from "./modules/ui/header";
|
||||
import { GameTile } from "./modules/ui/game-tile";
|
||||
import { ProductDetailsPage } from "./modules/ui/product-details";
|
||||
import { NavigationDialogManager } from "./modules/ui/dialog/navigation-dialog";
|
||||
import { PrefKey } from "./enums/pref-keys";
|
||||
import { getPref } from "./utils/settings-storages/global-settings-storage";
|
||||
|
||||
|
||||
// Handle login page
|
||||
@ -110,7 +110,7 @@ document.addEventListener('readystatechange', e => {
|
||||
return;
|
||||
}
|
||||
|
||||
STATES.isSignedIn = (window as any).xbcUser.isSignedIn;
|
||||
STATES.isSignedIn = (window as any).xbcUser?.isSignedIn;
|
||||
|
||||
if (STATES.isSignedIn) {
|
||||
// Preload Remote Play
|
||||
@ -125,6 +125,9 @@ document.addEventListener('readystatechange', e => {
|
||||
const $parent = document.querySelector('div[class*=PlayWithFriendsSkeleton]')?.closest('div[class*=HomePage-module]') as HTMLElement;
|
||||
$parent && ($parent.style.display = 'none');
|
||||
}
|
||||
|
||||
// Preload fonts
|
||||
preloadFonts();
|
||||
})
|
||||
|
||||
window.BX_EXPOSED = BxExposed;
|
||||
@ -159,9 +162,6 @@ window.addEventListener(BxEvent.STREAM_LOADING, e => {
|
||||
STATES.currentStream.titleId = 'remote-play';
|
||||
STATES.currentStream.productId = '';
|
||||
}
|
||||
|
||||
// Setup UI
|
||||
setupStreamUi();
|
||||
});
|
||||
|
||||
// Setup loading screen
|
||||
@ -223,7 +223,7 @@ function unload() {
|
||||
window.BX_EXPOSED.shouldShowSensorControls = false;
|
||||
window.BX_EXPOSED.stopTakRendering = false;
|
||||
|
||||
StreamSettings.getInstance().hide();
|
||||
NavigationDialogManager.getInstance().hide();
|
||||
StreamStats.getInstance().onStoppedPlaying();
|
||||
|
||||
MouseCursorHider.stop();
|
||||
@ -325,10 +325,8 @@ function main() {
|
||||
|
||||
// Setup UI
|
||||
addCss();
|
||||
preloadFonts();
|
||||
Toast.setup();
|
||||
(getPref(PrefKey.GAME_BAR_POSITION) !== 'off') && GameBar.getInstance();
|
||||
BX_FLAGS.PreloadUi && setupStreamUi();
|
||||
Screenshot.setup();
|
||||
|
||||
GuideMenu.observe();
|
||||
|
@ -11,3 +11,8 @@ const generatedCss = await (stylus(cssStr, {})
|
||||
export const renderStylus = () => {
|
||||
return generatedCss;
|
||||
};
|
||||
|
||||
|
||||
export const compressCss = async (css: string) => {
|
||||
return await (stylus(css, {}).set('compress', true)).render();
|
||||
};
|
||||
|
@ -7,13 +7,18 @@ import { EmulatedMkbHandler } from "./mkb/mkb-handler";
|
||||
import { StreamStats } from "./stream/stream-stats";
|
||||
import { MicrophoneShortcut } from "./shortcuts/shortcut-microphone";
|
||||
import { StreamUiShortcut } from "./shortcuts/shortcut-stream-ui";
|
||||
import { PrefKey, getPref } from "@utils/preferences";
|
||||
import { SoundShortcut } from "./shortcuts/shortcut-sound";
|
||||
import { BxEvent } from "@/utils/bx-event";
|
||||
import { AppInterface } from "@/utils/global";
|
||||
import { BxSelectElement } from "@/web-components/bx-select";
|
||||
import { setNearby } from "@/utils/navigation-utils";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref } from "@/utils/settings-storages/global-settings-storage";
|
||||
import { SettingsNavigationDialog } from "./ui/dialog/settings-dialog";
|
||||
|
||||
const enum ShortcutAction {
|
||||
BETTER_XCLOUD_SETTINGS_SHOW = 'bx-settings-show',
|
||||
|
||||
enum ShortcutAction {
|
||||
STREAM_SCREENSHOT_CAPTURE = 'stream-screenshot-capture',
|
||||
|
||||
STREAM_MENU_SHOW = 'stream-menu-show',
|
||||
@ -42,7 +47,7 @@ export class ControllerShortcut {
|
||||
static #$selectActions: Partial<{[key in GamepadKey]: HTMLSelectElement}> = {};
|
||||
static #$container: HTMLElement;
|
||||
|
||||
static #ACTIONS: {[key: string]: (ShortcutAction | null)[]} = {};
|
||||
static #ACTIONS: {[key: string]: (ShortcutAction | null)[]} | null = null;
|
||||
|
||||
static reset(index: number) {
|
||||
ControllerShortcut.#buttonsCache[index] = [];
|
||||
@ -50,8 +55,12 @@ export class ControllerShortcut {
|
||||
}
|
||||
|
||||
static handle(gamepad: Gamepad): boolean {
|
||||
if (!ControllerShortcut.#ACTIONS) {
|
||||
ControllerShortcut.#ACTIONS = ControllerShortcut.#getActionsFromStorage();
|
||||
}
|
||||
|
||||
const gamepadIndex = gamepad.index;
|
||||
const actions = ControllerShortcut.#ACTIONS[gamepad.id];
|
||||
const actions = ControllerShortcut.#ACTIONS![gamepad.id];
|
||||
if (!actions) {
|
||||
return false;
|
||||
}
|
||||
@ -83,6 +92,10 @@ export class ControllerShortcut {
|
||||
|
||||
static #runAction(action: ShortcutAction) {
|
||||
switch (action) {
|
||||
case ShortcutAction.BETTER_XCLOUD_SETTINGS_SHOW:
|
||||
SettingsNavigationDialog.getInstance().show();
|
||||
break;
|
||||
|
||||
case ShortcutAction.STREAM_SCREENSHOT_CAPTURE:
|
||||
Screenshot.takeScreenshot();
|
||||
break;
|
||||
@ -122,15 +135,16 @@ export class ControllerShortcut {
|
||||
}
|
||||
|
||||
static #updateAction(profile: string, button: GamepadKey, action: ShortcutAction | null) {
|
||||
if (!(profile in ControllerShortcut.#ACTIONS)) {
|
||||
ControllerShortcut.#ACTIONS[profile] = [];
|
||||
const actions = ControllerShortcut.#ACTIONS!;
|
||||
if (!(profile in actions)) {
|
||||
actions[profile] = [];
|
||||
}
|
||||
|
||||
if (!action) {
|
||||
action = null;
|
||||
}
|
||||
|
||||
ControllerShortcut.#ACTIONS[profile][button] = action;
|
||||
actions[profile][button] = action;
|
||||
|
||||
// Remove empty profiles
|
||||
for (const key in ControllerShortcut.#ACTIONS) {
|
||||
@ -194,7 +208,7 @@ export class ControllerShortcut {
|
||||
}
|
||||
|
||||
static #switchProfile(profile: string) {
|
||||
let actions = ControllerShortcut.#ACTIONS[profile];
|
||||
let actions = ControllerShortcut.#ACTIONS![profile];
|
||||
if (!actions) {
|
||||
actions = [];
|
||||
}
|
||||
@ -212,11 +226,15 @@ export class ControllerShortcut {
|
||||
}
|
||||
}
|
||||
|
||||
static #getActionsFromStorage() {
|
||||
return JSON.parse(window.localStorage.getItem(ControllerShortcut.#STORAGE_KEY) || '{}');
|
||||
}
|
||||
|
||||
static renderSettings() {
|
||||
const PREF_CONTROLLER_FRIENDLY_UI = getPref(PrefKey.UI_CONTROLLER_FRIENDLY);
|
||||
|
||||
// Read actions from localStorage
|
||||
ControllerShortcut.#ACTIONS = JSON.parse(window.localStorage.getItem(ControllerShortcut.#STORAGE_KEY) || '{}');
|
||||
ControllerShortcut.#ACTIONS = ControllerShortcut.#getActionsFromStorage();
|
||||
|
||||
const buttons: Map<GamepadKey, PrompFont> = new Map();
|
||||
buttons.set(GamepadKey.Y, PrompFont.Y);
|
||||
@ -242,6 +260,10 @@ export class ControllerShortcut {
|
||||
buttons.set(GamepadKey.R3, PrompFont.R3);
|
||||
|
||||
const actions: {[key: string]: Partial<{[key in ShortcutAction]: string | string[]}>} = {
|
||||
[t('better-xcloud')]: {
|
||||
[ShortcutAction.BETTER_XCLOUD_SETTINGS_SHOW]: [t('settings'), t('show')],
|
||||
},
|
||||
|
||||
[t('device')]: AppInterface && {
|
||||
[ShortcutAction.DEVICE_SOUND_TOGGLE]: [t('sound'), t('toggle')],
|
||||
[ShortcutAction.DEVICE_VOLUME_INC]: [t('volume'), t('increase')],
|
||||
@ -261,7 +283,7 @@ export class ControllerShortcut {
|
||||
[ShortcutAction.STREAM_MENU_SHOW]: [t('menu'), t('show')],
|
||||
[ShortcutAction.STREAM_STATS_TOGGLE]: [t('stats'), t('show-hide')],
|
||||
[ShortcutAction.STREAM_MICROPHONE_TOGGLE]: [t('microphone'), t('toggle')],
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const $baseSelect = CE<HTMLSelectElement>('select', {autocomplete: 'off'}, CE('option', {value: ''}, '---'));
|
||||
@ -293,13 +315,24 @@ export class ControllerShortcut {
|
||||
let $remap: HTMLElement;
|
||||
const $selectProfile = CE<HTMLSelectElement>('select', {class: 'bx-shortcut-profile', autocomplete: 'off'});
|
||||
|
||||
const $container = CE('div', {'data-has-gamepad': 'false'},
|
||||
const $profile = PREF_CONTROLLER_FRIENDLY_UI ? BxSelectElement.wrap($selectProfile) : $selectProfile;
|
||||
|
||||
const $container = CE('div', {
|
||||
'data-has-gamepad': 'false',
|
||||
_nearby: {
|
||||
focus: $profile,
|
||||
},
|
||||
},
|
||||
CE('div', {},
|
||||
CE('p', {class: 'bx-shortcut-note'}, t('controller-shortcuts-connect-note')),
|
||||
),
|
||||
|
||||
$remap = CE('div', {},
|
||||
PREF_CONTROLLER_FRIENDLY_UI ? CE('div', {'data-focus-container': 'true'}, BxSelectElement.wrap($selectProfile)) : $selectProfile,
|
||||
CE('div', {
|
||||
_nearby: {
|
||||
focus: $profile,
|
||||
},
|
||||
}, $profile),
|
||||
CE('p', {class: 'bx-shortcut-note'},
|
||||
CE('span', {class: 'bx-prompt'}, PrompFont.HOME),
|
||||
': ' + t('controller-shortcuts-xbox-note'),
|
||||
@ -337,7 +370,6 @@ export class ControllerShortcut {
|
||||
for (const [button, prompt] of buttons) {
|
||||
const $row = CE('div', {
|
||||
class: 'bx-shortcut-row',
|
||||
'data-focus-container': 'true',
|
||||
});
|
||||
|
||||
const $label = CE('label', {class: 'bx-prompt'}, `${PrompFont.HOME} + ${prompt}`);
|
||||
@ -359,9 +391,16 @@ export class ControllerShortcut {
|
||||
ControllerShortcut.#$selectActions[button] = $select;
|
||||
|
||||
if (PREF_CONTROLLER_FRIENDLY_UI) {
|
||||
$div.appendChild(BxSelectElement.wrap($select));
|
||||
const $bxSelect = BxSelectElement.wrap($select);
|
||||
$div.appendChild($bxSelect);
|
||||
setNearby($row, {
|
||||
focus: $bxSelect,
|
||||
});
|
||||
} else {
|
||||
$div.appendChild($select);
|
||||
setNearby($row, {
|
||||
focus: $select,
|
||||
});
|
||||
}
|
||||
|
||||
$row.appendChild($label);
|
||||
|
@ -5,8 +5,9 @@ import { BxEvent } from "@utils/bx-event";
|
||||
import { BxIcon } from "@utils/bx-icon";
|
||||
import type { BaseGameBarAction } from "./action-base";
|
||||
import { STATES } from "@utils/global";
|
||||
import { PrefKey, getPref } from "@utils/preferences";
|
||||
import { MicrophoneAction } from "./action-microphone";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref } from "@/utils/settings-storages/global-settings-storage";
|
||||
|
||||
|
||||
export class GameBar {
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { CE } from "@utils/html";
|
||||
import { getPreferredServerRegion } from "@utils/region";
|
||||
import { PrefKey, getPref } from "@utils/preferences";
|
||||
import { t } from "@utils/translation";
|
||||
import { STATES } from "@utils/global";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref } from "@/utils/settings-storages/global-settings-storage";
|
||||
|
||||
export class LoadingScreen {
|
||||
static #$bgStyle: HTMLElement;
|
||||
|
@ -2,7 +2,6 @@ import { MkbPreset } from "./mkb-preset";
|
||||
import { GamepadKey, MkbPresetKey, GamepadStick, MouseMapTo, WheelCode } from "@enums/mkb";
|
||||
import { createButton, ButtonStyle, CE } from "@utils/html";
|
||||
import { BxEvent } from "@utils/bx-event";
|
||||
import { PrefKey, getPref } from "@utils/preferences";
|
||||
import { Toast } from "@utils/toast";
|
||||
import { t } from "@utils/translation";
|
||||
import { LocalDb } from "@utils/local-db";
|
||||
@ -14,7 +13,10 @@ import { BxLogger } from "@utils/bx-logger";
|
||||
import { PointerClient } from "./pointer-client";
|
||||
import { NativeMkbHandler } from "./native-mkb-handler";
|
||||
import { MkbHandler, MouseDataProvider } from "./base-mkb-handler";
|
||||
import { StreamSettings } from "../stream/stream-settings";
|
||||
import { SettingsNavigationDialog } from "../ui/dialog/settings-dialog";
|
||||
import { NavigationDialogManager } from "../ui/dialog/navigation-dialog";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref } from "@/utils/settings-storages/global-settings-storage";
|
||||
|
||||
const LOG_TAG = 'MkbHandler';
|
||||
|
||||
@ -507,7 +509,10 @@ export class EmulatedMkbHandler extends MkbHandler {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
StreamSettings.getInstance().show('mkb');
|
||||
// Show Settings dialog & focus the MKB tab
|
||||
const dialog = SettingsNavigationDialog.getInstance();
|
||||
dialog.focusTab('mkb');
|
||||
NavigationDialogManager.getInstance().show(dialog);
|
||||
},
|
||||
}),
|
||||
),
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { t } from "@utils/translation";
|
||||
import { SettingElementType } from "@utils/settings";
|
||||
import { GamepadKey, MouseButtonCode, MouseMapTo, MkbPresetKey } from "@enums/mkb";
|
||||
import { EmulatedMkbHandler } from "./mkb-handler";
|
||||
import type { MkbPresetData, MkbConvertedPresetData } from "@/types/mkb";
|
||||
import type { PreferenceSettings } from "@/types/preferences";
|
||||
import { SettingElementType } from "@/utils/setting-element";
|
||||
|
||||
|
||||
export class MkbPreset {
|
||||
|
@ -1,16 +1,17 @@
|
||||
import { CE, createButton, ButtonStyle } from "@utils/html";
|
||||
import { t } from "@utils/translation";
|
||||
import { Dialog } from "@modules/dialog";
|
||||
import { getPref, setPref, PrefKey } from "@utils/preferences";
|
||||
import { KeyHelper } from "./key-helper";
|
||||
import { MkbPreset } from "./mkb-preset";
|
||||
import { EmulatedMkbHandler } from "./mkb-handler";
|
||||
import { LocalDb } from "@utils/local-db";
|
||||
import { BxIcon } from "@utils/bx-icon";
|
||||
import { SettingElement } from "@utils/settings";
|
||||
import type { MkbPresetData, MkbStoredPresets } from "@/types/mkb";
|
||||
import { MkbPresetKey, GamepadKey, GamepadKeyName } from "@enums/mkb";
|
||||
import { deepClone } from "@utils/global";
|
||||
import { SettingElement } from "@/utils/setting-element";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref, setPref } from "@/utils/settings-storages/global-settings-storage";
|
||||
|
||||
|
||||
type MkbRemapperElements = {
|
||||
@ -317,7 +318,7 @@ export class MkbRemapper {
|
||||
render() {
|
||||
this.#$.wrapper = CE('div', {'class': 'bx-mkb-settings'});
|
||||
|
||||
this.#$.presetsSelect = CE<HTMLSelectElement>('select', {});
|
||||
this.#$.presetsSelect = CE<HTMLSelectElement>('select', {tabindex: -1});
|
||||
this.#$.presetsSelect!.addEventListener('change', e => {
|
||||
this.#switchPreset(parseInt((e.target as HTMLSelectElement).value));
|
||||
});
|
||||
@ -336,80 +337,84 @@ export class MkbRemapper {
|
||||
};
|
||||
|
||||
const $header = CE('div', {'class': 'bx-mkb-preset-tools'},
|
||||
this.#$.presetsSelect,
|
||||
// Rename button
|
||||
createButton({
|
||||
title: t('rename'),
|
||||
icon: BxIcon.CURSOR_TEXT,
|
||||
onClick: e => {
|
||||
const preset = this.#getCurrentPreset();
|
||||
this.#$.presetsSelect,
|
||||
// Rename button
|
||||
createButton({
|
||||
title: t('rename'),
|
||||
icon: BxIcon.CURSOR_TEXT,
|
||||
tabIndex: -1,
|
||||
onClick: e => {
|
||||
const preset = this.#getCurrentPreset();
|
||||
|
||||
let newName = promptNewName(preset.name);
|
||||
if (!newName || newName === preset.name) {
|
||||
let newName = promptNewName(preset.name);
|
||||
if (!newName || newName === preset.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update preset with new name
|
||||
preset.name = newName;
|
||||
LocalDb.INSTANCE.updatePreset(preset).then(id => this.#refresh());
|
||||
},
|
||||
}),
|
||||
|
||||
// New button
|
||||
createButton({
|
||||
icon: BxIcon.NEW,
|
||||
title: t('new'),
|
||||
tabIndex: -1,
|
||||
onClick: e => {
|
||||
let newName = promptNewName('');
|
||||
if (!newName) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update preset with new name
|
||||
preset.name = newName;
|
||||
LocalDb.INSTANCE.updatePreset(preset).then(id => this.#refresh());
|
||||
// Create new preset selected name
|
||||
LocalDb.INSTANCE.newPreset(newName, MkbPreset.DEFAULT_PRESET).then(id => {
|
||||
this.#STATE.currentPresetId = id;
|
||||
this.#refresh();
|
||||
});
|
||||
},
|
||||
}),
|
||||
|
||||
// New button
|
||||
createButton({
|
||||
icon: BxIcon.NEW,
|
||||
title: t('new'),
|
||||
onClick: e => {
|
||||
let newName = promptNewName('');
|
||||
if (!newName) {
|
||||
return;
|
||||
}
|
||||
// Copy button
|
||||
createButton({
|
||||
icon: BxIcon.COPY,
|
||||
title: t('copy'),
|
||||
tabIndex: -1,
|
||||
onClick: e => {
|
||||
const preset = this.#getCurrentPreset();
|
||||
|
||||
// Create new preset selected name
|
||||
LocalDb.INSTANCE.newPreset(newName, MkbPreset.DEFAULT_PRESET).then(id => {
|
||||
this.#STATE.currentPresetId = id;
|
||||
this.#refresh();
|
||||
});
|
||||
},
|
||||
}),
|
||||
let newName = promptNewName(`${preset.name} (2)`);
|
||||
if (!newName) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Copy button
|
||||
createButton({
|
||||
icon: BxIcon.COPY,
|
||||
title: t('copy'),
|
||||
onClick: e => {
|
||||
const preset = this.#getCurrentPreset();
|
||||
// Create new preset selected name
|
||||
LocalDb.INSTANCE.newPreset(newName, preset.data).then(id => {
|
||||
this.#STATE.currentPresetId = id;
|
||||
this.#refresh();
|
||||
});
|
||||
},
|
||||
}),
|
||||
|
||||
let newName = promptNewName(`${preset.name} (2)`);
|
||||
if (!newName) {
|
||||
return;
|
||||
}
|
||||
// Delete button
|
||||
createButton({
|
||||
icon: BxIcon.TRASH,
|
||||
style: ButtonStyle.DANGER,
|
||||
title: t('delete'),
|
||||
tabIndex: -1,
|
||||
onClick: e => {
|
||||
if (!confirm(t('confirm-delete-preset'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new preset selected name
|
||||
LocalDb.INSTANCE.newPreset(newName, preset.data).then(id => {
|
||||
this.#STATE.currentPresetId = id;
|
||||
this.#refresh();
|
||||
});
|
||||
},
|
||||
}),
|
||||
|
||||
// Delete button
|
||||
createButton({
|
||||
icon: BxIcon.TRASH,
|
||||
style: ButtonStyle.DANGER,
|
||||
title: t('delete'),
|
||||
onClick: e => {
|
||||
if (!confirm(t('confirm-delete-preset'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
LocalDb.INSTANCE.deletePreset(this.#STATE.currentPresetId).then(id => {
|
||||
this.#STATE.currentPresetId = 0;
|
||||
this.#refresh();
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
LocalDb.INSTANCE.deletePreset(this.#STATE.currentPresetId).then(id => {
|
||||
this.#STATE.currentPresetId = 0;
|
||||
this.#refresh();
|
||||
});
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
this.#$.wrapper!.appendChild($header);
|
||||
|
||||
@ -426,11 +431,11 @@ export class MkbRemapper {
|
||||
const $fragment = document.createDocumentFragment();
|
||||
for (let i = 0; i < keysPerButton; i++) {
|
||||
$elm = CE('button', {
|
||||
type: 'button',
|
||||
'data-prompt': buttonPrompt,
|
||||
'data-button-index': buttonIndex,
|
||||
'data-key-slot': i,
|
||||
}, ' ');
|
||||
type: 'button',
|
||||
'data-prompt': buttonPrompt,
|
||||
'data-button-index': buttonIndex,
|
||||
'data-key-slot': i,
|
||||
}, ' ');
|
||||
|
||||
$elm.addEventListener('mouseup', this.#onBindingKey);
|
||||
$elm.addEventListener('contextmenu', this.#onContextMenu);
|
||||
@ -440,9 +445,9 @@ export class MkbRemapper {
|
||||
}
|
||||
|
||||
const $keyRow = CE('div', {'class': 'bx-mkb-key-row'},
|
||||
CE('label', {'title': buttonName}, buttonPrompt),
|
||||
$fragment,
|
||||
);
|
||||
CE('label', {'title': buttonName}, buttonPrompt),
|
||||
$fragment,
|
||||
);
|
||||
|
||||
$rows.appendChild($keyRow);
|
||||
}
|
||||
@ -460,10 +465,13 @@ export class MkbRemapper {
|
||||
const onChange = (e: Event, value: any) => {
|
||||
(this.#STATE.editingPresetData!.mouse as any)[key] = value;
|
||||
};
|
||||
const $row = CE('div', {'class': 'bx-stream-settings-row'},
|
||||
CE('label', {'for': `bx_setting_${key}`}, setting.label),
|
||||
$elm = SettingElement.render(setting.type, key, setting, value, onChange, setting.params),
|
||||
);
|
||||
const $row = CE('label', {
|
||||
class: 'bx-settings-row',
|
||||
for: `bx_setting_${key}`
|
||||
},
|
||||
CE('span', {class: 'bx-settings-label'}, setting.label),
|
||||
$elm = SettingElement.render(setting.type, key, setting, value, onChange, setting.params),
|
||||
);
|
||||
|
||||
$mouseSettings.appendChild($row);
|
||||
this.#$.allMouseElements[key as MkbPresetKey] = $elm;
|
||||
@ -474,59 +482,63 @@ export class MkbRemapper {
|
||||
|
||||
// Render action buttons
|
||||
const $actionButtons = CE('div', {'class': 'bx-mkb-action-buttons'},
|
||||
CE('div', {},
|
||||
// Edit button
|
||||
createButton({
|
||||
label: t('edit'),
|
||||
onClick: e => this.#toggleEditing(true),
|
||||
}),
|
||||
CE('div', {},
|
||||
// Edit button
|
||||
createButton({
|
||||
label: t('edit'),
|
||||
tabIndex: -1,
|
||||
onClick: e => this.#toggleEditing(true),
|
||||
}),
|
||||
|
||||
// Activate button
|
||||
this.#$.activateButton = createButton({
|
||||
label: t('activate'),
|
||||
style: ButtonStyle.PRIMARY,
|
||||
onClick: e => {
|
||||
setPref(PrefKey.MKB_DEFAULT_PRESET_ID, this.#STATE.currentPresetId);
|
||||
EmulatedMkbHandler.getInstance().refreshPresetData();
|
||||
// Activate button
|
||||
this.#$.activateButton = createButton({
|
||||
label: t('activate'),
|
||||
style: ButtonStyle.PRIMARY,
|
||||
tabIndex: -1,
|
||||
onClick: e => {
|
||||
setPref(PrefKey.MKB_DEFAULT_PRESET_ID, this.#STATE.currentPresetId);
|
||||
EmulatedMkbHandler.getInstance().refreshPresetData();
|
||||
|
||||
this.#refresh();
|
||||
},
|
||||
}),
|
||||
),
|
||||
this.#refresh();
|
||||
},
|
||||
}),
|
||||
),
|
||||
|
||||
CE('div', {},
|
||||
// Cancel button
|
||||
createButton({
|
||||
label: t('cancel'),
|
||||
style: ButtonStyle.GHOST,
|
||||
onClick: e => {
|
||||
// Restore preset
|
||||
this.#switchPreset(this.#STATE.currentPresetId);
|
||||
this.#toggleEditing(false);
|
||||
},
|
||||
}),
|
||||
CE('div', {},
|
||||
// Cancel button
|
||||
createButton({
|
||||
label: t('cancel'),
|
||||
style: ButtonStyle.GHOST,
|
||||
tabIndex: -1,
|
||||
onClick: e => {
|
||||
// Restore preset
|
||||
this.#switchPreset(this.#STATE.currentPresetId);
|
||||
this.#toggleEditing(false);
|
||||
},
|
||||
}),
|
||||
|
||||
// Save button
|
||||
createButton({
|
||||
label: t('save'),
|
||||
style: ButtonStyle.PRIMARY,
|
||||
onClick: e => {
|
||||
const updatedPreset = deepClone(this.#getCurrentPreset());
|
||||
updatedPreset.data = this.#STATE.editingPresetData as MkbPresetData;
|
||||
// Save button
|
||||
createButton({
|
||||
label: t('save'),
|
||||
style: ButtonStyle.PRIMARY,
|
||||
tabIndex: -1,
|
||||
onClick: e => {
|
||||
const updatedPreset = deepClone(this.#getCurrentPreset());
|
||||
updatedPreset.data = this.#STATE.editingPresetData as MkbPresetData;
|
||||
|
||||
LocalDb.INSTANCE.updatePreset(updatedPreset).then(id => {
|
||||
// If this is the default preset => refresh preset data
|
||||
if (id === getPref(PrefKey.MKB_DEFAULT_PRESET_ID)) {
|
||||
EmulatedMkbHandler.getInstance().refreshPresetData();
|
||||
}
|
||||
LocalDb.INSTANCE.updatePreset(updatedPreset).then(id => {
|
||||
// If this is the default preset => refresh preset data
|
||||
if (id === getPref(PrefKey.MKB_DEFAULT_PRESET_ID)) {
|
||||
EmulatedMkbHandler.getInstance().refreshPresetData();
|
||||
}
|
||||
|
||||
this.#toggleEditing(false);
|
||||
this.#refresh();
|
||||
});
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
this.#toggleEditing(false);
|
||||
this.#refresh();
|
||||
});
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
this.#$.wrapper!.appendChild($actionButtons);
|
||||
|
||||
|
@ -5,7 +5,8 @@ import { MkbHandler } from "./base-mkb-handler";
|
||||
import { t } from "@/utils/translation";
|
||||
import { BxEvent } from "@/utils/bx-event";
|
||||
import { ButtonStyle, CE, createButton } from "@/utils/html";
|
||||
import { PrefKey, getPref } from "@/utils/preferences";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref } from "@/utils/settings-storages/global-settings-storage";
|
||||
|
||||
type NativeMouseData = {
|
||||
X: number,
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { AppInterface, SCRIPT_VERSION, STATES } from "@utils/global";
|
||||
import { BX_FLAGS } from "@utils/bx-flags";
|
||||
import { getPref, PrefKey } from "@utils/preferences";
|
||||
import { VibrationManager } from "@modules/vibration-manager";
|
||||
import { BxLogger } from "@utils/bx-logger";
|
||||
import { hashCode, renderString } from "@utils/utils";
|
||||
@ -15,6 +14,9 @@ import codeRemotePlayKeepAlive from "./patches/remote-play-keep-alive.js" with {
|
||||
import codeVibrationAdjust from "./patches/vibration-adjust.js" with { type: "text" };
|
||||
import { FeatureGates } from "@/utils/feature-gates.js";
|
||||
import { UiSection } from "@/enums/ui-sections.js";
|
||||
import { PrefKey } from "@/enums/pref-keys.js";
|
||||
import { getPref } from "@/utils/settings-storages/global-settings-storage";
|
||||
import { GamePassCloudGallery } from "@/enums/game-pass-gallery.js";
|
||||
|
||||
type PatchArray = (keyof typeof PATCHES)[];
|
||||
|
||||
@ -27,7 +29,7 @@ const PATCHES = {
|
||||
disableAiTrack(str: string) {
|
||||
const text = '.track=function(';
|
||||
const index = str.indexOf(text);
|
||||
if (index === -1) {
|
||||
if (index < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -94,7 +96,7 @@ const PATCHES = {
|
||||
// Replace "/direct-connect" with "/play"
|
||||
remotePlayDirectConnectUrl(str: string) {
|
||||
const index = str.indexOf('/direct-connect');
|
||||
if (index === -1) {
|
||||
if (index < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -160,7 +162,7 @@ if (!!window.BX_REMOTE_PLAY_CONFIG) {
|
||||
|
||||
patchPollGamepads(str: string) {
|
||||
const index = str.indexOf('},this.pollGamepads=()=>{');
|
||||
if (index === -1) {
|
||||
if (index < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -231,7 +233,7 @@ logFunc(logTag, '//', logMessage);
|
||||
// Override website's settings
|
||||
overrideSettings(str: string) {
|
||||
const index = str.indexOf(',EnableStreamGate:');
|
||||
if (index === -1) {
|
||||
if (index < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -249,7 +251,7 @@ logFunc(logTag, '//', logMessage);
|
||||
|
||||
disableGamepadDisconnectedScreen(str: string) {
|
||||
const index = str.indexOf('"GamepadDisconnected_Title",');
|
||||
if (index === -1) {
|
||||
if (index < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -286,7 +288,7 @@ logFunc(logTag, '//', logMessage);
|
||||
// Disable StreamGate
|
||||
disableStreamGate(str: string) {
|
||||
const index = str.indexOf('case"partially-ready":');
|
||||
if (index === -1) {
|
||||
if (index < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -316,7 +318,7 @@ window.dispatchEvent(new Event("${BxEvent.TOUCH_LAYOUT_MANAGER_READY}"));
|
||||
patchBabylonRendererClass(str: string) {
|
||||
// ()=>{a.current.render(),h.current=window.requestAnimationFrame(l)
|
||||
let index = str.indexOf('.current.render(),');
|
||||
if (index === -1) {
|
||||
if (index < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -454,7 +456,7 @@ BxEvent.dispatch(window, BxEvent.XCLOUD_POLLING_MODE_CHANGED, {mode: e});
|
||||
|
||||
patchGamepadPolling(str: string) {
|
||||
let index = str.indexOf('.shouldHandleGamepadInput)())return void');
|
||||
if (index === -1) {
|
||||
if (index < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -466,7 +468,7 @@ BxEvent.dispatch(window, BxEvent.XCLOUD_POLLING_MODE_CHANGED, {mode: e});
|
||||
patchXcloudTitleInfo(str: string) {
|
||||
const text = 'async cloudConnect';
|
||||
let index = str.indexOf(text);
|
||||
if (index === -1) {
|
||||
if (index < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -488,7 +490,7 @@ BxLogger.info('patchXcloudTitleInfo', ${titleInfoVar});
|
||||
patchRemotePlayMkb(str: string) {
|
||||
const text = 'async homeConsoleConnect';
|
||||
let index = str.indexOf(text);
|
||||
if (index === -1) {
|
||||
if (index < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -709,7 +711,7 @@ true` + text;
|
||||
index > -1 && (index = str.indexOf('return ', index));
|
||||
index > -1 && (index = str.indexOf('?', index));
|
||||
|
||||
if (index === -1) {
|
||||
if (index < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -720,12 +722,12 @@ true` + text;
|
||||
// Don't render "Play With Friends" sections
|
||||
ignorePlayWithFriendsSection(str: string) {
|
||||
let index = str.indexOf('location:"PlayWithFriendsRow",');
|
||||
if (index === -1) {
|
||||
if (index < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
index = str.indexOf('return', index - 50);
|
||||
if (index === -1) {
|
||||
if (index < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -736,14 +738,14 @@ true` + text;
|
||||
// Don't render "All Games" sections
|
||||
ignoreAllGamesSection(str: string) {
|
||||
let index = str.indexOf('className:"AllGamesRow-module__allGamesRowContainer');
|
||||
if (index === -1) {
|
||||
if (index < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
index = str.indexOf('grid:!0,', index);
|
||||
index > -1 && (index = str.indexOf('(0,', index - 70));
|
||||
|
||||
if (index === -1) {
|
||||
if (index < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -751,6 +753,61 @@ true` + text;
|
||||
return str;
|
||||
},
|
||||
|
||||
// home-page.js
|
||||
ignorePlayWithTouchSection(str: string) {
|
||||
let index = str.indexOf('("Play_With_Touch"),');
|
||||
if (index < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
index = str.indexOf('const ', index - 100);
|
||||
if (index < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
str = str.substring(0, index) + 'return null;' + str.substring(index);
|
||||
return str;
|
||||
},
|
||||
|
||||
// home-page.js
|
||||
ignoreSiglSections(str: string) {
|
||||
let index = str.indexOf('SiglRow-module__heroCard___');
|
||||
if (index < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
index = str.indexOf('const[', index - 300);
|
||||
if (index < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const PREF_HIDE_SECTIONS = getPref(PrefKey.UI_HIDE_SECTIONS) as UiSection[];
|
||||
const siglIds: GamePassCloudGallery[] = [];
|
||||
|
||||
const sections: Partial<Record<UiSection, GamePassCloudGallery>> = {
|
||||
[UiSection.NATIVE_MKB]: GamePassCloudGallery.NATIVE_MKB,
|
||||
[UiSection.MOST_POPULAR]: GamePassCloudGallery.MOST_POPULAR,
|
||||
};
|
||||
|
||||
PREF_HIDE_SECTIONS.forEach(section => {
|
||||
const galleryId = sections[section];
|
||||
galleryId && siglIds.push(galleryId);
|
||||
});
|
||||
|
||||
const checkSyntax = siglIds.map(item => `siglId === "${item}"`).join(' || ');
|
||||
|
||||
const newCode = `
|
||||
if (e && e.id) {
|
||||
const siglId = e.id;
|
||||
if (${checkSyntax}) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
`;
|
||||
str = str.substring(0, index) + newCode + str.substring(index);
|
||||
return str;
|
||||
},
|
||||
|
||||
// Override Storage.getSettings()
|
||||
overrideStorageGetSettings(str: string) {
|
||||
const text = '}getSetting(e){';
|
||||
@ -774,12 +831,12 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) {
|
||||
// game-stream.js 24.16.4
|
||||
alwaysShowStreamHud(str: string) {
|
||||
let index = str.indexOf(',{onShowStreamMenu:');
|
||||
if (index === -1) {
|
||||
if (index < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
index = str.indexOf('&&(0,', index - 100);
|
||||
if (index === -1) {
|
||||
if (index < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -791,7 +848,7 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) {
|
||||
// 24225.js#4127, 24.17.11
|
||||
patchSetCurrentlyFocusedInteractable(str: string) {
|
||||
let index = str.indexOf('.setCurrentlyFocusedInteractable=(');
|
||||
if (index === -1) {
|
||||
if (index < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -803,12 +860,12 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) {
|
||||
// product-details-page.js#2388, 24.17.20
|
||||
detectProductDetailsPage(str: string) {
|
||||
let index = str.indexOf('{location:"ProductDetailPage",');
|
||||
if (index === -1) {
|
||||
if (index < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
index = str.indexOf('return', index - 40);
|
||||
if (index === -1) {
|
||||
if (index < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -847,6 +904,8 @@ let PATCH_ORDERS: PatchArray = [
|
||||
|
||||
getPref(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.FRIENDS) && 'ignorePlayWithFriendsSection',
|
||||
getPref(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.ALL_GAMES) && 'ignoreAllGamesSection',
|
||||
getPref(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.TOUCH) && 'ignorePlayWithTouchSection',
|
||||
(getPref(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.NATIVE_MKB) || getPref(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.MOST_POPULAR)) && 'ignoreSiglSections',
|
||||
|
||||
...(getPref(PrefKey.BLOCK_TRACKING) ? [
|
||||
'disableAiTrack',
|
||||
@ -978,7 +1037,8 @@ export class Patcher {
|
||||
}
|
||||
|
||||
const func = item[1][id];
|
||||
let str = func.toString();
|
||||
const funcStr = func.toString();
|
||||
let patchedFuncStr = funcStr;
|
||||
|
||||
let modified = false;
|
||||
|
||||
@ -993,15 +1053,15 @@ export class Patcher {
|
||||
}
|
||||
|
||||
// Check function against patch
|
||||
const patchedStr = PATCHES[patchName].call(null, str);
|
||||
const tmpStr = PATCHES[patchName].call(null, patchedFuncStr);
|
||||
|
||||
// Not patched
|
||||
if (!patchedStr) {
|
||||
if (!tmpStr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
modified = true;
|
||||
str = patchedStr;
|
||||
patchedFuncStr = tmpStr;
|
||||
|
||||
BxLogger.info(LOG_TAG, `✅ ${patchName}`);
|
||||
appliedPatches.push(patchName);
|
||||
@ -1014,7 +1074,13 @@ export class Patcher {
|
||||
|
||||
// Apply patched functions
|
||||
if (modified) {
|
||||
item[1][id] = eval(str);
|
||||
try {
|
||||
item[1][id] = eval(patchedFuncStr);
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
BxLogger.error(LOG_TAG, 'Error', appliedPatches, e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save to cache
|
||||
|
@ -1,7 +1,8 @@
|
||||
import vertClarityBoost from "./shaders/clarity_boost.vert" with { type: "text" };
|
||||
import fsClarityBoost from "./shaders/clarity_boost.fs" with { type: "text" };
|
||||
import { BxLogger } from "@/utils/bx-logger";
|
||||
import { getPref, PrefKey } from "@/utils/preferences";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref } from "@/utils/settings-storages/global-settings-storage";
|
||||
|
||||
|
||||
const LOG_TAG = 'WebGL2Player';
|
||||
|
@ -3,15 +3,16 @@ import { CE, createButton, ButtonStyle } from "@utils/html";
|
||||
import { BxIcon } from "@utils/bx-icon";
|
||||
import { Toast } from "@utils/toast";
|
||||
import { BxEvent } from "@utils/bx-event";
|
||||
import { getPref, PrefKey, setPref } from "@utils/preferences";
|
||||
import { t } from "@utils/translation";
|
||||
import { localRedirect } from "@modules/ui/ui";
|
||||
import { BxLogger } from "@utils/bx-logger";
|
||||
import { HeaderSection } from "./ui/header";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref, setPref } from "@/utils/settings-storages/global-settings-storage";
|
||||
|
||||
const LOG_TAG = 'RemotePlay';
|
||||
|
||||
enum RemotePlayConsoleState {
|
||||
const enum RemotePlayConsoleState {
|
||||
ON = 'On',
|
||||
OFF = 'Off',
|
||||
STANDBY = 'ConnectedStandby',
|
||||
@ -53,8 +54,8 @@ export class RemotePlay {
|
||||
env: {
|
||||
clientAppId: window.location.host,
|
||||
clientAppType: 'browser',
|
||||
clientAppVersion: '21.1.98',
|
||||
clientSdkVersion: '8.5.3',
|
||||
clientAppVersion: '24.17.36',
|
||||
clientSdkVersion: '10.1.14',
|
||||
httpEnvironment: 'prod',
|
||||
sdkInstallId: '',
|
||||
},
|
||||
@ -82,7 +83,7 @@ export class RemotePlay {
|
||||
},
|
||||
browser: {
|
||||
browserName: 'chrome',
|
||||
browserVersion: '119.0',
|
||||
browserVersion: '125.0',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { t } from "@utils/translation";
|
||||
import { STATES } from "@utils/global";
|
||||
import { PrefKey, getPref, setPref } from "@utils/preferences";
|
||||
import { Toast } from "@utils/toast";
|
||||
import { BxEvent } from "@/utils/bx-event";
|
||||
import { ceilToNearest, floorToNearest } from "@/utils/utils";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref, setPref } from "@/utils/settings-storages/global-settings-storage";
|
||||
|
||||
export class SoundShortcut {
|
||||
static adjustGainNodeVolume(amount: number): number {
|
||||
@ -27,14 +27,11 @@ export class SoundShortcut {
|
||||
newValue = currentValue + amount;
|
||||
}
|
||||
|
||||
newValue = setPref(PrefKey.AUDIO_VOLUME, newValue);
|
||||
newValue = setPref(PrefKey.AUDIO_VOLUME, newValue, true);
|
||||
SoundShortcut.setGainNodeVolume(newValue);
|
||||
|
||||
// Show toast
|
||||
Toast.show(`${t('stream')} ❯ ${t('volume')}`, newValue + '%', {instant: true});
|
||||
BxEvent.dispatch(window, BxEvent.GAINNODE_VOLUME_CHANGED, {
|
||||
volume: newValue,
|
||||
});
|
||||
|
||||
return newValue;
|
||||
}
|
||||
@ -51,10 +48,7 @@ export class SoundShortcut {
|
||||
let targetValue: number;
|
||||
if (settingValue === 0) { // settingValue is 0 => set to 100
|
||||
targetValue = 100;
|
||||
setPref(PrefKey.AUDIO_VOLUME, targetValue);
|
||||
BxEvent.dispatch(window, BxEvent.GAINNODE_VOLUME_CHANGED, {
|
||||
volume: targetValue,
|
||||
});
|
||||
setPref(PrefKey.AUDIO_VOLUME, targetValue, true);
|
||||
} else if (gainValue === 0) { // is being muted => set to settingValue
|
||||
targetValue = settingValue;
|
||||
} else { // not being muted => mute
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { CE } from "@/utils/html";
|
||||
import { WebGL2Player } from "./player/webgl2-player";
|
||||
import { getPref, PrefKey } from "@/utils/preferences";
|
||||
import { Screenshot } from "@/utils/screenshot";
|
||||
import { StreamPlayerType, StreamVideoProcessing } from "@enums/stream-player";
|
||||
import { STATES } from "@/utils/global";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref } from "@/utils/settings-storages/global-settings-storage";
|
||||
|
||||
export type StreamPlayerOptions = Partial<{
|
||||
processing: string,
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { StreamPlayerType, StreamVideoProcessing } from "@enums/stream-player";
|
||||
import { STATES } from "@utils/global";
|
||||
import { getPref, PrefKey, setPref } from "@utils/preferences";
|
||||
import { UserAgent } from "@utils/user-agent";
|
||||
import type { StreamPlayerOptions } from "../stream-player";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref, setPref } from "@/utils/settings-storages/global-settings-storage";
|
||||
|
||||
export function onChangeVideoPlayerType() {
|
||||
const playerType = getPref(PrefKey.VIDEO_PLAYER_TYPE);
|
||||
@ -10,16 +11,22 @@ export function onChangeVideoPlayerType() {
|
||||
const $videoSharpness = document.getElementById('bx_setting_video_sharpness') as HTMLElement;
|
||||
const $videoPowerPreference = document.getElementById('bx_setting_video_power_preference') as HTMLElement;
|
||||
|
||||
if (!$videoProcessing) {
|
||||
return;
|
||||
}
|
||||
|
||||
let isDisabled = false;
|
||||
|
||||
const $optCas = $videoProcessing.querySelector(`option[value=${StreamVideoProcessing.CAS}]`) as HTMLOptionElement;
|
||||
|
||||
if (playerType === StreamPlayerType.WEBGL2) {
|
||||
($videoProcessing.querySelector(`option[value=${StreamVideoProcessing.CAS}]`) as HTMLOptionElement).disabled = false;
|
||||
$optCas && ($optCas.disabled = false);
|
||||
} else {
|
||||
// Only allow USM when player type is Video
|
||||
$videoProcessing.value = StreamVideoProcessing.USM;
|
||||
setPref(PrefKey.VIDEO_PROCESSING, StreamVideoProcessing.USM);
|
||||
|
||||
($videoProcessing.querySelector(`option[value=${StreamVideoProcessing.CAS}]`) as HTMLOptionElement).disabled = true;
|
||||
$optCas && ($optCas.disabled = true);
|
||||
|
||||
if (UserAgent.isSafari()) {
|
||||
isDisabled = true;
|
||||
@ -30,7 +37,7 @@ export function onChangeVideoPlayerType() {
|
||||
$videoSharpness.dataset.disabled = isDisabled.toString();
|
||||
|
||||
// Hide Power Preference setting if renderer isn't WebGL2
|
||||
$videoPowerPreference.closest('.bx-stream-settings-row')!.classList.toggle('bx-gone', playerType !== StreamPlayerType.WEBGL2);
|
||||
$videoPowerPreference.closest('.bx-settings-row')!.classList.toggle('bx-gone', playerType !== StreamPlayerType.WEBGL2);
|
||||
|
||||
updateVideoPlayer();
|
||||
}
|
||||
|
@ -1,795 +0,0 @@
|
||||
import { BxEvent } from "@utils/bx-event";
|
||||
import { BxIcon } from "@utils/bx-icon";
|
||||
import { STATES, AppInterface } from "@utils/global";
|
||||
import { ButtonStyle, CE, createButton, createSvgIcon } from "@utils/html";
|
||||
import { PrefKey, Preferences, getPref, toPrefElement } from "@utils/preferences";
|
||||
import { t } from "@utils/translation";
|
||||
import { ControllerShortcut } from "../controller-shortcut";
|
||||
import { MkbRemapper } from "../mkb/mkb-remapper";
|
||||
import { NativeMkbHandler } from "../mkb/native-mkb-handler";
|
||||
import { SoundShortcut } from "../shortcuts/shortcut-sound";
|
||||
import { TouchController } from "../touch-controller";
|
||||
import { VibrationManager } from "../vibration-manager";
|
||||
import { StreamStats } from "./stream-stats";
|
||||
import { BxSelectElement } from "@/web-components/bx-select";
|
||||
import { onChangeVideoPlayerType, updateVideoPlayer } from "./stream-settings-utils";
|
||||
import { GamepadKey } from "@/enums/mkb";
|
||||
import { EmulatedMkbHandler } from "../mkb/mkb-handler";
|
||||
|
||||
enum NavigationDirection {
|
||||
UP = 1,
|
||||
RIGHT,
|
||||
DOWN,
|
||||
LEFT,
|
||||
}
|
||||
|
||||
enum FocusContainer {
|
||||
OUTSIDE,
|
||||
TABS,
|
||||
SETTINGS,
|
||||
}
|
||||
|
||||
export class StreamSettings {
|
||||
private static instance: StreamSettings;
|
||||
|
||||
public static getInstance(): StreamSettings {
|
||||
if (!StreamSettings.instance) {
|
||||
StreamSettings.instance = new StreamSettings();
|
||||
}
|
||||
|
||||
return StreamSettings.instance;
|
||||
}
|
||||
|
||||
static readonly MAIN_CLASS = 'bx-stream-settings-dialog';
|
||||
|
||||
private static readonly GAMEPAD_POLLING_INTERVAL = 50;
|
||||
private static readonly GAMEPAD_KEYS = [
|
||||
GamepadKey.UP,
|
||||
GamepadKey.DOWN,
|
||||
GamepadKey.LEFT,
|
||||
GamepadKey.RIGHT,
|
||||
GamepadKey.A,
|
||||
GamepadKey.B,
|
||||
GamepadKey.LB,
|
||||
GamepadKey.RB,
|
||||
];
|
||||
|
||||
private static readonly GAMEPAD_DIRECTION_MAP = {
|
||||
[GamepadKey.UP]: NavigationDirection.UP,
|
||||
[GamepadKey.DOWN]: NavigationDirection.DOWN,
|
||||
[GamepadKey.LEFT]: NavigationDirection.LEFT,
|
||||
[GamepadKey.RIGHT]: NavigationDirection.RIGHT,
|
||||
|
||||
[GamepadKey.LS_UP]: NavigationDirection.UP,
|
||||
[GamepadKey.LS_DOWN]: NavigationDirection.DOWN,
|
||||
[GamepadKey.LS_LEFT]: NavigationDirection.LEFT,
|
||||
[GamepadKey.LS_RIGHT]: NavigationDirection.RIGHT,
|
||||
};
|
||||
|
||||
private gamepadPollingIntervalId: number | null = null;
|
||||
private gamepadLastButtons: Array<GamepadKey | null> = [];
|
||||
|
||||
private $container: HTMLElement | undefined;
|
||||
private $tabs: HTMLElement | undefined;
|
||||
private $settings: HTMLElement | undefined;
|
||||
private $overlay: HTMLElement | undefined;
|
||||
|
||||
readonly SETTINGS_UI = [{
|
||||
icon: BxIcon.DISPLAY,
|
||||
group: 'stream',
|
||||
items: [{
|
||||
group: 'audio',
|
||||
label: t('audio'),
|
||||
help_url: 'https://better-xcloud.github.io/ingame-features/#audio',
|
||||
items: [{
|
||||
pref: PrefKey.AUDIO_VOLUME,
|
||||
onChange: (e: any, value: number) => {
|
||||
SoundShortcut.setGainNodeVolume(value);
|
||||
},
|
||||
params: {
|
||||
disabled: !getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL),
|
||||
},
|
||||
onMounted: ($elm: HTMLElement) => {
|
||||
const $range = $elm.querySelector('input[type=range') as HTMLInputElement;
|
||||
window.addEventListener(BxEvent.GAINNODE_VOLUME_CHANGED, e => {
|
||||
$range.value = (e as any).volume;
|
||||
BxEvent.dispatch($range, 'input', {
|
||||
ignoreOnChange: true,
|
||||
});
|
||||
});
|
||||
},
|
||||
}],
|
||||
}, {
|
||||
group: 'video',
|
||||
label: t('video'),
|
||||
help_url: 'https://better-xcloud.github.io/ingame-features/#video',
|
||||
items: [{
|
||||
pref: PrefKey.VIDEO_PLAYER_TYPE,
|
||||
onChange: onChangeVideoPlayerType,
|
||||
}, {
|
||||
pref: PrefKey.VIDEO_RATIO,
|
||||
onChange: updateVideoPlayer,
|
||||
}, {
|
||||
pref: PrefKey.VIDEO_PROCESSING,
|
||||
onChange: updateVideoPlayer,
|
||||
}, {
|
||||
pref: PrefKey.VIDEO_POWER_PREFERENCE,
|
||||
onChange: () => {
|
||||
const streamPlayer = STATES.currentStream.streamPlayer;
|
||||
if (!streamPlayer) {
|
||||
return;
|
||||
}
|
||||
|
||||
streamPlayer.reloadPlayer();
|
||||
updateVideoPlayer();
|
||||
},
|
||||
}, {
|
||||
pref: PrefKey.VIDEO_SHARPNESS,
|
||||
onChange: updateVideoPlayer,
|
||||
}, {
|
||||
pref: PrefKey.VIDEO_SATURATION,
|
||||
onChange: updateVideoPlayer,
|
||||
}, {
|
||||
pref: PrefKey.VIDEO_CONTRAST,
|
||||
onChange: updateVideoPlayer,
|
||||
}, {
|
||||
pref: PrefKey.VIDEO_BRIGHTNESS,
|
||||
onChange: updateVideoPlayer,
|
||||
}],
|
||||
}],
|
||||
}, {
|
||||
icon: BxIcon.CONTROLLER,
|
||||
group: 'controller',
|
||||
items: [{
|
||||
group: 'controller',
|
||||
label: t('controller'),
|
||||
help_url: 'https://better-xcloud.github.io/ingame-features/#controller',
|
||||
items: [{
|
||||
pref: PrefKey.CONTROLLER_ENABLE_VIBRATION,
|
||||
unsupported: !VibrationManager.supportControllerVibration(),
|
||||
onChange: () => VibrationManager.updateGlobalVars(),
|
||||
}, {
|
||||
pref: PrefKey.CONTROLLER_DEVICE_VIBRATION,
|
||||
unsupported: !VibrationManager.supportDeviceVibration(),
|
||||
onChange: () => VibrationManager.updateGlobalVars(),
|
||||
}, (VibrationManager.supportControllerVibration() || VibrationManager.supportDeviceVibration()) && {
|
||||
pref: PrefKey.CONTROLLER_VIBRATION_INTENSITY,
|
||||
unsupported: !VibrationManager.supportDeviceVibration(),
|
||||
onChange: () => VibrationManager.updateGlobalVars(),
|
||||
}],
|
||||
},
|
||||
|
||||
STATES.userAgent.capabilities.touch && {
|
||||
group: 'touch-controller',
|
||||
label: t('touch-controller'),
|
||||
items: [{
|
||||
label: t('layout'),
|
||||
content: CE('select', {disabled: true}, CE('option', {}, t('default'))),
|
||||
onMounted: ($elm: HTMLSelectElement) => {
|
||||
$elm.addEventListener('change', e => {
|
||||
TouchController.loadCustomLayout(STATES.currentStream?.xboxTitleId!, $elm.value, 1000);
|
||||
});
|
||||
|
||||
window.addEventListener(BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED, e => {
|
||||
const data = (e as any).data;
|
||||
|
||||
if (STATES.currentStream?.xboxTitleId && ($elm as any).xboxTitleId === STATES.currentStream?.xboxTitleId) {
|
||||
$elm.dispatchEvent(new Event('change'));
|
||||
return;
|
||||
}
|
||||
|
||||
($elm as any).xboxTitleId = STATES.currentStream?.xboxTitleId;
|
||||
|
||||
// Clear options
|
||||
while ($elm.firstChild) {
|
||||
$elm.removeChild($elm.firstChild);
|
||||
}
|
||||
|
||||
$elm.disabled = !data;
|
||||
if (!data) {
|
||||
$elm.appendChild(CE('option', {value: ''}, t('default')));
|
||||
$elm.value = '';
|
||||
$elm.dispatchEvent(new Event('change'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Add options
|
||||
const $fragment = document.createDocumentFragment();
|
||||
for (const key in data.layouts) {
|
||||
const layout = data.layouts[key];
|
||||
|
||||
let name;
|
||||
if (layout.author) {
|
||||
name = `${layout.name} (${layout.author})`;
|
||||
} else {
|
||||
name = layout.name;
|
||||
}
|
||||
|
||||
const $option = CE('option', {value: key}, name);
|
||||
$fragment.appendChild($option);
|
||||
}
|
||||
|
||||
$elm.appendChild($fragment);
|
||||
$elm.value = data.default_layout;
|
||||
$elm.dispatchEvent(new Event('change'));
|
||||
});
|
||||
},
|
||||
}],
|
||||
}],
|
||||
},
|
||||
|
||||
getPref(PrefKey.MKB_ENABLED) && {
|
||||
icon: BxIcon.VIRTUAL_CONTROLLER,
|
||||
group: 'mkb',
|
||||
items: [{
|
||||
group: 'mkb',
|
||||
label: t('virtual-controller'),
|
||||
help_url: 'https://better-xcloud.github.io/mouse-and-keyboard/',
|
||||
content: MkbRemapper.INSTANCE.render(),
|
||||
}],
|
||||
},
|
||||
|
||||
AppInterface && getPref(PrefKey.NATIVE_MKB_ENABLED) === 'on' && {
|
||||
icon: BxIcon.NATIVE_MKB,
|
||||
group: 'native-mkb',
|
||||
items: [{
|
||||
group: 'native-mkb',
|
||||
label: t('native-mkb'),
|
||||
items: [{
|
||||
pref: PrefKey.NATIVE_MKB_SCROLL_VERTICAL_SENSITIVITY,
|
||||
onChange: (e: any, value: number) => {
|
||||
NativeMkbHandler.getInstance().setVerticalScrollMultiplier(value / 100);
|
||||
},
|
||||
}, {
|
||||
pref: PrefKey.NATIVE_MKB_SCROLL_HORIZONTAL_SENSITIVITY,
|
||||
onChange: (e: any, value: number) => {
|
||||
NativeMkbHandler.getInstance().setHorizontalScrollMultiplier(value / 100);
|
||||
},
|
||||
}],
|
||||
}],
|
||||
}, {
|
||||
icon: BxIcon.COMMAND,
|
||||
group: 'shortcuts',
|
||||
items: [{
|
||||
group: 'shortcuts_controller',
|
||||
label: t('controller-shortcuts'),
|
||||
content: ControllerShortcut.renderSettings(),
|
||||
}],
|
||||
}, {
|
||||
icon: BxIcon.STREAM_STATS,
|
||||
group: 'stats',
|
||||
items: [{
|
||||
group: 'stats',
|
||||
label: t('stream-stats'),
|
||||
help_url: 'https://better-xcloud.github.io/stream-stats/',
|
||||
items: [{
|
||||
pref: PrefKey.STATS_SHOW_WHEN_PLAYING,
|
||||
}, {
|
||||
pref: PrefKey.STATS_QUICK_GLANCE,
|
||||
onChange: (e: InputEvent) => {
|
||||
const streamStats = StreamStats.getInstance();
|
||||
(e.target! as HTMLInputElement).checked ? streamStats.quickGlanceSetup() : streamStats.quickGlanceStop();
|
||||
},
|
||||
}, {
|
||||
pref: PrefKey.STATS_ITEMS,
|
||||
onChange: StreamStats.refreshStyles,
|
||||
}, {
|
||||
pref: PrefKey.STATS_POSITION,
|
||||
onChange: StreamStats.refreshStyles,
|
||||
}, {
|
||||
pref: PrefKey.STATS_TEXT_SIZE,
|
||||
onChange: StreamStats.refreshStyles,
|
||||
}, {
|
||||
pref: PrefKey.STATS_OPACITY,
|
||||
onChange: StreamStats.refreshStyles,
|
||||
}, {
|
||||
pref: PrefKey.STATS_TRANSPARENT,
|
||||
onChange: StreamStats.refreshStyles,
|
||||
}, {
|
||||
pref: PrefKey.STATS_CONDITIONAL_FORMATTING,
|
||||
onChange: StreamStats.refreshStyles,
|
||||
},
|
||||
],
|
||||
}],
|
||||
},
|
||||
];
|
||||
|
||||
constructor() {
|
||||
this.#setupDialog();
|
||||
|
||||
// Hide dialog when the Guide menu is shown
|
||||
window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, e => this.hide());
|
||||
}
|
||||
|
||||
isShowing() {
|
||||
return this.$container && !this.$container.classList.contains('bx-gone');
|
||||
}
|
||||
|
||||
show(tabId?: string) {
|
||||
const $container = this.$container!;
|
||||
// Select tab
|
||||
if (tabId) {
|
||||
const $tab = $container.querySelector(`.bx-stream-settings-tabs svg[data-tab-group=${tabId}]`);
|
||||
$tab && $tab.dispatchEvent(new Event('click'));
|
||||
}
|
||||
|
||||
// Show overlay
|
||||
this.$overlay!.classList.remove('bx-gone');
|
||||
this.$overlay!.dataset.isPlaying = STATES.isPlaying.toString();
|
||||
|
||||
// Show dialog
|
||||
$container.classList.remove('bx-gone');
|
||||
// Lock scroll bar
|
||||
document.body.classList.add('bx-no-scroll');
|
||||
|
||||
// Focus the first visible setting
|
||||
this.#focusDirection(NavigationDirection.DOWN);
|
||||
|
||||
// Add event listeners
|
||||
$container.addEventListener('keydown', this);
|
||||
|
||||
// Start gamepad polling
|
||||
this.#startGamepadPolling();
|
||||
|
||||
// Disable xCloud's navigation polling
|
||||
(window as any).BX_EXPOSED.disableGamepadPolling = true;
|
||||
|
||||
BxEvent.dispatch(window, BxEvent.XCLOUD_DIALOG_SHOWN);
|
||||
|
||||
// Update video's settings
|
||||
onChangeVideoPlayerType();
|
||||
}
|
||||
|
||||
hide() {
|
||||
// Hide overlay
|
||||
this.$overlay!.classList.add('bx-gone');
|
||||
// Hide dialog
|
||||
this.$container!.classList.add('bx-gone');
|
||||
// Show scroll bar
|
||||
document.body.classList.remove('bx-no-scroll');
|
||||
|
||||
// Remove event listeners
|
||||
this.$container!.removeEventListener('keydown', this);
|
||||
|
||||
// Stop gamepad polling();
|
||||
this.#stopGamepadPolling();
|
||||
|
||||
// Enable xCloud's navigation polling
|
||||
(window as any).BX_EXPOSED.disableGamepadPolling = false;
|
||||
|
||||
BxEvent.dispatch(window, BxEvent.XCLOUD_DIALOG_DISMISSED);
|
||||
}
|
||||
|
||||
#focusCurrentTab() {
|
||||
const $currentTab = this.$tabs!.querySelector('.bx-active') as HTMLElement;
|
||||
$currentTab && $currentTab.focus();
|
||||
}
|
||||
|
||||
#pollGamepad() {
|
||||
const gamepads = window.navigator.getGamepads();
|
||||
|
||||
let direction: NavigationDirection | null = null;
|
||||
for (const gamepad of gamepads) {
|
||||
if (!gamepad || !gamepad.connected) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ignore virtual controller
|
||||
if (gamepad.id === EmulatedMkbHandler.VIRTUAL_GAMEPAD_ID) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const axes = gamepad.axes;
|
||||
const buttons = gamepad.buttons;
|
||||
|
||||
let lastButton = this.gamepadLastButtons[gamepad.index];
|
||||
let pressedButton: GamepadKey | null = null;
|
||||
let holdingButton: GamepadKey | null = null;
|
||||
|
||||
for (const key of StreamSettings.GAMEPAD_KEYS) {
|
||||
if (typeof lastButton === 'number') {
|
||||
// Key released
|
||||
if (lastButton === key && !buttons[key].pressed) {
|
||||
pressedButton = key;
|
||||
break;
|
||||
}
|
||||
} else if (buttons[key].pressed) {
|
||||
// Key pressed
|
||||
holdingButton = key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (holdingButton === null && pressedButton === null && axes && axes.length >= 2) {
|
||||
// Check sticks
|
||||
// LEFT left-right, LEFT up-down
|
||||
|
||||
if (typeof lastButton === 'number') {
|
||||
const releasedHorizontal = Math.abs(axes[0]) < 0.1 && (lastButton === GamepadKey.LS_LEFT || lastButton === GamepadKey.LS_RIGHT);
|
||||
const releasedVertical = Math.abs(axes[1]) < 0.1 && (lastButton === GamepadKey.LS_UP || lastButton === GamepadKey.LS_DOWN);
|
||||
|
||||
if (releasedHorizontal || releasedVertical) {
|
||||
pressedButton = lastButton;
|
||||
}
|
||||
} else {
|
||||
if (axes[0] < -0.5) {
|
||||
holdingButton = GamepadKey.LS_LEFT;
|
||||
} else if (axes[0] > 0.5) {
|
||||
holdingButton = GamepadKey.LS_RIGHT;
|
||||
} else if (axes[1] < -0.5) {
|
||||
holdingButton = GamepadKey.LS_UP;
|
||||
} else if (axes[1] > 0.5) {
|
||||
holdingButton = GamepadKey.LS_DOWN;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (holdingButton !== null) {
|
||||
this.gamepadLastButtons[gamepad.index] = holdingButton;
|
||||
}
|
||||
|
||||
if (pressedButton === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.gamepadLastButtons[gamepad.index] = null;
|
||||
|
||||
if (pressedButton === GamepadKey.A) {
|
||||
document.activeElement && document.activeElement.dispatchEvent(new MouseEvent('click'));
|
||||
return;
|
||||
} else if (pressedButton === GamepadKey.B) {
|
||||
this.hide();
|
||||
return;
|
||||
} else if (pressedButton === GamepadKey.LB || pressedButton === GamepadKey.RB) {
|
||||
// Focus setting tabs
|
||||
this.#focusCurrentTab();
|
||||
return;
|
||||
}
|
||||
|
||||
direction = StreamSettings.GAMEPAD_DIRECTION_MAP[pressedButton as keyof typeof StreamSettings.GAMEPAD_DIRECTION_MAP];
|
||||
if (direction) {
|
||||
let handled = false;
|
||||
if (document.activeElement instanceof HTMLInputElement && document.activeElement.type === 'range') {
|
||||
const $range = document.activeElement;
|
||||
if (direction === NavigationDirection.LEFT || direction === NavigationDirection.RIGHT) {
|
||||
$range.value = (parseInt($range.value) + parseInt($range.step) * (direction === NavigationDirection.LEFT ? -1 : 1)).toString();
|
||||
$range.dispatchEvent(new InputEvent('input'));
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!handled) {
|
||||
this.#focusDirection(direction);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
#startGamepadPolling() {
|
||||
this.#stopGamepadPolling();
|
||||
|
||||
this.gamepadPollingIntervalId = window.setInterval(this.#pollGamepad.bind(this), StreamSettings.GAMEPAD_POLLING_INTERVAL);
|
||||
}
|
||||
|
||||
#stopGamepadPolling() {
|
||||
this.gamepadLastButtons = [];
|
||||
|
||||
this.gamepadPollingIntervalId && window.clearInterval(this.gamepadPollingIntervalId);
|
||||
this.gamepadPollingIntervalId = null;
|
||||
}
|
||||
|
||||
#handleTabsNavigation($focusing: HTMLElement, direction: NavigationDirection) {
|
||||
if (direction === NavigationDirection.UP || direction === NavigationDirection.DOWN) {
|
||||
let $sibling = $focusing;
|
||||
const siblingProperty = direction === NavigationDirection.UP ? 'previousElementSibling' : 'nextElementSibling';
|
||||
|
||||
while ($sibling[siblingProperty]) {
|
||||
$sibling = $sibling[siblingProperty] as HTMLElement;
|
||||
$sibling && $sibling.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// If it's the first/last item -> loop around
|
||||
const pseudo = direction === NavigationDirection.UP ? 'last-of-type' : 'first-of-type';
|
||||
const $target = this.$tabs!.querySelector(`svg:not(.bx-gone):${pseudo}`);
|
||||
$target && ($target as HTMLElement).focus();
|
||||
} else if (direction === NavigationDirection.RIGHT) {
|
||||
this.#focusFirstVisibleSetting();
|
||||
}
|
||||
}
|
||||
|
||||
#handleSettingsNavigation($focusing: HTMLElement, direction: NavigationDirection) {
|
||||
// If current element's tabIndex property is not 0
|
||||
if ($focusing.tabIndex !== 0) {
|
||||
// Find first visible setting
|
||||
const $childSetting = $focusing.querySelector('div[data-tab-group]:not(.bx-gone) [tabindex="0"]:not(a)') as HTMLElement;
|
||||
if ($childSetting) {
|
||||
$childSetting.focus();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Current element is setting -> Find the next one
|
||||
// Find parent
|
||||
let $parent = $focusing.closest('[data-focus-container]');
|
||||
|
||||
if (!$parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find sibling setting
|
||||
let $sibling = $parent;
|
||||
if (direction === NavigationDirection.UP || direction === NavigationDirection.DOWN) {
|
||||
const siblingProperty = direction === NavigationDirection.UP ? 'previousElementSibling' : 'nextElementSibling';
|
||||
|
||||
while ($sibling[siblingProperty]) {
|
||||
$sibling = $sibling[siblingProperty];
|
||||
const $childSetting = $sibling.querySelector('[tabindex="0"]:last-of-type') as HTMLElement;
|
||||
if ($childSetting) {
|
||||
$childSetting.focus();
|
||||
|
||||
// Only stop when it was focused successfully
|
||||
if (document.activeElement === $childSetting) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If it's the first/last item -> loop around
|
||||
// TODO: bugged if pseudo is "first-of-type" and the first setting is disabled
|
||||
const pseudo = direction === NavigationDirection.UP ? ':last-of-type' : '';
|
||||
const $target = this.$settings!.querySelector(`div[data-tab-group]:not(.bx-gone) div[data-focus-container]:not(.bx-gone)${pseudo} [tabindex="0"]:not(:disabled):last-of-type`);
|
||||
$target && ($target as HTMLElement).focus();
|
||||
} else if (direction === NavigationDirection.LEFT || direction === NavigationDirection.RIGHT) {
|
||||
// Find all child elements with tabindex
|
||||
const children = Array.from($parent.querySelectorAll('[tabindex="0"]'));
|
||||
const index = children.indexOf($focusing);
|
||||
let nextIndex;
|
||||
if (direction === NavigationDirection.LEFT) {
|
||||
nextIndex = index - 1;
|
||||
} else {
|
||||
nextIndex = index + 1;
|
||||
}
|
||||
|
||||
nextIndex = Math.max(-1, Math.min(nextIndex, children.length - 1));
|
||||
if (nextIndex === -1) {
|
||||
// Focus setting tabs
|
||||
const $tab = this.$tabs!.querySelector('svg.bx-active') as HTMLElement;
|
||||
$tab && $tab.focus();
|
||||
} else if (nextIndex !== index) {
|
||||
(children[nextIndex] as HTMLElement).focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#focusFirstVisibleSetting() {
|
||||
// Focus the first visible tab content
|
||||
const $tab = this.$settings!.querySelector('div[data-tab-group]:not(.bx-gone)') as HTMLElement;
|
||||
|
||||
if ($tab) {
|
||||
// Focus on the first focusable setting
|
||||
const $control = $tab.querySelector('[tabindex="0"]:not(a)') as HTMLElement;
|
||||
if ($control) {
|
||||
$control.focus();
|
||||
} else {
|
||||
// Focus tab
|
||||
$tab.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#focusDirection(direction: NavigationDirection) {
|
||||
const $tabs = this.$tabs!;
|
||||
const $settings = this.$settings!;
|
||||
|
||||
// Get current focused element
|
||||
let $focusing = document.activeElement as HTMLElement;
|
||||
|
||||
let focusContainer = FocusContainer.OUTSIDE;
|
||||
if ($focusing) {
|
||||
if ($settings.contains($focusing)) {
|
||||
focusContainer = FocusContainer.SETTINGS;
|
||||
} else if ($tabs.contains($focusing)) {
|
||||
focusContainer = FocusContainer.TABS;
|
||||
}
|
||||
}
|
||||
|
||||
// If not focusing any element or the focused element is not inside the dialog
|
||||
if (focusContainer === FocusContainer.OUTSIDE) {
|
||||
this.#focusFirstVisibleSetting();
|
||||
return;
|
||||
} else if (focusContainer === FocusContainer.SETTINGS) {
|
||||
this.#handleSettingsNavigation($focusing, direction);
|
||||
} else if (focusContainer === FocusContainer.TABS) {
|
||||
this.#handleTabsNavigation($focusing, direction);
|
||||
}
|
||||
}
|
||||
|
||||
handleEvent(event: Event) {
|
||||
switch (event.type) {
|
||||
case 'keydown':
|
||||
const $target = event.target as HTMLElement;
|
||||
const keyboardEvent = event as KeyboardEvent;
|
||||
const keyCode = keyboardEvent.code || keyboardEvent.key;
|
||||
|
||||
let handled = false;
|
||||
|
||||
if (keyCode === 'ArrowUp' || keyCode === 'ArrowDown') {
|
||||
handled = true;
|
||||
this.#focusDirection(keyCode === 'ArrowUp' ? NavigationDirection.UP : NavigationDirection.DOWN);
|
||||
} else if (keyCode === 'ArrowLeft' || keyCode === 'ArrowRight') {
|
||||
if (($target as any).type !== 'range') {
|
||||
handled = true;
|
||||
this.#focusDirection(keyCode === 'ArrowLeft' ? NavigationDirection.LEFT : NavigationDirection.RIGHT);
|
||||
}
|
||||
} else if (keyCode === 'Enter' || keyCode === 'Space') {
|
||||
if ($target instanceof SVGElement) {
|
||||
handled = true;
|
||||
$target.dispatchEvent(new Event('click'));
|
||||
}
|
||||
} else if (keyCode === 'Tab') {
|
||||
handled = true;
|
||||
this.#focusCurrentTab();
|
||||
} else if (keyCode === 'Escape') {
|
||||
handled = true;
|
||||
this.hide();
|
||||
}
|
||||
|
||||
if (handled) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#setupDialog() {
|
||||
let $tabs: HTMLElement;
|
||||
let $settings: HTMLElement;
|
||||
|
||||
const $overlay = CE('div', {class: 'bx-stream-settings-overlay bx-gone'});
|
||||
this.$overlay = $overlay;
|
||||
|
||||
const $container = CE('div', {class: StreamSettings.MAIN_CLASS + ' bx-gone'},
|
||||
$tabs = CE('div', {class: 'bx-stream-settings-tabs'}),
|
||||
$settings = CE('div', {
|
||||
class: 'bx-stream-settings-tab-contents',
|
||||
tabindex: 10,
|
||||
}),
|
||||
);
|
||||
|
||||
this.$container = $container;
|
||||
this.$tabs = $tabs;
|
||||
this.$settings = $settings;
|
||||
|
||||
// Close dialog when clicking on the overlay
|
||||
$overlay.addEventListener('click', e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.hide();
|
||||
});
|
||||
|
||||
// Close dialog when not clicking on any child elements in the dialog
|
||||
$container.addEventListener('click', e => {
|
||||
if (e.target === $container) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.hide();
|
||||
}
|
||||
});
|
||||
|
||||
for (const settingTab of this.SETTINGS_UI) {
|
||||
if (!settingTab) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const $svg = createSvgIcon(settingTab.icon);
|
||||
$svg.tabIndex = 0;
|
||||
|
||||
$svg.addEventListener('click', e => {
|
||||
// Switch tab
|
||||
for (const $child of Array.from($settings.children)) {
|
||||
if ($child.getAttribute('data-tab-group') === settingTab.group) {
|
||||
$child.classList.remove('bx-gone');
|
||||
} else {
|
||||
$child.classList.add('bx-gone');
|
||||
}
|
||||
}
|
||||
|
||||
// Highlight current tab button
|
||||
for (const $child of Array.from($tabs.children)) {
|
||||
$child.classList.remove('bx-active');
|
||||
}
|
||||
|
||||
$svg.classList.add('bx-active');
|
||||
});
|
||||
|
||||
$tabs.appendChild($svg);
|
||||
|
||||
const $group = CE('div', {'data-tab-group': settingTab.group, 'class': 'bx-gone'});
|
||||
|
||||
for (const settingGroup of settingTab.items) {
|
||||
if (!settingGroup) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$group.appendChild(CE('h2', {'data-focus-container': 'true'},
|
||||
CE('span', {}, settingGroup.label),
|
||||
settingGroup.help_url && createButton({
|
||||
icon: BxIcon.QUESTION,
|
||||
style: ButtonStyle.GHOST | ButtonStyle.FOCUSABLE,
|
||||
url: settingGroup.help_url,
|
||||
title: t('help'),
|
||||
tabIndex: 0,
|
||||
}),
|
||||
));
|
||||
if (settingGroup.note) {
|
||||
if (typeof settingGroup.note === 'string') {
|
||||
settingGroup.note = document.createTextNode(settingGroup.note);
|
||||
}
|
||||
$group.appendChild(settingGroup.note);
|
||||
}
|
||||
|
||||
if (settingGroup.content) {
|
||||
$group.appendChild(settingGroup.content);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!settingGroup.items) {
|
||||
settingGroup.items = [];
|
||||
}
|
||||
|
||||
for (const setting of settingGroup.items) {
|
||||
if (!setting) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const pref = setting.pref;
|
||||
|
||||
let $control;
|
||||
if (setting.content) {
|
||||
$control = setting.content;
|
||||
} else if (!setting.unsupported) {
|
||||
$control = toPrefElement(pref, setting.onChange, setting.params);
|
||||
|
||||
// Replace <select> with controller-friendly one
|
||||
if ($control instanceof HTMLSelectElement && getPref(PrefKey.UI_CONTROLLER_FRIENDLY)) {
|
||||
$control = BxSelectElement.wrap($control);
|
||||
}
|
||||
}
|
||||
|
||||
const label = Preferences.SETTINGS[pref as PrefKey]?.label || setting.label;
|
||||
const note = Preferences.SETTINGS[pref as PrefKey]?.note || setting.note;
|
||||
|
||||
const $content = CE('div', {
|
||||
class: 'bx-stream-settings-row',
|
||||
'data-type': settingGroup.group,
|
||||
'data-focus-container': 'true',
|
||||
},
|
||||
CE('label', {for: `bx_setting_${pref}`},
|
||||
label,
|
||||
note && CE('div', {'class': 'bx-stream-settings-dialog-note'}, note),
|
||||
setting.unsupported && CE('div', {'class': 'bx-stream-settings-dialog-note'}, t('browser-unsupported-feature')),
|
||||
),
|
||||
!setting.unsupported && $control,
|
||||
);
|
||||
|
||||
$group.appendChild($content);
|
||||
|
||||
setting.onMounted && setting.onMounted($control);
|
||||
}
|
||||
}
|
||||
|
||||
$settings.appendChild($group);
|
||||
}
|
||||
|
||||
// Select first tab
|
||||
$tabs.firstElementChild!.dispatchEvent(new Event('click'));
|
||||
|
||||
document.documentElement.appendChild($overlay);
|
||||
document.documentElement.appendChild($container);
|
||||
}
|
||||
}
|
@ -1,8 +1,9 @@
|
||||
import { PrefKey, getPref } from "@utils/preferences"
|
||||
import { BxEvent } from "@utils/bx-event"
|
||||
import { CE } from "@utils/html"
|
||||
import { t } from "@utils/translation"
|
||||
import { STATES } from "@utils/global"
|
||||
import { PrefKey } from "@/enums/pref-keys"
|
||||
import { getPref } from "@/utils/settings-storages/global-settings-storage"
|
||||
|
||||
export enum StreamStat {
|
||||
PING = 'ping',
|
||||
|
@ -5,7 +5,7 @@ import { BxEvent } from "@utils/bx-event.ts";
|
||||
import { t } from "@utils/translation.ts";
|
||||
import { StreamBadges } from "./stream-badges.ts";
|
||||
import { StreamStats } from "./stream-stats.ts";
|
||||
import { StreamSettings } from "./stream-settings.ts";
|
||||
import { SettingsNavigationDialog } from "../ui/dialog/settings-dialog.ts";
|
||||
|
||||
|
||||
function cloneStreamHudButton($orgButton: HTMLElement, label: string, svgIcon: typeof BxIcon) {
|
||||
@ -123,11 +123,6 @@ export function injectStreamMenuButtons() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Hide Stream Settings dialog when closing HUD
|
||||
$btnCloseHud.addEventListener('click', e => {
|
||||
StreamSettings.getInstance().hide();
|
||||
});
|
||||
|
||||
// Create Refresh button from the Close button
|
||||
const $btnRefresh = cloneCloseButton($btnCloseHud, BxIcon.REFRESH, 'bx-stream-refresh-button', () => {
|
||||
confirm(t('confirm-reload-stream')) && window.location.reload();
|
||||
@ -178,13 +173,13 @@ export function injectStreamMenuButtons() {
|
||||
|
||||
// Create Stream Settings button
|
||||
if (!$btnStreamSettings) {
|
||||
$btnStreamSettings = cloneStreamHudButton($orgButton, t('stream-settings'), BxIcon.STREAM_SETTINGS);
|
||||
$btnStreamSettings = cloneStreamHudButton($orgButton, t('better-xcloud'), BxIcon.BETTER_XCLOUD);
|
||||
$btnStreamSettings.addEventListener('click', e => {
|
||||
hideGripHandle();
|
||||
e.preventDefault();
|
||||
|
||||
// Show Stream Settings dialog
|
||||
StreamSettings.getInstance().show();
|
||||
SettingsNavigationDialog.getInstance().show();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,14 +1,27 @@
|
||||
import { STATES } from "@utils/global";
|
||||
import { escapeHtml } from "@utils/html";
|
||||
import { Toast } from "@utils/toast";
|
||||
import { BxEvent } from "@utils/bx-event";
|
||||
import { BX_FLAGS, NATIVE_FETCH } from "@utils/bx-flags";
|
||||
import { getPref, PrefKey } from "@utils/preferences";
|
||||
import { NATIVE_FETCH } from "@utils/bx-flags";
|
||||
import { t } from "@utils/translation";
|
||||
import { BxLogger } from "@utils/bx-logger";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref } from "@/utils/settings-storages/global-settings-storage";
|
||||
|
||||
const LOG_TAG = 'TouchController';
|
||||
|
||||
type TouchControlLayout = {
|
||||
name: string,
|
||||
author: string,
|
||||
content: any,
|
||||
};
|
||||
|
||||
type TouchControlDefinition = {
|
||||
name: string,
|
||||
product_id: string,
|
||||
default_layout: string,
|
||||
layouts: Record<string, TouchControlLayout>,
|
||||
};
|
||||
|
||||
export class TouchController {
|
||||
static readonly #EVENT_SHOW_DEFAULT_CONTROLLER = new MessageEvent('message', {
|
||||
data: JSON.stringify({
|
||||
@ -28,25 +41,40 @@ export class TouchController {
|
||||
|
||||
static #$style: HTMLStyleElement;
|
||||
|
||||
static #enable = false;
|
||||
static #enabled = false;
|
||||
static #dataChannel: RTCDataChannel | null;
|
||||
|
||||
static #customLayouts: {[index: string]: any} = {};
|
||||
static #baseCustomLayouts: {[index: string]: any} = {};
|
||||
static #customLayouts: Record<string, TouchControlDefinition | null> = {};
|
||||
static #baseCustomLayouts: Record<string, Record<string, TouchControlLayout>> = {};
|
||||
static #currentLayoutId: string;
|
||||
|
||||
static #customList: string[];
|
||||
|
||||
static #xboxTitleId: string | null = null;
|
||||
|
||||
static setXboxTitleId(xboxTitleId: string) {
|
||||
TouchController.#xboxTitleId = xboxTitleId;
|
||||
}
|
||||
|
||||
static getCustomLayouts() {
|
||||
const xboxTitleId = TouchController.#xboxTitleId;
|
||||
if (!xboxTitleId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return TouchController.#customLayouts[xboxTitleId];
|
||||
}
|
||||
|
||||
static enable() {
|
||||
TouchController.#enable = true;
|
||||
TouchController.#enabled = true;
|
||||
}
|
||||
|
||||
static disable() {
|
||||
TouchController.#enable = false;
|
||||
TouchController.#enabled = false;
|
||||
}
|
||||
|
||||
static isEnabled() {
|
||||
return TouchController.#enable;
|
||||
return TouchController.#enabled;
|
||||
}
|
||||
|
||||
static #showDefault() {
|
||||
@ -70,8 +98,9 @@ export class TouchController {
|
||||
}
|
||||
|
||||
static reset() {
|
||||
TouchController.#enable = false;
|
||||
TouchController.#enabled = false;
|
||||
TouchController.#dataChannel = null;
|
||||
TouchController.#xboxTitleId = null;
|
||||
|
||||
TouchController.#$style && (TouchController.#$style.textContent = '');
|
||||
}
|
||||
@ -83,12 +112,18 @@ export class TouchController {
|
||||
}
|
||||
|
||||
static #dispatchLayouts(data: any) {
|
||||
BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED, {
|
||||
data: data,
|
||||
});
|
||||
// Load default layout
|
||||
TouchController.applyCustomLayout(null, 1000);
|
||||
|
||||
BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED);
|
||||
};
|
||||
|
||||
static async getCustomLayouts(xboxTitleId: string, retries: number=1) {
|
||||
static async requestCustomLayouts(retries: number=1) {
|
||||
const xboxTitleId = TouchController.#xboxTitleId;
|
||||
if (!xboxTitleId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (xboxTitleId in TouchController.#customLayouts) {
|
||||
TouchController.#dispatchLayouts(TouchController.#customLayouts[xboxTitleId]);
|
||||
return;
|
||||
@ -102,7 +137,7 @@ export class TouchController {
|
||||
return;
|
||||
}
|
||||
|
||||
const baseUrl = `https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/touch-layouts${BX_FLAGS.UseDevTouchLayout ? '/dev' : ''}`;
|
||||
const baseUrl = 'https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/touch-layouts';
|
||||
const url = `${baseUrl}/${xboxTitleId}.json`;
|
||||
|
||||
// Get layout info
|
||||
@ -137,17 +172,17 @@ export class TouchController {
|
||||
window.setTimeout(() => TouchController.#dispatchLayouts(json), 1000);
|
||||
} catch (e) {
|
||||
// Retry
|
||||
TouchController.getCustomLayouts(xboxTitleId, retries + 1);
|
||||
TouchController.requestCustomLayouts(retries + 1);
|
||||
}
|
||||
}
|
||||
|
||||
static loadCustomLayout(xboxTitleId: string, layoutId: string, delay: number=0) {
|
||||
static applyCustomLayout(layoutId: string | null, delay: number=0) {
|
||||
// TODO: fix this
|
||||
if (!window.BX_EXPOSED.touchLayoutManager) {
|
||||
const listener = (e: Event) => {
|
||||
window.removeEventListener(BxEvent.TOUCH_LAYOUT_MANAGER_READY, listener);
|
||||
if (TouchController.#enable) {
|
||||
TouchController.loadCustomLayout(xboxTitleId, layoutId, 0);
|
||||
if (TouchController.#enabled) {
|
||||
TouchController.applyCustomLayout(layoutId, 0);
|
||||
}
|
||||
};
|
||||
window.addEventListener(BxEvent.TOUCH_LAYOUT_MANAGER_READY, listener);
|
||||
@ -155,13 +190,29 @@ export class TouchController {
|
||||
return;
|
||||
}
|
||||
|
||||
const xboxTitleId = TouchController.#xboxTitleId;
|
||||
if (!xboxTitleId) {
|
||||
BxLogger.error(LOG_TAG, 'Invalid xboxTitleId');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!layoutId) {
|
||||
// Get default layout ID from definition
|
||||
layoutId = TouchController.#customLayouts[xboxTitleId]?.default_layout || null;
|
||||
}
|
||||
|
||||
if (!layoutId) {
|
||||
BxLogger.error(LOG_TAG, 'Invalid layoutId');
|
||||
return;
|
||||
}
|
||||
|
||||
const layoutChanged = TouchController.#currentLayoutId !== layoutId;
|
||||
TouchController.#currentLayoutId = layoutId;
|
||||
|
||||
// Get layout data
|
||||
const layoutData = TouchController.#customLayouts[xboxTitleId];
|
||||
if (!xboxTitleId || !layoutId || !layoutData) {
|
||||
TouchController.#enable && TouchController.#showDefault();
|
||||
TouchController.#enabled && TouchController.#showDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -223,7 +274,7 @@ export class TouchController {
|
||||
|
||||
touchLayoutManager && touchLayoutManager.changeLayoutForScope({
|
||||
type: 'showLayout',
|
||||
scope: '' + STATES.currentStream?.xboxTitleId,
|
||||
scope: '' + TouchController.#xboxTitleId,
|
||||
subscope: 'base',
|
||||
layout: {
|
||||
id: 'System.Standard',
|
||||
@ -249,7 +300,7 @@ export class TouchController {
|
||||
|
||||
// Apply touch controller's style
|
||||
let filter = '';
|
||||
if (TouchController.#enable) {
|
||||
if (TouchController.#enabled) {
|
||||
if (PREF_STYLE_STANDARD === 'white') {
|
||||
filter = 'grayscale(1) brightness(2)';
|
||||
} else if (PREF_STYLE_STANDARD === 'muted') {
|
||||
@ -280,9 +331,9 @@ export class TouchController {
|
||||
|
||||
// Dispatch a message to display generic touch controller
|
||||
if (msg.data.includes('touchcontrols/showtitledefault')) {
|
||||
if (TouchController.#enable) {
|
||||
if (TouchController.#enabled) {
|
||||
if (focused) {
|
||||
TouchController.getCustomLayouts(STATES.currentStream?.xboxTitleId!);
|
||||
TouchController.requestCustomLayouts();
|
||||
} else {
|
||||
TouchController.#showDefault();
|
||||
}
|
||||
@ -300,7 +351,7 @@ export class TouchController {
|
||||
TouchController.#show();
|
||||
}
|
||||
|
||||
STATES.currentStream.xboxTitleId = parseInt(json.titleid, 16).toString();
|
||||
TouchController.setXboxTitleId(parseInt(json.titleid, 16).toString());
|
||||
}
|
||||
} catch (e) {
|
||||
BxLogger.error(LOG_TAG, 'Load custom layout', e);
|
||||
|
608
src/modules/ui/dialog/navigation-dialog.ts
Normal file
@ -0,0 +1,608 @@
|
||||
import { GamepadKey } from "@/enums/mkb";
|
||||
import { EmulatedMkbHandler } from "@/modules/mkb/mkb-handler";
|
||||
import { BxEvent } from "@/utils/bx-event";
|
||||
import { STATES } from "@/utils/global";
|
||||
import { CE } from "@/utils/html";
|
||||
import { setNearby } from "@/utils/navigation-utils";
|
||||
|
||||
export enum NavigationDirection {
|
||||
UP = 1,
|
||||
RIGHT,
|
||||
DOWN,
|
||||
LEFT,
|
||||
}
|
||||
|
||||
export type NavigationNearbyElements = Partial<{
|
||||
orientation: 'horizontal' | 'vertical',
|
||||
selfOrientation: 'horizontal' | 'vertical',
|
||||
|
||||
focus: NavigationElement | (() => boolean),
|
||||
loop: ((direction: NavigationDirection) => boolean),
|
||||
[NavigationDirection.UP]: NavigationElement | (() => void) | 'previous' | 'next',
|
||||
[NavigationDirection.DOWN]: NavigationElement | (() => void) | 'previous' | 'next',
|
||||
[NavigationDirection.LEFT]: NavigationElement | (() => void) | 'previous' | 'next',
|
||||
[NavigationDirection.RIGHT]: NavigationElement | (() => void) | 'previous' | 'next',
|
||||
}>;
|
||||
|
||||
export interface NavigationElement extends HTMLElement {
|
||||
nearby?: NavigationNearbyElements;
|
||||
}
|
||||
|
||||
|
||||
export abstract class NavigationDialog {
|
||||
abstract getDialog(): NavigationDialog;
|
||||
abstract getContent(): HTMLElement;
|
||||
|
||||
abstract focusIfNeeded(): void;
|
||||
|
||||
abstract $container: HTMLElement;
|
||||
dialogManager: NavigationDialogManager;
|
||||
|
||||
constructor() {
|
||||
this.dialogManager = NavigationDialogManager.getInstance();
|
||||
}
|
||||
|
||||
show() {
|
||||
NavigationDialogManager.getInstance().show(this);
|
||||
|
||||
const $currentFocus = this.getFocusedElement();
|
||||
// If not focusing on any element
|
||||
if (!$currentFocus) {
|
||||
this.focusIfNeeded();
|
||||
}
|
||||
}
|
||||
|
||||
hide() {
|
||||
NavigationDialogManager.getInstance().hide();
|
||||
}
|
||||
|
||||
getFocusedElement() {
|
||||
const $activeElement = document.activeElement as HTMLElement;
|
||||
if (!$activeElement) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if focused element is a child of dialog
|
||||
if (this.$container.contains($activeElement)) {
|
||||
return $activeElement;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
onBeforeMount(): void {}
|
||||
onMounted(): void {}
|
||||
onBeforeUnmount(): void {}
|
||||
onUnmounted(): void {}
|
||||
|
||||
handleKeyPress(key: string): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
handleGamepad(button: GamepadKey): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export class NavigationDialogManager {
|
||||
private static instance: NavigationDialogManager;
|
||||
public static getInstance(): NavigationDialogManager {
|
||||
if (!NavigationDialogManager.instance) {
|
||||
NavigationDialogManager.instance = new NavigationDialogManager();
|
||||
}
|
||||
return NavigationDialogManager.instance;
|
||||
}
|
||||
|
||||
private static readonly GAMEPAD_POLLING_INTERVAL = 50;
|
||||
private static readonly GAMEPAD_KEYS = [
|
||||
GamepadKey.UP,
|
||||
GamepadKey.DOWN,
|
||||
GamepadKey.LEFT,
|
||||
GamepadKey.RIGHT,
|
||||
GamepadKey.A,
|
||||
GamepadKey.B,
|
||||
GamepadKey.LB,
|
||||
GamepadKey.RB,
|
||||
GamepadKey.LT,
|
||||
GamepadKey.RT,
|
||||
];
|
||||
|
||||
private static readonly GAMEPAD_DIRECTION_MAP = {
|
||||
[GamepadKey.UP]: NavigationDirection.UP,
|
||||
[GamepadKey.DOWN]: NavigationDirection.DOWN,
|
||||
[GamepadKey.LEFT]: NavigationDirection.LEFT,
|
||||
[GamepadKey.RIGHT]: NavigationDirection.RIGHT,
|
||||
|
||||
[GamepadKey.LS_UP]: NavigationDirection.UP,
|
||||
[GamepadKey.LS_DOWN]: NavigationDirection.DOWN,
|
||||
[GamepadKey.LS_LEFT]: NavigationDirection.LEFT,
|
||||
[GamepadKey.LS_RIGHT]: NavigationDirection.RIGHT,
|
||||
};
|
||||
|
||||
private static readonly SIBLING_PROPERTY_MAP = {
|
||||
'horizontal': {
|
||||
[NavigationDirection.LEFT]: 'previousElementSibling',
|
||||
[NavigationDirection.RIGHT]: 'nextElementSibling',
|
||||
},
|
||||
|
||||
'vertical': {
|
||||
[NavigationDirection.UP]: 'previousElementSibling',
|
||||
[NavigationDirection.DOWN]: 'nextElementSibling',
|
||||
},
|
||||
};
|
||||
|
||||
private gamepadPollingIntervalId: number | null = null;
|
||||
private gamepadLastStates: Array<[number, GamepadKey, boolean] | null> = [];
|
||||
private gamepadHoldingIntervalId: number | null = null;
|
||||
|
||||
private $overlay: HTMLElement;
|
||||
private $container: HTMLElement;
|
||||
private dialog: NavigationDialog | null = null;
|
||||
|
||||
constructor() {
|
||||
this.$overlay = CE('div', {class: 'bx-navigation-dialog-overlay bx-gone'});
|
||||
this.$overlay.addEventListener('click', e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.hide();
|
||||
});
|
||||
|
||||
document.documentElement.appendChild(this.$overlay);
|
||||
|
||||
this.$container = CE('div', {class: 'bx-navigation-dialog bx-gone'});
|
||||
document.documentElement.appendChild(this.$container);
|
||||
|
||||
// Hide dialog when the Guide menu is shown
|
||||
window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, e => this.hide());
|
||||
}
|
||||
|
||||
handleEvent(event: Event) {
|
||||
switch (event.type) {
|
||||
case 'keydown':
|
||||
const $target = event.target as HTMLElement;
|
||||
const keyboardEvent = event as KeyboardEvent;
|
||||
const keyCode = keyboardEvent.code || keyboardEvent.key;
|
||||
|
||||
let handled = this.dialog?.handleKeyPress(keyCode);
|
||||
if (handled) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
if (keyCode === 'ArrowUp' || keyCode === 'ArrowDown') {
|
||||
handled = true;
|
||||
this.focusDirection(keyCode === 'ArrowUp' ? NavigationDirection.UP : NavigationDirection.DOWN);
|
||||
} else if (keyCode === 'ArrowLeft' || keyCode === 'ArrowRight') {
|
||||
if (!($target instanceof HTMLInputElement && ($target.type === 'text' || $target.type === 'range'))) {
|
||||
handled = true;
|
||||
this.focusDirection(keyCode === 'ArrowLeft' ? NavigationDirection.LEFT : NavigationDirection.RIGHT);
|
||||
}
|
||||
} else if (keyCode === 'Enter' || keyCode === 'NumpadEnter' || keyCode === 'Space') {
|
||||
if (!($target instanceof HTMLInputElement && $target.type === 'text')) {
|
||||
handled = true;
|
||||
$target.dispatchEvent(new MouseEvent('click'));
|
||||
}
|
||||
} else if (keyCode === 'Escape') {
|
||||
handled = true;
|
||||
this.hide();
|
||||
}
|
||||
|
||||
if (handled) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
isShowing() {
|
||||
return this.$container && !this.$container.classList.contains('bx-gone');
|
||||
}
|
||||
|
||||
private pollGamepad() {
|
||||
const gamepads = window.navigator.getGamepads();
|
||||
|
||||
for (const gamepad of gamepads) {
|
||||
if (!gamepad || !gamepad.connected) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ignore virtual controller
|
||||
if (gamepad.id === EmulatedMkbHandler.VIRTUAL_GAMEPAD_ID) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const axes = gamepad.axes;
|
||||
const buttons = gamepad.buttons;
|
||||
|
||||
let releasedButton: GamepadKey | null = null;
|
||||
let heldButton: GamepadKey | null = null;
|
||||
|
||||
let lastState = this.gamepadLastStates[gamepad.index];
|
||||
let lastTimestamp;
|
||||
let lastKey;
|
||||
let lastKeyPressed;
|
||||
if (lastState) {
|
||||
[lastTimestamp, lastKey, lastKeyPressed] = lastState;
|
||||
}
|
||||
|
||||
if (lastTimestamp && lastTimestamp === gamepad.timestamp) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const key of NavigationDialogManager.GAMEPAD_KEYS) {
|
||||
// Key released
|
||||
if (lastKey === key && !buttons[key].pressed) {
|
||||
releasedButton = key;
|
||||
break;
|
||||
} else if (buttons[key].pressed) {
|
||||
// Key pressed
|
||||
heldButton = key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If not pressing any key => check analog sticks
|
||||
if (heldButton === null && releasedButton === null && axes && axes.length >= 2) {
|
||||
// [LEFT left-right, LEFT up-down]
|
||||
if (lastKey) {
|
||||
const releasedHorizontal = Math.abs(axes[0]) < 0.1 && (lastKey === GamepadKey.LS_LEFT || lastKey === GamepadKey.LS_RIGHT);
|
||||
const releasedVertical = Math.abs(axes[1]) < 0.1 && (lastKey === GamepadKey.LS_UP || lastKey === GamepadKey.LS_DOWN);
|
||||
|
||||
if (releasedHorizontal || releasedVertical) {
|
||||
releasedButton = lastKey;
|
||||
} else {
|
||||
heldButton = lastKey;
|
||||
}
|
||||
} else {
|
||||
if (axes[0] < -0.5) {
|
||||
heldButton = GamepadKey.LS_LEFT;
|
||||
} else if (axes[0] > 0.5) {
|
||||
heldButton = GamepadKey.LS_RIGHT;
|
||||
} else if (axes[1] < -0.5) {
|
||||
heldButton = GamepadKey.LS_UP;
|
||||
} else if (axes[1] > 0.5) {
|
||||
heldButton = GamepadKey.LS_DOWN;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save state if holding a button
|
||||
if (heldButton !== null) {
|
||||
this.gamepadLastStates[gamepad.index] = [gamepad.timestamp, heldButton, false];
|
||||
|
||||
this.clearGamepadHoldingInterval();
|
||||
|
||||
// Only set turbo for d-pad and stick
|
||||
if (NavigationDialogManager.GAMEPAD_DIRECTION_MAP[heldButton as keyof typeof NavigationDialogManager.GAMEPAD_DIRECTION_MAP]) {
|
||||
this.gamepadHoldingIntervalId = window.setInterval(() => {
|
||||
const lastState = this.gamepadLastStates[gamepad.index];
|
||||
// Avoid pressing the incorrect key
|
||||
if (lastState) {
|
||||
[lastTimestamp, lastKey, lastKeyPressed] = lastState;
|
||||
if (lastKey === heldButton) {
|
||||
this.handleGamepad(gamepad, heldButton);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.clearGamepadHoldingInterval();
|
||||
}, 200);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Continue if the button hasn't been released
|
||||
if (releasedButton === null) {
|
||||
this.clearGamepadHoldingInterval();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Button released
|
||||
this.gamepadLastStates[gamepad.index] = null;
|
||||
|
||||
if (lastKeyPressed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (releasedButton === GamepadKey.A) {
|
||||
document.activeElement && document.activeElement.dispatchEvent(new MouseEvent('click'));
|
||||
return;
|
||||
} else if (releasedButton === GamepadKey.B) {
|
||||
this.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.handleGamepad(gamepad, releasedButton)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handleGamepad(gamepad: Gamepad, key: GamepadKey): boolean {
|
||||
let handled = this.dialog?.handleGamepad(key);
|
||||
if (handled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Handle d-pad & sticks
|
||||
let direction = NavigationDialogManager.GAMEPAD_DIRECTION_MAP[key as keyof typeof NavigationDialogManager.GAMEPAD_DIRECTION_MAP];
|
||||
if (!direction) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (document.activeElement instanceof HTMLInputElement && document.activeElement.type === 'range') {
|
||||
const $range = document.activeElement;
|
||||
if (direction === NavigationDirection.LEFT || direction === NavigationDirection.RIGHT) {
|
||||
$range.value = (parseInt($range.value) + parseInt($range.step) * (direction === NavigationDirection.LEFT ? -1 : 1)).toString();
|
||||
$range.dispatchEvent(new InputEvent('input'));
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!handled) {
|
||||
this.focusDirection(direction);
|
||||
}
|
||||
|
||||
this.gamepadLastStates[gamepad.index] && (this.gamepadLastStates[gamepad.index]![2] = true);
|
||||
return true;
|
||||
}
|
||||
|
||||
private clearGamepadHoldingInterval() {
|
||||
this.gamepadHoldingIntervalId && window.clearInterval(this.gamepadHoldingIntervalId);
|
||||
this.gamepadHoldingIntervalId = null;
|
||||
}
|
||||
|
||||
show(dialog: NavigationDialog) {
|
||||
this.clearGamepadHoldingInterval();
|
||||
|
||||
BxEvent.dispatch(window, BxEvent.XCLOUD_DIALOG_SHOWN);
|
||||
|
||||
// Stop xCloud's navigation polling
|
||||
(window as any).BX_EXPOSED.disableGamepadPolling = true;
|
||||
|
||||
// Lock scroll bar
|
||||
document.body.classList.add('bx-no-scroll');
|
||||
|
||||
// Show overlay
|
||||
this.$overlay.classList.remove('bx-gone');
|
||||
if (STATES.isPlaying) {
|
||||
this.$overlay.classList.add('bx-invisible');
|
||||
}
|
||||
|
||||
// Unmount current dialog
|
||||
this.unmountCurrentDialog();
|
||||
|
||||
// Setup new dialog
|
||||
this.dialog = dialog;
|
||||
dialog.onBeforeMount();
|
||||
this.$container.appendChild(dialog.getContent());
|
||||
dialog.onMounted();
|
||||
|
||||
// Show content
|
||||
this.$container.classList.remove('bx-gone');
|
||||
|
||||
// Add event listeners
|
||||
this.$container.addEventListener('keydown', this);
|
||||
|
||||
// Start gamepad polling
|
||||
this.startGamepadPolling();
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.clearGamepadHoldingInterval();
|
||||
|
||||
// Unlock scroll bar
|
||||
document.body.classList.remove('bx-no-scroll');
|
||||
|
||||
BxEvent.dispatch(window, BxEvent.XCLOUD_DIALOG_DISMISSED);
|
||||
|
||||
// Hide content
|
||||
this.$overlay.classList.add('bx-gone');
|
||||
this.$overlay.classList.remove('bx-invisible');
|
||||
this.$container.classList.add('bx-gone');
|
||||
|
||||
// Remove event listeners
|
||||
this.$container.removeEventListener('keydown', this);
|
||||
|
||||
// Stop gamepad polling
|
||||
this.stopGamepadPolling();
|
||||
|
||||
// Unmount dialog
|
||||
this.unmountCurrentDialog();
|
||||
|
||||
// Enable xCloud's navigation polling
|
||||
(window as any).BX_EXPOSED.disableGamepadPolling = false;
|
||||
}
|
||||
|
||||
focus($elm: NavigationElement | null): boolean {
|
||||
if (!$elm) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// console.log('focus', $elm);
|
||||
|
||||
if ($elm.nearby && $elm.nearby.focus) {
|
||||
if ($elm.nearby.focus instanceof HTMLElement) {
|
||||
return this.focus($elm.nearby.focus);
|
||||
} else {
|
||||
return $elm.nearby.focus();
|
||||
}
|
||||
}
|
||||
|
||||
$elm.focus();
|
||||
return $elm === document.activeElement;
|
||||
}
|
||||
|
||||
private getOrientation($elm: NavigationElement): NavigationNearbyElements['orientation'] {
|
||||
const nearby = $elm.nearby || {};
|
||||
if (nearby.selfOrientation) {
|
||||
return nearby.selfOrientation;
|
||||
}
|
||||
|
||||
let orientation;
|
||||
|
||||
let $current = $elm.parentElement! as NavigationElement;
|
||||
while ($current !== this.$container) {
|
||||
const tmp = $current.nearby?.orientation;
|
||||
if ($current.nearby && tmp) {
|
||||
orientation = tmp;
|
||||
break;
|
||||
}
|
||||
|
||||
$current = $current.parentElement!;
|
||||
}
|
||||
|
||||
orientation = orientation || 'vertical';
|
||||
setNearby($elm, {
|
||||
selfOrientation: orientation,
|
||||
});
|
||||
|
||||
return orientation;
|
||||
}
|
||||
|
||||
findNextTarget($focusing: HTMLElement | null, direction: NavigationDirection, checkParent = false, checked: Array<HTMLElement> = []): HTMLElement | null {
|
||||
if (!$focusing || $focusing === this.$container) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (checked.includes($focusing)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
checked.push($focusing);
|
||||
|
||||
let $target: HTMLElement = $focusing;
|
||||
const $parent = $target.parentElement;
|
||||
|
||||
const nearby = ($target as NavigationElement).nearby || {};
|
||||
const orientation = this.getOrientation($target)!;
|
||||
|
||||
// @ts-ignore
|
||||
let siblingProperty = (NavigationDialogManager.SIBLING_PROPERTY_MAP[orientation])[direction];
|
||||
if (siblingProperty) {
|
||||
let $sibling = $target as any;
|
||||
while ($sibling[siblingProperty]) {
|
||||
$sibling = $sibling[siblingProperty] as HTMLElement;
|
||||
|
||||
const $focusable = this.findFocusableElement($sibling, direction);
|
||||
if ($focusable) {
|
||||
return $focusable;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (nearby.loop) {
|
||||
// Loop
|
||||
if (nearby.loop(direction)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (checkParent) {
|
||||
return this.findNextTarget($parent, direction, checkParent, checked);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
findFocusableElement($elm: HTMLElement | null, direction?: NavigationDirection): HTMLElement | null {
|
||||
if (!$elm) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Ignore disabled element
|
||||
const isDisabled = !!($elm as any).disabled;
|
||||
if (isDisabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rect = $elm.getBoundingClientRect();
|
||||
const isVisible = !!rect.width && !!rect.height;
|
||||
|
||||
// Ignore hidden element
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Accept element with tabIndex
|
||||
if ($elm.tabIndex > -1) {
|
||||
return $elm;
|
||||
}
|
||||
|
||||
const focus = ($elm as NavigationElement).nearby?.focus;
|
||||
if (focus) {
|
||||
if (focus instanceof HTMLElement) {
|
||||
return this.findFocusableElement(focus, direction);
|
||||
} else if (typeof focus === 'function') {
|
||||
if (focus()) {
|
||||
return document.activeElement as HTMLElement;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Look for child focusable elemnet
|
||||
const children = Array.from($elm.children);
|
||||
|
||||
// Search from right to left if the orientation is horizontal
|
||||
const orientation = ($elm as NavigationElement).nearby?.orientation;
|
||||
if (orientation === 'horizontal' || (orientation === 'vertical' && direction === NavigationDirection.UP)) {
|
||||
children.reverse();
|
||||
}
|
||||
|
||||
for (const $child of children) {
|
||||
if (!$child || !($child instanceof HTMLElement)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const $target = this.findFocusableElement($child, direction);
|
||||
if ($target) {
|
||||
return $target;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private startGamepadPolling() {
|
||||
this.stopGamepadPolling();
|
||||
|
||||
this.gamepadPollingIntervalId = window.setInterval(this.pollGamepad.bind(this), NavigationDialogManager.GAMEPAD_POLLING_INTERVAL);
|
||||
}
|
||||
|
||||
private stopGamepadPolling() {
|
||||
this.gamepadLastStates = [];
|
||||
|
||||
this.gamepadPollingIntervalId && window.clearInterval(this.gamepadPollingIntervalId);
|
||||
this.gamepadPollingIntervalId = null;
|
||||
}
|
||||
|
||||
private focusDirection(direction: NavigationDirection) {
|
||||
const dialog = this.dialog;
|
||||
if (!dialog) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current focused element
|
||||
const $focusing = dialog.getFocusedElement();
|
||||
if (!$focusing || !this.findFocusableElement($focusing, direction)) {
|
||||
dialog.focusIfNeeded();
|
||||
return null;
|
||||
}
|
||||
|
||||
const $target = this.findNextTarget($focusing, direction, true);
|
||||
this.focus($target);
|
||||
}
|
||||
|
||||
private unmountCurrentDialog() {
|
||||
const dialog = this.dialog;
|
||||
|
||||
dialog && dialog.onBeforeUnmount();
|
||||
this.$container.firstChild?.remove();
|
||||
dialog && dialog.onUnmounted();
|
||||
|
||||
this.dialog = null;
|
||||
}
|
||||
}
|
1160
src/modules/ui/dialog/settings-dialog.ts
Normal file
@ -1,507 +0,0 @@
|
||||
import { STATES, AppInterface, SCRIPT_VERSION, deepClone } from "@utils/global";
|
||||
import { CE, createButton, ButtonStyle } from "@utils/html";
|
||||
import { BxIcon } from "@utils/bx-icon";
|
||||
import { getPreferredServerRegion } from "@utils/region";
|
||||
import { UserAgent } from "@utils/user-agent";
|
||||
import { getPref, Preferences, PrefKey, setPref, toPrefElement } from "@utils/preferences";
|
||||
import { t, Translations } from "@utils/translation";
|
||||
import { PatcherCache } from "../patcher";
|
||||
import { UserAgentProfile } from "@enums/user-agent";
|
||||
import { BxSelectElement } from "@/web-components/bx-select";
|
||||
import { StreamSettings } from "../stream/stream-settings";
|
||||
import { BX_FLAGS } from "@/utils/bx-flags";
|
||||
import { Toast } from "@/utils/toast";
|
||||
|
||||
const SETTINGS_UI = {
|
||||
'Better xCloud': {
|
||||
items: [
|
||||
PrefKey.BETTER_XCLOUD_LOCALE,
|
||||
PrefKey.SERVER_BYPASS_RESTRICTION,
|
||||
PrefKey.UI_CONTROLLER_FRIENDLY,
|
||||
PrefKey.REMOTE_PLAY_ENABLED,
|
||||
],
|
||||
},
|
||||
|
||||
[t('server')]: {
|
||||
items: [
|
||||
PrefKey.SERVER_REGION,
|
||||
PrefKey.STREAM_PREFERRED_LOCALE,
|
||||
PrefKey.PREFER_IPV6_SERVER,
|
||||
],
|
||||
},
|
||||
|
||||
[t('stream')]: {
|
||||
items: [
|
||||
PrefKey.STREAM_TARGET_RESOLUTION,
|
||||
PrefKey.STREAM_CODEC_PROFILE,
|
||||
|
||||
PrefKey.BITRATE_VIDEO_MAX,
|
||||
|
||||
PrefKey.AUDIO_ENABLE_VOLUME_CONTROL,
|
||||
PrefKey.STREAM_DISABLE_FEEDBACK_DIALOG,
|
||||
|
||||
PrefKey.SCREENSHOT_APPLY_FILTERS,
|
||||
|
||||
PrefKey.AUDIO_MIC_ON_PLAYING,
|
||||
PrefKey.GAME_FORTNITE_FORCE_CONSOLE,
|
||||
PrefKey.STREAM_COMBINE_SOURCES,
|
||||
],
|
||||
},
|
||||
|
||||
[t('game-bar')]: {
|
||||
items: [
|
||||
PrefKey.GAME_BAR_POSITION,
|
||||
],
|
||||
},
|
||||
|
||||
[t('local-co-op')]: {
|
||||
items: [
|
||||
PrefKey.LOCAL_CO_OP_ENABLED,
|
||||
],
|
||||
},
|
||||
|
||||
[t('mouse-and-keyboard')]: {
|
||||
items: [
|
||||
PrefKey.NATIVE_MKB_ENABLED,
|
||||
PrefKey.MKB_ENABLED,
|
||||
PrefKey.MKB_HIDE_IDLE_CURSOR,
|
||||
],
|
||||
},
|
||||
|
||||
[t('touch-controller')]: {
|
||||
note: !STATES.userAgent.capabilities.touch ? '⚠️ ' + t('device-unsupported-touch') : null,
|
||||
unsupported: !STATES.userAgent.capabilities.touch,
|
||||
items: [
|
||||
PrefKey.STREAM_TOUCH_CONTROLLER,
|
||||
PrefKey.STREAM_TOUCH_CONTROLLER_AUTO_OFF,
|
||||
PrefKey.STREAM_TOUCH_CONTROLLER_DEFAULT_OPACITY,
|
||||
PrefKey.STREAM_TOUCH_CONTROLLER_STYLE_STANDARD,
|
||||
PrefKey.STREAM_TOUCH_CONTROLLER_STYLE_CUSTOM,
|
||||
],
|
||||
},
|
||||
|
||||
[t('loading-screen')]: {
|
||||
items: [
|
||||
PrefKey.UI_LOADING_SCREEN_GAME_ART,
|
||||
PrefKey.UI_LOADING_SCREEN_WAIT_TIME,
|
||||
PrefKey.UI_LOADING_SCREEN_ROCKET,
|
||||
],
|
||||
},
|
||||
|
||||
[t('ui')]: {
|
||||
items: [
|
||||
PrefKey.UI_LAYOUT,
|
||||
PrefKey.UI_HOME_CONTEXT_MENU_DISABLED,
|
||||
PrefKey.UI_GAME_CARD_SHOW_WAIT_TIME,
|
||||
PrefKey.CONTROLLER_SHOW_CONNECTION_STATUS,
|
||||
PrefKey.STREAM_SIMPLIFY_MENU,
|
||||
PrefKey.SKIP_SPLASH_VIDEO,
|
||||
!AppInterface && PrefKey.UI_SCROLLBAR_HIDE,
|
||||
PrefKey.HIDE_DOTS_ICON,
|
||||
PrefKey.REDUCE_ANIMATIONS,
|
||||
PrefKey.BLOCK_SOCIAL_FEATURES,
|
||||
PrefKey.UI_HIDE_SECTIONS,
|
||||
],
|
||||
},
|
||||
|
||||
[t('other')]: {
|
||||
items: [
|
||||
PrefKey.BLOCK_TRACKING,
|
||||
],
|
||||
},
|
||||
|
||||
[t('advanced')]: {
|
||||
items: [
|
||||
PrefKey.USER_AGENT_PROFILE,
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
export function setupSettingsUi() {
|
||||
// Avoid rendering the Settings multiple times
|
||||
if (document.querySelector('.bx-settings-container')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const PREF_PREFERRED_REGION = getPreferredServerRegion();
|
||||
const PREF_LATEST_VERSION = getPref(PrefKey.LATEST_VERSION);
|
||||
|
||||
let $btnReload: HTMLButtonElement;
|
||||
|
||||
// Setup Settings UI
|
||||
const $container = CE('div', {
|
||||
'class': 'bx-settings-container bx-gone',
|
||||
});
|
||||
|
||||
const $wrapper = CE('div', {'class': 'bx-settings-wrapper'},
|
||||
CE('div', {'class': 'bx-settings-title-wrapper'},
|
||||
createButton({
|
||||
classes: ['bx-settings-title'],
|
||||
style: ButtonStyle.FOCUSABLE | ButtonStyle.GHOST,
|
||||
label: 'Better xCloud ' + SCRIPT_VERSION,
|
||||
url: 'https://github.com/redphx/better-xcloud/releases',
|
||||
}),
|
||||
createButton({
|
||||
icon: BxIcon.QUESTION,
|
||||
style: ButtonStyle.FOCUSABLE,
|
||||
label: t('help'),
|
||||
url: 'https://better-xcloud.github.io/features/',
|
||||
}),
|
||||
)
|
||||
);
|
||||
|
||||
const topButtons = [];
|
||||
|
||||
// "New version available" button
|
||||
if (!SCRIPT_VERSION.includes('beta') && PREF_LATEST_VERSION && PREF_LATEST_VERSION != SCRIPT_VERSION) {
|
||||
// Show new version indicator
|
||||
topButtons.push(createButton({
|
||||
label: `🌟 Version ${PREF_LATEST_VERSION} available`,
|
||||
style: ButtonStyle.PRIMARY | ButtonStyle.FOCUSABLE | ButtonStyle.FULL_WIDTH,
|
||||
url: 'https://github.com/redphx/better-xcloud/releases/latest',
|
||||
}));
|
||||
}
|
||||
|
||||
// "Stream settings" button
|
||||
(STATES.supportedRegion && STATES.isSignedIn) && topButtons.push(createButton({
|
||||
label: t('stream-settings'),
|
||||
icon: BxIcon.STREAM_SETTINGS,
|
||||
style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE,
|
||||
onClick: e => {
|
||||
StreamSettings.getInstance().show();
|
||||
},
|
||||
}));
|
||||
|
||||
// Buttons for Android app
|
||||
if (AppInterface) {
|
||||
// Show Android app settings button
|
||||
topButtons.push(createButton({
|
||||
label: t('app-settings'),
|
||||
icon: BxIcon.STREAM_SETTINGS,
|
||||
style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE,
|
||||
onClick: e => {
|
||||
AppInterface.openAppSettings && AppInterface.openAppSettings();
|
||||
},
|
||||
}));
|
||||
} else {
|
||||
// Show link to Android app
|
||||
const userAgent = UserAgent.getDefault().toLowerCase();
|
||||
if (userAgent.includes('android')) {
|
||||
topButtons.push(createButton({
|
||||
label: '🔥 ' + t('install-android'),
|
||||
style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE,
|
||||
url: 'https://better-xcloud.github.io/android',
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if (topButtons.length) {
|
||||
const $div = CE('div', {class: 'bx-top-buttons'});
|
||||
for (const $button of topButtons) {
|
||||
$div.appendChild($button);
|
||||
}
|
||||
|
||||
$wrapper.appendChild($div);
|
||||
}
|
||||
|
||||
let localeSwitchingTimeout: number | null;
|
||||
|
||||
const onChange = async (e: Event) => {
|
||||
// Clear PatcherCache;
|
||||
PatcherCache.clear();
|
||||
|
||||
$btnReload.classList.add('bx-danger');
|
||||
|
||||
// Highlight the Settings button in the Header to remind user to reload the page
|
||||
const $btnHeaderSettings = document.querySelector('.bx-header-settings-button');
|
||||
$btnHeaderSettings && $btnHeaderSettings.classList.add('bx-danger');
|
||||
|
||||
if ((e.target as HTMLElement).id === 'bx_setting_' + PrefKey.BETTER_XCLOUD_LOCALE) {
|
||||
if (getPref(PrefKey.UI_CONTROLLER_FRIENDLY)) {
|
||||
localeSwitchingTimeout && window.clearTimeout(localeSwitchingTimeout);
|
||||
localeSwitchingTimeout = window.setTimeout(() => {
|
||||
Translations.refreshCurrentLocale();
|
||||
Translations.updateTranslations();
|
||||
}, 1000);
|
||||
} else {
|
||||
// Update locale
|
||||
Translations.refreshCurrentLocale();
|
||||
await Translations.updateTranslations();
|
||||
|
||||
$btnReload.textContent = t('settings-reloading');
|
||||
$btnReload.click();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Render settings
|
||||
for (let groupLabel in SETTINGS_UI) {
|
||||
// Don't render other settings when not signed in
|
||||
if (groupLabel !== 'Better xCloud' && (!STATES.supportedRegion || !STATES.isSignedIn)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const $group = CE('span', {'class': 'bx-settings-group-label'}, groupLabel);
|
||||
|
||||
// Render note
|
||||
if (SETTINGS_UI[groupLabel].note) {
|
||||
const $note = CE('b', {}, SETTINGS_UI[groupLabel].note);
|
||||
$group.appendChild($note);
|
||||
}
|
||||
|
||||
$wrapper.appendChild($group);
|
||||
|
||||
// Don't render settings if this is an unsupported feature
|
||||
if (SETTINGS_UI[groupLabel].unsupported) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const settingItems = SETTINGS_UI[groupLabel].items;
|
||||
for (let settingId of settingItems) {
|
||||
// Don't render custom settings
|
||||
if (!settingId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const setting = Preferences.SETTINGS[settingId];
|
||||
if (!setting) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let settingLabel = setting.label;
|
||||
let settingNote = setting.note || '';
|
||||
|
||||
// Add Experimental text
|
||||
if (setting.experimental) {
|
||||
settingLabel = '🧪 ' + settingLabel;
|
||||
if (!settingNote) {
|
||||
settingNote = t('experimental');
|
||||
} else {
|
||||
settingNote = `${t('experimental')}: ${settingNote}`;
|
||||
}
|
||||
}
|
||||
|
||||
let $control: any;
|
||||
let $inpCustomUserAgent: HTMLInputElement;
|
||||
let labelAttrs: any = {
|
||||
tabindex: '-1',
|
||||
};
|
||||
|
||||
if (settingId === PrefKey.USER_AGENT_PROFILE) {
|
||||
let defaultUserAgent = (window.navigator as any).orgUserAgent || window.navigator.userAgent;
|
||||
$inpCustomUserAgent = CE('input', {
|
||||
id: `bx_setting_inp_${settingId}`,
|
||||
type: 'text',
|
||||
placeholder: defaultUserAgent,
|
||||
'class': 'bx-settings-custom-user-agent',
|
||||
});
|
||||
$inpCustomUserAgent.addEventListener('input', e => {
|
||||
const profile = $control.value;
|
||||
const custom = (e.target as HTMLInputElement).value.trim();
|
||||
|
||||
UserAgent.updateStorage(profile, custom);
|
||||
onChange(e);
|
||||
});
|
||||
|
||||
$control = toPrefElement(PrefKey.USER_AGENT_PROFILE, (e: Event) => {
|
||||
const value = (e.target as HTMLInputElement).value as UserAgentProfile;
|
||||
let isCustom = value === UserAgentProfile.CUSTOM;
|
||||
let userAgent = UserAgent.get(value as UserAgentProfile);
|
||||
|
||||
UserAgent.updateStorage(value);
|
||||
|
||||
$inpCustomUserAgent.value = userAgent;
|
||||
$inpCustomUserAgent.readOnly = !isCustom;
|
||||
$inpCustomUserAgent.disabled = !isCustom;
|
||||
|
||||
!(e.target as HTMLInputElement).disabled && onChange(e);
|
||||
});
|
||||
} else if (settingId === PrefKey.SERVER_REGION) {
|
||||
let selectedValue;
|
||||
|
||||
$control = CE<HTMLSelectElement>('select', {
|
||||
id: `bx_setting_${settingId}`,
|
||||
title: settingLabel,
|
||||
tabindex: 0,
|
||||
});
|
||||
$control.name = $control.id;
|
||||
|
||||
$control.addEventListener('input', (e: Event) => {
|
||||
setPref(settingId, (e.target as HTMLSelectElement).value);
|
||||
onChange(e);
|
||||
});
|
||||
|
||||
selectedValue = PREF_PREFERRED_REGION;
|
||||
|
||||
setting.options = {};
|
||||
for (let regionName in STATES.serverRegions) {
|
||||
const region = STATES.serverRegions[regionName];
|
||||
let value = regionName;
|
||||
|
||||
let label = `${region.shortName} - ${regionName}`;
|
||||
if (region.isDefault) {
|
||||
label += ` (${t('default')})`;
|
||||
value = 'default';
|
||||
|
||||
if (selectedValue === regionName) {
|
||||
selectedValue = 'default';
|
||||
}
|
||||
}
|
||||
|
||||
setting.options[value] = label;
|
||||
}
|
||||
|
||||
for (let value in setting.options) {
|
||||
const label = setting.options[value];
|
||||
|
||||
const $option = CE('option', {value: value}, label);
|
||||
$control.appendChild($option);
|
||||
}
|
||||
|
||||
$control.disabled = Object.keys(STATES.serverRegions).length === 0;
|
||||
|
||||
// Select preferred region
|
||||
$control.value = selectedValue;
|
||||
} else {
|
||||
if (settingId === PrefKey.BETTER_XCLOUD_LOCALE) {
|
||||
$control = toPrefElement(settingId, (e: Event) => {
|
||||
localStorage.setItem('better_xcloud_locale', (e.target as HTMLSelectElement).value);
|
||||
onChange(e);
|
||||
});
|
||||
} else {
|
||||
$control = toPrefElement(settingId, onChange);
|
||||
}
|
||||
}
|
||||
|
||||
if (!!$control.id) {
|
||||
labelAttrs['for'] = $control.id;
|
||||
} else {
|
||||
labelAttrs['for'] = `bx_setting_${settingId}`;
|
||||
}
|
||||
|
||||
// Disable unsupported settings
|
||||
if (setting.unsupported) {
|
||||
($control as HTMLInputElement).disabled = true;
|
||||
}
|
||||
|
||||
// Make disabled control elements un-focusable
|
||||
if ($control.disabled && !!$control.getAttribute('tabindex')) {
|
||||
$control.setAttribute('tabindex', -1);
|
||||
}
|
||||
|
||||
const $label = CE<HTMLLabelElement>('label', labelAttrs, settingLabel);
|
||||
if (settingNote) {
|
||||
$label.appendChild(CE('b', {}, settingNote));
|
||||
}
|
||||
|
||||
let $elm: HTMLElement;
|
||||
|
||||
if ($control instanceof HTMLSelectElement && getPref(PrefKey.UI_CONTROLLER_FRIENDLY)) {
|
||||
// Controller-friendly <select>
|
||||
$elm = CE('div', {'class': 'bx-settings-row', 'data-group': 0},
|
||||
$label,
|
||||
CE('div', {class: 'bx-setting-control'}, BxSelectElement.wrap($control)),
|
||||
);
|
||||
} else {
|
||||
$elm = CE('div', {'class': 'bx-settings-row', 'data-group': 0},
|
||||
$label,
|
||||
$control instanceof HTMLInputElement ? CE('label', {
|
||||
class: 'bx-setting-control',
|
||||
for: $label.getAttribute('for'),
|
||||
}, $control) : CE('div', {class: 'bx-setting-control'}, $control),
|
||||
);
|
||||
}
|
||||
|
||||
$wrapper.appendChild($elm);
|
||||
|
||||
// Add User-Agent input
|
||||
if (settingId === PrefKey.USER_AGENT_PROFILE) {
|
||||
$wrapper.appendChild($inpCustomUserAgent!);
|
||||
// Trigger 'change' event
|
||||
$control.disabled = true;
|
||||
$control.dispatchEvent(new Event('input'));
|
||||
$control.disabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Setup Reload button
|
||||
$btnReload = createButton({
|
||||
label: t('settings-reload'),
|
||||
classes: ['bx-settings-reload-button'],
|
||||
style: ButtonStyle.FOCUSABLE | ButtonStyle.FULL_WIDTH | ButtonStyle.TALL,
|
||||
onClick: e => {
|
||||
window.location.reload();
|
||||
$btnReload.disabled = true;
|
||||
$btnReload.textContent = t('settings-reloading');
|
||||
},
|
||||
});
|
||||
$btnReload.setAttribute('tabindex', '0');
|
||||
|
||||
$wrapper.appendChild($btnReload);
|
||||
|
||||
// Donation link
|
||||
const $donationLink = CE('a', {
|
||||
'class': 'bx-donation-link',
|
||||
href: 'https://ko-fi.com/redphx',
|
||||
target: '_blank',
|
||||
tabindex: 0,
|
||||
}, `❤️ ${t('support-better-xcloud')}`);
|
||||
$wrapper.appendChild($donationLink);
|
||||
|
||||
// Show Game Pass app version
|
||||
try {
|
||||
const appVersion = (document.querySelector('meta[name=gamepass-app-version]') as HTMLMetaElement).content;
|
||||
const appDate = new Date((document.querySelector('meta[name=gamepass-app-date]') as HTMLMetaElement).content).toISOString().substring(0, 10);
|
||||
$wrapper.appendChild(CE('div', {'class': 'bx-settings-app-version'}, `xCloud website version ${appVersion} (${appDate})`));
|
||||
} catch (e) {}
|
||||
|
||||
// Show Debug info
|
||||
const debugInfo = deepClone(BX_FLAGS.DeviceInfo);
|
||||
const debugSettings = [
|
||||
PrefKey.STREAM_TARGET_RESOLUTION,
|
||||
PrefKey.STREAM_CODEC_PROFILE,
|
||||
|
||||
PrefKey.VIDEO_PLAYER_TYPE,
|
||||
PrefKey.VIDEO_PROCESSING,
|
||||
PrefKey.VIDEO_POWER_PREFERENCE,
|
||||
PrefKey.VIDEO_SHARPNESS,
|
||||
];
|
||||
|
||||
debugInfo['settings'] = {};
|
||||
for (const key of debugSettings) {
|
||||
debugInfo['settings'][key] = getPref(key);
|
||||
}
|
||||
|
||||
const $debugInfo = CE('div', {class: 'bx-debug-info'},
|
||||
createButton({
|
||||
label: 'Debug info',
|
||||
style: ButtonStyle.GHOST | ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE,
|
||||
onClick: e => {
|
||||
console.log(e);
|
||||
(e.target as HTMLElement).closest('button')?.nextElementSibling?.classList.toggle('bx-gone');
|
||||
},
|
||||
}),
|
||||
CE('pre', {
|
||||
class: 'bx-gone',
|
||||
on: {
|
||||
click: async (e: Event) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText((e.target as HTMLElement).innerText);
|
||||
Toast.show('Copied to clipboard', '', {instant: true});
|
||||
} catch (err) {
|
||||
console.error('Failed to copy: ', err);
|
||||
}
|
||||
},
|
||||
},
|
||||
}, '```\n' + JSON.stringify(debugInfo, null, ' ') + '\n```'),
|
||||
);
|
||||
$wrapper.appendChild($debugInfo);
|
||||
|
||||
$container.appendChild($wrapper);
|
||||
|
||||
// Add Settings UI to the web page
|
||||
const $pageContent = document.getElementById('PageContent');
|
||||
$pageContent?.parentNode?.insertBefore($container, $pageContent);
|
||||
}
|
@ -2,21 +2,21 @@ import { BxEvent } from "@/utils/bx-event";
|
||||
import { AppInterface, STATES } from "@/utils/global";
|
||||
import { createButton, ButtonStyle, CE } from "@/utils/html";
|
||||
import { t } from "@/utils/translation";
|
||||
import { StreamSettings } from "../stream/stream-settings";
|
||||
import { SettingsNavigationDialog } from "./dialog/settings-dialog";
|
||||
|
||||
export enum GuideMenuTab {
|
||||
HOME,
|
||||
HOME = 'home',
|
||||
}
|
||||
|
||||
export class GuideMenu {
|
||||
static #BUTTONS = {
|
||||
streamSetting: createButton({
|
||||
label: t('stream-settings'),
|
||||
style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE,
|
||||
scriptSettings: createButton({
|
||||
label: t('better-xcloud'),
|
||||
style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE | ButtonStyle.PRIMARY,
|
||||
onClick: e => {
|
||||
// Wait until the Guide dialog is closed
|
||||
window.addEventListener(BxEvent.XCLOUD_DIALOG_DISMISSED, e => {
|
||||
setTimeout(() => StreamSettings.getInstance().show(), 50);
|
||||
setTimeout(() => SettingsNavigationDialog.getInstance().show(), 50);
|
||||
}, {once: true});
|
||||
|
||||
// Close all xCloud's dialogs
|
||||
@ -86,8 +86,8 @@ export class GuideMenu {
|
||||
|
||||
const buttons: HTMLElement[] = [];
|
||||
|
||||
// "Stream settings" button
|
||||
buttons.push(GuideMenu.#BUTTONS.streamSetting);
|
||||
// "Better xCloud" button
|
||||
buttons.push(GuideMenu.#BUTTONS.scriptSettings);
|
||||
|
||||
// "App settings" button
|
||||
AppInterface && buttons.push(GuideMenu.#BUTTONS.appSettings);
|
||||
@ -112,7 +112,7 @@ export class GuideMenu {
|
||||
|
||||
const buttons: HTMLElement[] = [];
|
||||
|
||||
buttons.push(GuideMenu.#BUTTONS.streamSetting);
|
||||
buttons.push(GuideMenu.#BUTTONS.scriptSettings);
|
||||
AppInterface && buttons.push(GuideMenu.#BUTTONS.appSettings);
|
||||
|
||||
// Reload page
|
||||
|
@ -2,17 +2,18 @@ import { SCRIPT_VERSION } from "@utils/global";
|
||||
import { createButton, ButtonStyle, CE } from "@utils/html";
|
||||
import { BxIcon } from "@utils/bx-icon";
|
||||
import { getPreferredServerRegion } from "@utils/region";
|
||||
import { PrefKey, getPref } from "@utils/preferences";
|
||||
import { RemotePlay } from "@modules/remote-play";
|
||||
import { t } from "@utils/translation";
|
||||
import { setupSettingsUi } from "./global-settings";
|
||||
import { SettingsNavigationDialog } from "./dialog/settings-dialog";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref } from "@/utils/settings-storages/global-settings-storage";
|
||||
|
||||
export class HeaderSection {
|
||||
static #$remotePlayBtn = createButton({
|
||||
classes: ['bx-header-remote-play-button', 'bx-gone'],
|
||||
icon: BxIcon.REMOTE_PLAY,
|
||||
title: t('remote-play'),
|
||||
style: ButtonStyle.GHOST | ButtonStyle.FOCUSABLE,
|
||||
style: ButtonStyle.GHOST | ButtonStyle.FOCUSABLE | ButtonStyle.CIRCULAR,
|
||||
onClick: e => {
|
||||
RemotePlay.togglePopup();
|
||||
},
|
||||
@ -21,14 +22,9 @@ export class HeaderSection {
|
||||
static #$settingsBtn = createButton({
|
||||
classes: ['bx-header-settings-button'],
|
||||
label: '???',
|
||||
style: ButtonStyle.GHOST | ButtonStyle.FOCUSABLE | ButtonStyle.FULL_HEIGHT,
|
||||
style: ButtonStyle.FROSTED | ButtonStyle.DROP_SHADOW | ButtonStyle.FOCUSABLE | ButtonStyle.FULL_HEIGHT,
|
||||
onClick: e => {
|
||||
setupSettingsUi();
|
||||
|
||||
const $settings = document.querySelector('.bx-settings-container')!;
|
||||
$settings.classList.toggle('bx-gone');
|
||||
window.scrollTo(0, 0);
|
||||
document.activeElement && (document.activeElement as HTMLElement).blur();
|
||||
SettingsNavigationDialog.getInstance().show();
|
||||
},
|
||||
});
|
||||
|
||||
@ -49,7 +45,7 @@ export class HeaderSection {
|
||||
|
||||
// Setup Settings button
|
||||
const $settingsBtn = HeaderSection.#$settingsBtn;
|
||||
$settingsBtn.querySelector('span')!.textContent = getPreferredServerRegion(true);
|
||||
$settingsBtn.querySelector('span')!.textContent = getPreferredServerRegion(true) || t('better-xcloud');
|
||||
|
||||
// Show new update status
|
||||
if (!SCRIPT_VERSION.includes('beta') && PREF_LATEST_VERSION && PREF_LATEST_VERSION !== SCRIPT_VERSION) {
|
||||
|
@ -19,7 +19,7 @@ export class ProductDetailsPage {
|
||||
private static shortcutTimeoutId: number | null = null;
|
||||
|
||||
static injectShortcutButton() {
|
||||
if (!AppInterface || BX_FLAGS.DeviceInfo?.deviceType !== 'android') {
|
||||
if (!AppInterface || BX_FLAGS.DeviceInfo!.deviceType !== 'android') {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,4 @@
|
||||
import { CE } from "@utils/html";
|
||||
import { onChangeVideoPlayerType } from "../stream/stream-settings-utils";
|
||||
import { StreamSettings } from "../stream/stream-settings";
|
||||
|
||||
|
||||
export function localRedirect(path: string) {
|
||||
@ -26,10 +24,4 @@ export function localRedirect(path: string) {
|
||||
$anchor.click();
|
||||
}
|
||||
|
||||
export function setupStreamUi() {
|
||||
StreamSettings.getInstance();
|
||||
onChangeVideoPlayerType();
|
||||
}
|
||||
|
||||
|
||||
(window as any).localRedirect = localRedirect;
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { AppInterface } from "@utils/global";
|
||||
import { BxEvent } from "@utils/bx-event";
|
||||
import { PrefKey, getPref } from "@utils/preferences";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref } from "@/utils/settings-storages/global-settings-storage";
|
||||
|
||||
const VIBRATION_DATA_MAP = {
|
||||
'gamepadIndex': 8,
|
||||
|
1
src/types/index.d.ts
vendored
@ -49,7 +49,6 @@ type BxStates = {
|
||||
|
||||
currentStream: Partial<{
|
||||
titleId: string;
|
||||
xboxTitleId: string;
|
||||
productId: string;
|
||||
titleInfo: XcloudTitleInfo;
|
||||
|
||||
|
19
src/types/setting-definition.d.ts
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
export type SettingDefinition = {
|
||||
default: any;
|
||||
optionsGroup?: string;
|
||||
options?: {[index: string]: string};
|
||||
multipleOptions?: {[index: string]: string};
|
||||
unsupported?: string | boolean;
|
||||
note?: string | HTMLElement;
|
||||
type?: SettingElementType;
|
||||
ready?: (setting: SettingDefinition) => void;
|
||||
// migrate?: (this: Preferences, savedPrefs: any, value: any) => void;
|
||||
min?: number;
|
||||
max?: number;
|
||||
steps?: number;
|
||||
experimental?: boolean;
|
||||
params?: any;
|
||||
label?: string;
|
||||
};
|
||||
|
||||
export type SettingDefinitions = {[index in PrefKey]: SettingDefinition};
|
@ -1,63 +1,63 @@
|
||||
import { AppInterface } from "@utils/global";
|
||||
|
||||
export enum BxEvent {
|
||||
JUMP_BACK_IN_READY = 'bx-jump-back-in-ready',
|
||||
POPSTATE = 'bx-popstate',
|
||||
|
||||
TITLE_INFO_READY = 'bx-title-info-ready',
|
||||
|
||||
STREAM_LOADING = 'bx-stream-loading',
|
||||
STREAM_STARTING = 'bx-stream-starting',
|
||||
STREAM_STARTED = 'bx-stream-started',
|
||||
STREAM_PLAYING = 'bx-stream-playing',
|
||||
STREAM_STOPPED = 'bx-stream-stopped',
|
||||
STREAM_ERROR_PAGE = 'bx-stream-error-page',
|
||||
|
||||
STREAM_WEBRTC_CONNECTED = 'bx-stream-webrtc-connected',
|
||||
STREAM_WEBRTC_DISCONNECTED = 'bx-stream-webrtc-disconnected',
|
||||
|
||||
// STREAM_EVENT_TARGET_READY = 'bx-stream-event-target-ready',
|
||||
STREAM_SESSION_READY = 'bx-stream-session-ready',
|
||||
|
||||
CUSTOM_TOUCH_LAYOUTS_LOADED = 'bx-custom-touch-layouts-loaded',
|
||||
TOUCH_LAYOUT_MANAGER_READY = 'bx-touch-layout-manager-ready',
|
||||
|
||||
REMOTE_PLAY_READY = 'bx-remote-play-ready',
|
||||
REMOTE_PLAY_FAILED = 'bx-remote-play-failed',
|
||||
|
||||
XCLOUD_SERVERS_READY = 'bx-servers-ready',
|
||||
XCLOUD_SERVERS_UNAVAILABLE = 'bx-servers-unavailable',
|
||||
|
||||
DATA_CHANNEL_CREATED = 'bx-data-channel-created',
|
||||
|
||||
GAME_BAR_ACTION_ACTIVATED = 'bx-game-bar-action-activated',
|
||||
MICROPHONE_STATE_CHANGED = 'bx-microphone-state-changed',
|
||||
|
||||
CAPTURE_SCREENSHOT = 'bx-capture-screenshot',
|
||||
GAINNODE_VOLUME_CHANGED = 'bx-gainnode-volume-changed',
|
||||
|
||||
POINTER_LOCK_REQUESTED = 'bx-pointer-lock-requested',
|
||||
POINTER_LOCK_EXITED = 'bx-pointer-lock-exited',
|
||||
|
||||
NAVIGATION_FOCUS_CHANGED = 'bx-nav-focus-changed',
|
||||
|
||||
// xCloud Dialog events
|
||||
XCLOUD_DIALOG_SHOWN = 'bx-xcloud-dialog-shown',
|
||||
XCLOUD_DIALOG_DISMISSED = 'bx-xcloud-dialog-dismissed',
|
||||
|
||||
XCLOUD_GUIDE_MENU_SHOWN = 'bx-xcloud-guide-menu-shown',
|
||||
|
||||
XCLOUD_POLLING_MODE_CHANGED = 'bx-xcloud-polling-mode-changed',
|
||||
|
||||
XCLOUD_RENDERING_COMPONENT = 'bx-xcloud-rendering-page',
|
||||
}
|
||||
|
||||
export enum XcloudEvent {
|
||||
MICROPHONE_STATE_CHANGED = 'microphoneStateChanged',
|
||||
}
|
||||
|
||||
export namespace BxEvent {
|
||||
export function dispatch(target: HTMLElement | Window, eventName: string, data?: any) {
|
||||
export const JUMP_BACK_IN_READY = 'bx-jump-back-in-ready';
|
||||
export const POPSTATE = 'bx-popstate';
|
||||
|
||||
export const TITLE_INFO_READY = 'bx-title-info-ready';
|
||||
|
||||
export const SETTINGS_CHANGED = 'bx-settings-changed';
|
||||
|
||||
export const STREAM_LOADING = 'bx-stream-loading';
|
||||
export const STREAM_STARTING = 'bx-stream-starting';
|
||||
export const STREAM_STARTED = 'bx-stream-started';
|
||||
export const STREAM_PLAYING = 'bx-stream-playing';
|
||||
export const STREAM_STOPPED = 'bx-stream-stopped';
|
||||
export const STREAM_ERROR_PAGE = 'bx-stream-error-page';
|
||||
|
||||
export const STREAM_WEBRTC_CONNECTED = 'bx-stream-webrtc-connected';
|
||||
export const STREAM_WEBRTC_DISCONNECTED = 'bx-stream-webrtc-disconnected';
|
||||
|
||||
// export const STREAM_EVENT_TARGET_READY = 'bx-stream-event-target-ready';
|
||||
export const STREAM_SESSION_READY = 'bx-stream-session-ready';
|
||||
|
||||
export const CUSTOM_TOUCH_LAYOUTS_LOADED = 'bx-custom-touch-layouts-loaded';
|
||||
export const TOUCH_LAYOUT_MANAGER_READY = 'bx-touch-layout-manager-ready';
|
||||
|
||||
export const REMOTE_PLAY_READY = 'bx-remote-play-ready';
|
||||
export const REMOTE_PLAY_FAILED = 'bx-remote-play-failed';
|
||||
|
||||
export const XCLOUD_SERVERS_READY = 'bx-servers-ready';
|
||||
export const XCLOUD_SERVERS_UNAVAILABLE = 'bx-servers-unavailable';
|
||||
|
||||
export const DATA_CHANNEL_CREATED = 'bx-data-channel-created';
|
||||
|
||||
export const GAME_BAR_ACTION_ACTIVATED = 'bx-game-bar-action-activated';
|
||||
export const MICROPHONE_STATE_CHANGED = 'bx-microphone-state-changed';
|
||||
|
||||
export const CAPTURE_SCREENSHOT = 'bx-capture-screenshot';
|
||||
|
||||
export const POINTER_LOCK_REQUESTED = 'bx-pointer-lock-requested';
|
||||
export const POINTER_LOCK_EXITED = 'bx-pointer-lock-exited';
|
||||
|
||||
export const NAVIGATION_FOCUS_CHANGED = 'bx-nav-focus-changed';
|
||||
|
||||
// xCloud Dialog events
|
||||
export const XCLOUD_DIALOG_SHOWN = 'bx-xcloud-dialog-shown';
|
||||
export const XCLOUD_DIALOG_DISMISSED = 'bx-xcloud-dialog-dismissed';
|
||||
|
||||
export const XCLOUD_GUIDE_MENU_SHOWN = 'bx-xcloud-guide-menu-shown';
|
||||
|
||||
export const XCLOUD_POLLING_MODE_CHANGED = 'bx-xcloud-polling-mode-changed';
|
||||
|
||||
export const XCLOUD_RENDERING_COMPONENT = 'bx-xcloud-rendering-page';
|
||||
|
||||
export function dispatch(target: Element | Window | null, eventName: string, data?: any) {
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!eventName) {
|
||||
alert('BxEvent.dispatch(): eventName is null');
|
||||
return;
|
||||
|
@ -1,19 +1,21 @@
|
||||
import { ControllerShortcut } from "@/modules/controller-shortcut";
|
||||
import { BxEvent } from "@utils/bx-event";
|
||||
import { deepClone, STATES } from "@utils/global";
|
||||
import { getPref, PrefKey } from "@utils/preferences";
|
||||
import { BxLogger } from "./bx-logger";
|
||||
import { BX_FLAGS } from "./bx-flags";
|
||||
import { StreamSettings } from "@/modules/stream/stream-settings";
|
||||
import { NavigationDialogManager } from "@/modules/ui/dialog/navigation-dialog";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref } from "./settings-storages/global-settings-storage";
|
||||
|
||||
export enum InputType {
|
||||
export enum SupportedInputType {
|
||||
CONTROLLER = 'Controller',
|
||||
MKB = 'MKB',
|
||||
CUSTOM_TOUCH_OVERLAY = 'CustomTouchOverlay',
|
||||
GENERIC_TOUCH = 'GenericTouch',
|
||||
NATIVE_TOUCH = 'NativeTouch',
|
||||
BATIVE_SENSOR = 'NativeSensor',
|
||||
}
|
||||
};
|
||||
export type SupportedInputTypeValue = (typeof SupportedInputType)[keyof typeof SupportedInputType];
|
||||
|
||||
export const BxExposed = {
|
||||
getTitleInfo: () => STATES.currentStream.titleInfo,
|
||||
@ -25,15 +27,15 @@ export const BxExposed = {
|
||||
let supportedInputTypes = titleInfo.details.supportedInputTypes;
|
||||
|
||||
if (BX_FLAGS.ForceNativeMkbTitles?.includes(titleInfo.details.productId)) {
|
||||
supportedInputTypes.push(InputType.MKB);
|
||||
supportedInputTypes.push(SupportedInputType.MKB);
|
||||
}
|
||||
|
||||
// Remove native MKB support on mobile browsers or by user's choice
|
||||
if (getPref(PrefKey.NATIVE_MKB_ENABLED) === 'off') {
|
||||
supportedInputTypes = supportedInputTypes.filter(i => i !== InputType.MKB);
|
||||
supportedInputTypes = supportedInputTypes.filter(i => i !== SupportedInputType.MKB);
|
||||
}
|
||||
|
||||
titleInfo.details.hasMkbSupport = supportedInputTypes.includes(InputType.MKB);
|
||||
titleInfo.details.hasMkbSupport = supportedInputTypes.includes(SupportedInputType.MKB);
|
||||
|
||||
if (STATES.userAgent.capabilities.touch) {
|
||||
let touchControllerAvailability = getPref(PrefKey.STREAM_TOUCH_CONTROLLER);
|
||||
@ -55,21 +57,21 @@ export const BxExposed = {
|
||||
|
||||
if (touchControllerAvailability === 'off') {
|
||||
// Disable touch on all games (not native touch)
|
||||
supportedInputTypes = supportedInputTypes.filter(i => i !== InputType.CUSTOM_TOUCH_OVERLAY && i !== InputType.GENERIC_TOUCH);
|
||||
supportedInputTypes = supportedInputTypes.filter(i => i !== SupportedInputType.CUSTOM_TOUCH_OVERLAY && i !== SupportedInputType.GENERIC_TOUCH);
|
||||
// Empty TABs
|
||||
titleInfo.details.supportedTabs = [];
|
||||
}
|
||||
|
||||
// Pre-check supported input types
|
||||
titleInfo.details.hasNativeTouchSupport = supportedInputTypes.includes(InputType.NATIVE_TOUCH);
|
||||
titleInfo.details.hasNativeTouchSupport = supportedInputTypes.includes(SupportedInputType.NATIVE_TOUCH);
|
||||
titleInfo.details.hasTouchSupport = titleInfo.details.hasNativeTouchSupport ||
|
||||
supportedInputTypes.includes(InputType.CUSTOM_TOUCH_OVERLAY) ||
|
||||
supportedInputTypes.includes(InputType.GENERIC_TOUCH);
|
||||
supportedInputTypes.includes(SupportedInputType.CUSTOM_TOUCH_OVERLAY) ||
|
||||
supportedInputTypes.includes(SupportedInputType.GENERIC_TOUCH);
|
||||
|
||||
if (!titleInfo.details.hasTouchSupport && touchControllerAvailability === 'all') {
|
||||
// Add generic touch support for non touch-supported games
|
||||
titleInfo.details.hasFakeTouchSupport = true;
|
||||
supportedInputTypes.push(InputType.GENERIC_TOUCH);
|
||||
supportedInputTypes.push(SupportedInputType.GENERIC_TOUCH);
|
||||
}
|
||||
}
|
||||
|
||||
@ -120,9 +122,9 @@ export const BxExposed = {
|
||||
disableGamepadPolling: false,
|
||||
|
||||
backButtonPressed: () => {
|
||||
const streamSettings = StreamSettings.getInstance();
|
||||
if (streamSettings.isShowing()) {
|
||||
streamSettings.hide();
|
||||
const navigationDialogManager = NavigationDialogManager.getInstance();
|
||||
if (navigationDialogManager.isShowing()) {
|
||||
navigationDialogManager.hide();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -5,8 +5,6 @@ type BxFlags = Partial<{
|
||||
EnableXcloudLogging: boolean;
|
||||
SafariWorkaround: boolean;
|
||||
|
||||
UseDevTouchLayout: boolean;
|
||||
|
||||
ForceNativeMkbTitles: string[];
|
||||
FeatureGates: {[key: string]: boolean} | null,
|
||||
|
||||
@ -26,8 +24,6 @@ const DEFAULT_FLAGS: BxFlags = {
|
||||
EnableXcloudLogging: false,
|
||||
SafariWorkaround: true,
|
||||
|
||||
UseDevTouchLayout: false,
|
||||
|
||||
ForceNativeMkbTitles: [],
|
||||
FeatureGates: null,
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
import iconBetterXcloud from "@assets/svg/better-xcloud.svg" with { type: "text" };
|
||||
import iconClose from "@assets/svg/close.svg" with { type: "text" };
|
||||
import iconCommand from "@assets/svg/command.svg" with { type: "text" };
|
||||
import iconController from "@assets/svg/controller.svg" with { type: "text" };
|
||||
import iconCopy from "@assets/svg/copy.svg" with { type: "text" };
|
||||
@ -34,8 +36,10 @@ import iconUpload from "@assets/svg/upload.svg" with { type: "text" };
|
||||
|
||||
|
||||
export const BxIcon = {
|
||||
BETTER_XCLOUD: iconBetterXcloud,
|
||||
STREAM_SETTINGS: iconStreamSettings,
|
||||
STREAM_STATS: iconStreamStats,
|
||||
CLOSE: iconClose,
|
||||
COMMAND: iconCommand,
|
||||
CONTROLLER: iconController,
|
||||
CREATE_SHORTCUT: iconCreateShortcut,
|
||||
|
@ -1,4 +1,4 @@
|
||||
enum TextColor {
|
||||
const enum TextColor {
|
||||
INFO = '#008746',
|
||||
WARNING = '#c1a404',
|
||||
ERROR = '#c10404',
|
||||
@ -19,7 +19,7 @@ export class BxLogger {
|
||||
BxLogger.#log(TextColor.ERROR, tag, ...args);
|
||||
}
|
||||
|
||||
static #log(color: TextColor, tag: string, ...args: any) {
|
||||
static #log(color: string, tag: string, ...args: any) {
|
||||
console.log(`%c${BxLogger.#PREFIX}`, `color:${color};font-weight:bold;`, tag, '//', ...args);
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { CE } from "@utils/html";
|
||||
import { PrefKey, getPref } from "@utils/preferences";
|
||||
import { renderStylus } from "@macros/build" with {type: "macro"};
|
||||
import { compressCss, renderStylus } from "@macros/build" with {type: "macro"};
|
||||
import { UiSection } from "@/enums/ui-sections";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref } from "./settings-storages/global-settings-storage";
|
||||
|
||||
|
||||
export function addCss() {
|
||||
@ -19,6 +20,17 @@ export function addCss() {
|
||||
// Hide "All games" section
|
||||
if (PREF_HIDE_SECTIONS.includes(UiSection.ALL_GAMES)) {
|
||||
selectorToHide.push('#BodyContent div[class*=AllGamesRow-module__gridContainer]');
|
||||
selectorToHide.push('#BodyContent div[class*=AllGamesRow-module__rowHeader]');
|
||||
}
|
||||
|
||||
// Hide "Most popular" section
|
||||
if (PREF_HIDE_SECTIONS.includes(UiSection.MOST_POPULAR)) {
|
||||
selectorToHide.push('#BodyContent div[class*=HomePage-module__bottomSpacing]:has(a[href="/play/gallery/popular"])');
|
||||
}
|
||||
|
||||
// Hide "Play with touch" section
|
||||
if (PREF_HIDE_SECTIONS.includes(UiSection.TOUCH)) {
|
||||
selectorToHide.push('#BodyContent div[class*=HomePage-module__bottomSpacing]:has(a[href="/play/gallery/touch"])');
|
||||
}
|
||||
|
||||
// Hide "Start a party" button in the Guide menu
|
||||
@ -32,18 +44,18 @@ export function addCss() {
|
||||
|
||||
// Reduce animations
|
||||
if (getPref(PrefKey.REDUCE_ANIMATIONS)) {
|
||||
css += `
|
||||
css += compressCss(`
|
||||
div[class*=GameCard-module__gameTitleInnerWrapper],
|
||||
div[class*=GameCard-module__card],
|
||||
div[class*=ScrollArrows-module] {
|
||||
transition: none !important;
|
||||
}
|
||||
`;
|
||||
`);
|
||||
}
|
||||
|
||||
// Hide the top-left dots icon while playing
|
||||
if (getPref(PrefKey.HIDE_DOTS_ICON)) {
|
||||
css += `
|
||||
css += compressCss(`
|
||||
div[class*=Grip-module__container] {
|
||||
visibility: hidden;
|
||||
}
|
||||
@ -65,18 +77,18 @@ button[class*=GripHandle-module__container][aria-expanded=false] {
|
||||
div[class*=StreamHUD-module__buttonsContainer] {
|
||||
padding: 0px !important;
|
||||
}
|
||||
`;
|
||||
`);
|
||||
}
|
||||
|
||||
css += `
|
||||
css += compressCss(`
|
||||
div[class*=StreamMenu-module__menu] {
|
||||
min-width: 100vw !important;
|
||||
}
|
||||
`;
|
||||
`);
|
||||
|
||||
// Simplify Stream's menu
|
||||
if (getPref(PrefKey.STREAM_SIMPLIFY_MENU)) {
|
||||
css += `
|
||||
css += compressCss(`
|
||||
div[class*=Menu-module__scrollable] {
|
||||
--bxStreamMenuItemSize: 80px;
|
||||
--streamMenuItemSize: calc(var(--bxStreamMenuItemSize) + 40px) !important;
|
||||
@ -107,9 +119,9 @@ svg[class*=MenuItem-module__icon] {
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
`;
|
||||
`);
|
||||
} else {
|
||||
css += `
|
||||
css += compressCss(`
|
||||
body[data-media-type=tv] .bx-badges {
|
||||
top: calc(var(--streamMenuItemSize) + 30px);
|
||||
}
|
||||
@ -131,12 +143,12 @@ body:not([data-media-type=tv]) div[class*=MenuItem-module__label] {
|
||||
margin-left: 8px !important;
|
||||
margin-right: 8px !important;
|
||||
}
|
||||
`;
|
||||
`);
|
||||
}
|
||||
|
||||
// Hide scrollbar
|
||||
if (getPref(PrefKey.UI_SCROLLBAR_HIDE)) {
|
||||
css += `
|
||||
css += compressCss(`
|
||||
html {
|
||||
scrollbar-width: none;
|
||||
}
|
||||
@ -144,7 +156,7 @@ html {
|
||||
body::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
`);
|
||||
}
|
||||
|
||||
const $style = CE('style', {}, css);
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { BX_FLAGS } from "./bx-flags";
|
||||
import { getPref, PrefKey } from "./preferences";
|
||||
import { getPref } from "./settings-storages/global-settings-storage";
|
||||
|
||||
export let FeatureGates: {[key: string]: boolean} = {
|
||||
'PwaPrompt': false,
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { EmulatedMkbHandler } from "@modules/mkb/mkb-handler";
|
||||
import { PrefKey, getPref } from "@utils/preferences";
|
||||
import { t } from "@utils/translation";
|
||||
import { Toast } from "@utils/toast";
|
||||
import { BxLogger } from "@utils/bx-logger";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref } from "./settings-storages/global-settings-storage";
|
||||
|
||||
// Show a toast when connecting/disconecting controller
|
||||
export function showGamepadToast(gamepad: Gamepad) {
|
||||
|
@ -1,3 +1,4 @@
|
||||
import type { BaseSettingsStore } from "./settings-storages/base-settings-storage";
|
||||
import { UserAgent } from "./user-agent";
|
||||
|
||||
export const SCRIPT_VERSION = Bun.env.SCRIPT_VERSION!;
|
||||
@ -42,6 +43,8 @@ export const STATES: BxStates = {
|
||||
pointerServerPort: 9269,
|
||||
};
|
||||
|
||||
export const STORAGE: {[key: string]: BaseSettingsStore} = {};
|
||||
|
||||
export function deepClone(obj: any): any {
|
||||
if ('structuredClone' in window) {
|
||||
return structuredClone(obj);
|
||||
|
@ -2,7 +2,7 @@ import { BxEvent } from "@utils/bx-event";
|
||||
import { LoadingScreen } from "@modules/loading-screen";
|
||||
import { RemotePlay } from "@modules/remote-play";
|
||||
import { HeaderSection } from "@/modules/ui/header";
|
||||
import { StreamSettings } from "@/modules/stream/stream-settings";
|
||||
import { NavigationDialogManager } from "@/modules/ui/dialog/navigation-dialog";
|
||||
|
||||
export function patchHistoryMethod(type: 'pushState' | 'replaceState') {
|
||||
const orig = window.history[type];
|
||||
@ -32,10 +32,8 @@ export function onHistoryChanged(e: PopStateEvent) {
|
||||
$settings.classList.add('bx-gone');
|
||||
}
|
||||
|
||||
// Hide Stream settings
|
||||
if (document.querySelector('.' + StreamSettings.MAIN_CLASS)) {
|
||||
StreamSettings.getInstance().hide();
|
||||
}
|
||||
// Hide Navigation dialog
|
||||
NavigationDialogManager.getInstance().hide();
|
||||
|
||||
// Hide Remote Play popup
|
||||
RemotePlay.detachPopup();
|
||||
|
@ -1,4 +1,6 @@
|
||||
import type { BxIcon } from "@utils/bx-icon";
|
||||
import { setNearby } from "./navigation-utils";
|
||||
import type { NavigationNearbyElements } from "@/modules/ui/dialog/navigation-dialog";
|
||||
|
||||
type BxButton = {
|
||||
style?: number | string | ButtonStyle;
|
||||
@ -16,7 +18,13 @@ type BxButton = {
|
||||
type ButtonStyle = {[index: string]: number} & {[index: number]: string};
|
||||
|
||||
// Quickly create a tree of elements without having to use innerHTML
|
||||
function createElement<T=HTMLElement>(elmName: string, props: {[index: string]: any}={}, ..._: any): T {
|
||||
type CreateElementOptions = {
|
||||
[index: string]: any;
|
||||
_nearby?: NavigationNearbyElements;
|
||||
};
|
||||
|
||||
|
||||
function createElement<T=HTMLElement>(elmName: string, props: CreateElementOptions={}, ..._: any): T {
|
||||
let $elm;
|
||||
const hasNs = 'xmlns' in props;
|
||||
|
||||
@ -27,6 +35,11 @@ function createElement<T=HTMLElement>(elmName: string, props: {[index: string]:
|
||||
$elm = document.createElement(elmName);
|
||||
}
|
||||
|
||||
if (props['_nearby']) {
|
||||
setNearby($elm, props['_nearby']);
|
||||
delete props['_nearby'];
|
||||
}
|
||||
|
||||
for (const key in props) {
|
||||
if ($elm.hasOwnProperty(key)) {
|
||||
continue;
|
||||
@ -71,10 +84,14 @@ export const ButtonStyle: DualEnum = {};
|
||||
ButtonStyle[ButtonStyle.PRIMARY = 1] = 'bx-primary';
|
||||
ButtonStyle[ButtonStyle.DANGER = 2] = 'bx-danger';
|
||||
ButtonStyle[ButtonStyle.GHOST = 4] = 'bx-ghost';
|
||||
ButtonStyle[ButtonStyle.FOCUSABLE = 8] = 'bx-focusable';
|
||||
ButtonStyle[ButtonStyle.FULL_WIDTH = 16] = 'bx-full-width';
|
||||
ButtonStyle[ButtonStyle.FULL_HEIGHT = 32] = 'bx-full-height';
|
||||
ButtonStyle[ButtonStyle.TALL = 64] = 'bx-tall';
|
||||
ButtonStyle[ButtonStyle.FROSTED = 8] = 'bx-frosted';
|
||||
ButtonStyle[ButtonStyle.DROP_SHADOW = 16] = 'bx-drop-shadow';
|
||||
ButtonStyle[ButtonStyle.FOCUSABLE = 32] = 'bx-focusable';
|
||||
ButtonStyle[ButtonStyle.FULL_WIDTH = 64] = 'bx-full-width';
|
||||
ButtonStyle[ButtonStyle.FULL_HEIGHT = 128] = 'bx-full-height';
|
||||
ButtonStyle[ButtonStyle.TALL = 256] = 'bx-tall';
|
||||
ButtonStyle[ButtonStyle.CIRCULAR = 512] = 'bx-circular';
|
||||
ButtonStyle[ButtonStyle.NORMAL_CASE = 1024] = 'bx-normal-case';
|
||||
|
||||
const ButtonStyleIndices = Object.keys(ButtonStyle).splice(0, Object.keys(ButtonStyle).length / 2).map(i => parseInt(i));
|
||||
|
||||
@ -100,7 +117,7 @@ export const createButton = <T=HTMLButtonElement>(options: BxButton): T => {
|
||||
options.title && $btn.setAttribute('title', options.title);
|
||||
options.disabled && (($btn as HTMLButtonElement).disabled = true);
|
||||
options.onClick && $btn.addEventListener('click', options.onClick);
|
||||
typeof options.tabIndex === 'number' && ($btn.tabIndex = options.tabIndex!);
|
||||
$btn.tabIndex = typeof options.tabIndex === 'number' ? options.tabIndex : 0;
|
||||
|
||||
for (const key in options.attributes) {
|
||||
if (!$btn.hasOwnProperty(key)) {
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { MkbPreset } from "@modules/mkb/mkb-preset";
|
||||
import { PrefKey, setPref } from "@utils/preferences";
|
||||
import { t } from "@utils/translation";
|
||||
import type { MkbStoredPreset, MkbStoredPresets } from "@/types/mkb";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { setPref } from "./settings-storages/global-settings-storage";
|
||||
|
||||
export class LocalDb {
|
||||
static #instance: LocalDb;
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { BxEvent } from "@utils/bx-event";
|
||||
import { getPref, PrefKey } from "@utils/preferences";
|
||||
import { STATES } from "@utils/global";
|
||||
import { BxLogger } from "@utils/bx-logger";
|
||||
import { patchSdpBitrate, setCodecPreferences } from "./sdp";
|
||||
import { StreamPlayer, type StreamPlayerOptions } from "@/modules/stream-player";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref } from "./settings-storages/global-settings-storage";
|
||||
|
||||
export function patchVideoApi() {
|
||||
const PREF_SKIP_SPLASH_VIDEO = getPref(PrefKey.SKIP_SPLASH_VIDEO);
|
||||
@ -99,8 +100,6 @@ export function patchRtcPeerConnection() {
|
||||
BxLogger.error('setLocalDescription', e);
|
||||
}
|
||||
|
||||
BxLogger.info('setLocalDescription', arguments[0].sdp);
|
||||
|
||||
// @ts-ignore
|
||||
return nativeSetLocalDescription.apply(this, arguments);
|
||||
};
|
||||
|
14
src/utils/navigation-utils.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import type { NavigationElement, NavigationNearbyElements } from "@/modules/ui/dialog/navigation-dialog";
|
||||
|
||||
export class NavigationUtils {
|
||||
static setNearby($elm: NavigationElement, nearby: NavigationNearbyElements) {
|
||||
$elm.nearby = $elm.nearby || {};
|
||||
|
||||
let key: keyof typeof nearby;
|
||||
for (key in nearby) {
|
||||
$elm.nearby[key] = nearby[key] as any;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const setNearby = NavigationUtils.setNearby;
|
@ -1,6 +1,5 @@
|
||||
import { BxEvent } from "@utils/bx-event";
|
||||
import { BX_FLAGS, NATIVE_FETCH } from "@utils/bx-flags";
|
||||
import { PrefKey, getPref } from "@utils/preferences";
|
||||
import { TouchController } from "@modules/touch-controller";
|
||||
import { STATES } from "@utils/global";
|
||||
import { GamePassCloudGallery } from "../enums/game-pass-gallery";
|
||||
@ -8,11 +7,10 @@ import { FeatureGates } from "./feature-gates";
|
||||
import { BxLogger } from "./bx-logger";
|
||||
import { XhomeInterceptor } from "./xhome-interceptor";
|
||||
import { XcloudInterceptor } from "./xcloud-interceptor";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref } from "./settings-storages/global-settings-storage";
|
||||
|
||||
enum RequestType {
|
||||
XCLOUD = 'xcloud',
|
||||
XHOME = 'xhome',
|
||||
};
|
||||
type RequestType = 'xcloud' | 'xhome';
|
||||
|
||||
function clearApplicationInsightsBuffers() {
|
||||
window.sessionStorage.removeItem('AI_buffer');
|
||||
@ -258,12 +256,12 @@ export function interceptHttpRequests() {
|
||||
|
||||
let requestType: RequestType;
|
||||
if (url.includes('/sessions/home') || url.includes('xhome.') || (STATES.remotePlay.isPlaying && url.endsWith('/inputconfigs'))) {
|
||||
requestType = RequestType.XHOME;
|
||||
requestType = 'xhome';
|
||||
} else {
|
||||
requestType = RequestType.XCLOUD;
|
||||
requestType = 'xcloud';
|
||||
}
|
||||
|
||||
if (requestType === RequestType.XHOME) {
|
||||
if (requestType === 'xhome') {
|
||||
return XhomeInterceptor.handle(request as Request);
|
||||
}
|
||||
|
||||
|
@ -2,8 +2,9 @@ import { deepClone, STATES } from "@utils/global";
|
||||
import { BxLogger } from "./bx-logger";
|
||||
import { TouchController } from "@modules/touch-controller";
|
||||
import { GamePassCloudGallery } from "../enums/game-pass-gallery";
|
||||
import { getPref, PrefKey } from "./preferences";
|
||||
import { BX_FLAGS } from "./bx-flags";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref } from "./settings-storages/global-settings-storage";
|
||||
|
||||
const LOG_TAG = 'PreloadState';
|
||||
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { getPref, PrefKey } from "@utils/preferences";
|
||||
import { STATES } from "@utils/global";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref } from "./settings-storages/global-settings-storage";
|
||||
|
||||
|
||||
export function getPreferredServerRegion(shortName = false) {
|
||||
export function getPreferredServerRegion(shortName = false): string | null {
|
||||
let preferredRegion = getPref(PrefKey.SERVER_REGION);
|
||||
if (preferredRegion in STATES.serverRegions) {
|
||||
if (shortName && STATES.serverRegions[preferredRegion].shortName) {
|
||||
@ -25,5 +26,5 @@ export function getPreferredServerRegion(shortName = false) {
|
||||
}
|
||||
}
|
||||
|
||||
return '???';
|
||||
return null;
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { StreamPlayerType } from "@enums/stream-player";
|
||||
import { AppInterface, STATES } from "./global";
|
||||
import { CE } from "./html";
|
||||
import { getPref, PrefKey } from "./preferences";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref } from "./settings-storages/global-settings-storage";
|
||||
|
||||
|
||||
export class Screenshot {
|
||||
|
@ -1,5 +1,8 @@
|
||||
import type { PreferenceSetting } from "@/types/preferences";
|
||||
import { CE } from "@utils/html";
|
||||
import { setNearby } from "./navigation-utils";
|
||||
import type { PrefKey } from "@/enums/pref-keys";
|
||||
import type { BaseSettingsStore } from "./settings-storages/base-settings-storage";
|
||||
|
||||
type MultipleOptionsParams = {
|
||||
size?: number;
|
||||
@ -50,7 +53,8 @@ export class SettingElement {
|
||||
onChange && $control.addEventListener('input', e => {
|
||||
const target = e.target as HTMLSelectElement;
|
||||
const value = (setting.type && setting.type === 'number') ? parseInt(target.value) : target.value;
|
||||
onChange(e, value);
|
||||
|
||||
!(e as any).ignoreOnChange && onChange(e, value);
|
||||
});
|
||||
|
||||
// Custom method
|
||||
@ -102,7 +106,8 @@ export class SettingElement {
|
||||
onChange && $control.addEventListener('input', (e: Event) => {
|
||||
const target = e.target as HTMLSelectElement
|
||||
const values = Array.from(target.selectedOptions).map(i => i.value);
|
||||
onChange(e, values);
|
||||
|
||||
!(e as any).ignoreOnChange && onChange(e, values);
|
||||
});
|
||||
|
||||
return $control;
|
||||
@ -117,7 +122,7 @@ export class SettingElement {
|
||||
const value = Math.max(setting.min!, Math.min(setting.max!, parseInt(target.value)));
|
||||
target.value = value.toString();
|
||||
|
||||
onChange(e, value);
|
||||
!(e as any).ignoreOnChange && onChange(e, value);
|
||||
});
|
||||
|
||||
return $control;
|
||||
@ -128,7 +133,7 @@ export class SettingElement {
|
||||
$control.checked = currentValue;
|
||||
|
||||
onChange && $control.addEventListener('change', e => {
|
||||
onChange(e, (e.target as HTMLInputElement).checked);
|
||||
!(e as any).ignoreOnChange && onChange(e, (e.target as HTMLInputElement).checked);
|
||||
});
|
||||
|
||||
return $control;
|
||||
@ -143,7 +148,7 @@ export class SettingElement {
|
||||
let $text: HTMLSpanElement;
|
||||
let $btnDec: HTMLButtonElement;
|
||||
let $btnInc: HTMLButtonElement;
|
||||
let $range: HTMLInputElement;
|
||||
let $range: HTMLInputElement | null = null;
|
||||
|
||||
let controlValue = value;
|
||||
|
||||
@ -187,6 +192,10 @@ export class SettingElement {
|
||||
}, '+') as HTMLButtonElement,
|
||||
);
|
||||
|
||||
if (options.disabled) {
|
||||
($wrapper as any).disabled = true;
|
||||
}
|
||||
|
||||
if (!options.disabled && !options.hideSlider) {
|
||||
$range = CE('input', {
|
||||
id: `bx_setting_${key}`,
|
||||
@ -212,6 +221,7 @@ export class SettingElement {
|
||||
|
||||
!(e as any).ignoreOnChange && onChange && onChange(e, value);
|
||||
});
|
||||
|
||||
$wrapper.appendChild($range);
|
||||
|
||||
if (options.ticks || options.exactTicks) {
|
||||
@ -277,7 +287,7 @@ export class SettingElement {
|
||||
$range && ($range.value = value.toString());
|
||||
|
||||
isHolding = false;
|
||||
onChange && onChange(e, value);
|
||||
!(e as any).ignoreOnChange && onChange && onChange(e, value);
|
||||
}
|
||||
|
||||
const onMouseDown = (e: PointerEvent) => {
|
||||
@ -322,6 +332,10 @@ export class SettingElement {
|
||||
$btnInc.addEventListener('pointerup', onMouseUp);
|
||||
$btnInc.addEventListener('contextmenu', onContextMenu);
|
||||
|
||||
setNearby($wrapper, {
|
||||
focus: $range || $btnInc,
|
||||
})
|
||||
|
||||
return $wrapper;
|
||||
}
|
||||
|
||||
@ -349,4 +363,34 @@ export class SettingElement {
|
||||
|
||||
return $control;
|
||||
}
|
||||
|
||||
static fromPref(key: PrefKey, storage: BaseSettingsStore, onChange: any, overrideParams={}) {
|
||||
const definition = storage.getDefinition(key);
|
||||
let currentValue = storage.getSetting(key);
|
||||
|
||||
let type;
|
||||
if ('type' in definition) {
|
||||
type = definition.type;
|
||||
} else if ('options' in definition) {
|
||||
type = SettingElementType.OPTIONS;
|
||||
} else if ('multipleOptions' in definition) {
|
||||
type = SettingElementType.MULTIPLE_OPTIONS;
|
||||
} else if (typeof definition.default === 'number') {
|
||||
type = SettingElementType.NUMBER;
|
||||
} else {
|
||||
type = SettingElementType.CHECKBOX;
|
||||
}
|
||||
|
||||
const params = Object.assign(overrideParams, definition.params || {});
|
||||
if (params.disabled) {
|
||||
currentValue = definition.default;
|
||||
}
|
||||
|
||||
const $control = SettingElement.render(type!, key as string, definition, currentValue, (e: any, value: any) => {
|
||||
storage.setSetting(key, value);
|
||||
onChange && onChange(e, value);
|
||||
}, params);
|
||||
|
||||
return $control;
|
||||
}
|
||||
}
|
124
src/utils/settings-storages/base-settings-storage.ts
Normal file
@ -0,0 +1,124 @@
|
||||
import type { PrefKey } from "@/enums/pref-keys";
|
||||
import type { SettingDefinitions } from "@/types/setting-definition";
|
||||
import { BxEvent } from "../bx-event";
|
||||
|
||||
export class BaseSettingsStore {
|
||||
private storage: Storage;
|
||||
private storageKey: string;
|
||||
private _settings: object | null;
|
||||
private definitions: SettingDefinitions;
|
||||
|
||||
constructor(storageKey: string, definitions: SettingDefinitions) {
|
||||
this.storage = window.localStorage;
|
||||
this.storageKey = storageKey;
|
||||
|
||||
for (const settingId in definitions) {
|
||||
const setting = definitions[settingId];
|
||||
|
||||
/*
|
||||
if (setting.migrate && settingId in savedPrefs) {
|
||||
setting.migrate.call(this, savedPrefs, savedPrefs[settingId]);
|
||||
}
|
||||
*/
|
||||
|
||||
setting.ready && setting.ready.call(this, setting);
|
||||
}
|
||||
this.definitions = definitions;
|
||||
|
||||
this._settings = null;
|
||||
}
|
||||
|
||||
get settings() {
|
||||
if (this._settings) {
|
||||
return this._settings;
|
||||
}
|
||||
|
||||
const settings = JSON.parse(this.storage.getItem(this.storageKey) || '{}');
|
||||
this._settings = settings;
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
getDefinition(key: PrefKey) {
|
||||
if (!this.definitions[key]) {
|
||||
const error = 'Request invalid definition: ' + key;
|
||||
alert(error);
|
||||
throw Error(error);
|
||||
}
|
||||
|
||||
return this.definitions[key];
|
||||
}
|
||||
|
||||
getSetting(key: PrefKey) {
|
||||
if (typeof key === 'undefined') {
|
||||
debugger;
|
||||
return;
|
||||
}
|
||||
|
||||
// Return default value if the feature is not supported
|
||||
if (this.definitions[key].unsupported) {
|
||||
return this.definitions[key].default;
|
||||
}
|
||||
|
||||
if (!(key in this.settings)) {
|
||||
this.settings[key] = this.validateValue(key, null);
|
||||
}
|
||||
|
||||
return this.settings[key];
|
||||
}
|
||||
|
||||
setSetting(key: PrefKey, value: any, emitEvent = false) {
|
||||
value = this.validateValue(key, value);
|
||||
|
||||
this.settings[key] = value;
|
||||
this.saveSettings();
|
||||
|
||||
emitEvent && BxEvent.dispatch(window, BxEvent.SETTINGS_CHANGED, {
|
||||
storageKey: this.storageKey,
|
||||
settingKey: key,
|
||||
settingValue: value,
|
||||
});
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
saveSettings() {
|
||||
this.storage.setItem(this.storageKey, JSON.stringify(this.settings));
|
||||
}
|
||||
|
||||
private validateValue(key: PrefKey, value: any) {
|
||||
const def = this.definitions[key];
|
||||
if (!def) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (typeof value === 'undefined' || value === null) {
|
||||
value = def.default;
|
||||
}
|
||||
|
||||
if ('min' in def) {
|
||||
value = Math.max(def.min!, value);
|
||||
}
|
||||
|
||||
if ('max' in def) {
|
||||
value = Math.min(def.max!, value);
|
||||
}
|
||||
|
||||
if ('options' in def && !(value in def.options!)) {
|
||||
value = def.default;
|
||||
} else if ('multipleOptions' in def) {
|
||||
if (value.length) {
|
||||
const validOptions = Object.keys(def.multipleOptions!);
|
||||
value.forEach((item: any, idx: number) => {
|
||||
(validOptions.indexOf(item) === -1) && value.splice(idx, 1);
|
||||
});
|
||||
}
|
||||
|
||||
if (!value.length) {
|
||||
value = def.default;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
@ -1,116 +1,78 @@
|
||||
import { CE } from "@utils/html";
|
||||
import { SUPPORTED_LANGUAGES, t} from "@utils/translation";
|
||||
import { SettingElement, SettingElementType } from "@utils/settings";
|
||||
import { UserAgent } from "@utils/user-agent";
|
||||
import { StreamStat } from "@modules/stream/stream-stats";
|
||||
import type { PreferenceSetting, PreferenceSettings } from "@/types/preferences";
|
||||
import { AppInterface, STATES } from "@utils/global";
|
||||
import { StreamPlayerType, StreamVideoProcessing } from "@enums/stream-player";
|
||||
import { UserAgentProfile } from "@/enums/user-agent";
|
||||
import { UiSection } from "@/enums/ui-sections";
|
||||
import { BypassServers } from "@/enums/bypass-servers";
|
||||
import { BX_FLAGS } from "./bx-flags";
|
||||
import { PrefKey, StorageKey } from "@/enums/pref-keys";
|
||||
import { StreamPlayerType, StreamVideoProcessing } from "@/enums/stream-player";
|
||||
import { UiSection } from "@/enums/ui-sections";
|
||||
import { UserAgentProfile } from "@/enums/user-agent";
|
||||
import { StreamStat } from "@/modules/stream/stream-stats";
|
||||
import type { PreferenceSetting } from "@/types/preferences";
|
||||
import type { SettingDefinitions } from "@/types/setting-definition";
|
||||
import { BX_FLAGS } from "../bx-flags";
|
||||
import { STATES, AppInterface, STORAGE } from "../global";
|
||||
import { CE } from "../html";
|
||||
import { SettingElementType } from "../setting-element";
|
||||
import { t, SUPPORTED_LANGUAGES } from "../translation";
|
||||
import { UserAgent } from "../user-agent";
|
||||
import { BaseSettingsStore as BaseSettingsStorage } from "./base-settings-storage";
|
||||
|
||||
export enum PrefKey {
|
||||
LAST_UPDATE_CHECK = 'version_last_check',
|
||||
LATEST_VERSION = 'version_latest',
|
||||
CURRENT_VERSION = 'version_current',
|
||||
|
||||
BETTER_XCLOUD_LOCALE = 'bx_locale',
|
||||
function getSupportedCodecProfiles() {
|
||||
const options: {[index: string]: string} = {
|
||||
default: t('default'),
|
||||
};
|
||||
|
||||
SERVER_REGION = 'server_region',
|
||||
SERVER_BYPASS_RESTRICTION = 'server_bypass_restriction',
|
||||
if (!('getCapabilities' in RTCRtpReceiver)) {
|
||||
return options;
|
||||
}
|
||||
|
||||
PREFER_IPV6_SERVER = 'prefer_ipv6_server',
|
||||
STREAM_TARGET_RESOLUTION = 'stream_target_resolution',
|
||||
STREAM_PREFERRED_LOCALE = 'stream_preferred_locale',
|
||||
STREAM_CODEC_PROFILE = 'stream_codec_profile',
|
||||
let hasLowCodec = false;
|
||||
let hasNormalCodec = false;
|
||||
let hasHighCodec = false;
|
||||
|
||||
USER_AGENT_PROFILE = 'user_agent_profile',
|
||||
STREAM_SIMPLIFY_MENU = 'stream_simplify_menu',
|
||||
const codecs = RTCRtpReceiver.getCapabilities('video')!.codecs;
|
||||
for (let codec of codecs) {
|
||||
if (codec.mimeType.toLowerCase() !== 'video/h264' || !codec.sdpFmtpLine) {
|
||||
continue;
|
||||
}
|
||||
|
||||
STREAM_COMBINE_SOURCES = 'stream_combine_sources',
|
||||
const fmtp = codec.sdpFmtpLine.toLowerCase();
|
||||
if (fmtp.includes('profile-level-id=4d')) {
|
||||
hasHighCodec = true;
|
||||
} else if (fmtp.includes('profile-level-id=42e')) {
|
||||
hasNormalCodec = true;
|
||||
} else if (fmtp.includes('profile-level-id=420')) {
|
||||
hasLowCodec = true;
|
||||
}
|
||||
}
|
||||
|
||||
STREAM_TOUCH_CONTROLLER = 'stream_touch_controller',
|
||||
STREAM_TOUCH_CONTROLLER_AUTO_OFF = 'stream_touch_controller_auto_off',
|
||||
STREAM_TOUCH_CONTROLLER_DEFAULT_OPACITY = 'stream_touch_controller_default_opacity',
|
||||
STREAM_TOUCH_CONTROLLER_STYLE_STANDARD = 'stream_touch_controller_style_standard',
|
||||
STREAM_TOUCH_CONTROLLER_STYLE_CUSTOM = 'stream_touch_controller_style_custom',
|
||||
if (hasHighCodec) {
|
||||
if (!hasLowCodec && !hasNormalCodec) {
|
||||
options.default = `${t('visual-quality-high')} (${t('default')})`;
|
||||
} else {
|
||||
options.high = t('visual-quality-high');
|
||||
}
|
||||
}
|
||||
|
||||
STREAM_DISABLE_FEEDBACK_DIALOG = 'stream_disable_feedback_dialog',
|
||||
if (hasNormalCodec) {
|
||||
if (!hasLowCodec && !hasHighCodec) {
|
||||
options.default = `${t('visual-quality-normal')} (${t('default')})`;
|
||||
} else {
|
||||
options.normal = t('visual-quality-normal');
|
||||
}
|
||||
}
|
||||
|
||||
BITRATE_VIDEO_MAX = 'bitrate_video_max',
|
||||
if (hasLowCodec) {
|
||||
if (!hasNormalCodec && !hasHighCodec) {
|
||||
options.default = `${t('visual-quality-low')} (${t('default')})`;
|
||||
} else {
|
||||
options.low = t('visual-quality-low');
|
||||
}
|
||||
}
|
||||
|
||||
GAME_BAR_POSITION = 'game_bar_position',
|
||||
|
||||
LOCAL_CO_OP_ENABLED = 'local_co_op_enabled',
|
||||
// LOCAL_CO_OP_SEPARATE_TOUCH_CONTROLLER = 'local_co_op_separate_touch_controller',
|
||||
|
||||
CONTROLLER_ENABLE_SHORTCUTS = 'controller_enable_shortcuts',
|
||||
CONTROLLER_ENABLE_VIBRATION = 'controller_enable_vibration',
|
||||
CONTROLLER_DEVICE_VIBRATION = 'controller_device_vibration',
|
||||
CONTROLLER_VIBRATION_INTENSITY = 'controller_vibration_intensity',
|
||||
CONTROLLER_SHOW_CONNECTION_STATUS = 'controller_show_connection_status',
|
||||
|
||||
NATIVE_MKB_ENABLED = 'native_mkb_enabled',
|
||||
NATIVE_MKB_SCROLL_HORIZONTAL_SENSITIVITY = 'native_mkb_scroll_x_sensitivity',
|
||||
NATIVE_MKB_SCROLL_VERTICAL_SENSITIVITY = 'native_mkb_scroll_y_sensitivity',
|
||||
|
||||
MKB_ENABLED = 'mkb_enabled',
|
||||
MKB_HIDE_IDLE_CURSOR = 'mkb_hide_idle_cursor',
|
||||
MKB_ABSOLUTE_MOUSE = 'mkb_absolute_mouse',
|
||||
MKB_DEFAULT_PRESET_ID = 'mkb_default_preset_id',
|
||||
|
||||
SCREENSHOT_APPLY_FILTERS = 'screenshot_apply_filters',
|
||||
|
||||
BLOCK_TRACKING = 'block_tracking',
|
||||
BLOCK_SOCIAL_FEATURES = 'block_social_features',
|
||||
SKIP_SPLASH_VIDEO = 'skip_splash_video',
|
||||
HIDE_DOTS_ICON = 'hide_dots_icon',
|
||||
REDUCE_ANIMATIONS = 'reduce_animations',
|
||||
|
||||
UI_LOADING_SCREEN_GAME_ART = 'ui_loading_screen_game_art',
|
||||
UI_LOADING_SCREEN_WAIT_TIME = 'ui_loading_screen_wait_time',
|
||||
UI_LOADING_SCREEN_ROCKET = 'ui_loading_screen_rocket',
|
||||
|
||||
UI_CONTROLLER_FRIENDLY = 'ui_controller_friendly',
|
||||
UI_LAYOUT = 'ui_layout',
|
||||
UI_SCROLLBAR_HIDE = 'ui_scrollbar_hide',
|
||||
UI_HIDE_SECTIONS = 'ui_hide_sections',
|
||||
|
||||
UI_HOME_CONTEXT_MENU_DISABLED = 'ui_home_context_menu_disabled',
|
||||
UI_GAME_CARD_SHOW_WAIT_TIME = 'ui_game_card_show_wait_time',
|
||||
|
||||
VIDEO_PLAYER_TYPE = 'video_player_type',
|
||||
VIDEO_PROCESSING = 'video_processing',
|
||||
VIDEO_POWER_PREFERENCE = 'video_power_preference',
|
||||
VIDEO_SHARPNESS = 'video_sharpness',
|
||||
VIDEO_RATIO = 'video_ratio',
|
||||
VIDEO_BRIGHTNESS = 'video_brightness',
|
||||
VIDEO_CONTRAST = 'video_contrast',
|
||||
VIDEO_SATURATION = 'video_saturation',
|
||||
|
||||
AUDIO_MIC_ON_PLAYING = 'audio_mic_on_playing',
|
||||
AUDIO_ENABLE_VOLUME_CONTROL = 'audio_enable_volume_control',
|
||||
AUDIO_VOLUME = 'audio_volume',
|
||||
|
||||
STATS_ITEMS = 'stats_items',
|
||||
STATS_SHOW_WHEN_PLAYING = 'stats_show_when_playing',
|
||||
STATS_QUICK_GLANCE = 'stats_quick_glance',
|
||||
STATS_POSITION = 'stats_position',
|
||||
STATS_TEXT_SIZE = 'stats_text_size',
|
||||
STATS_TRANSPARENT = 'stats_transparent',
|
||||
STATS_OPACITY = 'stats_opacity',
|
||||
STATS_CONDITIONAL_FORMATTING = 'stats_conditional_formatting',
|
||||
|
||||
REMOTE_PLAY_ENABLED = 'xhome_enabled',
|
||||
REMOTE_PLAY_RESOLUTION = 'xhome_resolution',
|
||||
|
||||
GAME_FORTNITE_FORCE_CONSOLE = 'game_fortnite_force_console',
|
||||
return options;
|
||||
}
|
||||
|
||||
export class Preferences {
|
||||
static SETTINGS: PreferenceSettings = {
|
||||
export class GlobalSettingsStorage extends BaseSettingsStorage {
|
||||
private static readonly DEFINITIONS: SettingDefinitions = {
|
||||
[PrefKey.LAST_UPDATE_CHECK]: {
|
||||
default: 0,
|
||||
},
|
||||
@ -185,61 +147,7 @@ export class Preferences {
|
||||
[PrefKey.STREAM_CODEC_PROFILE]: {
|
||||
label: t('visual-quality'),
|
||||
default: 'default',
|
||||
options: (() => {
|
||||
const options: {[index: string]: string} = {
|
||||
default: t('default'),
|
||||
};
|
||||
|
||||
if (!('getCapabilities' in RTCRtpReceiver)) {
|
||||
return options;
|
||||
}
|
||||
|
||||
let hasLowCodec = false;
|
||||
let hasNormalCodec = false;
|
||||
let hasHighCodec = false;
|
||||
|
||||
const codecs = RTCRtpReceiver.getCapabilities('video')!.codecs;
|
||||
for (let codec of codecs) {
|
||||
if (codec.mimeType.toLowerCase() !== 'video/h264' || !codec.sdpFmtpLine) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const fmtp = codec.sdpFmtpLine.toLowerCase();
|
||||
if (!hasHighCodec && fmtp.includes('profile-level-id=4d')) {
|
||||
hasHighCodec = true;
|
||||
} else if (!hasNormalCodec && fmtp.includes('profile-level-id=42e')) {
|
||||
hasNormalCodec = true;
|
||||
} else if (!hasLowCodec && fmtp.includes('profile-level-id=420')) {
|
||||
hasLowCodec = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasHighCodec) {
|
||||
if (!hasLowCodec && !hasNormalCodec) {
|
||||
options.default = `${t('visual-quality-high')} (${t('default')})`;
|
||||
} else {
|
||||
options.high = t('visual-quality-high');
|
||||
}
|
||||
}
|
||||
|
||||
if (hasNormalCodec) {
|
||||
if (!hasLowCodec && !hasHighCodec) {
|
||||
options.default = `${t('visual-quality-normal')} (${t('default')})`;
|
||||
} else {
|
||||
options.normal = t('visual-quality-normal');
|
||||
}
|
||||
}
|
||||
|
||||
if (hasLowCodec) {
|
||||
if (!hasNormalCodec && !hasHighCodec) {
|
||||
options.default = `${t('visual-quality-low')} (${t('default')})`;
|
||||
} else {
|
||||
options.low = t('visual-quality-low');
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
})(),
|
||||
options: getSupportedCodecProfiles(),
|
||||
ready: (setting: PreferenceSetting) => {
|
||||
const options: any = setting.options;
|
||||
const keys = Object.keys(options);
|
||||
@ -366,16 +274,6 @@ export class Preferences {
|
||||
}
|
||||
},
|
||||
},
|
||||
migrate: function(savedPrefs: any, value: any) {
|
||||
try {
|
||||
value = parseInt(value);
|
||||
if (value !== 0 && value < 100) {
|
||||
value *= 1024 * 1000;
|
||||
}
|
||||
this.set(PrefKey.BITRATE_VIDEO_MAX, value, true);
|
||||
savedPrefs[PrefKey.BITRATE_VIDEO_MAX] = value;
|
||||
} catch (e) {}
|
||||
},
|
||||
},
|
||||
|
||||
[PrefKey.GAME_BAR_POSITION]: {
|
||||
@ -559,7 +457,7 @@ export class Preferences {
|
||||
|
||||
[PrefKey.UI_CONTROLLER_FRIENDLY]: {
|
||||
label: t('controller-friendly-ui'),
|
||||
default: !STATES.browser.capabilities.touch || BX_FLAGS.DeviceInfo?.deviceType === "android-tv",
|
||||
default: BX_FLAGS.DeviceInfo!.deviceType !== 'unknown',
|
||||
},
|
||||
|
||||
[PrefKey.UI_LAYOUT]: {
|
||||
@ -588,11 +486,13 @@ export class Preferences {
|
||||
multipleOptions: {
|
||||
[UiSection.NEWS]: t('section-news'),
|
||||
[UiSection.FRIENDS]: t('section-play-with-friends'),
|
||||
// [UiSection.MOST_POPULAR]: t('section-most-popular'),
|
||||
[UiSection.NATIVE_MKB]: t('section-native-mkb'),
|
||||
[UiSection.TOUCH]: t('section-touch'),
|
||||
[UiSection.MOST_POPULAR]: t('section-most-popular'),
|
||||
[UiSection.ALL_GAMES]: t('section-all-games'),
|
||||
},
|
||||
params: {
|
||||
size: 3,
|
||||
size: 6,
|
||||
},
|
||||
},
|
||||
|
||||
@ -612,14 +512,14 @@ export class Preferences {
|
||||
[PrefKey.USER_AGENT_PROFILE]: {
|
||||
label: t('user-agent-profile'),
|
||||
note: '⚠️ ' + t('unexpected-behavior'),
|
||||
default: BX_FLAGS.DeviceInfo?.deviceType === 'android-tv' ? UserAgentProfile.VR_OCULUS : 'default',
|
||||
default: BX_FLAGS.DeviceInfo!.deviceType === 'android-tv' ? UserAgentProfile.VR_OCULUS : 'default',
|
||||
options: {
|
||||
[UserAgentProfile.DEFAULT]: t('default'),
|
||||
[UserAgentProfile.WINDOWS_EDGE]: 'Edge + Windows',
|
||||
[UserAgentProfile.MACOS_SAFARI]: 'Safari + macOS',
|
||||
[UserAgentProfile.VR_OCULUS]: 'Android TV',
|
||||
[UserAgentProfile.SMART_TV_GENERIC]: 'Smart TV',
|
||||
[UserAgentProfile.SMART_TV_TIZEN]: 'Samsung Smart TV',
|
||||
[UserAgentProfile.VR_OCULUS]: 'Meta Quest VR',
|
||||
[UserAgentProfile.CUSTOM]: t('custom'),
|
||||
},
|
||||
},
|
||||
@ -640,7 +540,7 @@ export class Preferences {
|
||||
},
|
||||
},
|
||||
[PrefKey.VIDEO_POWER_PREFERENCE]: {
|
||||
label: t('gpu-configuration'),
|
||||
label: t('renderer-configuration'),
|
||||
default: 'default',
|
||||
options: {
|
||||
'default': t('default'),
|
||||
@ -725,7 +625,7 @@ export class Preferences {
|
||||
default: 100,
|
||||
min: 0,
|
||||
max: 600,
|
||||
steps: 20,
|
||||
steps: 10,
|
||||
params: {
|
||||
suffix: '%',
|
||||
ticks: 100,
|
||||
@ -813,169 +713,16 @@ export class Preferences {
|
||||
default: false,
|
||||
note: t('fortnite-allow-stw-mode'),
|
||||
},
|
||||
|
||||
// Deprecated
|
||||
/*
|
||||
[Preferences.DEPRECATED_CONTROLLER_SUPPORT_LOCAL_CO_OP]: {
|
||||
default: false,
|
||||
'migrate': function(savedPrefs, value) {
|
||||
this.set(Preferences.LOCAL_CO_OP_ENABLED, value);
|
||||
savedPrefs[Preferences.LOCAL_CO_OP_ENABLED] = value;
|
||||
},
|
||||
},
|
||||
*/
|
||||
}
|
||||
|
||||
#storage = localStorage;
|
||||
#key = 'better_xcloud';
|
||||
#prefs: {[index: string]: any} = {};
|
||||
};
|
||||
|
||||
constructor() {
|
||||
let savedPrefsStr = this.#storage.getItem(this.#key);
|
||||
if (savedPrefsStr == null) {
|
||||
savedPrefsStr = '{}';
|
||||
}
|
||||
|
||||
const savedPrefs = JSON.parse(savedPrefsStr);
|
||||
|
||||
for (let settingId in Preferences.SETTINGS) {
|
||||
const setting = Preferences.SETTINGS[settingId];
|
||||
|
||||
if (setting.migrate && settingId in savedPrefs) {
|
||||
setting.migrate.call(this, savedPrefs, savedPrefs[settingId]);
|
||||
}
|
||||
|
||||
setting.ready && setting.ready.call(this, setting);
|
||||
}
|
||||
|
||||
for (let settingId in Preferences.SETTINGS) {
|
||||
const setting = Preferences.SETTINGS[settingId];
|
||||
if (!setting) {
|
||||
alert(`Undefined setting key: ${settingId}`);
|
||||
console.log('Undefined setting key');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ignore deprecated/migrated settings
|
||||
if (setting.migrate) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (settingId in savedPrefs) {
|
||||
this.#prefs[settingId] = this.#validateValue(settingId, savedPrefs[settingId]);
|
||||
} else {
|
||||
this.#prefs[settingId] = setting.default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#validateValue(key: keyof typeof Preferences.SETTINGS, value: any) {
|
||||
const config = Preferences.SETTINGS[key];
|
||||
if (!config) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (typeof value === 'undefined' || value === null) {
|
||||
value = config.default;
|
||||
}
|
||||
|
||||
if ('min' in config) {
|
||||
value = Math.max(config.min!, value);
|
||||
}
|
||||
|
||||
if ('max' in config) {
|
||||
value = Math.min(config.max!, value);
|
||||
}
|
||||
|
||||
if ('options' in config && !(value in config.options!)) {
|
||||
value = config.default;
|
||||
} else if ('multipleOptions' in config) {
|
||||
if (value.length) {
|
||||
const validOptions = Object.keys(config.multipleOptions!);
|
||||
value.forEach((item: any, idx: number) => {
|
||||
(validOptions.indexOf(item) === -1) && value.splice(idx, 1);
|
||||
});
|
||||
}
|
||||
|
||||
if (!value.length) {
|
||||
value = config.default;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
get(key: PrefKey) {
|
||||
if (typeof key === 'undefined') {
|
||||
debugger;
|
||||
return;
|
||||
}
|
||||
|
||||
// Return default value if the feature is not supported
|
||||
if (Preferences.SETTINGS[key].unsupported) {
|
||||
return Preferences.SETTINGS[key].default;
|
||||
}
|
||||
|
||||
if (!(key in this.#prefs)) {
|
||||
this.#prefs[key] = this.#validateValue(key, null);
|
||||
}
|
||||
|
||||
return this.#prefs[key];
|
||||
}
|
||||
|
||||
set(key: PrefKey, value: any, skipSave?: boolean): any {
|
||||
value = this.#validateValue(key, value);
|
||||
|
||||
this.#prefs[key] = value;
|
||||
!skipSave && this.#updateStorage();
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
#updateStorage() {
|
||||
this.#storage.setItem(this.#key, JSON.stringify(this.#prefs));
|
||||
}
|
||||
|
||||
toElement(key: keyof typeof Preferences.SETTINGS, onChange: any, overrideParams={}) {
|
||||
const setting = Preferences.SETTINGS[key];
|
||||
let currentValue = this.get(key);
|
||||
|
||||
let type;
|
||||
if ('type' in setting) {
|
||||
type = setting.type;
|
||||
} else if ('options' in setting) {
|
||||
type = SettingElementType.OPTIONS;
|
||||
} else if ('multipleOptions' in setting) {
|
||||
type = SettingElementType.MULTIPLE_OPTIONS;
|
||||
} else if (typeof setting.default === 'number') {
|
||||
type = SettingElementType.NUMBER;
|
||||
} else {
|
||||
type = SettingElementType.CHECKBOX;
|
||||
}
|
||||
|
||||
const params = Object.assign(overrideParams, setting.params || {});
|
||||
if (params.disabled) {
|
||||
currentValue = Preferences.SETTINGS[key].default;
|
||||
}
|
||||
|
||||
const $control = SettingElement.render(type!, key as string, setting, currentValue, (e: any, value: any) => {
|
||||
this.set(key, value);
|
||||
onChange && onChange(e, value);
|
||||
}, params);
|
||||
|
||||
return $control;
|
||||
}
|
||||
|
||||
toNumberStepper(key: keyof typeof Preferences.SETTINGS, onChange: any, options={}) {
|
||||
return SettingElement.render(SettingElementType.NUMBER_STEPPER, key, Preferences.SETTINGS[key], this.get(key), (e: any, value: any) => {
|
||||
this.set(key, value);
|
||||
onChange && onChange(e, value);
|
||||
}, options);
|
||||
super(StorageKey.GLOBAL, GlobalSettingsStorage.DEFINITIONS);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const prefs = new Preferences();
|
||||
export const getPref = prefs.get.bind(prefs);
|
||||
export const setPref = prefs.set.bind(prefs);
|
||||
export const toPrefElement = prefs.toElement.bind(prefs);
|
||||
const globalSettings = new GlobalSettingsStorage();
|
||||
export const getPrefDefinition = globalSettings.getDefinition.bind(globalSettings);
|
||||
export const getPref = globalSettings.getSetting.bind(globalSettings);
|
||||
export const setPref = globalSettings.setSetting.bind(globalSettings);
|
||||
STORAGE.Global = globalSettings;
|
@ -46,10 +46,12 @@ const Texts = {
|
||||
"badge-playtime": "Playtime",
|
||||
"badge-server": "Server",
|
||||
"badge-video": "Video",
|
||||
"better-xcloud": "Better xCloud",
|
||||
"bitrate-audio-maximum": "Maximum audio bitrate",
|
||||
"bitrate-video-maximum": "Maximum video bitrate",
|
||||
"bottom-left": "Bottom-left",
|
||||
"bottom-right": "Bottom-right",
|
||||
"brazil": "Brazil",
|
||||
"brightness": "Brightness",
|
||||
"browser-unsupported-feature": "Your browser doesn't support this feature",
|
||||
"bypass-region-restriction": "Bypass region restriction",
|
||||
@ -76,7 +78,7 @@ const Texts = {
|
||||
"controller-shortcuts-xbox-note": "Button to open the Guide menu",
|
||||
"controller-vibration": "Controller vibration",
|
||||
"copy": "Copy",
|
||||
"create-shortcut": "Create shortcut",
|
||||
"create-shortcut": "Shortcut",
|
||||
"custom": "Custom",
|
||||
"deadzone-counterweight": "Deadzone counterweight",
|
||||
"decrease": "Decrease",
|
||||
@ -110,7 +112,6 @@ const Texts = {
|
||||
"fortnite-force-console-version": "Fortnite: force console version",
|
||||
"game-bar": "Game Bar",
|
||||
"getting-consoles-list": "Getting the list of consoles...",
|
||||
"gpu-configuration": "GPU configuration",
|
||||
"help": "Help",
|
||||
"hide": "Hide",
|
||||
"hide-idle-cursor": "Hide mouse cursor on idle",
|
||||
@ -124,7 +125,8 @@ const Texts = {
|
||||
"ignore": "Ignore",
|
||||
"import": "Import",
|
||||
"increase": "Increase",
|
||||
"install-android": "Install Better xCloud app for Android",
|
||||
"install-android": "Better xCloud app for Android",
|
||||
"japan": "Japan",
|
||||
"keyboard-shortcuts": "Keyboard shortcuts",
|
||||
"language": "Language",
|
||||
"large": "Large",
|
||||
@ -154,6 +156,7 @@ const Texts = {
|
||||
"opacity": "Opacity",
|
||||
"other": "Other",
|
||||
"playing": "Playing",
|
||||
"poland": "Poland",
|
||||
"position": "Position",
|
||||
"powered-off": "Powered off",
|
||||
"powered-on": "Powered on",
|
||||
@ -163,9 +166,9 @@ const Texts = {
|
||||
"press-esc-to-cancel": "Press Esc to cancel",
|
||||
"press-key-to-toggle-mkb": [
|
||||
(e: any) => `Press ${e.key} to toggle this feature`,
|
||||
,
|
||||
(e: any) => `Premeu ${e.key} per alternar aquesta funció`,
|
||||
(e: any) => `${e.key}: Funktion an-/ausschalten`,
|
||||
,
|
||||
(e: any) => `Tekan ${e.key} untuk mengaktifkan fitur ini`,
|
||||
(e: any) => `Pulsa ${e.key} para alternar esta función`,
|
||||
(e: any) => `Appuyez sur ${e.key} pour activer cette fonctionnalité`,
|
||||
(e: any) => `Premi ${e.key} per attivare questa funzionalità`,
|
||||
@ -174,7 +177,7 @@ const Texts = {
|
||||
(e: any) => `Naciśnij ${e.key} aby przełączyć tę funkcję`,
|
||||
(e: any) => `Pressione ${e.key} para alternar este recurso`,
|
||||
(e: any) => `Нажмите ${e.key} для переключения этой функции`,
|
||||
,
|
||||
(e: any) => `กด ${e.key} เพื่อสลับคุณสมบัตินี้`,
|
||||
(e: any) => `Etkinleştirmek için ${e.key} tuşuna basın`,
|
||||
(e: any) => `Натисніть ${e.key} щоб перемкнути цю функцію`,
|
||||
(e: any) => `Nhấn ${e.key} để bật/tắt tính năng này`,
|
||||
@ -189,6 +192,7 @@ const Texts = {
|
||||
"remote-play": "Remote Play",
|
||||
"rename": "Rename",
|
||||
"renderer": "Renderer",
|
||||
"renderer-configuration": "Renderer configuration",
|
||||
"right-click-to-unbind": "Right-click on a key to unbind it",
|
||||
"right-stick": "Right stick",
|
||||
"rocket-always-hide": "Always hide",
|
||||
@ -202,12 +206,15 @@ const Texts = {
|
||||
"screenshot-apply-filters": "Applies video filters to screenshots",
|
||||
"section-all-games": "All games",
|
||||
"section-most-popular": "Most popular",
|
||||
"section-native-mkb": "Play with mouse & keyboard",
|
||||
"section-news": "News",
|
||||
"section-play-with-friends": "Play with friends",
|
||||
"section-touch": "Play with touch",
|
||||
"separate-touch-controller": "Separate Touch controller & Controller #1",
|
||||
"separate-touch-controller-note": "Touch controller is Player 1, Controller #1 is Player 2",
|
||||
"server": "Server",
|
||||
"settings": "Settings",
|
||||
"settings-reload-note": "Settings in this tab only go into effect on the next page load",
|
||||
"settings-reload": "Reload page to reflect changes",
|
||||
"settings-reloading": "Reloading...",
|
||||
"sharpness": "Sharpness",
|
||||
@ -271,7 +278,7 @@ const Texts = {
|
||||
(e: any) => `Układ sterowania dotykowego stworzony przez ${e.name}`,
|
||||
(e: any) => `Disposição de controle por toque feito por ${e.name}`,
|
||||
(e: any) => `Сенсорная раскладка по ${e.name}`,
|
||||
,
|
||||
(e: any) => `รูปแบบการควบคุมแบบสัมผัสโดย ${e.name}`,
|
||||
(e: any) => `${e.name} kişisinin dokunmatik kontrolcü tuş şeması`,
|
||||
(e: any) => `Розташування сенсорного керування від ${e.name}`,
|
||||
(e: any) => `Bố cục điều khiển cảm ứng tạo bởi ${e.name}`,
|
||||
@ -282,6 +289,7 @@ const Texts = {
|
||||
"transparent-background": "Transparent background",
|
||||
"ui": "UI",
|
||||
"unexpected-behavior": "May cause unexpected behavior",
|
||||
"united-states": "United States",
|
||||
"unknown": "Unknown",
|
||||
"unlimited": "Unlimited",
|
||||
"unmuted": "Unmuted",
|
||||
@ -320,14 +328,20 @@ export class Translations {
|
||||
static async init() {
|
||||
Translations.#enUsIndex = Translations.#supportedLocales.indexOf(Translations.#EN_US);
|
||||
|
||||
Translations.refreshCurrentLocale();
|
||||
Translations.refreshLocale();
|
||||
await Translations.#loadTranslations();
|
||||
}
|
||||
|
||||
static refreshCurrentLocale() {
|
||||
static refreshLocale(newLocale?: string) {
|
||||
let locale;
|
||||
if (newLocale) {
|
||||
localStorage.setItem(Translations.#KEY_LOCALE, newLocale);
|
||||
locale = newLocale;
|
||||
} else {
|
||||
locale = localStorage.getItem(Translations.#KEY_LOCALE);
|
||||
}
|
||||
const supportedLocales = Translations.#supportedLocales;
|
||||
|
||||
let locale = localStorage.getItem(Translations.#KEY_LOCALE);
|
||||
if (!locale) {
|
||||
// Get browser's locale
|
||||
locale = window.navigator.language || Translations.#EN_US;
|
||||
@ -417,6 +431,10 @@ export class Translations {
|
||||
Translations.#foreignTranslations = translations;
|
||||
});
|
||||
}
|
||||
|
||||
static switchLocale(locale: string) {
|
||||
localStorage.setItem(Translations.#KEY_LOCALE, locale);
|
||||
}
|
||||
}
|
||||
|
||||
export const t = Translations.get;
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { UserAgentProfile } from "@enums/user-agent";
|
||||
import { deepClone } from "./global";
|
||||
import { BX_FLAGS } from "./bx-flags";
|
||||
|
||||
type UserAgentConfig = {
|
||||
@ -48,14 +47,14 @@ export class UserAgent {
|
||||
}
|
||||
|
||||
static updateStorage(profile: UserAgentProfile, custom?: string) {
|
||||
const clonedConfig = deepClone(UserAgent.#config);
|
||||
clonedConfig.profile = profile;
|
||||
const config = UserAgent.#config;
|
||||
config.profile = profile;
|
||||
|
||||
if (typeof custom !== 'undefined') {
|
||||
clonedConfig.custom = custom;
|
||||
if (profile === UserAgentProfile.CUSTOM && typeof custom !== 'undefined') {
|
||||
config.custom = custom;
|
||||
}
|
||||
|
||||
window.localStorage.setItem(UserAgent.STORAGE_KEY, JSON.stringify(clonedConfig));
|
||||
window.localStorage.setItem(UserAgent.STORAGE_KEY, JSON.stringify(config));
|
||||
}
|
||||
|
||||
static getDefault(): string {
|
||||
|
@ -1,7 +1,9 @@
|
||||
import { PrefKey, getPref, setPref } from "@utils/preferences";
|
||||
import { AppInterface, SCRIPT_VERSION } from "@utils/global";
|
||||
import { UserAgent } from "@utils/user-agent";
|
||||
import { Translations } from "./translation";
|
||||
import { Toast } from "./toast";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref, setPref } from "./settings-storages/global-settings-storage";
|
||||
|
||||
/**
|
||||
* Check for update
|
||||
@ -95,3 +97,16 @@ export function floorToNearest(value: number, interval: number): number {
|
||||
export function roundToNearest(value: number, interval: number): number {
|
||||
return Math.round(value / interval) * interval;
|
||||
}
|
||||
|
||||
export async function copyToClipboard(text: string, showToast=true): Promise<boolean> {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
showToast && Toast.show('Copied to clipboard', '', {instant: true});
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('Failed to copy: ', err);
|
||||
showToast && Toast.show('Failed to copy', '', {instant: true});
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
@ -6,9 +6,10 @@ import { BxEvent } from "./bx-event";
|
||||
import { NATIVE_FETCH, BX_FLAGS } from "./bx-flags";
|
||||
import { STATES } from "./global";
|
||||
import { patchIceCandidates } from "./network";
|
||||
import { getPref, PrefKey } from "./preferences";
|
||||
import { getPreferredServerRegion } from "./region";
|
||||
import { BypassServerIps } from "@/enums/bypass-servers";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref } from "./settings-storages/global-settings-storage";
|
||||
|
||||
export
|
||||
class XcloudInterceptor {
|
||||
@ -74,7 +75,7 @@ class XcloudInterceptor {
|
||||
BxEvent.dispatch(window, BxEvent.XCLOUD_SERVERS_READY);
|
||||
|
||||
const preferredRegion = getPreferredServerRegion();
|
||||
if (preferredRegion in STATES.serverRegions) {
|
||||
if (preferredRegion && preferredRegion in STATES.serverRegions) {
|
||||
const tmp = Object.assign({}, STATES.serverRegions[preferredRegion]);
|
||||
tmp.isDefault = true;
|
||||
|
||||
|
@ -1,11 +1,12 @@
|
||||
import { RemotePlay } from "@/modules/remote-play";
|
||||
import { TouchController } from "@/modules/touch-controller";
|
||||
import { BxEvent } from "./bx-event";
|
||||
import { InputType } from "./bx-exposed";
|
||||
import { SupportedInputType } from "./bx-exposed";
|
||||
import { NATIVE_FETCH } from "./bx-flags";
|
||||
import { STATES } from "./global";
|
||||
import { getPref, PrefKey } from "./preferences";
|
||||
import { patchIceCandidates } from "./network";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref } from "./settings-storages/global-settings-storage";
|
||||
|
||||
export class XhomeInterceptor {
|
||||
static #consoleAddrs: {[index: string]: number} = {};
|
||||
@ -67,14 +68,14 @@ export class XhomeInterceptor {
|
||||
const obj = await response.clone().json() as any;
|
||||
|
||||
const xboxTitleId = JSON.parse(opts.body).titleIds[0];
|
||||
STATES.currentStream.xboxTitleId = xboxTitleId;
|
||||
TouchController.setXboxTitleId(xboxTitleId);
|
||||
|
||||
const inputConfigs = obj[0];
|
||||
|
||||
let hasTouchSupport = inputConfigs.supportedTabs.length > 0;
|
||||
if (!hasTouchSupport) {
|
||||
const supportedInputTypes = inputConfigs.supportedInputTypes;
|
||||
hasTouchSupport = supportedInputTypes.includes(InputType.NATIVE_TOUCH) || supportedInputTypes.includes(InputType.CUSTOM_TOUCH_OVERLAY);
|
||||
hasTouchSupport = supportedInputTypes.includes(SupportedInputType.NATIVE_TOUCH) || supportedInputTypes.includes(SupportedInputType.CUSTOM_TOUCH_OVERLAY);
|
||||
}
|
||||
|
||||
if (hasTouchSupport) {
|
||||
@ -85,7 +86,7 @@ export class XhomeInterceptor {
|
||||
});
|
||||
} else {
|
||||
TouchController.enable();
|
||||
TouchController.getCustomLayouts(xboxTitleId);
|
||||
TouchController.requestCustomLayouts(xboxTitleId);
|
||||
}
|
||||
|
||||
response.json = () => Promise.resolve(obj);
|
||||
|
@ -1,3 +1,4 @@
|
||||
import type { NavigationElement } from "@/modules/ui/dialog/navigation-dialog";
|
||||
import { ButtonStyle, CE, createButton } from "@utils/html";
|
||||
|
||||
export class BxSelectElement {
|
||||
@ -8,17 +9,11 @@ export class BxSelectElement {
|
||||
const $btnPrev = createButton({
|
||||
label: '<',
|
||||
style: ButtonStyle.FOCUSABLE,
|
||||
attributes: {
|
||||
tabindex: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const $btnNext = createButton({
|
||||
label: '>',
|
||||
style: ButtonStyle.FOCUSABLE,
|
||||
attributes: {
|
||||
tabindex: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const isMultiple = $select.multiple;
|
||||
@ -109,7 +104,11 @@ export class BxSelectElement {
|
||||
}
|
||||
|
||||
const onPrevNext = (e: Event) => {
|
||||
const goNext = e.target === $btnNext;
|
||||
if (!e.target) {
|
||||
return;
|
||||
}
|
||||
|
||||
const goNext = (e.target as any).closest('button') === $btnNext;
|
||||
|
||||
const currentIndex = visibleIndex;
|
||||
let newIndex = goNext ? currentIndex + 1 : currentIndex - 1;
|
||||
@ -147,11 +146,40 @@ export class BxSelectElement {
|
||||
|
||||
render();
|
||||
|
||||
return CE('div', {class: 'bx-select'},
|
||||
const $div = CE<NavigationElement>('div', {
|
||||
class: 'bx-select',
|
||||
_nearby: {
|
||||
orientation: 'horizontal',
|
||||
focus: $btnNext,
|
||||
}
|
||||
},
|
||||
$select,
|
||||
$btnPrev,
|
||||
$content,
|
||||
$btnNext,
|
||||
);
|
||||
|
||||
Object.defineProperty($div, 'value', {
|
||||
get() {
|
||||
return $select.value;
|
||||
}
|
||||
});
|
||||
|
||||
$div.addEventListener = function() {
|
||||
// @ts-ignore
|
||||
$select.addEventListener.apply($select, arguments);
|
||||
};
|
||||
|
||||
$div.removeEventListener = function() {
|
||||
// @ts-ignore
|
||||
$select.removeEventListener.apply($select, arguments);
|
||||
};
|
||||
|
||||
$div.dispatchEvent = function() {
|
||||
// @ts-ignore
|
||||
return $select.dispatchEvent.apply($select, arguments);
|
||||
}
|
||||
|
||||
return $div;
|
||||
}
|
||||
}
|
||||
|