diff --git a/build.ts b/build.ts
index 39db8fa..ae68b44 100644
--- a/build.ts
+++ b/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;
}
diff --git a/bun.lockb b/bun.lockb
index 0931019..db5d9bc 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/package.json b/package.json
index d82cf54..58bc63a 100644
--- a/package.json
+++ b/package.json
@@ -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"
},
diff --git a/src/assets/css/button.styl b/src/assets/css/button.styl
index 1be6139..9f778de 100644
--- a/src/assets/css/button.styl
+++ b/src/assets/css/button.styl
@@ -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) {
diff --git a/src/assets/css/global-settings.styl b/src/assets/css/global-settings.styl
deleted file mode 100644
index 7a3d381..0000000
--- a/src/assets/css/global-settings.styl
+++ /dev/null
@@ -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;
- }
- }
-}
diff --git a/src/assets/css/header.styl b/src/assets/css/header.styl
index ad08266..1dadfb0 100644
--- a/src/assets/css/header.styl
+++ b/src/assets/css/header.styl
@@ -4,7 +4,7 @@
svg {
width: 24px;
- height: 46px;
+ height: 24px;
}
}
diff --git a/src/assets/css/navigation-dialog.styl b/src/assets/css/navigation-dialog.styl
new file mode 100644
index 0000000..0d83d81
--- /dev/null
+++ b/src/assets/css/navigation-dialog.styl
@@ -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;
+ }
+}
diff --git a/src/assets/css/root.styl b/src/assets/css/root.styl
index 0d2ce1e..3857f63 100644
--- a/src/assets/css/root.styl
+++ b/src/assets/css/root.styl
@@ -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] {
diff --git a/src/assets/css/stream-settings.styl b/src/assets/css/settings-dialog.styl
similarity index 51%
rename from src/assets/css/stream-settings.styl
rename to src/assets/css/settings-dialog.styl
index d137a7f..07432e0 100644
--- a/src/assets/css/stream-settings.styl
+++ b/src/assets/css/settings-dialog.styl
@@ -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;
}
diff --git a/src/assets/css/styles.styl b/src/assets/css/styles.styl
index 574666c..e7a9d24 100644
--- a/src/assets/css/styles.styl
+++ b/src/assets/css/styles.styl
@@ -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';
diff --git a/src/assets/header_script.txt b/src/assets/header_script.txt
index 198b4fd..c12c145 100644
--- a/src/assets/header_script.txt
+++ b/src/assets/header_script.txt
@@ -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";
diff --git a/src/assets/svg/better-xcloud.svg b/src/assets/svg/better-xcloud.svg
new file mode 100644
index 0000000..e0f73cf
--- /dev/null
+++ b/src/assets/svg/better-xcloud.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/assets/svg/close.svg b/src/assets/svg/close.svg
new file mode 100644
index 0000000..16898ba
--- /dev/null
+++ b/src/assets/svg/close.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/assets/svg/create-shortcut.svg b/src/assets/svg/create-shortcut.svg
index fe5dd0d..f3cda7f 100644
--- a/src/assets/svg/create-shortcut.svg
+++ b/src/assets/svg/create-shortcut.svg
@@ -1,4 +1,4 @@
diff --git a/src/assets/svg/native-mkb.svg b/src/assets/svg/native-mkb.svg
index 1fb6bd1..494d049 100644
--- a/src/assets/svg/native-mkb.svg
+++ b/src/assets/svg/native-mkb.svg
@@ -1,10 +1,10 @@
diff --git a/src/assets/svg/virtual-controller.svg b/src/assets/svg/virtual-controller.svg
index 13d6446..1a61ef6 100644
--- a/src/assets/svg/virtual-controller.svg
+++ b/src/assets/svg/virtual-controller.svg
@@ -1,11 +1,11 @@
diff --git a/src/enums/bypass-servers.ts b/src/enums/bypass-servers.ts
index 7230ff4..2c9dbbc 100644
--- a/src/enums/bypass-servers.ts
+++ b/src/enums/bypass-servers.ts
@@ -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 = {
diff --git a/src/enums/game-pass-gallery.ts b/src/enums/game-pass-gallery.ts
index 3a9dcbe..490a3d8 100644
--- a/src/enums/game-pass-gallery.ts
+++ b/src/enums/game-pass-gallery.ts
@@ -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',
}
diff --git a/src/enums/pref-keys.ts b/src/enums/pref-keys.ts
new file mode 100644
index 0000000..b371965
--- /dev/null
+++ b/src/enums/pref-keys.ts
@@ -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',
+}
diff --git a/src/enums/ui-sections.ts b/src/enums/ui-sections.ts
index b704dc5..1b6f706 100644
--- a/src/enums/ui-sections.ts
+++ b/src/enums/ui-sections.ts
@@ -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',
}
diff --git a/src/index.ts b/src/index.ts
index 00d63cf..9bf1b28 100644
--- a/src/index.ts
+++ b/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();
diff --git a/src/macros/build.ts b/src/macros/build.ts
index 4056a0c..750f719 100644
--- a/src/macros/build.ts
+++ b/src/macros/build.ts
@@ -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();
+};
diff --git a/src/modules/controller-shortcut.ts b/src/modules/controller-shortcut.ts
index 804db87..6fdb69f 100644
--- a/src/modules/controller-shortcut.ts
+++ b/src/modules/controller-shortcut.ts
@@ -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 = 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('select', {autocomplete: 'off'}, CE('option', {value: ''}, '---'));
@@ -293,13 +315,24 @@ export class ControllerShortcut {
let $remap: HTMLElement;
const $selectProfile = CE('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);
diff --git a/src/modules/game-bar/game-bar.ts b/src/modules/game-bar/game-bar.ts
index d2fedf4..d57ec1a 100644
--- a/src/modules/game-bar/game-bar.ts
+++ b/src/modules/game-bar/game-bar.ts
@@ -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 {
diff --git a/src/modules/loading-screen.ts b/src/modules/loading-screen.ts
index 2981b6b..4ac4e5c 100644
--- a/src/modules/loading-screen.ts
+++ b/src/modules/loading-screen.ts
@@ -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;
diff --git a/src/modules/mkb/mkb-handler.ts b/src/modules/mkb/mkb-handler.ts
index 31dc297..6a371f0 100644
--- a/src/modules/mkb/mkb-handler.ts
+++ b/src/modules/mkb/mkb-handler.ts
@@ -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);
},
}),
),
diff --git a/src/modules/mkb/mkb-preset.ts b/src/modules/mkb/mkb-preset.ts
index 81ea4a8..fde9e84 100644
--- a/src/modules/mkb/mkb-preset.ts
+++ b/src/modules/mkb/mkb-preset.ts
@@ -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 {
diff --git a/src/modules/mkb/mkb-remapper.ts b/src/modules/mkb/mkb-remapper.ts
index 3dc997f..868d158 100644
--- a/src/modules/mkb/mkb-remapper.ts
+++ b/src/modules/mkb/mkb-remapper.ts
@@ -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('select', {});
+ this.#$.presetsSelect = CE('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);
diff --git a/src/modules/mkb/native-mkb-handler.ts b/src/modules/mkb/native-mkb-handler.ts
index e7df62b..a292fdb 100644
--- a/src/modules/mkb/native-mkb-handler.ts
+++ b/src/modules/mkb/native-mkb-handler.ts
@@ -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,
diff --git a/src/modules/patcher.ts b/src/modules/patcher.ts
index 060c6c3..5019285 100644
--- a/src/modules/patcher.ts
+++ b/src/modules/patcher.ts
@@ -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> = {
+ [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
diff --git a/src/modules/player/webgl2-player.ts b/src/modules/player/webgl2-player.ts
index e90c7b6..ff2bebe 100644
--- a/src/modules/player/webgl2-player.ts
+++ b/src/modules/player/webgl2-player.ts
@@ -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';
diff --git a/src/modules/remote-play.ts b/src/modules/remote-play.ts
index 27c89bd..06d2db1 100644
--- a/src/modules/remote-play.ts
+++ b/src/modules/remote-play.ts
@@ -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',
},
},
};
diff --git a/src/modules/shortcuts/shortcut-sound.ts b/src/modules/shortcuts/shortcut-sound.ts
index effef8e..98a00b9 100644
--- a/src/modules/shortcuts/shortcut-sound.ts
+++ b/src/modules/shortcuts/shortcut-sound.ts
@@ -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
diff --git a/src/modules/stream-player.ts b/src/modules/stream-player.ts
index 2ba5037..17733b1 100644
--- a/src/modules/stream-player.ts
+++ b/src/modules/stream-player.ts
@@ -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,
diff --git a/src/modules/stream/stream-settings-utils.ts b/src/modules/stream/stream-settings-utils.ts
index d3bcbfd..2f3c245 100644
--- a/src/modules/stream/stream-settings-utils.ts
+++ b/src/modules/stream/stream-settings-utils.ts
@@ -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();
}
diff --git a/src/modules/stream/stream-settings.ts b/src/modules/stream/stream-settings.ts
deleted file mode 100644
index 06a188f..0000000
--- a/src/modules/stream/stream-settings.ts
+++ /dev/null
@@ -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 = [];
-
- 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