From 9dfdeb8f122865dfd848cd9bf2507980c2e3a80a Mon Sep 17 00:00:00 2001 From: redphx <96280+redphx@users.noreply.github.com> Date: Sat, 27 Jul 2024 16:09:13 +0700 Subject: [PATCH] Merge Global settings and Stream settings into one dialog --- build.ts | 9 + bun.lockb | Bin 10334 -> 10343 bytes package.json | 2 +- src/assets/css/button.styl | 97 +- src/assets/css/global-settings.styl | 226 ---- src/assets/css/header.styl | 2 +- src/assets/css/navigation-dialog.styl | 18 + src/assets/css/root.styl | 54 +- ...eam-settings.styl => settings-dialog.styl} | 377 ++++-- src/assets/css/styles.styl | 4 +- src/assets/header_script.txt | 2 +- src/assets/svg/better-xcloud.svg | 4 + src/assets/svg/close.svg | 4 + src/assets/svg/create-shortcut.svg | 4 +- src/assets/svg/native-mkb.svg | 12 +- src/assets/svg/virtual-controller.svg | 14 +- src/enums/bypass-servers.ts | 10 +- src/enums/game-pass-gallery.ts | 1 + src/enums/pref-keys.ts | 101 ++ src/enums/ui-sections.ts | 6 +- src/index.ts | 18 +- src/macros/build.ts | 5 + src/modules/controller-shortcut.ts | 67 +- src/modules/game-bar/game-bar.ts | 3 +- src/modules/loading-screen.ts | 3 +- src/modules/mkb/mkb-handler.ts | 11 +- src/modules/mkb/mkb-preset.ts | 2 +- src/modules/mkb/mkb-remapper.ts | 262 ++-- src/modules/mkb/native-mkb-handler.ts | 3 +- src/modules/patcher.ts | 118 +- src/modules/player/webgl2-player.ts | 3 +- src/modules/remote-play.ts | 11 +- src/modules/shortcuts/shortcut-sound.ts | 14 +- src/modules/stream-player.ts | 3 +- src/modules/stream/stream-settings-utils.ts | 15 +- src/modules/stream/stream-settings.ts | 795 ----------- src/modules/stream/stream-stats.ts | 3 +- src/modules/stream/stream-ui.ts | 11 +- src/modules/touch-controller.ts | 101 +- src/modules/ui/dialog/navigation-dialog.ts | 608 +++++++++ src/modules/ui/dialog/settings-dialog.ts | 1160 +++++++++++++++++ src/modules/ui/global-settings.ts | 507 ------- src/modules/ui/guide-menu.ts | 18 +- src/modules/ui/header.ts | 18 +- src/modules/ui/product-details.ts | 2 +- src/modules/ui/ui.ts | 8 - src/modules/vibration-manager.ts | 3 +- src/types/index.d.ts | 1 - src/types/setting-definition.d.ts | 19 + src/utils/bx-event.ts | 112 +- src/utils/bx-exposed.ts | 32 +- src/utils/bx-flags.ts | 4 - src/utils/bx-icon.ts | 4 + src/utils/bx-logger.ts | 4 +- src/utils/css.ts | 40 +- src/utils/feature-gates.ts | 3 +- src/utils/gamepad.ts | 3 +- src/utils/global.ts | 3 + src/utils/history.ts | 8 +- src/utils/html.ts | 29 +- src/utils/local-db.ts | 3 +- src/utils/monkey-patches.ts | 5 +- src/utils/navigation-utils.ts | 14 + src/utils/network.ts | 14 +- src/utils/preload-state.ts | 3 +- src/utils/region.ts | 7 +- src/utils/screenshot.ts | 3 +- src/utils/{settings.ts => setting-element.ts} | 56 +- .../base-settings-storage.ts | 124 ++ .../global-settings-storage.ts} | 411 ++---- src/utils/translation.ts | 38 +- src/utils/user-agent.ts | 11 +- src/utils/utils.ts | 17 +- src/utils/xcloud-interceptor.ts | 5 +- src/utils/xhome-interceptor.ts | 11 +- src/web-components/bx-select.ts | 44 +- 76 files changed, 3281 insertions(+), 2466 deletions(-) delete mode 100644 src/assets/css/global-settings.styl create mode 100644 src/assets/css/navigation-dialog.styl rename src/assets/css/{stream-settings.styl => settings-dialog.styl} (51%) create mode 100644 src/assets/svg/better-xcloud.svg create mode 100644 src/assets/svg/close.svg create mode 100644 src/enums/pref-keys.ts delete mode 100644 src/modules/stream/stream-settings.ts create mode 100644 src/modules/ui/dialog/navigation-dialog.ts create mode 100644 src/modules/ui/dialog/settings-dialog.ts delete mode 100644 src/modules/ui/global-settings.ts create mode 100644 src/types/setting-definition.d.ts create mode 100644 src/utils/navigation-utils.ts rename src/utils/{settings.ts => setting-element.ts} (86%) create mode 100644 src/utils/settings-storages/base-settings-storage.ts rename src/utils/{preferences.ts => settings-storages/global-settings-storage.ts} (62%) 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 0931019aafc5720a4a1954c29fb79e972219ffff..db5d9bc46d47144696084dd1666a408749dbdf4c 100755 GIT binary patch delta 713 zcmZ9JZ%9*77{{^y^CEC z%6vV;#jjr6xHqC7Ix%e-#z(h1+B3UH!yQK!XUk{zHO?qa5-5sdi$s92HH@7(p6u*m zwEBD6Qz@CTFxAae7dd0ANykW6kq(g-lO~egT|J4u-bC{7(L}tv&#!#8Nog|3oB2sC zq!pxvq&YofMgQz=RL`5;G%Wq(Z=V{0YJmMHa}h={OPoQCi`)Ph)HT2%lyxFp#VqkT zYC1aL7}Phw0hINkIYZg%$(CVSS7&%nyFK_zw-c%`U=X1TW#VPb66a7eh-M4L+D82x zC4C(PP%aSRBxZ@zs1bi+z$mT_QOF>5t*6j*b5?|2UXCelf_}^zMVLa3sA0e)f)8b4 zubMU0n!^+KEv?rJ<;sF4=^Hc!=fB>0FrL!CxqGa0OFT3=_^I6Jy!Q6{z?DcnZ?Os& zljn@h(xpwAk+bX4QLZj-RjaIXVA{3(R;sk|HK@4_X`^F1V=FJFRJWXWPlW;|D5Jjo z!2CE^>Vq^sERBPLl@2@jajzo^VMK=yBKVjn;tz+tWMz~Gn_os-zdTvs(B>69f`_p& ztaiqs0;4WF>lIVThN5V*nH zLNV`*``!=xK+Q;J@@iM2Wb|#z)NE0D{bTa!#@F8Wi}4OG#Nu&7TQe}Wm$9*n(R1C5 z#!!E#uTNskLv=mX>p8~ukWP~pl1`HDCXGaUy89!81Ci+Y3z2ZopgsQG;2kD|oSB`p znAApEM4C}Ew(Fm@f$BN4JDTl7{`Sdj-~c#*5+}kGD#S(1aw7MWVS}m(0w}3ONTNb~ zj#(9L05+(bz=x7r)Ze9S$H3JLW!6_g}8)Sji^^sEQ&hD zp;zqzCrSk(TtbDI!YpwE1+7@(rjQNPRYsxd=ByB1Ts8J_I*6j86=4Rm#5ELjA{<1C z*efeKx4zl6eEZ6$`3EnTm$>$Y>NnqhC(57fYg8s`g2S%B)=10g)gj;gTfTzA^y;Sh zQOe+dJn67?+?;zjA!hvAE;RwW_(}f)0YU zPTu9_1v_4^sDojguLwg751EZ%N1xdb9z=5;G~)uX9yiTKQ~oPEPrde^U4N10An4+q zypyqMbXdY*L%-Ds8JxB3(3JnL%U{B;Vx^xeIdJhbJ&a{=%VISBpNfOgT&i0B=(bDF e5g~|as{x*2*4iq!+gdeft+ZmaS}QA6Dfj~oanDu& 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 with controller-friendly one + if ($control instanceof HTMLSelectElement && getPref(PrefKey.UI_CONTROLLER_FRIENDLY)) { + $control = BxSelectElement.wrap($control); + } + } + + let prefDefinition: SettingDefinition | null = null; + if (pref) { + prefDefinition = getPrefDefinition(pref); + } + + let label = prefDefinition?.label || setting.label; + let note = prefDefinition?.note || setting.note; + const experimental = prefDefinition?.experimental || setting.experimental; + + // Add Experimental text + if (experimental) { + label = '🧪 ' + label; + if (!note) { + note = t('experimental'); + } else { + note = `${t('experimental')}: ${note}`; + } + } + + let $label; + const $row = CE('label', { + class: 'bx-settings-row', + for: `bx_setting_${pref}`, + 'data-type': settingTabContent.group, + _nearby: { + orientation: 'horizontal', + } + }, + $label = CE('span', {class: 'bx-settings-label'}, + label, + note && CE('div', {class: 'bx-settings-dialog-note'}, note), + setting.unsupported && CE('div', {class: 'bx-settings-dialog-note'}, t('browser-unsupported-feature')), + ), + !setting.unsupported && $control, + ); + + // Make link inside