This commit is contained in:
redphx
2024-12-05 17:10:39 +07:00
parent c836e33f7b
commit 9199351af1
207 changed files with 9833 additions and 6953 deletions

38
src/assets/css/button.styl Normal file → Executable file
View File

@@ -67,6 +67,24 @@
}
}
&.bx-warning {
--button-rgb: var(--bx-warning-button-rgb);
&:not([disabled]):active {
--button-active-rgb: var(--bx-warning-button-active-rgb);
}
&:not([disabled]):not(:active) {
&:hover, &.bx-focusable:focus {
--button-hover-rgb: var(--bx-warning-button-hover-rgb);
}
}
&:disabled {
--button-disabled-rgb: var(--bx-warning-button-disabled-rgb);
}
}
&.bx-danger {
--button-rgb: var(--bx-danger-button-rgb);
@@ -107,6 +125,7 @@
&.bx-circular {
border-radius: var(--bx-button-height);
width: var(--bx-button-height);
height: var(--bx-button-height);
}
@@ -130,6 +149,25 @@
margin-left: 10px;
}
}
&.bx-button-multi-lines {
height: auto;
text-align: left;
padding: 10px 0;
span {
line-height: unset;
display: block;
&:last-of-type {
text-transform: none;
font-weight: normal;
font-family: "Segoe Sans Variable Text";
font-size: 12px;
margin-top: 4px;
}
}
}
}
.bx-focusable {

0
src/assets/css/game-bar.styl Normal file → Executable file
View File

0
src/assets/css/guide-menu.styl Normal file → Executable file
View File

0
src/assets/css/header.styl Normal file → Executable file
View File

View File

@@ -1,12 +1,12 @@
.bx-dialog-overlay {
.bx-key-binding-dialog-overlay {
position: fixed;
inset: 0;
z-index: var(--bx-dialog-overlay-z-index);
z-index: var(--bx-key-binding-dialog-overlay-z-index);
background: black;
opacity: 50%;
}
.bx-dialog {
.bx-key-binding-dialog {
display: flex;
flex-flow: column;
max-height: 90vh;
@@ -18,7 +18,7 @@
min-width: 420px;
padding: 20px;
border-radius: 8px;
z-index: var(--bx-dialog-z-index);
z-index: var(--bx-key-binding-dialog-z-index);
background: #1a1b1e;
color: #fff;
font-weight: 400;
@@ -33,26 +33,13 @@
}
h2 {
display: flex;
margin-bottom: 12px;
b {
flex: 1;
color: #fff;
display: block;
font-family: var(--bx-title-font);
font-size: 26px;
font-weight: 400;
line-height: var(--bx-button-height);
}
}
&.bx-binding-dialog {
h2 {
b {
font-family: var(--bx-promptfont-font) !important;
}
}
color: #fff;
display: block;
font-family: var(--bx-title-font);
font-size: 32px;
font-weight: 400;
line-height: var(--bx-button-height);
}
> div {
@@ -85,11 +72,26 @@
background-color: #515863;
}
}
ul {
margin-bottom: 1rem;
li {
display: none;
}
&[data-flags*="[1]"] > li[data-flag="1"],
&[data-flags*="[2]"] > li[data-flag="2"],
&[data-flags*="[4]"] > li[data-flag="4"],
&[data-flags*="[8]"] > li[data-flag="8"] {
display: list-item;
}
}
}
@media screen and (max-width: 450px) {
.bx-dialog {
.bx-key-binding-dialog {
min-width: 100%;
}
}

0
src/assets/css/loading-screen.styl Normal file → Executable file
View File

0
src/assets/css/misc.styl Normal file → Executable file
View File

174
src/assets/css/mkb.styl Normal file → Executable file
View File

@@ -4,15 +4,6 @@
flex: 1;
padding-bottom: 10px;
overflow: hidden;
select:disabled {
-webkit-appearance: none;
background: transparent;
text-align-last: right;
text-align: right;
border: none;
color: #fff;
}
}
.bx-mkb-pointer-lock-msg {
@@ -20,13 +11,12 @@
-webkit-user-select: none;
position: fixed;
left: 50%;
top: 50%;
transform: translateX(-50%) translateY(-50%);
bottom: 40px;
transform: translateX(-50%);
margin: auto;
background: #151515;
z-index: var(--bx-mkb-pointer-lock-msg-z-index);
color: #fff;
text-align: center;
font-weight: 400;
font-family: "Segoe UI", Arial, Helvetica, sans-serif;
font-size: 1.3rem;
@@ -34,117 +24,55 @@
border-radius: 8px;
align-items: center;
box-shadow: 0 0 6px #000;
min-width: 220px;
min-width: 300px;
opacity: 0.9;
display: flex;
flex-direction: column;
gap: 10px;
&:hover {
opacity: 1;
}
> div:first-of-type {
display: flex;
flex-direction: column;
> p {
margin: 0;
width: 100%;
font-size: 22px;
margin-bottom: 4px;
font-weight: bold;
text-align: left;
}
p {
margin: 0;
> div {
width: 100%;
display: flex;
flex-direction: row;
&:first-child {
font-size: 22px;
margin-bottom: 4px;
font-weight: bold;
}
&:last-child {
font-size: 12px;
font-style: italic;
}
}
> div:last-of-type {
margin-top: 10px;
&[data-type='native'] {
button {
&:first-of-type {
margin-bottom: 8px;
}
gap: 10px;
button {
&:first-of-type {
flex-shrink: 1;
}
}
&[data-type='virtual'] {
div {
display: flex;
flex-flow: row;
margin-top: 8px;
button {
flex: 1;
&:first-of-type {
margin-right: 5px;
}
&:last-of-type {
margin-left: 5px;
}
}
&:last-of-type {
flex-grow: 1;
}
}
}
}
.bx-mkb-preset-tools {
display: flex;
margin-bottom: 12px;
select {
flex: 1;
}
button {
margin-left: 6px;
}
}
.bx-mkb-settings-rows {
flex: 1;
overflow: scroll;
}
.bx-mkb-key-row {
display: flex;
margin-bottom: 10px;
align-items: center;
gap: 20px;
label {
margin-bottom: 0;
font-family: var(--bx-promptfont-font);
font-size: 26px;
font-size: 32px;
text-align: center;
width: 26px;
height: 32px;
line-height: 32px;
}
button {
flex: 1;
height: 32px;
line-height: 32px;
margin: 0 0 0 10px;
background: transparent;
border: none;
color: white;
border-radius: 0;
border-left: 1px solid #373737;
&:hover {
background: transparent;
cursor: default;
}
}
}
@@ -181,10 +109,58 @@
.bx-mkb-note {
display: block;
margin: 16px 0 10px;
margin: 0 0 10px;
font-size: 12px;
text-align: center;
}
&:first-of-type {
margin-top: 0;
button.bx-binding-button {
flex: 1;
min-height: 38px;
border: none;
border-radius: 4px;
font-size: 14px;
color: #fff;
display: flex;
align-items: center;
align-self: center;
padding: 0 6px;
&:disabled {
background: #131416;
padding: 0 8px;
}
&:not(:disabled) {
border: 2px solid transparent;
border-top: none;
border-bottom: 4px solid #252525;
background: #3b3b3b;
cursor: pointer;
&:hover, &.bx-focusable:focus {
background: #20b217;
border-bottom-color: #186c13;
}
&:active {
background: #16900f;
border-bottom: 3px solid #0c4e08;
border-left-width: 2px;
border-right-width: 2px;
}
&.bx-focusable:focus {
&::after {
top: -6px;
left: -8px;
right: -8px;
bottom: -10px;
}
}
}
.bx-settings-row .bx-binding-button-wrapper & {
min-width: 60px;
}
}

195
src/assets/css/navigation-dialog.styl Normal file → Executable file
View File

@@ -6,6 +6,17 @@
*:focus {
outline: none !important;
}
select:disabled {
-webkit-appearance: none;
text-align-last: right;
text-align: right;
color: #fff;
background: #131416;
border: none;
border-radius: 4px;
padding: 0 5px;
}
}
.bx-navigation-dialog-overlay {
@@ -21,3 +32,187 @@
background: transparent;
}
}
.bx-centered-dialog {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
background: #1a1b1e;
border-radius: 10px;
width: 450px;
max-width: calc(100vw - 20px);
margin: 0 0 0 auto;
padding: 20px;
max-height: 95vh;
flex-direction: column;
overflow: hidden;
display: flex;
flex-direction: column;
.bx-dialog-title {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 10px;
p {
padding: 0;
margin: 0;
flex: 1;
font-size: 1.2rem;
font-weight: bold;
}
button {
flex-shrink: 0;
}
}
.bx-dialog-content {
flex: 1;
overflow: auto;
overflow-x: hidden;
> div {
}
}
.bx-dialog-preset-tools {
display: flex;
margin-bottom: 12px;
gap: 6px;
select {
flex: 1;
}
}
}
.bx-centered-dialog,
.bx-settings-dialog {
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;
}
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);
}
}
a {
color: #1c9d1c;
text-decoration: none;
&:hover, &:focus {
color: #5dc21e;
}
}
label {
margin: 0;
}
}
.bx-controller-shortcuts-manager-container {
.bx-shortcut-note {
margin-top: 10px;
font-size: 14px;
text-align: center;
}
.bx-shortcut-row {
display: flex;
gap: 10px;
margin-bottom: 10px;
align-items: center;
label.bx-prompt {
flex-shrink: 0;
font-size: 32px;
margin: 0;
&::first-letter {
letter-spacing: 6px;
}
}
.bx-shortcut-actions {
flex: 1;
position: relative;
select {
width: 100%;
height: 100%;
min-height: 38px;
display: block;
&:first-of-type {
position: absolute;
top: 0;
left: 0;
}
&:last-of-type {
opacity: 0;
z-index: calc(var(--bx-settings-z-index) + 1);
}
}
}
}
select:disabled {
text-align: left;
text-align-last: left;
}
}
.bx-keyboard-shortcuts-manager-container {
display: flex;
flex-direction: column;
gap: 16px;
fieldset {
background: #2a2a2a;
border: 1px solid #2a2a2a;
border-radius: 4px;
padding: 4px;
}
legend {
width: auto;
padding: 4px 8px;
margin: 0 4px 4px;
background: #004f87;
box-shadow: 0px 2px 0px #071e3d;
border-radius: 4px;
font-size: 14px;
font-weight: bold;
text-transform: uppercase;
}
.bx-settings-row {
background: none;
}
}

70
src/assets/css/number-stepper.styl Normal file → Executable file
View File

@@ -1,46 +1,54 @@
.bx-number-stepper {
text-align: center;
span {
display: inline-block;
min-width: 40px;
font-family: var(--bx-monospaced-font);
font-size: 13px;
margin: 0 4px;
}
> div {
display: flex;
align-items: center;
button {
border: none;
width: 24px;
height: 24px;
margin: 0;
line-height: 24px;
background-color: var(--bx-default-button-color);
color: #fff;
border-radius: 4px;
font-weight: bold;
font-size: 14px;
font-family: var(--bx-monospaced-font);
span {
flex: 1;
display: inline-block;
min-width: 40px;
font-family: var(--bx-monospaced-font);
font-size: 13px;
margin: 0 4px;
}
&:hover {
@media (hover: hover) {
button {
flex-shrink: 0;
border: none;
width: 24px;
height: 24px;
margin: 0;
line-height: 24px;
background-color: var(--bx-default-button-color);
color: #fff;
border-radius: 4px;
font-weight: bold;
font-size: 14px;
font-family: var(--bx-monospaced-font);
&:hover {
@media (hover: hover) {
background-color: var(--bx-default-button-hover-color);
}
}
&:active {
background-color: var(--bx-default-button-hover-color);
}
}
&:active {
background-color: var(--bx-default-button-hover-color);
}
&:disabled + span {
font-family: var(--bx-title-font);
&:disabled + span {
font-family: var(--bx-title-font);
}
}
}
input[type="range"] {
display: block;
margin: 12px auto 2px;
width: 180px;
margin: 8px 0 2px auto;
min-width: 180px;
width: 100%;
color: #959595 !important;
}
@@ -48,7 +56,7 @@
display: none;
}
&[data-disabled=true] {
&[data-disabled=true], &[disabled=true] {
input[type=range], button {
display: none;
}

0
src/assets/css/remote-play.styl Normal file → Executable file
View File

36
src/assets/css/root.styl Normal file → Executable file
View File

@@ -24,22 +24,24 @@ button_color(name, normal, hover, active, disabled)
button_color('default', #2d3036, #515863, #222428, #8e8e8e);
button_color('primary', #008746, #04b358, #044e2a, #448262);
button_color('warning', #c16e04, #fa9005, #965603, #a2816c);
button_color('danger', #c10404, #e61d1d, #a26c6c, #df5656);
--bx-fullscreen-text-z-index: 99999;
--bx-toast-z-index: 60000;
--bx-dialog-z-index: 50000;
--bx-fullscreen-text-z-index: 9999;
--bx-toast-z-index: 6000;
--bx-key-binding-dialog-z-index: 5010;
--bx-key-binding-dialog-overlay-z-index: 5000;
--bx-dialog-overlay-z-index: 40200;
--bx-stats-bar-z-index: 40100;
--bx-mkb-pointer-lock-msg-z-index: 40000;
--bx-stats-bar-z-index: 4010;
--bx-navigation-dialog-z-index: 30100;
--bx-navigation-dialog-overlay-z-index: 30000;
--bx-navigation-dialog-z-index: 3010;
--bx-navigation-dialog-overlay-z-index: 3000;
--bx-game-bar-z-index: 10000;
--bx-screenshot-animation-z-index: 9000;
--bx-wait-time-box-z-index: 1000;
--bx-mkb-pointer-lock-msg-z-index: 2000;
--bx-game-bar-z-index: 1000;
--bx-screenshot-animation-z-index: 200;
--bx-wait-time-box-z-index: 100;
}
@font-face {
@@ -120,7 +122,7 @@ div[class^=HUDButton-module__hiddenContainer] ~ div:not([class^=HUDButton-module
}
.bx-prompt {
font-family: var(--bx-promptfont-font);
font-family: var(--bx-promptfont-font) !important;
}
.bx-line-through {
@@ -226,3 +228,13 @@ div[class*=SupportedInputsBadge] {
display: none;
}
}
.bx-blink-me {
animation: bx-blinker 1s linear infinite;
}
@keyframes bx-blinker {
100% {
opacity: 0;
}
}

193
src/assets/css/settings-dialog.styl Normal file → Executable file
View File

@@ -31,42 +31,6 @@
font-weight: normal;
height: var(--bx-button-height);
}
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;
}
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);
}
}
a {
color: #1c9d1c;
text-decoration: none;
&:hover, &:focus {
color: #5dc21e;
}
}
}
.bx-settings-tabs-container {
@@ -170,69 +134,6 @@
overflow: hidden;
}
> div[data-tab-group=shortcuts] {
> div {
&[data-has-gamepad=true] {
> div:first-of-type {
display: none;
}
> div:last-of-type {
display: block;
}
}
&[data-has-gamepad=false] {
> div:first-of-type {
display: block;
}
> div:last-of-type {
display: none;
}
}
}
.bx-shortcut-profile {
width: 100%;
height: 36px;
display: block;
}
.bx-shortcut-note {
margin-top: 10px;
font-size: 14px;
}
.bx-shortcut-row {
display: flex;
margin-bottom: 10px;
label.bx-prompt {
flex: 1;
font-size: 26px;
margin-bottom: 0;
}
.bx-shortcut-actions {
flex: 2;
position: relative;
select {
position: absolute;
width: 100%;
height: 100%;
display: block;
&:last-of-type {
opacity: 0;
z-index: calc(var(--bx-settings-z-index) + 1);
}
}
}
}
}
.bx-top-buttons {
display: flex;
flex-direction: column;
@@ -262,6 +163,8 @@
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
min-height: var(--bx-button-height);
align-content: center;
}
}
}
@@ -306,6 +209,18 @@
margin: 0 0 0 auto;
}
}
&[data-multi-lines="true"] {
flex-direction: column;
> span.bx-settings-label {
align-self: start;
+ * {
margin: unset;
}
}
}
}
.bx-settings-dialog-note {
@@ -339,6 +254,7 @@
line-height: 20px;
font-size: 14px;
margin-top: 10px;
margin-bottom: 10px;
}
.bx-debug-info {
@@ -378,24 +294,26 @@
}
.bx-settings-tab-contents {
border-radius-size = 6px;
> div {
// Label at the beginning
*:not(.bx-settings-row):has(+ .bx-settings-row) + .bx-settings-row:has(+ .bx-settings-row) {
border-top-left-radius: 10px;
border-top-right-radius: 10px;
border-top-left-radius: border-radius-size;
border-top-right-radius: border-radius-size;
}
// Label at the end
.bx-settings-row:not(:has(+ .bx-settings-row)) {
border: none;
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
border-bottom-left-radius: border-radius-size;
border-bottom-right-radius: border-radius-size;
}
// Single label
*:not(.bx-settings-row):has(+ .bx-settings-row) + .bx-settings-row:not(:has(+ .bx-settings-row)) {
border: none;
border-radius: 10px;
border-radius: border-radius-size;
}
}
}
@@ -409,7 +327,6 @@
label {
flex: 1;
margin-bottom: 0;
padding: 10px;
background: #004f87;
}
@@ -444,10 +361,6 @@
.bx-suggest-box {
display: none;
background: #161616;
padding: 10px;
box-shadow: 0px 0px 12px #0f0f0f inset;
border-radius: 10px;
}
.bx-suggest-wrapper {
@@ -563,3 +476,65 @@
}
}
}
.bx-sub-content-box {
background: #161616;
padding: 10px;
box-shadow: 0px 0px 12px #0f0f0f inset;
border-radius: 10px;
.bx-settings-row & {
background: #202020;
padding: 12px;
box-shadow: 0 0 4px #000000 inset;
border-radius: 6px;
}
}
.bx-controller-extra-settings {
&[data-has-gamepad=true] {
> :first-child {
display: none;
}
> :last-child {
display: block;
}
}
&[data-has-gamepad=false] {
> :first-child {
display: block;
}
> :last-child {
display: none;
}
}
.bx-controller-extra-wrapper {
flex: 1;
min-width: 1px;
}
.bx-sub-content-box {
flex: 1;
text-align: left;
display: flex;
flex-direction: column;
margin-top: 10px;
> label {
font-size: 14px;
}
}
}
.bx-preset-row {
display: flex;
gap: 8px;
.bx-select {
flex: 1;
}
}

0
src/assets/css/stream-stats.styl Normal file → Executable file
View File

0
src/assets/css/stream.styl Normal file → Executable file
View File

2
src/assets/css/styles.styl Normal file → Executable file
View File

@@ -2,7 +2,7 @@
@import 'button.styl';
@import 'header.styl';
@import 'dialog.styl';
@import 'key-binding-dialog.styl';
@import 'navigation-dialog.styl';
@import 'settings-dialog.styl';
@import 'toast.styl';

0
src/assets/css/toast.styl Normal file → Executable file
View File

66
src/assets/css/web-components.styl Normal file → Executable file
View File

@@ -1,7 +1,12 @@
.bx-select {
select.bx-select {
min-height: 30px;
}
div.bx-select {
display: flex;
align-items: center;
flex: 0 1 auto;
gap: 8px;
select {
// Render offscreen instead of "display: none" so we could get its size
@@ -9,23 +14,41 @@
top: -9999px !important;
left: -9999px !important;
visibility: hidden !important;
&:disabled {
& ~ button {
display: none;
}
& ~ div {
background: #131416;
color: white;
pointer-events: none;
.bx-select-indicators {
visibility: hidden;
}
}
}
}
> div, button.bx-select-value {
min-width: 120px;
text-align: left;
margin: 0 8px;
line-height: 24px;
vertical-align: middle;
background: #fff;
color: #000;
border-radius: 4px;
padding: 2px 8px;
display: flex;
flex: 1;
flex-direction: column;
}
> div {
display: inline-block;
min-height: 24px;
box-sizing: content-box;
input {
display: inline-block;
@@ -36,6 +59,9 @@
margin-bottom: 0;
font-size: 14px;
width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
span {
display: block;
@@ -43,18 +69,23 @@
font-weight: bold;
text-align: left;
line-height: initial;
white-space: pre;
}
}
}
button.bx-select-value {
border: none;
display: inline-flex;
cursor: pointer;
min-height: 30px;
font-size: 0.9rem;
align-items: center;
> div {
display: flex;
width: 100%;
}
span {
flex: 1;
text-align: left;
@@ -97,3 +128,30 @@
}
}
}
.bx-select-indicators {
display: flex;
height: 4px;
gap: 2px;
margin-bottom: 2px;
span {
content: ' ';
display: inline-block;
flex: 1;
background: #cfcfcf;
border-radius: 4px;
&[data-highlighted] {
background: #9c9c9c;
}
&[data-selected] {
background: #aacfe7;
}
&[data-highlighted][data-selected] {
background: #5fa3d0;
}
}
}

0
src/assets/header_meta.txt Normal file → Executable file
View File

0
src/assets/header_script.lite.txt Normal file → Executable file
View File

0
src/assets/header_script.txt Normal file → Executable file
View File

0
src/assets/svg/battery-full.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 821 B

After

Width:  |  Height:  |  Size: 821 B

0
src/assets/svg/better-xcloud.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

0
src/assets/svg/camera.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 494 B

After

Width:  |  Height:  |  Size: 494 B

0
src/assets/svg/caret-left.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 386 B

After

Width:  |  Height:  |  Size: 386 B

0
src/assets/svg/caret-right.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 385 B

After

Width:  |  Height:  |  Size: 385 B

0
src/assets/svg/clock.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 374 B

After

Width:  |  Height:  |  Size: 374 B

0
src/assets/svg/close.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 264 B

After

Width:  |  Height:  |  Size: 264 B

0
src/assets/svg/cloud.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 427 B

After

Width:  |  Height:  |  Size: 427 B

0
src/assets/svg/command.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 667 B

After

Width:  |  Height:  |  Size: 667 B

0
src/assets/svg/controller.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 646 B

After

Width:  |  Height:  |  Size: 646 B

0
src/assets/svg/copy.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 250 B

After

Width:  |  Height:  |  Size: 250 B

0
src/assets/svg/create-shortcut.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 711 B

After

Width:  |  Height:  |  Size: 711 B

0
src/assets/svg/cursor-text.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 370 B

After

Width:  |  Height:  |  Size: 370 B

0
src/assets/svg/display.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 382 B

After

Width:  |  Height:  |  Size: 382 B

0
src/assets/svg/download.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 355 B

After

Width:  |  Height:  |  Size: 355 B

0
src/assets/svg/eye-slash.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

0
src/assets/svg/eye.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

0
src/assets/svg/home.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 358 B

After

Width:  |  Height:  |  Size: 358 B

0
src/assets/svg/microphone-slash.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 551 B

After

Width:  |  Height:  |  Size: 551 B

0
src/assets/svg/microphone.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 411 B

After

Width:  |  Height:  |  Size: 411 B

0
src/assets/svg/mouse-settings.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 981 B

After

Width:  |  Height:  |  Size: 981 B

0
src/assets/svg/mouse.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 344 B

After

Width:  |  Height:  |  Size: 344 B

0
src/assets/svg/native-mkb.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 619 B

After

Width:  |  Height:  |  Size: 619 B

0
src/assets/svg/new.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 411 B

After

Width:  |  Height:  |  Size: 411 B

0
src/assets/svg/power.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 339 B

After

Width:  |  Height:  |  Size: 339 B

0
src/assets/svg/question.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 421 B

After

Width:  |  Height:  |  Size: 421 B

0
src/assets/svg/refresh.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 378 B

After

Width:  |  Height:  |  Size: 378 B

0
src/assets/svg/remote-play.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

0
src/assets/svg/speaker-high.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 410 B

After

Width:  |  Height:  |  Size: 410 B

0
src/assets/svg/speaker-slash.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

0
src/assets/svg/stream-settings.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 768 B

After

Width:  |  Height:  |  Size: 768 B

0
src/assets/svg/stream-stats.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 430 B

After

Width:  |  Height:  |  Size: 430 B

0
src/assets/svg/touch-control-disable.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 915 B

After

Width:  |  Height:  |  Size: 915 B

0
src/assets/svg/touch-control-enable.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 796 B

After

Width:  |  Height:  |  Size: 796 B

0
src/assets/svg/trash.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 433 B

After

Width:  |  Height:  |  Size: 433 B

0
src/assets/svg/true-achievements.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

0
src/assets/svg/upload.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 349 B

After

Width:  |  Height:  |  Size: 349 B

0
src/assets/svg/virtual-controller.svg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 680 B

After

Width:  |  Height:  |  Size: 680 B

0
src/build-config.ts Normal file → Executable file
View File

24
src/enums/bypass-servers.ts Normal file → Executable file
View File

@@ -1,17 +1,17 @@
import { t } from "@/utils/translation"
export const BypassServers = {
'br': t('brazil'),
'jp': t('japan'),
'kr': t('korea'),
'pl': t('poland'),
'us': t('united-states'),
}
br: t('brazil'),
jp: t('japan'),
kr: t('korea'),
pl: t('poland'),
us: t('united-states'),
} as const;
export const BypassServerIps: Record<keyof typeof BypassServers, string> = {
'br': '169.150.198.66',
'kr': '121.125.60.151',
'jp': '138.199.21.239',
'pl': '45.134.212.66',
'us': '143.244.47.65',
}
br: '169.150.198.66',
kr: '121.125.60.151',
jp: '138.199.21.239',
pl: '45.134.212.66',
us: '143.244.47.65',
} as const;

0
src/enums/game-pass-gallery.ts Normal file → Executable file
View File

71
src/enums/gamepad.ts Executable file
View File

@@ -0,0 +1,71 @@
import { PrompFont } from "./prompt-font";
export enum GamepadKey {
A = 0,
B = 1,
X = 2,
Y = 3,
LB = 4,
RB = 5,
LT = 6,
RT = 7,
SELECT = 8,
START = 9,
L3 = 10,
R3 = 11,
UP = 12,
DOWN = 13,
LEFT = 14,
RIGHT = 15,
HOME = 16,
SHARE = 17,
LS_UP = 100,
LS_DOWN = 101,
LS_LEFT = 102,
LS_RIGHT = 103,
RS_UP = 200,
RS_DOWN = 201,
RS_LEFT = 202,
RS_RIGHT = 203,
};
export const GamepadKeyName: Record<number, [string, PrompFont]> = {
[GamepadKey.A]: ['A', PrompFont.A],
[GamepadKey.B]: ['B', PrompFont.B],
[GamepadKey.X]: ['X', PrompFont.X],
[GamepadKey.Y]: ['Y', PrompFont.Y],
[GamepadKey.LB]: ['LB', PrompFont.LB],
[GamepadKey.RB]: ['RB', PrompFont.RB],
[GamepadKey.LT]: ['LT', PrompFont.LT],
[GamepadKey.RT]: ['RT', PrompFont.RT],
[GamepadKey.SELECT]: ['Select', PrompFont.SELECT],
[GamepadKey.START]: ['Start', PrompFont.START],
[GamepadKey.HOME]: ['Home', PrompFont.HOME],
[GamepadKey.UP]: ['D-Pad Up', PrompFont.UP],
[GamepadKey.DOWN]: ['D-Pad Down', PrompFont.DOWN],
[GamepadKey.LEFT]: ['D-Pad Left', PrompFont.LEFT],
[GamepadKey.RIGHT]: ['D-Pad Right', PrompFont.RIGHT],
[GamepadKey.L3]: ['L3', PrompFont.L3],
[GamepadKey.LS_UP]: ['Left Stick Up', PrompFont.LS_UP],
[GamepadKey.LS_DOWN]: ['Left Stick Down', PrompFont.LS_DOWN],
[GamepadKey.LS_LEFT]: ['Left Stick Left', PrompFont.LS_LEFT],
[GamepadKey.LS_RIGHT]: ['Left Stick Right', PrompFont.LS_RIGHT],
[GamepadKey.R3]: ['R3', PrompFont.R3],
[GamepadKey.RS_UP]: ['Right Stick Up', PrompFont.RS_UP],
[GamepadKey.RS_DOWN]: ['Right Stick Down', PrompFont.RS_DOWN],
[GamepadKey.RS_LEFT]: ['Right Stick Left', PrompFont.RS_LEFT],
[GamepadKey.RS_RIGHT]: ['Right Stick Right', PrompFont.RS_RIGHT],
};
export enum GamepadStick {
LEFT = 0,
RIGHT = 1,
};

227
src/enums/mkb.ts Normal file → Executable file
View File

@@ -1,102 +1,169 @@
import type { GamepadKeyNameType } from "@/types/mkb";
import { PrompFont } from "@enums/prompt-font";
export enum GamepadKey {
A = 0,
B = 1,
X = 2,
Y = 3,
LB = 4,
RB = 5,
LT = 6,
RT = 7,
SELECT = 8,
START = 9,
L3 = 10,
R3 = 11,
UP = 12,
DOWN = 13,
LEFT = 14,
RIGHT = 15,
HOME = 16,
SHARE = 17,
LS_UP = 100,
LS_DOWN = 101,
LS_LEFT = 102,
LS_RIGHT = 103,
RS_UP = 200,
RS_DOWN = 201,
RS_LEFT = 202,
RS_RIGHT = 203,
};
export const enum MouseConstant {
DEFAULT_PANNING_SENSITIVITY = 0.0010,
DEFAULT_DEADZONE_COUNTERWEIGHT = 0.01,
MAXIMUM_STICK_RANGE = 1.1,
}
export const GamepadKeyName: GamepadKeyNameType = {
[GamepadKey.A]: ['A', PrompFont.A],
[GamepadKey.B]: ['B', PrompFont.B],
[GamepadKey.X]: ['X', PrompFont.X],
[GamepadKey.Y]: ['Y', PrompFont.Y],
[GamepadKey.LB]: ['LB', PrompFont.LB],
[GamepadKey.RB]: ['RB', PrompFont.RB],
[GamepadKey.LT]: ['LT', PrompFont.LT],
[GamepadKey.RT]: ['RT', PrompFont.RT],
[GamepadKey.SELECT]: ['Select', PrompFont.SELECT],
[GamepadKey.START]: ['Start', PrompFont.START],
[GamepadKey.HOME]: ['Home', PrompFont.HOME],
[GamepadKey.UP]: ['D-Pad Up', PrompFont.UP],
[GamepadKey.DOWN]: ['D-Pad Down', PrompFont.DOWN],
[GamepadKey.LEFT]: ['D-Pad Left', PrompFont.LEFT],
[GamepadKey.RIGHT]: ['D-Pad Right', PrompFont.RIGHT],
[GamepadKey.L3]: ['L3', PrompFont.L3],
[GamepadKey.LS_UP]: ['Left Stick Up', PrompFont.LS_UP],
[GamepadKey.LS_DOWN]: ['Left Stick Down', PrompFont.LS_DOWN],
[GamepadKey.LS_LEFT]: ['Left Stick Left', PrompFont.LS_LEFT],
[GamepadKey.LS_RIGHT]: ['Left Stick Right', PrompFont.LS_RIGHT],
[GamepadKey.R3]: ['R3', PrompFont.R3],
[GamepadKey.RS_UP]: ['Right Stick Up', PrompFont.RS_UP],
[GamepadKey.RS_DOWN]: ['Right Stick Down', PrompFont.RS_DOWN],
[GamepadKey.RS_LEFT]: ['Right Stick Left', PrompFont.RS_LEFT],
[GamepadKey.RS_RIGHT]: ['Right Stick Right', PrompFont.RS_RIGHT],
};
export enum GamepadStick {
LEFT = 0,
RIGHT = 1,
};
export enum MouseButtonCode {
export const enum MouseButtonCode {
LEFT_CLICK = 'Mouse0',
RIGHT_CLICK = 'Mouse2',
MIDDLE_CLICK = 'Mouse1',
};
export enum MouseMapTo {
export const enum MouseMapTo {
OFF = 0,
LS = 1,
RS = 2,
}
export enum WheelCode {
export const enum WheelCode {
SCROLL_UP = 'ScrollUp',
SCROLL_DOWN = 'ScrollDown',
SCROLL_LEFT = 'ScrollLeft',
SCROLL_RIGHT = 'ScrollRight',
};
export enum MkbPresetKey {
MOUSE_MAP_TO = 'map_to',
MOUSE_SENSITIVITY_X = 'sensitivity_x',
MOUSE_SENSITIVITY_Y = 'sensitivity_y',
export const enum MkbPresetKey {
MOUSE_MAP_TO = 'mapTo',
MOUSE_DEADZONE_COUNTERWEIGHT = 'deadzone_counterweight',
MOUSE_SENSITIVITY_X = 'sensitivityX',
MOUSE_SENSITIVITY_Y = 'sensitivityY',
MOUSE_DEADZONE_COUNTERWEIGHT = 'deadzoneCounterweight',
}
export type KeyCode =
| 'Backspace'
| 'Tab'
| 'Enter'
| 'ShiftLeft'
| 'ShiftRight'
| 'ControlLeft'
| 'ControlRight'
| 'AltLeft'
| 'AltRight'
| 'Pause'
| 'CapsLock'
| 'Escape'
| 'Space'
| 'PageUp'
| 'PageDown'
| 'End'
| 'Home'
| 'ArrowLeft'
| 'ArrowUp'
| 'ArrowRight'
| 'ArrowDown'
| 'PrintScreen'
| 'Insert'
| 'Delete'
| 'Digit0'
| 'Digit1'
| 'Digit2'
| 'Digit3'
| 'Digit4'
| 'Digit5'
| 'Digit6'
| 'Digit7'
| 'Digit8'
| 'Digit9'
| 'KeyA'
| 'KeyB'
| 'KeyC'
| 'KeyD'
| 'KeyE'
| 'KeyF'
| 'KeyG'
| 'KeyH'
| 'KeyI'
| 'KeyJ'
| 'KeyK'
| 'KeyL'
| 'KeyM'
| 'KeyN'
| 'KeyO'
| 'KeyP'
| 'KeyQ'
| 'KeyR'
| 'KeyS'
| 'KeyT'
| 'KeyU'
| 'KeyV'
| 'KeyW'
| 'KeyX'
| 'KeyY'
| 'KeyZ'
| 'MetaLeft'
| 'MetaRight'
| 'ContextMenu'
| 'F1'
| 'F2'
| 'F3'
| 'F4'
| 'F5'
| 'F6'
| 'F7'
| 'F8'
| 'F9'
| 'F10'
| 'F11'
| 'F12'
| 'NumLock'
| 'ScrollLock'
| 'AudioVolumeMute'
| 'AudioVolumeDown'
| 'AudioVolumeUp'
| 'MediaTrackNext'
| 'MediaTrackPrevious'
| 'MediaStop'
| 'MediaPlayPause'
| 'LaunchMail'
| 'LaunchMediaPlayer'
| 'LaunchApplication1'
| 'LaunchApplication2'
| 'Semicolon'
| 'Equal'
| 'Comma'
| 'Minus'
| 'Period'
| 'Slash'
| 'Backquote'
| 'BracketLeft'
| 'Backslash'
| 'BracketRight'
| 'Quote'
| 'Numpad0'
| 'Numpad1'
| 'Numpad2'
| 'Numpad3'
| 'Numpad4'
| 'Numpad5'
| 'Numpad6'
| 'Numpad7'
| 'Numpad8'
| 'Numpad9'
| 'NumpadMultiply'
| 'NumpadAdd'
| 'NumpadSubtract'
| 'NumpadDecimal'
| 'NumpadDivide';
export type KeyCodeExcludeModifiers = Exclude<KeyCode,
'ShiftLeft'
| 'ShiftRight'
| 'ControlLeft'
| 'ControlRight'
| 'AltLeft'
| 'AltRight'
>
export const enum KeyModifier {
CTRL = 1,
ALT = 2,
SHIFT = 4,
}

164
src/enums/pref-keys.ts Normal file → Executable file
View File

@@ -1,102 +1,114 @@
export enum StorageKey {
GLOBAL = 'better_xcloud',
export const enum StorageKey {
GLOBAL = 'BetterXcloud',
LOCALE = 'BetterXcloud.Locale',
LOCALE_TRANSLATIONS = 'BetterXcloud.Locale.Translations',
PATCHES_CACHE = 'BetterXcloud.Patches.Cache',
PATCHES_SIGNATURE = 'BetterXcloud.Patches.Cache.Signature',
USER_AGENT = 'BetterXcloud.UserAgent',
GH_PAGES_COMMIT_HASH = 'BetterXcloud.GhPages.CommitHash',
LIST_CUSTOM_TOUCH_LAYOUTS = 'BetterXcloud.GhPages.CustomTouchLayouts',
LIST_FORCE_NATIVE_MKB = 'BetterXcloud.GhPages.ForceNativeMkb',
}
export enum PrefKey {
LAST_UPDATE_CHECK = 'version_last_check',
LATEST_VERSION = 'version_latest',
CURRENT_VERSION = 'version_current',
export const enum PrefKey {
VERSION_LAST_CHECK = 'version.lastCheck',
VERSION_LATEST = 'version.latest',
VERSION_CURRENT = 'version.current',
BETTER_XCLOUD_LOCALE = 'bx_locale',
SCRIPT_LOCALE = 'bx.locale',
SERVER_REGION = 'server_region',
SERVER_BYPASS_RESTRICTION = 'server_bypass_restriction',
SERVER_REGION = 'server.region',
SERVER_BYPASS_RESTRICTION = 'server.bypassRestriction',
SERVER_PREFER_IPV6 = 'server.ipv6.prefer',
PREFER_IPV6_SERVER = 'prefer_ipv6_server',
STREAM_TARGET_RESOLUTION = 'stream_target_resolution',
STREAM_PREFERRED_LOCALE = 'stream_preferred_locale',
STREAM_CODEC_PROFILE = 'stream_codec_profile',
STREAM_PREFERRED_LOCALE = 'stream.locale',
STREAM_RESOLUTION = 'stream.video.resolution',
STREAM_CODEC_PROFILE = 'stream.video.codecProfile',
STREAM_MAX_VIDEO_BITRATE = 'stream.video.maxBitrate',
STREAM_COMBINE_SOURCES = 'stream.video.combineAudio',
USER_AGENT_PROFILE = 'user_agent_profile',
STREAM_SIMPLIFY_MENU = 'stream_simplify_menu',
USER_AGENT_PROFILE = 'userAgent.profile',
STREAM_COMBINE_SOURCES = 'stream_combine_sources',
TOUCH_CONTROLLER_MODE = 'touchController.mode',
TOUCH_CONTROLLER_AUTO_OFF = 'touchController.autoOff',
TOUCH_CONTROLLER_DEFAULT_OPACITY = 'touchController.opacity.default',
TOUCH_CONTROLLER_STYLE_STANDARD = 'touchController.style.standard',
TOUCH_CONTROLLER_STYLE_CUSTOM = 'touchController.style.custom',
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',
GAME_BAR_POSITION = 'gameBar.position',
STREAM_DISABLE_FEEDBACK_DIALOG = 'stream_disable_feedback_dialog',
LOCAL_CO_OP_ENABLED = 'localCoOp.enabled',
BITRATE_VIDEO_MAX = 'bitrate_video_max',
DEVICE_VIBRATION_MODE = 'deviceVibration.mode',
DEVICE_VIBRATION_INTENSITY = 'deviceVibration.intensity',
GAME_BAR_POSITION = 'game_bar_position',
CONTROLLER_POLLING_RATE = 'controller.pollingRate',
LOCAL_CO_OP_ENABLED = 'local_co_op_enabled',
// LOCAL_CO_OP_SEPARATE_TOUCH_CONTROLLER = 'local_co_op_separate_touch_controller',
NATIVE_MKB_MODE = 'nativeMkb.mode',
FORCE_NATIVE_MKB_GAMES = 'nativeMkb.forcedGames',
NATIVE_MKB_SCROLL_HORIZONTAL_SENSITIVITY = 'nativeMkb.scroll.sensitivityX',
NATIVE_MKB_SCROLL_VERTICAL_SENSITIVITY = 'nativeMkb.scroll.sensitivityY',
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',
CONTROLLER_POLLING_RATE = 'controller_polling_rate',
MKB_ENABLED = 'mkb.enabled',
MKB_HIDE_IDLE_CURSOR = 'mkb.cursor.hideIdle',
MKB_P1_MAPPING_PRESET_ID = 'mkb.p1.preset.mappingId',
MKB_P1_SLOT = 'mkb.p1.slot',
MKB_P2_MAPPING_PRESET_ID = 'mkb.p2.preset.mappingId',
MKB_P2_SLOT = 'mkb.p2.slot',
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',
KEYBOARD_SHORTCUTS_IN_GAME_PRESET_ID = 'keyboardShortcuts.preset.inGameId',
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.applyFilters',
SCREENSHOT_APPLY_FILTERS = 'screenshot_apply_filters',
BLOCK_TRACKING = 'block.tracking',
BLOCK_SOCIAL_FEATURES = 'block.social',
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',
LOADING_SCREEN_GAME_ART = 'loadingScreen.gameArt.show',
LOADING_SCREEN_SHOW_WAIT_TIME = 'loadingScreen.waitTime.show',
LOADING_SCREEN_ROCKET = 'loadingScreen.rocket',
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.controllerFriendly',
UI_LAYOUT = 'ui.layout',
UI_SCROLLBAR_HIDE = 'ui.hideScrollbar',
UI_HIDE_SECTIONS = 'ui.hideSections',
BYOG_DISABLED = 'feature.byog.disabled',
UI_CONTROLLER_FRIENDLY = 'ui_controller_friendly',
UI_LAYOUT = 'ui_layout',
UI_SCROLLBAR_HIDE = 'ui_scrollbar_hide',
UI_HIDE_SECTIONS = 'ui_hide_sections',
UI_GAME_CARD_SHOW_WAIT_TIME = 'ui.gameCard.waitTime.show',
UI_SIMPLIFY_STREAM_MENU = 'ui.streamMenu.simplify',
UI_DISABLE_FEEDBACK_DIALOG = 'ui.feedbackDialog.disabled',
UI_CONTROLLER_SHOW_STATUS = 'ui.controllerStatus.show',
UI_GAME_CARD_SHOW_WAIT_TIME = 'ui_game_card_show_wait_time',
UI_SKIP_SPLASH_VIDEO = 'ui.splashVideo.skip',
UI_HIDE_SYSTEM_MENU_ICON = 'ui.systemMenu.hideHandle',
UI_REDUCE_ANIMATIONS = 'ui.reduceAnimations',
VIDEO_PLAYER_TYPE = 'video_player_type',
VIDEO_PROCESSING = 'video_processing',
VIDEO_POWER_PREFERENCE = 'video_power_preference',
VIDEO_MAX_FPS = 'video_max_fps',
VIDEO_SHARPNESS = 'video_sharpness',
VIDEO_RATIO = 'video_ratio',
VIDEO_BRIGHTNESS = 'video_brightness',
VIDEO_CONTRAST = 'video_contrast',
VIDEO_SATURATION = 'video_saturation',
VIDEO_PLAYER_TYPE = 'video.player.type',
VIDEO_POWER_PREFERENCE = 'video.player.powerPreference',
VIDEO_PROCESSING = 'video.processing',
VIDEO_SHARPNESS = 'video.processing.sharpness',
VIDEO_MAX_FPS = 'video.maxFps',
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',
AUDIO_MIC_ON_PLAYING = 'audio.mic.onPlaying',
AUDIO_VOLUME_CONTROL_ENABLED = 'audio.volume.booster.enabled',
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',
STATS_ITEMS = 'stats.items',
STATS_SHOW_WHEN_PLAYING = 'stats.showWhenPlaying',
STATS_QUICK_GLANCE_ENABLED = 'stats.quickGlance.enabled',
STATS_POSITION = 'stats.position',
STATS_TEXT_SIZE = 'stats.textSize',
STATS_TRANSPARENT = 'stats.transparent',
STATS_OPACITY = 'stats.opacity',
STATS_CONDITIONAL_FORMATTING = 'stats.colors',
REMOTE_PLAY_ENABLED = 'xhome_enabled',
REMOTE_PLAY_RESOLUTION = 'xhome_resolution',
REMOTE_PLAY_ENABLED = 'xhome.enabled',
REMOTE_PLAY_STREAM_RESOLUTION = 'xhome.video.resolution',
GAME_FORTNITE_FORCE_CONSOLE = 'game_fortnite_force_console',
GAME_MSFS2020_FORCE_NATIVE_MKB = 'game_msfs2020_force_native_mkb',
GAME_FORTNITE_FORCE_CONSOLE = 'game.fortnite.forceConsole',
}

104
src/enums/pref-values.ts Executable file
View File

@@ -0,0 +1,104 @@
export const enum UiSection {
ALL_GAMES = 'all-games',
FRIENDS = 'friends',
MOST_POPULAR = 'most-popular',
NATIVE_MKB = 'native-mkb',
NEWS = 'news',
TOUCH = 'touch',
BOYG = 'byog',
}
export const enum GameBarPosition {
BOTTOM_LEFT = 'bottom-left',
BOTTOM_RIGHT = 'bottom-right',
OFF = 'off',
};
export const enum UiLayout {
TV = 'tv',
NORMAL = 'normal',
DEFAULT = 'default',
}
export const enum LoadingScreenRocket {
SHOW = 'show',
HIDE = 'hide',
HIDE_QUEUE = 'hide-queue',
}
export const enum StreamResolution {
DIM_720P = '720p',
DIM_1080P = '1080p',
DIM_1080P_HQ = '1080p-hq',
AUTO = 'auto',
}
export const enum CodecProfile {
DEFAULT = 'default',
LOW = 'low',
NORMAL = 'normal',
HIGH = 'high',
};
export const enum TouchControllerMode {
DEFAULT = 'default',
ALL = 'all',
OFF = 'off',
}
export const enum TouchControllerStyleStandard {
DEFAULT = 'default',
WHITE = 'white',
MUTED = 'muted',
}
export const enum TouchControllerStyleCustom {
DEFAULT = 'default',
MUTED = 'muted',
}
export const enum DeviceVibrationMode {
ON = 'on',
AUTO = 'auto',
OFF = 'off',
}
export const enum NativeMkbMode {
DEFAULT = 'default',
ON = 'on',
OFF = 'off',
}
export const enum StreamStat {
PING = 'ping',
JITTER = 'jit',
FPS = 'fps',
BITRATE = 'btr',
DECODE_TIME = 'dt',
PACKETS_LOST = 'pl',
FRAMES_LOST = 'fl',
DOWNLOAD = 'dl',
UPLOAD = 'ul',
PLAYTIME = 'play',
BATTERY = 'batt',
CLOCK = 'time',
};
export const enum VideoRatio {
'16:9' = '16:9',
'18:9' = '18:9',
'21:9' = '21:9',
'16:10' = '16:10',
'4:3' = '4:3',
FILL = 'fill',
}
export const enum StreamPlayerType {
VIDEO = 'default',
WEBGL2 = 'webgl2',
}
export const enum StreamVideoProcessing {
USM = 'usm',
CAS = 'cas',
}

0
src/enums/prompt-font.ts Normal file → Executable file
View File

25
src/enums/shortcut-actions.ts Executable file
View File

@@ -0,0 +1,25 @@
export const enum ShortcutAction {
BETTER_XCLOUD_SETTINGS_SHOW = 'bx.settings.show',
STREAM_VIDEO_TOGGLE = 'stream.video.toggle',
STREAM_SCREENSHOT_CAPTURE = 'stream.screenshot.capture',
STREAM_MENU_SHOW = 'stream.menu.show',
STREAM_STATS_TOGGLE = 'stream.stats.toggle',
STREAM_SOUND_TOGGLE = 'stream.sound.toggle',
STREAM_MICROPHONE_TOGGLE = 'stream.microphone.toggle',
STREAM_VOLUME_INC = 'stream.volume.inc',
STREAM_VOLUME_DEC = 'stream.volume.dec',
DEVICE_SOUND_TOGGLE = 'device.sound.toggle',
DEVICE_VOLUME_INC = 'device.volume.inc',
DEVICE_VOLUME_DEC = 'device.volume.dec',
DEVICE_BRIGHTNESS_INC = 'device.brightness.inc',
DEVICE_BRIGHTNESS_DEC = 'device.brightness.dec',
MKB_TOGGLE = 'mkb.toggle',
TRUE_ACHIEVEMENTS_OPEN = 'ta.open',
};

View File

@@ -1,9 +0,0 @@
export enum StreamPlayerType {
VIDEO = 'default',
WEBGL2 = 'webgl2',
}
export enum StreamVideoProcessing {
USM = 'usm',
CAS = 'cas',
}

View File

@@ -1,8 +0,0 @@
export enum UiSection {
ALL_GAMES = 'all-games',
FRIENDS = 'friends',
MOST_POPULAR = 'most-popular',
NATIVE_MKB = 'native-mkb',
NEWS = 'news',
TOUCH = 'touch',
}

0
src/enums/user-agent.ts Normal file → Executable file
View File

88
src/index.ts Normal file → Executable file
View File

@@ -7,7 +7,7 @@ import { BxExposed } from "@utils/bx-exposed";
import { t } from "@utils/translation";
import { interceptHttpRequests } from "@utils/network";
import { CE } from "@utils/html";
import { showGamepadToast, updatePollingRate } from "@utils/gamepad";
import { showGamepadToast } from "@utils/gamepad";
import { EmulatedMkbHandler } from "@modules/mkb/mkb-handler";
import { StreamBadges } from "@modules/stream/stream-badges";
import { StreamStats } from "@modules/stream/stream-stats";
@@ -19,7 +19,6 @@ import { checkForUpdate, disablePwa, productTitleToSlug } from "@utils/utils";
import { Patcher } from "@modules/patcher";
import { RemotePlayManager } from "@/modules/remote-play-manager";
import { onHistoryChanged, patchHistoryMethod } from "@utils/history";
import { VibrationManager } from "@modules/vibration-manager";
import { disableAdobeAudienceManager, patchAudioContext, patchCanvasContext, patchMeControl, patchPointerLockApi, patchRtcCodecs, patchRtcPeerConnection, patchVideoApi } from "@utils/monkey-patches";
import { AppInterface, STATES } from "@utils/global";
import { BxLogger } from "@utils/bx-logger";
@@ -28,19 +27,23 @@ import { ScreenshotManager } from "./utils/screenshot-manager";
import { NativeMkbHandler } from "./modules/mkb/native-mkb-handler";
import { GuideMenu } from "./modules/ui/guide-menu";
import { updateVideoPlayer } from "./modules/stream/stream-settings-utils";
import { UiSection } from "./enums/ui-sections";
import { NativeMkbMode, TouchControllerMode, UiSection } from "./enums/pref-values";
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, StreamTouchController } from "./utils/settings-storages/global-settings-storage";
import { SettingsNavigationDialog } from "./modules/ui/dialog/settings-dialog";
import { getPref } from "./utils/settings-storages/global-settings-storage";
import { SettingsDialog } from "./modules/ui/dialog/settings-dialog";
import { StreamUiHandler } from "./modules/stream/stream-ui";
import { UserAgent } from "./utils/user-agent";
import { XboxApi } from "./utils/xbox-api";
import { StreamStatsCollector } from "./utils/stream-stats-collector";
import { RootDialogObserver } from "./utils/root-dialog-observer";
import { StreamSettings } from "./utils/stream-settings";
import { KeyboardShortcutHandler } from "./modules/mkb/keyboard-shortcut-handler";
import { GhPagesUtils } from "./utils/gh-pages";
import { DeviceVibrationManager } from "./modules/device-vibration-manager";
// Handle login page
if (window.location.pathname.includes('/auth/msa')) {
@@ -160,14 +163,14 @@ document.addEventListener('readystatechange', e => {
if (STATES.isSignedIn) {
// Preload Remote Play
getPref(PrefKey.REMOTE_PLAY_ENABLED) && RemotePlayManager.getInstance().initialize();
RemotePlayManager.getInstance()?.initialize();
} else {
// Show Settings button in the header when not signed in
window.setTimeout(HeaderSection.watchHeader, 2000);
}
// Hide "Play with Friends" skeleton section
if (getPref(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.FRIENDS)) {
if (getPref<UiSection[]>(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.FRIENDS)) {
const $parent = document.querySelector('div[class*=PlayWithFriendsSkeleton]')?.closest<HTMLElement>('div[class*=HomePage-module]');
$parent && ($parent.style.display = 'none');
}
@@ -194,7 +197,7 @@ window.addEventListener(BxEvent.XCLOUD_SERVERS_UNAVAILABLE, e => {
// Open Settings dialog on Unsupported page
const $unsupportedPage = document.querySelector<HTMLElement>('div[class^=UnsupportedMarketPage-module__container]');
if ($unsupportedPage) {
SettingsNavigationDialog.getInstance().show();
SettingsDialog.getInstance().show();
}
}, {once: true});
@@ -213,35 +216,49 @@ window.addEventListener(BxEvent.STREAM_LOADING, e => {
});
// Setup loading screen
getPref(PrefKey.UI_LOADING_SCREEN_GAME_ART) && window.addEventListener(BxEvent.TITLE_INFO_READY, LoadingScreen.setup);
getPref(PrefKey.LOADING_SCREEN_GAME_ART) && window.addEventListener(BxEvent.TITLE_INFO_READY, LoadingScreen.setup);
window.addEventListener(BxEvent.STREAM_STARTING, e => {
// Hide loading screen
LoadingScreen.hide();
// Start hiding cursor
if (!getPref(PrefKey.MKB_ENABLED) && getPref(PrefKey.MKB_HIDE_IDLE_CURSOR)) {
MouseCursorHider.start();
MouseCursorHider.hide();
if (isFullVersion()) {
// Start hiding cursor
const cursorHider = MouseCursorHider.getInstance();
if (cursorHider) {
cursorHider.start();
cursorHider.hide();
}
}
});
window.addEventListener(BxEvent.STREAM_PLAYING, e => {
window.BX_STREAM_SETTINGS = StreamSettings.settings;
StreamSettings.refreshAllSettings();
STATES.isPlaying = true;
StreamUiHandler.observe();
if (isFullVersion() && getPref(PrefKey.GAME_BAR_POSITION) !== 'off') {
const gameBar = GameBar.getInstance();
gameBar.reset();
gameBar.enable();
gameBar.showBar();
}
if (isFullVersion()) {
const gameBar = GameBar.getInstance();
if (gameBar) {
gameBar.reset();
gameBar.enable();
gameBar.showBar();
}
// Setup Keyboard shortcuts
KeyboardShortcutHandler.getInstance().start();
// Setup screenshot
const $video = (e as any).$video as HTMLVideoElement;
ScreenshotManager.getInstance().updateCanvasSize($video.videoWidth, $video.videoHeight);
// Setup local co-op
getPref(PrefKey.LOCAL_CO_OP_ENABLED) && BxExposed.toggleLocalCoOp(getPref(PrefKey.LOCAL_CO_OP_ENABLED));
}
updateVideoPlayer();
});
@@ -294,9 +311,13 @@ function unload() {
}
if (isFullVersion()) {
KeyboardShortcutHandler.getInstance().stop();
// Stop MKB listeners
EmulatedMkbHandler.getInstance().destroy();
NativeMkbHandler.getInstance().destroy();
EmulatedMkbHandler.getInstance()?.destroy();
NativeMkbHandler.getInstance()?.destroy();
DeviceVibrationManager.getInstance()?.reset();
}
// Destroy StreamPlayer
@@ -312,9 +333,10 @@ function unload() {
StreamBadges.getInstance().destroy();
if (isFullVersion()) {
MouseCursorHider.stop();
MouseCursorHider.getInstance()?.stop();
TouchController.reset();
(getPref(PrefKey.GAME_BAR_POSITION) !== 'off') && GameBar.getInstance().disable();
GameBar.getInstance()?.disable();
}
}
@@ -329,10 +351,15 @@ isFullVersion() && window.addEventListener(BxEvent.CAPTURE_SCREENSHOT, e => {
function main() {
if (getPref(PrefKey.GAME_MSFS2020_FORCE_NATIVE_MKB)) {
BX_FLAGS.ForceNativeMkbTitles.push('9PMQDM08SNK9');
GhPagesUtils.fetchLatestCommit();
if (getPref<NativeMkbMode>(PrefKey.NATIVE_MKB_MODE) === NativeMkbMode.ON) {
const customList = getPref<string[]>(PrefKey.FORCE_NATIVE_MKB_GAMES);
BX_FLAGS.ForceNativeMkbTitles.push(...customList);
}
StreamSettings.setup();
// Monkey patches
patchRtcPeerConnection();
patchRtcCodecs();
@@ -341,7 +368,7 @@ function main() {
patchCanvasContext();
isFullVersion() && AppInterface && patchPointerLockApi();
getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && patchAudioContext();
getPref(PrefKey.AUDIO_VOLUME_CONTROL_ENABLED) && patchAudioContext();
if (getPref(PrefKey.BLOCK_TRACKING)) {
patchMeControl();
@@ -359,10 +386,9 @@ function main() {
StreamStats.setupEvents();
if (isFullVersion()) {
updatePollingRate();
STATES.userAgent.capabilities.touch && TouchController.updateCustomList();
VibrationManager.initialSetup();
DeviceVibrationManager.getInstance();
// Check for Update
BX_FLAGS.CheckForUpdate && checkForUpdate();
@@ -375,7 +401,7 @@ function main() {
RemotePlayManager.detect();
}
if (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === StreamTouchController.ALL) {
if (getPref<TouchControllerMode>(PrefKey.TOUCH_CONTROLLER_MODE) === TouchControllerMode.ALL) {
TouchController.setup();
}
@@ -392,7 +418,7 @@ function main() {
}
// Show a toast when connecting/disconecting controller
if (getPref(PrefKey.CONTROLLER_SHOW_CONNECTION_STATUS)) {
if (getPref(PrefKey.UI_CONTROLLER_SHOW_STATUS)) {
window.addEventListener('gamepadconnected', e => showGamepadToast(e.gamepad));
window.addEventListener('gamepaddisconnected', e => showGamepadToast(e.gamepad));
}

0
src/macros/build.ts Normal file → Executable file
View File

396
src/modules/controller-shortcut.ts Normal file → Executable file
View File

@@ -1,70 +1,29 @@
import { ScreenshotManager } from "@/utils/screenshot-manager";
import { GamepadKey } from "@enums/mkb";
import { PrompFont } from "@enums/prompt-font";
import { CE, removeChildElements } from "@utils/html";
import { t } from "@utils/translation";
import { StreamStats } from "./stream/stream-stats";
import { MicrophoneShortcut } from "./shortcuts/shortcut-microphone";
import { StreamUiShortcut } from "./shortcuts/shortcut-stream-ui";
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";
import { VIRTUAL_GAMEPAD_ID } from "./mkb/mkb-handler";
import { GamepadKey } from "@enums/gamepad";
import { ShortcutHandler } from "@/utils/shortcut-handler";
const enum ShortcutAction {
BETTER_XCLOUD_SETTINGS_SHOW = 'bx-settings-show',
STREAM_SCREENSHOT_CAPTURE = 'stream-screenshot-capture',
STREAM_MENU_SHOW = 'stream-menu-show',
STREAM_STATS_TOGGLE = 'stream-stats-toggle',
STREAM_SOUND_TOGGLE = 'stream-sound-toggle',
STREAM_MICROPHONE_TOGGLE = 'stream-microphone-toggle',
STREAM_VOLUME_INC = 'stream-volume-inc',
STREAM_VOLUME_DEC = 'stream-volume-dec',
DEVICE_SOUND_TOGGLE = 'device-sound-toggle',
DEVICE_VOLUME_INC = 'device-volume-inc',
DEVICE_VOLUME_DEC = 'device-volume-dec',
DEVICE_BRIGHTNESS_INC = 'device-brightness-inc',
DEVICE_BRIGHTNESS_DEC = 'device-brightness-dec',
}
export class ControllerShortcut {
private static readonly STORAGE_KEY = 'better_xcloud_controller_shortcuts';
private static buttonsCache: {[key: string]: boolean[]} = {};
private static buttonsStatus: {[key: string]: boolean[]} = {};
private static $selectProfile: HTMLSelectElement;
private static $selectActions: Partial<{[key in GamepadKey]: HTMLSelectElement}> = {};
private static $container: HTMLElement;
private static ACTIONS: {[key: string]: (ShortcutAction | null)[]} | null = null;
static reset(index: number) {
ControllerShortcut.buttonsCache[index] = [];
ControllerShortcut.buttonsStatus[index] = [];
}
static handle(gamepad: Gamepad): boolean {
if (!ControllerShortcut.ACTIONS) {
ControllerShortcut.ACTIONS = ControllerShortcut.getActionsFromStorage();
const controllerSettings = window.BX_STREAM_SETTINGS.controllers[gamepad.id];
if (!controllerSettings) {
return false;
}
const gamepadIndex = gamepad.index;
const actions = ControllerShortcut.ACTIONS![gamepad.id];
const actions = controllerSettings.shortcuts;
if (!actions) {
return false;
}
const gamepadIndex = gamepad.index;
// Move the buttons status from the previous frame to the cache
ControllerShortcut.buttonsCache[gamepadIndex] = ControllerShortcut.buttonsStatus[gamepadIndex].slice(0);
// Clear the buttons status
@@ -74,7 +33,9 @@ export class ControllerShortcut {
let otherButtonPressed = false;
const entries = gamepad.buttons.entries();
for (const [index, button] of entries) {
let index: GamepadKey;
let button: GamepadButton;
for ([index, button] of entries) {
// Only add the newly pressed button to the array (holding doesn't count)
if (button.pressed && index !== GamepadKey.HOME) {
otherButtonPressed = true;
@@ -82,7 +43,8 @@ export class ControllerShortcut {
// If this is newly pressed button -> run action
if (actions[index] && !ControllerShortcut.buttonsCache[gamepadIndex][index]) {
setTimeout(() => ControllerShortcut.runAction(actions[index]!), 0);
const idx = index;
setTimeout(() => ShortcutHandler.runAction(actions[idx]!), 0);
}
}
};
@@ -90,336 +52,4 @@ export class ControllerShortcut {
ControllerShortcut.buttonsStatus[gamepadIndex] = pressed;
return otherButtonPressed;
}
private static runAction(action: ShortcutAction) {
switch (action) {
case ShortcutAction.BETTER_XCLOUD_SETTINGS_SHOW:
SettingsNavigationDialog.getInstance().show();
break;
case ShortcutAction.STREAM_SCREENSHOT_CAPTURE:
ScreenshotManager.getInstance().takeScreenshot();
break;
case ShortcutAction.STREAM_STATS_TOGGLE:
StreamStats.getInstance().toggle();
break;
case ShortcutAction.STREAM_MICROPHONE_TOGGLE:
MicrophoneShortcut.toggle();
break;
case ShortcutAction.STREAM_MENU_SHOW:
StreamUiShortcut.showHideStreamMenu();
break;
case ShortcutAction.STREAM_SOUND_TOGGLE:
SoundShortcut.muteUnmute();
break;
case ShortcutAction.STREAM_VOLUME_INC:
SoundShortcut.adjustGainNodeVolume(10);
break;
case ShortcutAction.STREAM_VOLUME_DEC:
SoundShortcut.adjustGainNodeVolume(-10);
break;
case ShortcutAction.DEVICE_BRIGHTNESS_INC:
case ShortcutAction.DEVICE_BRIGHTNESS_DEC:
case ShortcutAction.DEVICE_SOUND_TOGGLE:
case ShortcutAction.DEVICE_VOLUME_INC:
case ShortcutAction.DEVICE_VOLUME_DEC:
AppInterface && AppInterface.runShortcut && AppInterface.runShortcut(action);
break;
}
}
private static updateAction(profile: string, button: GamepadKey, action: ShortcutAction | null) {
const actions = ControllerShortcut.ACTIONS!;
if (!(profile in actions)) {
actions[profile] = [];
}
if (!action) {
action = null;
}
actions[profile][button] = action;
// Remove empty profiles
for (const key in ControllerShortcut.ACTIONS) {
let empty = true;
for (const value of ControllerShortcut.ACTIONS[key]) {
if (!!value) {
empty = false;
break;
}
}
if (empty) {
delete ControllerShortcut.ACTIONS[key];
}
}
// Save to storage
window.localStorage.setItem(ControllerShortcut.STORAGE_KEY, JSON.stringify(ControllerShortcut.ACTIONS));
}
private static updateProfileList(e?: GamepadEvent) {
const $select = ControllerShortcut.$selectProfile;
const $container = ControllerShortcut.$container;
const $fragment = document.createDocumentFragment();
// Remove old profiles
removeChildElements($select);
const gamepads = navigator.getGamepads();
let hasGamepad = false;
for (const gamepad of gamepads) {
if (!gamepad || !gamepad.connected) {
continue;
}
// Ignore emulated gamepad
if (gamepad.id === VIRTUAL_GAMEPAD_ID) {
continue;
}
hasGamepad = true;
const $option = CE<HTMLOptionElement>('option', {value: gamepad.id}, gamepad.id);
$fragment.appendChild($option);
}
$container.dataset.hasGamepad = hasGamepad.toString();
if (hasGamepad) {
$select.appendChild($fragment);
$select.selectedIndex = 0;
$select.dispatchEvent(new Event('input'));
}
}
private static switchProfile(profile: string) {
let actions = ControllerShortcut.ACTIONS![profile];
if (!actions) {
actions = [];
}
// Reset selects' values
let button: any;
for (button in ControllerShortcut.$selectActions) {
const $select = ControllerShortcut.$selectActions[button as GamepadKey]!;
$select.value = actions[button] || '';
BxEvent.dispatch($select, 'input', {
ignoreOnChange: true,
manualTrigger: true,
});
}
}
private 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 = ControllerShortcut.getActionsFromStorage();
const buttons: Map<GamepadKey, PrompFont> = new Map();
buttons.set(GamepadKey.Y, PrompFont.Y);
buttons.set(GamepadKey.A, PrompFont.A);
buttons.set(GamepadKey.B, PrompFont.B);
buttons.set(GamepadKey.X, PrompFont.X);
buttons.set(GamepadKey.UP, PrompFont.UP);
buttons.set(GamepadKey.DOWN, PrompFont.DOWN);
buttons.set(GamepadKey.LEFT, PrompFont.LEFT);
buttons.set(GamepadKey.RIGHT, PrompFont.RIGHT);
buttons.set(GamepadKey.SELECT, PrompFont.SELECT);
buttons.set(GamepadKey.START, PrompFont.START);
buttons.set(GamepadKey.LB, PrompFont.LB);
buttons.set(GamepadKey.RB, PrompFont.RB);
buttons.set(GamepadKey.LT, PrompFont.LT);
buttons.set(GamepadKey.RT, PrompFont.RT);
buttons.set(GamepadKey.L3, PrompFont.L3);
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')],
[ShortcutAction.DEVICE_VOLUME_DEC]: [t('volume'), t('decrease')],
[ShortcutAction.DEVICE_BRIGHTNESS_INC]: [t('brightness'), t('increase')],
[ShortcutAction.DEVICE_BRIGHTNESS_DEC]: [t('brightness'), t('decrease')],
},
[t('stream')]: {
[ShortcutAction.STREAM_SCREENSHOT_CAPTURE]: t('take-screenshot'),
[ShortcutAction.STREAM_SOUND_TOGGLE]: [t('sound'), t('toggle')],
[ShortcutAction.STREAM_VOLUME_INC]: getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && [t('volume'), t('increase')],
[ShortcutAction.STREAM_VOLUME_DEC]: getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && [t('volume'), t('decrease')],
[ShortcutAction.STREAM_MENU_SHOW]: [t('menu'), t('show')],
[ShortcutAction.STREAM_STATS_TOGGLE]: [t('stats'), t('show-hide')],
[ShortcutAction.STREAM_MICROPHONE_TOGGLE]: [t('microphone'), t('toggle')],
},
};
const $baseSelect = CE<HTMLSelectElement>('select', {autocomplete: 'off'}, CE('option', {value: ''}, '---'));
for (const groupLabel in actions) {
const items = actions[groupLabel];
if (!items) {
continue;
}
const $optGroup = CE<HTMLOptGroupElement>('optgroup', {'label': groupLabel});
for (const action in items) {
let label = items[action as keyof typeof items];
if (!label) {
continue;
}
if (Array.isArray(label)) {
label = label.join(' ');
}
const $option = CE<HTMLOptionElement>('option', {value: action}, label);
$optGroup.appendChild($option);
}
$baseSelect.appendChild($optGroup);
}
let $remap: HTMLElement;
const $selectProfile = CE<HTMLSelectElement>('select', {class: 'bx-shortcut-profile', autocomplete: 'off'});
const $profile = PREF_CONTROLLER_FRIENDLY_UI ? BxSelectElement.wrap($selectProfile) : $selectProfile;
$profile.classList.add('bx-full-width');
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', {},
CE('div', {
_nearby: {
focus: $profile,
},
}, $profile),
CE('p', {class: 'bx-shortcut-note'},
CE('span', {class: 'bx-prompt'}, PrompFont.HOME),
': ' + t('controller-shortcuts-xbox-note'),
),
),
);
$selectProfile.addEventListener('input', e => {
ControllerShortcut.switchProfile($selectProfile.value);
});
const onActionChanged = (e: Event) => {
const $target = e.target as HTMLSelectElement;
const profile = $selectProfile.value;
const button: unknown = $target.dataset.button;
const action = $target.value as ShortcutAction;
if (!PREF_CONTROLLER_FRIENDLY_UI) {
const $fakeSelect = $target.previousElementSibling! as HTMLSelectElement;
let fakeText = '---';
if (action) {
const $selectedOption = $target.options[$target.selectedIndex];
const $optGroup = $selectedOption.parentElement as HTMLOptGroupElement;
fakeText = $optGroup.label + ' ' + $selectedOption.text;
}
($fakeSelect.firstElementChild as HTMLOptionElement).text = fakeText;
}
!(e as any).ignoreOnChange && ControllerShortcut.updateAction(profile, button as GamepadKey, action);
};
// @ts-ignore
for (const [button, prompt] of buttons) {
const $row = CE('div', {
class: 'bx-shortcut-row',
});
const $label = CE('label', {class: 'bx-prompt'}, `${PrompFont.HOME} + ${prompt}`);
const $div = CE('div', {class: 'bx-shortcut-actions'});
if (!PREF_CONTROLLER_FRIENDLY_UI) {
const $fakeSelect = CE<HTMLSelectElement>('select', {autocomplete: 'off'},
CE('option', {}, '---'),
);
$div.appendChild($fakeSelect);
}
const $select = $baseSelect.cloneNode(true) as HTMLSelectElement;
$select.dataset.button = button.toString();
$select.addEventListener('input', onActionChanged);
ControllerShortcut.$selectActions[button] = $select;
if (PREF_CONTROLLER_FRIENDLY_UI) {
const $bxSelect = BxSelectElement.wrap($select);
$bxSelect.classList.add('bx-full-width');
$div.appendChild($bxSelect);
setNearby($row, {
focus: $bxSelect,
});
} else {
$div.appendChild($select);
setNearby($row, {
focus: $select,
});
}
$row.appendChild($label);
$row.appendChild($div);
$remap.appendChild($row);
}
$container.appendChild($remap);
ControllerShortcut.$selectProfile = $selectProfile;
ControllerShortcut.$container = $container;
// Detect when gamepad connected/disconnect
window.addEventListener('gamepadconnected', ControllerShortcut.updateProfileList);
window.addEventListener('gamepaddisconnected', ControllerShortcut.updateProfileList);
ControllerShortcut.updateProfileList();
return $container;
}
}

View File

@@ -0,0 +1,145 @@
import { AppInterface, STATES } from "@utils/global";
import { BxEvent } from "@utils/bx-event";
import { StreamSettings } from "@/utils/stream-settings";
const VIBRATION_DATA_MAP = {
gamepadIndex: 8,
leftMotorPercent: 8,
rightMotorPercent: 8,
leftTriggerMotorPercent: 8,
rightTriggerMotorPercent: 8,
durationMs: 16,
// delayMs: 16,
// repeat: 8,
};
type VibrationData = {
[key in keyof typeof VIBRATION_DATA_MAP]?: number;
}
export class DeviceVibrationManager {
private static instance: DeviceVibrationManager | null | undefined;
public static getInstance(): typeof DeviceVibrationManager['instance'] {
if (typeof DeviceVibrationManager.instance === 'undefined') {
if (STATES.browser.capabilities.deviceVibration) {
DeviceVibrationManager.instance = new DeviceVibrationManager();
} else {
DeviceVibrationManager.instance = null;
}
}
return DeviceVibrationManager.instance;
}
private dataChannel: RTCDataChannel | null = null;
private boundOnMessage: (e: MessageEvent) => void;
constructor() {
this.boundOnMessage = this.onMessage.bind(this);
window.addEventListener(BxEvent.DATA_CHANNEL_CREATED, e => {
const dataChannel = (e as any).dataChannel as RTCDataChannel;
if (dataChannel?.label === 'input') {
this.reset();
this.dataChannel = dataChannel;
this.setupDataChannel();
}
});
window.addEventListener(BxEvent.DEVICE_VIBRATION_CHANGED, e => {
this.setupDataChannel();
});
}
private setupDataChannel() {
if (!this.dataChannel) {
return;
}
this.removeEventListeners();
if (window.BX_STREAM_SETTINGS.deviceVibrationIntensity > 0) {
this.dataChannel.addEventListener('message', this.boundOnMessage);
}
}
private playVibration(data: Required<VibrationData>) {
const vibrationIntensity = StreamSettings.settings.deviceVibrationIntensity;
if (AppInterface) {
AppInterface.vibrate(JSON.stringify(data), vibrationIntensity);
return;
}
const realIntensity = Math.min(100, data.leftMotorPercent + data.rightMotorPercent / 2) * vibrationIntensity;
if (realIntensity === 0 || realIntensity === 100) {
// Stop vibration
window.navigator.vibrate(realIntensity ? data.durationMs : 0);
return;
}
const pulseDuration = 200;
const onDuration = Math.floor(pulseDuration * realIntensity / 100);
const offDuration = pulseDuration - onDuration;
const repeats = Math.ceil(data.durationMs / pulseDuration);
const pulses = Array(repeats).fill([onDuration, offDuration]).flat();
window.navigator.vibrate(pulses);
}
onMessage(e: MessageEvent) {
if (typeof e !== 'object' || !(e.data instanceof ArrayBuffer)) {
return;
}
const dataView = new DataView(e.data);
let offset = 0;
let messageType;
if (dataView.byteLength === 13) { // version >= 8
messageType = dataView.getUint16(offset, true);
offset += Uint16Array.BYTES_PER_ELEMENT;
} else {
messageType = dataView.getUint8(offset);
offset += Uint8Array.BYTES_PER_ELEMENT;
}
if (!(messageType & 128)) { // Vibration
return;
}
const vibrationType = dataView.getUint8(offset);
offset += Uint8Array.BYTES_PER_ELEMENT;
if (vibrationType !== 0) { // FourMotorRumble
return;
}
const data: VibrationData = {};
let key: keyof typeof VIBRATION_DATA_MAP;
for (key in VIBRATION_DATA_MAP) {
if (VIBRATION_DATA_MAP[key] === 16) {
data[key] = dataView.getUint16(offset, true);
offset += Uint16Array.BYTES_PER_ELEMENT;
} else {
data[key] = dataView.getUint8(offset);
offset += Uint8Array.BYTES_PER_ELEMENT;
}
}
this.playVibration(data as Required<VibrationData>);
}
private removeEventListeners() {
// Clear event listeners in previous DataChannel
try {
this.dataChannel?.removeEventListener('message', this.boundOnMessage);
} catch (e) {}
}
reset() {
this.removeEventListeners();
this.dataChannel = null;
}
}

View File

@@ -1,102 +0,0 @@
import { t } from "@utils/translation";
import { CE, createButton, ButtonStyle } from "@utils/html";
import { BxIcon } from "@utils/bx-icon";
type DialogOptions = Partial<{
title: string;
className: string;
content: string | HTMLElement;
hideCloseButton: boolean;
onClose: string;
helpUrl: string;
}>;
export class Dialog {
$dialog: HTMLElement;
$title: HTMLElement;
$content: HTMLElement;
$overlay: HTMLElement;
onClose: any;
constructor(options: DialogOptions) {
const {
title,
className,
content,
hideCloseButton,
onClose,
helpUrl,
} = options;
// Create dialog overlay
const $overlay = document.querySelector<HTMLElement>('.bx-dialog-overlay');
if (!$overlay) {
this.$overlay = CE('div', {'class': 'bx-dialog-overlay bx-gone'});
// Disable right click
this.$overlay.addEventListener('contextmenu', e => e.preventDefault());
document.documentElement.appendChild(this.$overlay);
} else {
this.$overlay = $overlay;
}
let $close;
this.onClose = onClose;
this.$dialog = CE('div', {'class': `bx-dialog ${className || ''} bx-gone`},
this.$title = CE('h2', {}, CE('b', {}, title),
helpUrl && createButton({
icon: BxIcon.QUESTION,
style: ButtonStyle.GHOST,
title: t('help'),
url: helpUrl,
}),
),
this.$content = CE('div', {'class': 'bx-dialog-content'}, content),
!hideCloseButton && ($close = CE('button', {type: 'button'}, t('close'))),
);
$close && $close.addEventListener('click', e => {
this.hide(e);
});
!title && this.$title.classList.add('bx-gone');
!content && this.$content.classList.add('bx-gone');
// Disable right click
this.$dialog.addEventListener('contextmenu', e => e.preventDefault());
document.documentElement.appendChild(this.$dialog);
}
show(newOptions: DialogOptions) {
// Clear focus
document.activeElement && (document.activeElement as HTMLElement).blur();
if (newOptions && newOptions.title) {
this.$title.querySelector('b')!.textContent = newOptions.title;
this.$title.classList.remove('bx-gone');
}
this.$dialog.classList.remove('bx-gone');
this.$overlay.classList.remove('bx-gone');
document.body.classList.add('bx-no-scroll');
}
hide(e?: any) {
this.$dialog.classList.add('bx-gone');
this.$overlay.classList.add('bx-gone');
document.body.classList.remove('bx-no-scroll');
this.onClose && this.onClose(e);
}
toggle() {
this.$dialog.classList.toggle('bx-gone');
this.$overlay.classList.toggle('bx-gone');
}
}

53
src/modules/game-bar/game-bar.ts Normal file → Executable file
View File

@@ -1,22 +1,34 @@
import { CE, createSvgIcon } from "@utils/html";
import { ScreenshotAction } from "./action-screenshot";
import { TouchControlAction } from "./action-touch-control";
import { ScreenshotAction } from "./screenshot-action";
import { TouchControlAction } from "./touch-control-action";
import { BxEvent } from "@utils/bx-event";
import { BxIcon } from "@utils/bx-icon";
import type { BaseGameBarAction } from "./action-base";
import type { BaseGameBarAction } from "./base-action";
import { STATES } from "@utils/global";
import { MicrophoneAction } from "./action-microphone";
import { MicrophoneAction } from "./microphone-action";
import { PrefKey } from "@/enums/pref-keys";
import { getPref, StreamTouchController, type GameBarPosition } from "@/utils/settings-storages/global-settings-storage";
import { TrueAchievementsAction } from "./action-true-achievements";
import { SpeakerAction } from "./action-speaker";
import { RendererAction } from "./action-renderer";
import { getPref } from "@/utils/settings-storages/global-settings-storage";
import { TrueAchievementsAction } from "./true-achievements-action";
import { SpeakerAction } from "./speaker-action";
import { RendererAction } from "./renderer-action";
import { BxLogger } from "@/utils/bx-logger";
import { GameBarPosition, TouchControllerMode } from "@/enums/pref-values";
export class GameBar {
private static instance: GameBar;
public static getInstance = () => GameBar.instance ?? (GameBar.instance = new GameBar());
private static instance: GameBar | null | undefined;
public static getInstance(): typeof GameBar['instance'] {
if (typeof GameBar.instance === 'undefined') {
if (getPref<GameBarPosition>(PrefKey.GAME_BAR_POSITION) !== GameBarPosition.OFF) {
GameBar.instance = new GameBar();
} else {
GameBar.instance = null;
}
}
return GameBar.instance;
}
private readonly LOG_TAG = 'GameBar';
private static readonly VISIBLE_DURATION = 2000;
@@ -33,7 +45,7 @@ export class GameBar {
let $container;
const position = getPref(PrefKey.GAME_BAR_POSITION) as GameBarPosition;
const position = getPref<GameBarPosition>(PrefKey.GAME_BAR_POSITION);
const $gameBar = CE('div', {id: 'bx-game-bar', class: 'bx-gone', 'data-position': position},
$container = CE('div', {class: 'bx-game-bar-container bx-offscreen'}),
@@ -42,7 +54,7 @@ export class GameBar {
this.actions = [
new ScreenshotAction(),
...(STATES.userAgent.capabilities.touch && (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) !== StreamTouchController.OFF) ? [new TouchControlAction()] : []),
...(STATES.userAgent.capabilities.touch && (getPref<TouchControllerMode>(PrefKey.TOUCH_CONTROLLER_MODE) !== TouchControllerMode.OFF) ? [new TouchControlAction()] : []),
new SpeakerAction(),
new RendererAction(),
new MicrophoneAction(),
@@ -69,10 +81,10 @@ export class GameBar {
});
// Hide game bar after clicking on an action
window.addEventListener(BxEvent.GAME_BAR_ACTION_ACTIVATED, this.hideBar.bind(this));
window.addEventListener(BxEvent.GAME_BAR_ACTION_ACTIVATED, this.hideBar);
$container.addEventListener('pointerover', this.clearHideTimeout.bind(this));
$container.addEventListener('pointerout', this.beginHideTimeout.bind(this));
$container.addEventListener('pointerover', this.clearHideTimeout);
$container.addEventListener('pointerout', this.beginHideTimeout);
// Add animation when hiding game bar
$container.addEventListener('transitionend', e => {
@@ -84,16 +96,15 @@ export class GameBar {
this.$container = $container;
// Enable/disable Game Bar when playing/pausing
getPref(PrefKey.GAME_BAR_POSITION) !== 'off' && window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, ((e: Event) => {
position !== GameBarPosition.OFF && window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, ((e: Event) => {
// Toggle Game bar
if (STATES.isPlaying) {
const mode = (e as any).mode;
mode !== 'none' ? this.disable() : this.enable();
window.BX_STREAM_SETTINGS.xCloudPollingMode !== 'none' ? this.disable() : this.enable();
}
}).bind(this));
}
private beginHideTimeout() {
private beginHideTimeout = () => {
this.clearHideTimeout();
this.timeoutId = window.setTimeout(() => {
@@ -102,7 +113,7 @@ export class GameBar {
}, GameBar.VISIBLE_DURATION);
}
private clearHideTimeout() {
private clearHideTimeout = () => {
this.timeoutId && clearTimeout(this.timeoutId);
this.timeoutId = null;
}
@@ -123,7 +134,7 @@ export class GameBar {
this.beginHideTimeout();
}
hideBar() {
hideBar = () => {
this.clearHideTimeout();
this.$container.classList.replace('bx-show', 'bx-hide');
}

View File

@@ -1,8 +1,8 @@
import { BxEvent } from "@utils/bx-event";
import { BxIcon } from "@utils/bx-icon";
import { createButton, ButtonStyle, CE } from "@utils/html";
import { BaseGameBarAction } from "./action-base";
import { MicrophoneShortcut, MicrophoneState } from "../shortcuts/shortcut-microphone";
import { BaseGameBarAction } from "./base-action";
import { MicrophoneShortcut, MicrophoneState } from "../shortcuts/microphone-shortcut";
export class MicrophoneAction extends BaseGameBarAction {
@@ -14,14 +14,14 @@ export class MicrophoneAction extends BaseGameBarAction {
const $btnDefault = createButton({
style: ButtonStyle.GHOST,
icon: BxIcon.MICROPHONE,
onClick: this.onClick.bind(this),
onClick: this.onClick,
classes: ['bx-activated'],
});
const $btnMuted = createButton({
style: ButtonStyle.GHOST,
icon: BxIcon.MICROPHONE_MUTED,
onClick: this.onClick.bind(this),
onClick: this.onClick,
});
this.$content = CE('div', {}, $btnMuted, $btnDefault);
@@ -36,7 +36,7 @@ export class MicrophoneAction extends BaseGameBarAction {
});
}
onClick(e: Event) {
onClick = (e: Event) => {
super.onClick(e);
const enabled = MicrophoneShortcut.toggle(false);
this.$content.dataset.activated = enabled.toString();

View File

@@ -1,7 +1,8 @@
import { BxIcon } from "@utils/bx-icon";
import { createButton, ButtonStyle, CE } from "@utils/html";
import { BaseGameBarAction } from "./action-base";
import { RendererShortcut } from "../shortcuts/shortcut-renderer";
import { BaseGameBarAction } from "./base-action";
import { RendererShortcut } from "../shortcuts/renderer-shortcut";
import { BxEvent } from "@/utils/bx-event";
export class RendererAction extends BaseGameBarAction {
@@ -13,23 +14,27 @@ export class RendererAction extends BaseGameBarAction {
const $btnDefault = createButton({
style: ButtonStyle.GHOST,
icon: BxIcon.EYE,
onClick: this.onClick.bind(this),
onClick: this.onClick,
});
const $btnActivated = createButton({
style: ButtonStyle.GHOST,
icon: BxIcon.EYE_SLASH,
onClick: this.onClick.bind(this),
onClick: this.onClick,
classes: ['bx-activated'],
});
this.$content = CE('div', {}, $btnDefault, $btnActivated);
window.addEventListener(BxEvent.VIDEO_VISIBILITY_CHANGED, e => {
const isShowing = (e as any).isShowing;
this.$content.dataset.activated = (!isShowing).toString();
});
}
onClick(e: Event) {
onClick = (e: Event) => {
super.onClick(e);
const isVisible = RendererShortcut.toggleVisibility();
this.$content.dataset.activated = (!isVisible).toString();
RendererShortcut.toggleVisibility();
}
reset(): void {

View File

@@ -1,6 +1,6 @@
import { BxIcon } from "@utils/bx-icon";
import { createButton, ButtonStyle } from "@utils/html";
import { BaseGameBarAction } from "./action-base";
import { BaseGameBarAction } from "./base-action";
import { t } from "@utils/translation";
import { ScreenshotManager } from "@/utils/screenshot-manager";
@@ -14,11 +14,11 @@ export class ScreenshotAction extends BaseGameBarAction {
style: ButtonStyle.GHOST,
icon: BxIcon.SCREENSHOT,
title: t('take-screenshot'),
onClick: this.onClick.bind(this),
onClick: this.onClick,
});
}
onClick(e: Event): void {
onClick = (e: Event) => {
super.onClick(e);
ScreenshotManager.getInstance().takeScreenshot();
}

View File

@@ -1,8 +1,8 @@
import { BxEvent } from "@utils/bx-event";
import { BxIcon } from "@utils/bx-icon";
import { createButton, ButtonStyle, CE } from "@utils/html";
import { BaseGameBarAction } from "./action-base";
import { SoundShortcut, SpeakerState } from "../shortcuts/shortcut-sound";
import { BaseGameBarAction } from "./base-action";
import { SoundShortcut, SpeakerState } from "../shortcuts/sound-shortcut";
export class SpeakerAction extends BaseGameBarAction {
@@ -14,13 +14,13 @@ export class SpeakerAction extends BaseGameBarAction {
const $btnEnable = createButton({
style: ButtonStyle.GHOST,
icon: BxIcon.AUDIO,
onClick: this.onClick.bind(this),
onClick: this.onClick,
});
const $btnMuted = createButton({
style: ButtonStyle.GHOST,
icon: BxIcon.SPEAKER_MUTED,
onClick: this.onClick.bind(this),
onClick: this.onClick,
classes: ['bx-activated'],
});
@@ -34,7 +34,7 @@ export class SpeakerAction extends BaseGameBarAction {
});
}
onClick(e: Event) {
onClick = (e: Event) => {
super.onClick(e);
SoundShortcut.muteUnmute();
}

View File

@@ -1,7 +1,7 @@
import { BxIcon } from "@utils/bx-icon";
import { createButton, ButtonStyle, CE } from "@utils/html";
import { TouchController } from "@modules/touch-controller";
import { BaseGameBarAction } from "./action-base";
import { BaseGameBarAction } from "./base-action";
import { t } from "@utils/translation";
export class TouchControlAction extends BaseGameBarAction {
@@ -14,21 +14,21 @@ export class TouchControlAction extends BaseGameBarAction {
style: ButtonStyle.GHOST,
icon: BxIcon.TOUCH_CONTROL_ENABLE,
title: t('show-touch-controller'),
onClick: this.onClick.bind(this),
onClick: this.onClick,
});
const $btnDisable = createButton({
style: ButtonStyle.GHOST,
icon: BxIcon.TOUCH_CONTROL_DISABLE,
title: t('hide-touch-controller'),
onClick: this.onClick.bind(this),
onClick: this.onClick,
classes: ['bx-activated'],
});
this.$content = CE('div', {}, $btnEnable, $btnDisable);
}
onClick(e: Event) {
onClick = (e: Event) => {
super.onClick(e);
const isVisible = TouchController.toggleVisibility();
this.$content.dataset.activated = (!isVisible).toString();

View File

@@ -1,6 +1,6 @@
import { BxIcon } from "@/utils/bx-icon";
import { createButton, ButtonStyle } from "@/utils/html";
import { BaseGameBarAction } from "./action-base";
import { BaseGameBarAction } from "./base-action";
import { TrueAchievements } from "@/utils/true-achievements";
export class TrueAchievementsAction extends BaseGameBarAction {
@@ -12,11 +12,11 @@ export class TrueAchievementsAction extends BaseGameBarAction {
this.$content = createButton({
style: ButtonStyle.GHOST,
icon: BxIcon.TRUE_ACHIEVEMENTS,
onClick: this.onClick.bind(this),
onClick: this.onClick,
});
}
onClick(e: Event) {
onClick = (e: Event) => {
super.onClick(e);
TrueAchievements.getInstance().open(false);
}

23
src/modules/loading-screen.ts Normal file → Executable file
View File

@@ -5,6 +5,7 @@ import { STATES } from "@utils/global";
import { PrefKey } from "@/enums/pref-keys";
import { getPref } from "@/utils/settings-storages/global-settings-storage";
import { compressCss } from "@macros/build" with {type: "macro"};
import { LoadingScreenRocket } from "@/enums/pref-values";
export class LoadingScreen {
private static $bgStyle: HTMLElement;
@@ -36,7 +37,7 @@ export class LoadingScreen {
LoadingScreen.setBackground(titleInfo.product.heroImageUrl || titleInfo.product.titledHeroImageUrl || titleInfo.product.tileImageUrl);
if (getPref(PrefKey.UI_LOADING_SCREEN_ROCKET) === 'hide') {
if (getPref<LoadingScreenRocket>(PrefKey.LOADING_SCREEN_ROCKET) === LoadingScreenRocket.HIDE) {
LoadingScreen.hideRocket();
}
}
@@ -88,7 +89,7 @@ export class LoadingScreen {
static setupWaitTime(waitTime: number) {
// Hide rocket when queing
if (getPref(PrefKey.UI_LOADING_SCREEN_ROCKET) === 'hide-queue') {
if (getPref<LoadingScreenRocket>(PrefKey.LOADING_SCREEN_ROCKET) === LoadingScreenRocket.HIDE_QUEUE) {
LoadingScreen.hideRocket();
}
@@ -108,14 +109,14 @@ export class LoadingScreen {
let $waitTimeBox = LoadingScreen.$waitTimeBox;
if (!$waitTimeBox) {
$waitTimeBox = CE('div', {'class': 'bx-wait-time-box'},
CE('label', {}, t('server')),
CE('span', {}, getPreferredServerRegion()),
CE('label', {}, t('wait-time-estimated')),
$estimated = CE('span', {}),
CE('label', {}, t('wait-time-countdown')),
$countDown = CE('span', {}),
);
$waitTimeBox = CE('div', { class: 'bx-wait-time-box' },
CE('label', {}, t('server')),
CE('span', {}, getPreferredServerRegion()),
CE('label', {}, t('wait-time-estimated')),
$estimated = CE('span', {}),
CE('label', {}, t('wait-time-countdown')),
$countDown = CE('span', {}),
);
document.documentElement.appendChild($waitTimeBox);
LoadingScreen.$waitTimeBox = $waitTimeBox;
@@ -145,7 +146,7 @@ export class LoadingScreen {
LoadingScreen.orgWebTitle && (document.title = LoadingScreen.orgWebTitle);
LoadingScreen.$waitTimeBox && LoadingScreen.$waitTimeBox.classList.add('bx-gone');
if (getPref(PrefKey.UI_LOADING_SCREEN_GAME_ART) && LoadingScreen.$bgStyle) {
if (getPref(PrefKey.LOADING_SCREEN_GAME_ART) && LoadingScreen.$bgStyle) {
const $rocketBg = document.querySelector('#game-stream rect[width="800"]');
$rocketBg && $rocketBg.addEventListener('transitionend', e => {
LoadingScreen.$bgStyle.textContent += compressCss(`

6
src/modules/mkb/base-mkb-handler.ts Normal file → Executable file
View File

@@ -4,10 +4,11 @@ export abstract class MouseDataProvider {
this.mkbHandler = handler;
}
abstract init(): void;
init() {};
destroy() {};
abstract start(): void;
abstract stop(): void;
abstract destroy(): void;
}
export abstract class MkbHandler {
@@ -15,6 +16,7 @@ export abstract class MkbHandler {
abstract start(): void;
abstract stop(): void;
abstract destroy(): void;
abstract toggle(force: boolean): void;
abstract handleMouseMove(data: MkbMouseMove): void;
abstract handleMouseClick(data: MkbMouseClick): void;
abstract handleMouseWheel(data: MkbMouseWheel): boolean;

113
src/modules/mkb/key-helper.ts Normal file → Executable file
View File

@@ -1,8 +1,36 @@
import { MouseButtonCode, WheelCode } from "@enums/mkb";
import { MouseButtonCode, WheelCode, type KeyCode } from "@/enums/mkb";
export const enum KeyModifier {
CTRL = 1,
SHIFT = 2,
ALT = 4,
};
export type KeyEventInfo = {
code: KeyCode | MouseButtonCode | WheelCode;
modifiers?: number;
};
export class KeyHelper {
static #NON_PRINTABLE_KEYS = {
'Backquote': '`',
private static readonly NON_PRINTABLE_KEYS = {
Backquote: '`',
Minus: '-',
Equal: '=',
BracketLeft: '[',
BracketRight: ']',
Backslash: '\\',
Semicolon: ';',
Quote: '\'',
Comma: ',',
Period: '.',
Slash: '/',
NumpadMultiply: 'Numpad *',
NumpadAdd: 'Numpad +',
NumpadSubtract: 'Numpad -',
NumpadDecimal: 'Numpad .',
NumpadDivide: 'Numpad /',
NumpadEqual: 'Numpad =',
// Mouse buttons
[MouseButtonCode.LEFT_CLICK]: 'Left Click',
@@ -15,12 +43,19 @@ export class KeyHelper {
[WheelCode.SCROLL_RIGHT]: 'Scroll Right',
};
static getKeyFromEvent(e: Event) {
let code;
let name;
static getKeyFromEvent(e: Event): KeyEventInfo | null {
let code: KeyEventInfo['code'] | null = null;
let modifiers;
if (e instanceof KeyboardEvent) {
code = e.code || e.key;
code = (e.code || e.key) as KeyCode;
// Modifiers
modifiers = 0;
modifiers ^= e.ctrlKey ? KeyModifier.CTRL : 0;
modifiers ^= e.shiftKey ? KeyModifier.SHIFT : 0;
modifiers ^= e.altKey ? KeyModifier.ALT : 0;
} else if (e instanceof WheelEvent) {
if (e.deltaY < 0) {
code = WheelCode.SCROLL_UP;
@@ -32,20 +67,47 @@ export class KeyHelper {
code = WheelCode.SCROLL_RIGHT;
}
} else if (e instanceof MouseEvent) {
code = 'Mouse' + e.button;
code = 'Mouse' + e.button as MouseButtonCode;
}
if (code) {
name = KeyHelper.codeToKeyName(code);
const results: KeyEventInfo = { code };
if (modifiers) {
results.modifiers = modifiers;
}
return results;
}
return code ? {code, name} : null;
return null;
}
static codeToKeyName(code: string) {
return (
// @ts-ignore
KeyHelper.#NON_PRINTABLE_KEYS[code]
static getFullKeyCodeFromEvent(e: KeyboardEvent): string {
const key = KeyHelper.getKeyFromEvent(e);
return key ? `${key.code}:${key.modifiers || 0}` : '';
}
static parseFullKeyCode(str: string | undefined | null): KeyEventInfo | null {
if (!str) {
return null;
}
const tmp = str.split(':');
const code = tmp[0] as KeyEventInfo['code'];
const modifiers = parseInt(tmp[1]);
return {
code,
modifiers,
} as KeyEventInfo;
}
static codeToKeyName(key: KeyEventInfo): string {
const { code, modifiers } = key;
const text = [(
KeyHelper.NON_PRINTABLE_KEYS[code as keyof typeof KeyHelper.NON_PRINTABLE_KEYS]
||
(code.startsWith('Key') && code.substring(3))
||
@@ -62,6 +124,27 @@ export class KeyHelper {
(code.endsWith('Right') && ('Right ' + code.replace('Right', '')))
||
code
);
)];
if (modifiers && modifiers !== 0) {
if (!code.startsWith('Control') && !code.startsWith('Shift') && !code.startsWith('Alt')) {
// Shift
if (modifiers & KeyModifier.SHIFT) {
text.unshift('Shift');
}
// Alt
if (modifiers & KeyModifier.ALT) {
text.unshift('Alt');
}
// Ctrl
if (modifiers & KeyModifier.CTRL) {
text.unshift('Ctrl');
}
}
}
return text.join(' + ');
}
}

View File

@@ -0,0 +1,40 @@
import { ShortcutHandler } from "@/utils/shortcut-handler";
import { KeyHelper } from "./key-helper";
export class KeyboardShortcutHandler {
private static instance: KeyboardShortcutHandler;
public static getInstance = () => KeyboardShortcutHandler.instance ?? (KeyboardShortcutHandler.instance = new KeyboardShortcutHandler());
start() {
window.addEventListener('keydown', this.onKeyDown);
}
stop() {
window.removeEventListener('keydown', this.onKeyDown);
}
onKeyDown = (e: KeyboardEvent) => {
// Don't run when the stream is not being focused
if (window.BX_STREAM_SETTINGS.xCloudPollingMode !== 'none') {
return;
}
// Don't activate repeated key
if (e.repeat) {
return;
}
// Check unknown key
const fullKeyCode = KeyHelper.getFullKeyCodeFromEvent(e);
if (!fullKeyCode) {
return;
}
const action = window.BX_STREAM_SETTINGS.keyboardShortcuts?.[fullKeyCode];
if (action) {
e.preventDefault();
e.stopPropagation();
ShortcutHandler.runAction(action);
}
}
}

551
src/modules/mkb/mkb-handler.ts Normal file → Executable file
View File

@@ -1,24 +1,21 @@
import { isFullVersion } from "@macros/build" with {type: "macro"};
import { MkbPreset } from "./mkb-preset";
import { GamepadKey, MkbPresetKey, GamepadStick, MouseMapTo, WheelCode } from "@enums/mkb";
import { createButton, ButtonStyle, CE } from "@utils/html";
import { MkbPresetKey, MouseConstant, MouseMapTo, WheelCode } from "@/enums/mkb";
import { BxEvent } from "@utils/bx-event";
import { Toast } from "@utils/toast";
import { t } from "@utils/translation";
import { KeyHelper } from "./key-helper";
import type { MkbStoredPreset } from "@/types/mkb";
import { AppInterface, STATES } from "@utils/global";
import { UserAgent } from "@utils/user-agent";
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 { 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";
import { MkbPresetsDb } from "@/utils/local-db/mkb-presets-db";
import { GamepadKey, GamepadStick } from "@/enums/gamepad";
import { MkbPopup } from "./mkb-popup";
import type { MkbConvertedPresetData } from "@/types/presets";
const PointerToMouseButton = {
1: 0,
@@ -26,79 +23,74 @@ const PointerToMouseButton = {
4: 1,
}
export const VIRTUAL_GAMEPAD_ID = 'Xbox 360 Controller';
export const VIRTUAL_GAMEPAD_ID = 'Better xCloud Virtual Controller';
class WebSocketMouseDataProvider extends MouseDataProvider {
#pointerClient: PointerClient | undefined
#connected = false
private pointerClient: PointerClient | undefined
private isConnected = false
init(): void {
this.#pointerClient = PointerClient.getInstance();
this.#connected = false;
this.pointerClient = PointerClient.getInstance();
this.isConnected = false;
try {
this.#pointerClient.start(STATES.pointerServerPort, this.mkbHandler);
this.#connected = true;
this.pointerClient.start(STATES.pointerServerPort, this.mkbHandler);
this.isConnected = true;
} catch (e) {
Toast.show('Cannot enable Mouse & Keyboard feature');
}
}
start(): void {
this.#connected && AppInterface.requestPointerCapture();
this.isConnected && AppInterface.requestPointerCapture();
}
stop(): void {
this.#connected && AppInterface.releasePointerCapture();
this.isConnected && AppInterface.releasePointerCapture();
}
destroy(): void {
this.#connected && this.#pointerClient?.stop();
this.isConnected && this.pointerClient?.stop();
}
}
class PointerLockMouseDataProvider extends MouseDataProvider {
init(): void {}
start(): void {
window.addEventListener('mousemove', this.#onMouseMoveEvent);
window.addEventListener('mousedown', this.#onMouseEvent);
window.addEventListener('mouseup', this.#onMouseEvent);
window.addEventListener('wheel', this.#onWheelEvent, {passive: false});
window.addEventListener('contextmenu', this.#disableContextMenu);
start() {
window.addEventListener('mousemove', this.onMouseMoveEvent);
window.addEventListener('mousedown', this.onMouseEvent);
window.addEventListener('mouseup', this.onMouseEvent);
window.addEventListener('wheel', this.onWheelEvent, { passive: false });
window.addEventListener('contextmenu', this.disableContextMenu);
}
stop(): void {
stop() {
document.pointerLockElement && document.exitPointerLock();
window.removeEventListener('mousemove', this.#onMouseMoveEvent);
window.removeEventListener('mousedown', this.#onMouseEvent);
window.removeEventListener('mouseup', this.#onMouseEvent);
window.removeEventListener('wheel', this.#onWheelEvent);
window.removeEventListener('contextmenu', this.#disableContextMenu);
window.removeEventListener('mousemove', this.onMouseMoveEvent);
window.removeEventListener('mousedown', this.onMouseEvent);
window.removeEventListener('mouseup', this.onMouseEvent);
window.removeEventListener('wheel', this.onWheelEvent);
window.removeEventListener('contextmenu', this.disableContextMenu);
}
destroy(): void {}
#onMouseMoveEvent = (e: MouseEvent) => {
private onMouseMoveEvent = (e: MouseEvent) => {
this.mkbHandler.handleMouseMove({
movementX: e.movementX,
movementY: e.movementY,
});
}
#onMouseEvent = (e: MouseEvent) => {
private onMouseEvent = (e: MouseEvent) => {
e.preventDefault();
const isMouseDown = e.type === 'mousedown';
const data: MkbMouseClick = {
mouseButton: e.button,
pressed: isMouseDown,
pressed: e.type === 'mousedown',
};
this.mkbHandler.handleMouseClick(data);
}
#onWheelEvent = (e: WheelEvent) => {
private onWheelEvent = (e: WheelEvent) => {
const key = KeyHelper.getKeyFromEvent(e);
if (!key) {
return;
@@ -114,7 +106,7 @@ class PointerLockMouseDataProvider extends MouseDataProvider {
}
}
#disableContextMenu = (e: Event) => e.preventDefault();
private disableContextMenu = (e: Event) => e.preventDefault();
}
/*
@@ -122,80 +114,95 @@ This class uses some code from Yuzu emulator to handle mouse's movements
Source: https://github.com/yuzu-emu/yuzu-mainline/blob/master/src/input_common/drivers/mouse.cpp
*/
export class EmulatedMkbHandler extends MkbHandler {
private static instance: EmulatedMkbHandler;
public static getInstance = () => EmulatedMkbHandler.instance ?? (EmulatedMkbHandler.instance = new EmulatedMkbHandler());
private static instance: EmulatedMkbHandler | null | undefined;
public static getInstance(): typeof EmulatedMkbHandler['instance'] {
if (typeof EmulatedMkbHandler.instance === 'undefined') {
if (EmulatedMkbHandler.isAllowed()) {
EmulatedMkbHandler.instance = new EmulatedMkbHandler();
} else {
EmulatedMkbHandler.instance = null;
}
}
return EmulatedMkbHandler.instance;
}
private static readonly LOG_TAG = 'EmulatedMkbHandler';
#CURRENT_PRESET_DATA = MkbPreset.convert(MkbPreset.DEFAULT_PRESET);
static isAllowed() {
return getPref(PrefKey.MKB_ENABLED) && (AppInterface || !UserAgent.isMobile());
}
static readonly DEFAULT_PANNING_SENSITIVITY = 0.0010;
static readonly DEFAULT_DEADZONE_COUNTERWEIGHT = 0.01;
static readonly MAXIMUM_STICK_RANGE = 1.1;
private PRESET!: MkbConvertedPresetData | null;
private VIRTUAL_GAMEPAD = {
id: VIRTUAL_GAMEPAD_ID,
index: 0,
connected: false,
hapticActuators: null,
mapping: 'standard',
#VIRTUAL_GAMEPAD = {
id: VIRTUAL_GAMEPAD_ID,
index: 3,
connected: false,
hapticActuators: null,
mapping: 'standard',
axes: [0, 0, 0, 0],
buttons: new Array(17).fill(null).map(() => ({pressed: false, value: 0})),
timestamp: performance.now(),
axes: [0, 0, 0, 0],
buttons: new Array(17).fill(null).map(() => ({pressed: false, value: 0})),
timestamp: performance.now(),
vibrationActuator: null,
};
private nativeGetGamepads: Navigator['getGamepads'];
vibrationActuator: null,
};
#nativeGetGamepads = window.navigator.getGamepads.bind(window.navigator);
private initialized = false;
private enabled = false;
private mouseDataProvider: MouseDataProvider | undefined;
private isPolling = false;
#enabled = false;
#mouseDataProvider: MouseDataProvider | undefined;
#isPolling = false;
private prevWheelCode = null;
private wheelStoppedTimeoutId: number | null = null;
#prevWheelCode = null;
#wheelStoppedTimeout?: number | null;
private detectMouseStoppedTimeoutId: number | null = null;
#detectMouseStoppedTimeout?: number | null;
private escKeyDownTime: number = -1;
#$message?: HTMLElement;
private LEFT_STICK_X: GamepadKey[] = [];
private LEFT_STICK_Y: GamepadKey[] = [];
private RIGHT_STICK_X: GamepadKey[] = [];
private RIGHT_STICK_Y: GamepadKey[] = [];
#escKeyDownTime: number = -1;
private popup: MkbPopup;
#STICK_MAP: {[key in GamepadKey]?: [GamepadKey[], number, number]};
#LEFT_STICK_X: GamepadKey[] = [];
#LEFT_STICK_Y: GamepadKey[] = [];
#RIGHT_STICK_X: GamepadKey[] = [];
#RIGHT_STICK_Y: GamepadKey[] = [];
private STICK_MAP: {[key in GamepadKey]?: [GamepadKey[], number, number]} = {
[GamepadKey.LS_LEFT]: [this.LEFT_STICK_X, 0, -1],
[GamepadKey.LS_RIGHT]: [this.LEFT_STICK_X, 0, 1],
[GamepadKey.LS_UP]: [this.LEFT_STICK_Y, 1, -1],
[GamepadKey.LS_DOWN]: [this.LEFT_STICK_Y, 1, 1],
[GamepadKey.RS_LEFT]: [this.RIGHT_STICK_X, 2, -1],
[GamepadKey.RS_RIGHT]: [this.RIGHT_STICK_X, 2, 1],
[GamepadKey.RS_UP]: [this.RIGHT_STICK_Y, 3, -1],
[GamepadKey.RS_DOWN]: [this.RIGHT_STICK_Y, 3, 1],
};
private constructor() {
super();
BxLogger.info(EmulatedMkbHandler.LOG_TAG, 'constructor()');
this.#STICK_MAP = {
[GamepadKey.LS_LEFT]: [this.#LEFT_STICK_X, 0, -1],
[GamepadKey.LS_RIGHT]: [this.#LEFT_STICK_X, 0, 1],
[GamepadKey.LS_UP]: [this.#LEFT_STICK_Y, 1, -1],
[GamepadKey.LS_DOWN]: [this.#LEFT_STICK_Y, 1, 1],
this.nativeGetGamepads = window.navigator.getGamepads.bind(window.navigator);
[GamepadKey.RS_LEFT]: [this.#RIGHT_STICK_X, 2, -1],
[GamepadKey.RS_RIGHT]: [this.#RIGHT_STICK_X, 2, 1],
[GamepadKey.RS_UP]: [this.#RIGHT_STICK_Y, 3, -1],
[GamepadKey.RS_DOWN]: [this.#RIGHT_STICK_Y, 3, 1],
};
this.popup = MkbPopup.getInstance();
this.popup.attachMkbHandler(this);
}
isEnabled = () => this.#enabled;
isEnabled = () => this.enabled;
#patchedGetGamepads = () => {
const gamepads = this.#nativeGetGamepads() || [];
(gamepads as any)[this.#VIRTUAL_GAMEPAD.index] = this.#VIRTUAL_GAMEPAD;
private patchedGetGamepads = () => {
const gamepads = (this.nativeGetGamepads() || []) as any;
gamepads[this.VIRTUAL_GAMEPAD.index] = this.VIRTUAL_GAMEPAD;
return gamepads;
}
#getVirtualGamepad = () => this.#VIRTUAL_GAMEPAD;
private getVirtualGamepad = () => this.VIRTUAL_GAMEPAD;
#updateStick(stick: GamepadStick, x: number, y: number) {
const virtualGamepad = this.#getVirtualGamepad();
private updateStick(stick: GamepadStick, x: number, y: number) {
const virtualGamepad = this.getVirtualGamepad();
virtualGamepad.axes[stick * 2] = x;
virtualGamepad.axes[stick * 2 + 1] = y;
@@ -212,10 +219,10 @@ export class EmulatedMkbHandler extends MkbHandler {
}
*/
#vectorLength = (x: number, y: number): number => Math.sqrt(x ** 2 + y ** 2);
private vectorLength = (x: number, y: number): number => Math.sqrt(x ** 2 + y ** 2);
#resetGamepad = () => {
const gamepad = this.#getVirtualGamepad();
private resetGamepad() {
const gamepad = this.getVirtualGamepad();
// Reset axes
gamepad.axes = [0, 0, 0, 0];
@@ -229,11 +236,11 @@ export class EmulatedMkbHandler extends MkbHandler {
gamepad.timestamp = performance.now();
}
#pressButton = (buttonIndex: GamepadKey, pressed: boolean) => {
const virtualGamepad = this.#getVirtualGamepad();
private pressButton(buttonIndex: GamepadKey, pressed: boolean) {
const virtualGamepad = this.getVirtualGamepad();
if (buttonIndex >= 100) {
let [valueArr, axisIndex] = this.#STICK_MAP[buttonIndex]!;
let [valueArr, axisIndex] = this.STICK_MAP[buttonIndex]!;
valueArr = valueArr as number[];
axisIndex = axisIndex as number;
@@ -249,7 +256,7 @@ export class EmulatedMkbHandler extends MkbHandler {
let value;
if (valueArr.length) {
// Get value of the last key of the axis
value = this.#STICK_MAP[valueArr[valueArr.length - 1]]![2] as number;
value = this.STICK_MAP[valueArr[valueArr.length - 1]]![2] as number;
} else {
value = 0;
}
@@ -263,41 +270,35 @@ export class EmulatedMkbHandler extends MkbHandler {
virtualGamepad.timestamp = performance.now();
}
#onKeyboardEvent = (e: KeyboardEvent) => {
private onKeyboardEvent = (e: KeyboardEvent) => {
const isKeyDown = e.type === 'keydown';
// Toggle MKB feature
if (e.code === 'F8') {
if (!isKeyDown) {
e.preventDefault();
this.toggle();
}
return;
}
// Hijack the Esc button
if (e.code === 'Escape') {
e.preventDefault();
// Hold the Esc for 1 second to disable MKB
if (this.#enabled && isKeyDown) {
if (this.#escKeyDownTime === -1) {
this.#escKeyDownTime = performance.now();
} else if (performance.now() - this.#escKeyDownTime >= 1000) {
if (this.enabled && isKeyDown) {
if (this.escKeyDownTime === -1) {
this.escKeyDownTime = performance.now();
} else if (performance.now() - this.escKeyDownTime >= 1000) {
this.stop();
}
} else {
this.#escKeyDownTime = -1;
this.escKeyDownTime = -1;
}
return;
}
if (!this.#isPolling) {
if (!this.isPolling || !this.PRESET) {
return;
}
const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[e.code || e.key]!;
if (window.BX_STREAM_SETTINGS.xCloudPollingMode !== 'none') {
return;
}
const buttonIndex = this.PRESET.mapping[e.code || e.key]!;
if (typeof buttonIndex === 'undefined') {
return;
}
@@ -308,19 +309,23 @@ export class EmulatedMkbHandler extends MkbHandler {
}
e.preventDefault();
this.#pressButton(buttonIndex, isKeyDown);
this.pressButton(buttonIndex, isKeyDown);
}
#onMouseStopped = () => {
private onMouseStopped = () => {
// Reset stick position
this.#detectMouseStoppedTimeout = null;
this.detectMouseStoppedTimeoutId = null;
const mouseMapTo = this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_MAP_TO];
if (!this.PRESET) {
return;
}
const mouseMapTo = this.PRESET.mouse[MkbPresetKey.MOUSE_MAP_TO];
const analog = mouseMapTo === MouseMapTo.LS ? GamepadStick.LEFT : GamepadStick.RIGHT;
this.#updateStick(analog, 0, 0);
this.updateStick(analog, 0, 0);
}
handleMouseClick = (data: MkbMouseClick) => {
handleMouseClick(data: MkbMouseClick) {
let mouseButton;
if (typeof data.mouseButton !== 'undefined') {
mouseButton = data.mouseButton;
@@ -331,51 +336,54 @@ export class EmulatedMkbHandler extends MkbHandler {
const keyCode = 'Mouse' + mouseButton;
const key = {
code: keyCode,
name: KeyHelper.codeToKeyName(keyCode),
};
if (!key.name) {
if (!this.PRESET) {
return;
}
const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[key.code]!;
const buttonIndex = this.PRESET.mapping[key.code]!;
if (typeof buttonIndex === 'undefined') {
return;
}
this.#pressButton(buttonIndex, data.pressed);
this.pressButton(buttonIndex, data.pressed);
}
handleMouseMove = (data: MkbMouseMove) => {
handleMouseMove(data: MkbMouseMove) {
const preset = this.PRESET;
if (!preset) {
return;
}
// TODO: optimize this
const mouseMapTo = this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_MAP_TO];
const mouseMapTo = preset.mouse[MkbPresetKey.MOUSE_MAP_TO];
if (mouseMapTo === MouseMapTo.OFF) {
// Ignore mouse movements
return;
}
this.#detectMouseStoppedTimeout && clearTimeout(this.#detectMouseStoppedTimeout);
this.#detectMouseStoppedTimeout = window.setTimeout(this.#onMouseStopped.bind(this), 50);
this.detectMouseStoppedTimeoutId && clearTimeout(this.detectMouseStoppedTimeoutId);
this.detectMouseStoppedTimeoutId = window.setTimeout(this.onMouseStopped, 50);
const deadzoneCounterweight = this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT];
const deadzoneCounterweight = preset.mouse[MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT];
let x = data.movementX * this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_SENSITIVITY_X];
let y = data.movementY * this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_SENSITIVITY_Y];
let x = data.movementX * preset.mouse[MkbPresetKey.MOUSE_SENSITIVITY_X];
let y = data.movementY * preset.mouse[MkbPresetKey.MOUSE_SENSITIVITY_Y];
let length = this.#vectorLength(x, y);
let length = this.vectorLength(x, y);
if (length !== 0 && length < deadzoneCounterweight) {
x *= deadzoneCounterweight / length;
y *= deadzoneCounterweight / length;
} else if (length > EmulatedMkbHandler.MAXIMUM_STICK_RANGE) {
x *= EmulatedMkbHandler.MAXIMUM_STICK_RANGE / length;
y *= EmulatedMkbHandler.MAXIMUM_STICK_RANGE / length;
} else if (length > MouseConstant.MAXIMUM_STICK_RANGE) {
x *= MouseConstant.MAXIMUM_STICK_RANGE / length;
y *= MouseConstant.MAXIMUM_STICK_RANGE / length;
}
const analog = mouseMapTo === MouseMapTo.LS ? GamepadStick.LEFT : GamepadStick.RIGHT;
this.#updateStick(analog, x, y);
this.updateStick(analog, x, y);
}
handleMouseWheel = (data: MkbMouseWheel): boolean => {
handleMouseWheel(data: MkbMouseWheel): boolean {
let code = '';
if (data.vertical < 0) {
code = WheelCode.SCROLL_UP;
@@ -391,136 +399,69 @@ export class EmulatedMkbHandler extends MkbHandler {
return false;
}
if (!this.PRESET) {
return false;
}
const key = {
code: code,
name: KeyHelper.codeToKeyName(code),
};
const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[key.code]!;
const buttonIndex = this.PRESET.mapping[key.code]!;
if (typeof buttonIndex === 'undefined') {
return false;
}
if (this.#prevWheelCode === null || this.#prevWheelCode === key.code) {
this.#wheelStoppedTimeout && clearTimeout(this.#wheelStoppedTimeout);
this.#pressButton(buttonIndex, true);
if (this.prevWheelCode === null || this.prevWheelCode === key.code) {
this.wheelStoppedTimeoutId && clearTimeout(this.wheelStoppedTimeoutId);
this.pressButton(buttonIndex, true);
}
this.#wheelStoppedTimeout = window.setTimeout(() => {
this.#prevWheelCode = null;
this.#pressButton(buttonIndex, false);
this.wheelStoppedTimeoutId = window.setTimeout(() => {
this.prevWheelCode = null;
this.pressButton(buttonIndex, false);
}, 20);
return true;
}
toggle = (force?: boolean) => {
if (typeof force !== 'undefined') {
this.#enabled = force;
} else {
this.#enabled = !this.#enabled;
toggle(force?: boolean) {
if (!this.initialized) {
return;
}
if (this.#enabled) {
if (typeof force !== 'undefined') {
this.enabled = force;
} else {
this.enabled = !this.enabled;
}
if (this.enabled) {
document.body.requestPointerLock();
} else {
document.pointerLockElement && document.exitPointerLock();
}
}
#getCurrentPreset = (): Promise<MkbStoredPreset> => {
return new Promise(resolve => {
const presetId = getPref(PrefKey.MKB_DEFAULT_PRESET_ID);
MkbPresetsDb.getInstance().getPreset(presetId).then((preset: MkbStoredPreset) => {
resolve(preset);
});
});
refreshPresetData() {
this.PRESET = window.BX_STREAM_SETTINGS.mkbPreset;
this.resetGamepad();
}
refreshPresetData = () => {
this.#getCurrentPreset().then((preset: MkbStoredPreset) => {
this.#CURRENT_PRESET_DATA = MkbPreset.convert(preset ? preset.data : MkbPreset.DEFAULT_PRESET);
this.#resetGamepad();
});
waitForMouseData(showPopup: boolean) {
this.popup.toggleVisibility(showPopup);
}
waitForMouseData = (wait: boolean) => {
this.#$message && this.#$message.classList.toggle('bx-gone', !wait);
private onPollingModeChanged = (e: Event) => {
const move = window.BX_STREAM_SETTINGS.xCloudPollingMode !== 'none';
this.popup.moveOffscreen(move);
}
#onPollingModeChanged = (e: Event) => {
if (!this.#$message) {
return;
}
const mode = (e as any).mode;
if (mode === 'none') {
this.#$message.classList.remove('bx-offscreen');
} else {
this.#$message.classList.add('bx-offscreen');
}
}
#onDialogShown = () => {
private onDialogShown = () => {
document.pointerLockElement && document.exitPointerLock();
}
#initMessage = () => {
if (!this.#$message) {
this.#$message = CE('div', {'class': 'bx-mkb-pointer-lock-msg bx-gone'},
CE('div', {},
CE('p', {}, t('virtual-controller')),
CE('p', {}, t('press-key-to-toggle-mkb', {key: 'F8'})),
),
CE('div', {'data-type': 'virtual'},
createButton({
style: ButtonStyle.PRIMARY | ButtonStyle.TALL | ButtonStyle.FULL_WIDTH,
label: t('activate'),
onClick: ((e: Event) => {
e.preventDefault();
e.stopPropagation();
this.toggle(true);
}).bind(this),
}),
CE('div', {},
createButton({
label: t('ignore'),
style: ButtonStyle.GHOST,
onClick: e => {
e.preventDefault();
e.stopPropagation();
this.toggle(false);
this.waitForMouseData(false);
},
}),
createButton({
label: t('edit'),
onClick: e => {
e.preventDefault();
e.stopPropagation();
// Show Settings dialog & focus the MKB tab
const dialog = SettingsNavigationDialog.getInstance();
dialog.focusTab('mkb');
NavigationDialogManager.getInstance().show(dialog);
},
}),
),
),
);
}
if (!this.#$message.isConnected) {
document.documentElement.appendChild(this.#$message);
}
}
#onPointerLockChange = () => {
private onPointerLockChange = () => {
if (document.pointerLockElement) {
this.start();
} else {
@@ -528,58 +469,64 @@ export class EmulatedMkbHandler extends MkbHandler {
}
}
#onPointerLockError = (e: Event) => {
private onPointerLockError = (e: Event) => {
console.log(e);
this.stop();
}
#onPointerLockRequested = () => {
private onPointerLockRequested = () => {
this.start();
}
#onPointerLockExited = () => {
this.#mouseDataProvider?.stop();
private onPointerLockExited = () => {
this.mouseDataProvider?.stop();
}
handleEvent(event: Event) {
switch (event.type) {
case BxEvent.POINTER_LOCK_REQUESTED:
this.#onPointerLockRequested();
this.onPointerLockRequested();
break;
case BxEvent.POINTER_LOCK_EXITED:
this.#onPointerLockExited();
this.onPointerLockExited();
break;
}
}
init = () => {
init() {
if (!STATES.browser.capabilities.mkb) {
this.initialized = false;
return;
}
this.initialized = true;
this.refreshPresetData();
this.#enabled = false;
this.enabled = false;
if (AppInterface) {
this.#mouseDataProvider = new WebSocketMouseDataProvider(this);
this.mouseDataProvider = new WebSocketMouseDataProvider(this);
} else {
this.#mouseDataProvider = new PointerLockMouseDataProvider(this);
this.mouseDataProvider = new PointerLockMouseDataProvider(this);
}
this.#mouseDataProvider.init();
this.mouseDataProvider.init();
window.addEventListener('keydown', this.#onKeyboardEvent);
window.addEventListener('keyup', this.#onKeyboardEvent);
window.addEventListener('keydown', this.onKeyboardEvent);
window.addEventListener('keyup', this.onKeyboardEvent);
window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.#onPollingModeChanged);
window.addEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this.#onDialogShown);
window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.onPollingModeChanged);
window.addEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this.onDialogShown);
if (AppInterface) {
// Android app doesn't support PointerLock API so we need to use a different method
window.addEventListener(BxEvent.POINTER_LOCK_REQUESTED, this);
window.addEventListener(BxEvent.POINTER_LOCK_EXITED, this);
} else {
document.addEventListener('pointerlockchange', this.#onPointerLockChange);
document.addEventListener('pointerlockerror', this.#onPointerLockError);
document.addEventListener('pointerlockchange', this.onPointerLockChange);
document.addEventListener('pointerlockerror', this.onPointerLockError);
}
this.#initMessage();
this.#$message?.classList.add('bx-gone');
MkbPopup.getInstance().reset();
if (AppInterface) {
Toast.show(t('press-key-to-toggle-mkb', {key: `<b>F8</b>`}), t('virtual-controller'), {html: true});
@@ -589,51 +536,62 @@ export class EmulatedMkbHandler extends MkbHandler {
}
}
destroy = () => {
this.#isPolling = false;
this.#enabled = false;
destroy() {
if (!this.initialized) {
return;
}
this.initialized = false;
this.isPolling = false;
this.enabled = false;
this.stop();
this.waitForMouseData(false);
document.pointerLockElement && document.exitPointerLock();
window.removeEventListener('keydown', this.#onKeyboardEvent);
window.removeEventListener('keyup', this.#onKeyboardEvent);
window.removeEventListener('keydown', this.onKeyboardEvent);
window.removeEventListener('keyup', this.onKeyboardEvent);
if (AppInterface) {
window.removeEventListener(BxEvent.POINTER_LOCK_REQUESTED, this);
window.removeEventListener(BxEvent.POINTER_LOCK_EXITED, this);
} else {
document.removeEventListener('pointerlockchange', this.#onPointerLockChange);
document.removeEventListener('pointerlockerror', this.#onPointerLockError);
document.removeEventListener('pointerlockchange', this.onPointerLockChange);
document.removeEventListener('pointerlockerror', this.onPointerLockError);
}
window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.#onPollingModeChanged);
window.removeEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this.#onDialogShown);
window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.onPollingModeChanged);
window.removeEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this.onDialogShown);
this.#mouseDataProvider?.destroy();
this.mouseDataProvider?.destroy();
window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.#onPollingModeChanged);
window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.onPollingModeChanged);
}
start = () => {
if (!this.#enabled) {
this.#enabled = true;
Toast.show(t('virtual-controller'), t('enabled'), {instant: true});
updateGamepadSlots() {
// Set gamepad slot
this.VIRTUAL_GAMEPAD.index = getPref<number>(PrefKey.MKB_P1_SLOT) - 1;
}
start() {
if (!this.enabled) {
this.enabled = true;
Toast.show(t('virtual-controller'), t('enabled'), { instant: true });
}
this.#isPolling = true;
this.#escKeyDownTime = -1;
this.isPolling = true;
this.escKeyDownTime = -1;
this.#resetGamepad();
window.navigator.getGamepads = this.#patchedGetGamepads;
this.resetGamepad();
this.updateGamepadSlots();
window.navigator.getGamepads = this.patchedGetGamepads;
this.waitForMouseData(false);
this.#mouseDataProvider?.start();
this.mouseDataProvider?.start();
// Dispatch "gamepadconnected" event
const virtualGamepad = this.#getVirtualGamepad();
const virtualGamepad = this.getVirtualGamepad();
virtualGamepad.connected = true;
virtualGamepad.timestamp = performance.now();
@@ -643,46 +601,51 @@ export class EmulatedMkbHandler extends MkbHandler {
window.BX_EXPOSED.stopTakRendering = true;
Toast.show(t('virtual-controller'), t('enabled'), {instant: true});
Toast.show(t('virtual-controller'), t('enabled'), { instant: true });
}
stop = () => {
this.#enabled = false;
this.#isPolling = false;
this.#escKeyDownTime = -1;
stop() {
this.enabled = false;
this.isPolling = false;
this.escKeyDownTime = -1;
const virtualGamepad = this.#getVirtualGamepad();
const virtualGamepad = this.getVirtualGamepad();
if (virtualGamepad.connected) {
// Dispatch "gamepaddisconnected" event
this.#resetGamepad();
this.resetGamepad();
virtualGamepad.connected = false;
virtualGamepad.timestamp = performance.now();
BxEvent.dispatch(window, 'gamepaddisconnected', {
gamepad: virtualGamepad,
});
gamepad: virtualGamepad,
});
window.navigator.getGamepads = this.#nativeGetGamepads;
window.navigator.getGamepads = this.nativeGetGamepads;
}
this.waitForMouseData(true);
this.#mouseDataProvider?.stop();
this.mouseDataProvider?.stop();
// Toast.show(t('virtual-controller'), t('disabled'), {instant: true});
}
static setupEvents() {
isFullVersion() && window.addEventListener(BxEvent.STREAM_PLAYING, () => {
if (STATES.currentStream.titleInfo?.details.hasMkbSupport) {
// Enable native MKB in Android app
if (AppInterface && getPref(PrefKey.NATIVE_MKB_ENABLED) === 'on') {
AppInterface && NativeMkbHandler.getInstance().init();
if (isFullVersion()) {
window.addEventListener(BxEvent.STREAM_PLAYING, () => {
if (STATES.currentStream.titleInfo?.details.hasMkbSupport) {
// Enable native MKB in Android app
NativeMkbHandler.getInstance()?.init();
} else {
EmulatedMkbHandler.getInstance()?.init();
}
} else if (getPref(PrefKey.MKB_ENABLED) && (AppInterface || !UserAgent.isMobile())) {
BxLogger.info(EmulatedMkbHandler.LOG_TAG, 'Emulate MKB');
EmulatedMkbHandler.getInstance().init();
});
if (EmulatedMkbHandler.isAllowed()) {
window.addEventListener(BxEvent.MKB_UPDATED, () => {
EmulatedMkbHandler.getInstance()?.refreshPresetData();
});
}
});
}
}
}

110
src/modules/mkb/mkb-popup.ts Executable file
View File

@@ -0,0 +1,110 @@
import { CE, createButton, ButtonStyle, type BxButtonOptions } from "@/utils/html";
import { t } from "@/utils/translation";
import { BxEvent } from "@/utils/bx-event";
import { ShortcutAction } from "@/enums/shortcut-actions";
import { SettingsDialog } from "../ui/dialog/settings-dialog";
import type { MkbHandler } from "./base-mkb-handler";
import { NativeMkbHandler } from "./native-mkb-handler";
import { StreamSettings } from "@/utils/stream-settings";
import { KeyHelper } from "./key-helper";
type MkbPopupType = 'virtual' | 'native';
export class MkbPopup {
private static instance: MkbPopup;
public static getInstance = () => MkbPopup.instance ?? (MkbPopup.instance = new MkbPopup());
private popupType!: MkbPopupType;
private $popup!: HTMLElement;
private $title!: HTMLElement;
private $btnActivate!: HTMLButtonElement;
private mkbHandler!: MkbHandler;
constructor() {
this.render();
window.addEventListener(BxEvent.KEYBOARD_SHORTCUTS_UPDATED, e => {
const $newButton = this.createActivateButton();
this.$btnActivate.replaceWith($newButton);
this.$btnActivate = $newButton;
});
}
attachMkbHandler(handler: MkbHandler) {
this.mkbHandler = handler;
// Set popupType
this.popupType = (handler instanceof NativeMkbHandler) ? 'native' : 'virtual';
this.$popup.dataset.type = this.popupType;
// Update popup title
this.$title.innerText = t(this.popupType === 'native' ? 'native-mkb' : 'virtual-controller');
}
toggleVisibility(show: boolean) {
this.$popup.classList.toggle('bx-gone', !show);
show && this.moveOffscreen(false);
}
moveOffscreen(doMove: boolean) {
this.$popup.classList.toggle('bx-offscreen', doMove);
}
private createActivateButton() {
const options: BxButtonOptions = {
style: ButtonStyle.PRIMARY | ButtonStyle.TALL | ButtonStyle.FULL_WIDTH,
label: t('activate'),
onClick: this.onActivate,
};
// Find shortcut key
const shortcutKey = StreamSettings.findKeyboardShortcut(ShortcutAction.MKB_TOGGLE);
if (shortcutKey) {
options.secondaryText = t('press-key-to-toggle-mkb', { key: KeyHelper.codeToKeyName(shortcutKey) });
}
return createButton(options);
}
private onActivate = (e: Event) => {
e.preventDefault();
this.mkbHandler.toggle(true);
}
private render() {
this.$popup = CE('div', { class: 'bx-mkb-pointer-lock-msg bx-gone' },
this.$title = CE('p'),
this.$btnActivate = this.createActivateButton(),
CE('div', {},
createButton({
label: t('ignore'),
style: ButtonStyle.GHOST,
onClick: e => {
e.preventDefault();
this.mkbHandler.toggle(false);
this.mkbHandler.waitForMouseData(false);
},
}),
createButton({
label: t('manage'),
style: ButtonStyle.FOCUSABLE,
onClick: () => {
const dialog = SettingsDialog.getInstance();
dialog.focusTab('mkb');
dialog.show();
},
}),
),
);
document.documentElement.appendChild(this.$popup);
}
reset() {
this.toggleVisibility(true);
this.moveOffscreen(false);
}
}

View File

@@ -1,135 +0,0 @@
import { t } from "@utils/translation";
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 {
static MOUSE_SETTINGS: PreferenceSettings = {
[MkbPresetKey.MOUSE_MAP_TO]: {
label: t('map-mouse-to'),
type: SettingElementType.OPTIONS,
default: MouseMapTo[MouseMapTo.RS],
options: {
[MouseMapTo[MouseMapTo.RS]]: t('right-stick'),
[MouseMapTo[MouseMapTo.LS]]: t('left-stick'),
[MouseMapTo[MouseMapTo.OFF]]: t('off'),
},
},
[MkbPresetKey.MOUSE_SENSITIVITY_Y]: {
label: t('horizontal-sensitivity'),
type: SettingElementType.NUMBER_STEPPER,
default: 50,
min: 1,
max: 300,
params: {
suffix: '%',
exactTicks: 50,
},
},
[MkbPresetKey.MOUSE_SENSITIVITY_X]: {
label: t('vertical-sensitivity'),
type: SettingElementType.NUMBER_STEPPER,
default: 50,
min: 1,
max: 300,
params: {
suffix: '%',
exactTicks: 50,
},
},
[MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT]: {
label: t('deadzone-counterweight'),
type: SettingElementType.NUMBER_STEPPER,
default: 20,
min: 1,
max: 50,
params: {
suffix: '%',
exactTicks: 10,
},
},
};
static DEFAULT_PRESET: MkbPresetData = {
'mapping': {
// Use "e.code" value from https://keyjs.dev
[GamepadKey.UP]: ['ArrowUp'],
[GamepadKey.DOWN]: ['ArrowDown'],
[GamepadKey.LEFT]: ['ArrowLeft'],
[GamepadKey.RIGHT]: ['ArrowRight'],
[GamepadKey.LS_UP]: ['KeyW'],
[GamepadKey.LS_DOWN]: ['KeyS'],
[GamepadKey.LS_LEFT]: ['KeyA'],
[GamepadKey.LS_RIGHT]: ['KeyD'],
[GamepadKey.RS_UP]: ['KeyI'],
[GamepadKey.RS_DOWN]: ['KeyK'],
[GamepadKey.RS_LEFT]: ['KeyJ'],
[GamepadKey.RS_RIGHT]: ['KeyL'],
[GamepadKey.A]: ['Space', 'KeyE'],
[GamepadKey.X]: ['KeyR'],
[GamepadKey.B]: ['ControlLeft', 'Backspace'],
[GamepadKey.Y]: ['KeyV'],
[GamepadKey.START]: ['Enter'],
[GamepadKey.SELECT]: ['Tab'],
[GamepadKey.LB]: ['KeyC', 'KeyG'],
[GamepadKey.RB]: ['KeyQ'],
[GamepadKey.HOME]: ['Backquote'],
[GamepadKey.RT]: [MouseButtonCode.LEFT_CLICK],
[GamepadKey.LT]: [MouseButtonCode.RIGHT_CLICK],
[GamepadKey.L3]: ['ShiftLeft'],
[GamepadKey.R3]: ['KeyF'],
},
'mouse': {
[MkbPresetKey.MOUSE_MAP_TO]: MouseMapTo[MouseMapTo.RS],
[MkbPresetKey.MOUSE_SENSITIVITY_X]: 100,
[MkbPresetKey.MOUSE_SENSITIVITY_Y]: 100,
[MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT]: 20,
},
};
static convert(preset: MkbPresetData): MkbConvertedPresetData {
const obj: MkbConvertedPresetData = {
mapping: {},
mouse: Object.assign({}, preset.mouse),
};
for (const buttonIndex in preset.mapping) {
for (const keyName of preset.mapping[parseInt(buttonIndex)]) {
obj.mapping[keyName!] = parseInt(buttonIndex);
}
}
// Pre-calculate mouse's sensitivities
const mouse = obj.mouse;
mouse[MkbPresetKey.MOUSE_SENSITIVITY_X] *= EmulatedMkbHandler.DEFAULT_PANNING_SENSITIVITY;
mouse[MkbPresetKey.MOUSE_SENSITIVITY_Y] *= EmulatedMkbHandler.DEFAULT_PANNING_SENSITIVITY;
mouse[MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT] *= EmulatedMkbHandler.DEFAULT_DEADZONE_COUNTERWEIGHT;
const mouseMapTo = MouseMapTo[mouse[MkbPresetKey.MOUSE_MAP_TO]!];
if (typeof mouseMapTo !== 'undefined') {
mouse[MkbPresetKey.MOUSE_MAP_TO] = mouseMapTo;
} else {
mouse[MkbPresetKey.MOUSE_MAP_TO] = MkbPreset.MOUSE_SETTINGS[MkbPresetKey.MOUSE_MAP_TO].default;
}
return obj;
}
}

View File

@@ -1,541 +0,0 @@
import { CE, createButton, ButtonStyle, removeChildElements } from "@utils/html";
import { t } from "@utils/translation";
import { Dialog } from "@modules/dialog";
import { KeyHelper } from "./key-helper";
import { MkbPreset } from "./mkb-preset";
import { EmulatedMkbHandler } from "./mkb-handler";
import { BxIcon } from "@utils/bx-icon";
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";
import { MkbPresetsDb } from "@/utils/local-db/mkb-presets-db";
import { BxLogger } from "@/utils/bx-logger";
type MkbRemapperStates = {
currentPresetId: number;
presets: MkbStoredPresets;
editingPresetData?: MkbPresetData | null;
isEditing: boolean;
};
export class MkbRemapper {
private readonly BUTTON_ORDERS = [
GamepadKey.UP,
GamepadKey.DOWN,
GamepadKey.LEFT,
GamepadKey.RIGHT,
GamepadKey.A,
GamepadKey.B,
GamepadKey.X,
GamepadKey.Y,
GamepadKey.LB,
GamepadKey.RB,
GamepadKey.LT,
GamepadKey.RT,
GamepadKey.SELECT,
GamepadKey.START,
GamepadKey.HOME,
GamepadKey.L3,
GamepadKey.LS_UP,
GamepadKey.LS_DOWN,
GamepadKey.LS_LEFT,
GamepadKey.LS_RIGHT,
GamepadKey.R3,
GamepadKey.RS_UP,
GamepadKey.RS_DOWN,
GamepadKey.RS_LEFT,
GamepadKey.RS_RIGHT,
];
private static instance: MkbRemapper;
public static getInstance = () => MkbRemapper.instance ?? (MkbRemapper.instance = new MkbRemapper());
private readonly LOG_TAG = 'MkbRemapper';
private states: MkbRemapperStates = {
currentPresetId: 0,
presets: {},
editingPresetData: null,
isEditing: false,
};
private $wrapper!: HTMLElement;
private $presetsSelect!: HTMLSelectElement;
private $activateButton!: HTMLButtonElement;
private $currentBindingKey!: HTMLElement;
private allKeyElements: HTMLElement[] = [];
private allMouseElements: {[key in MkbPresetKey]?: HTMLElement} = {};
bindingDialog: Dialog;
private constructor() {
BxLogger.info(this.LOG_TAG, 'constructor()');
this.states.currentPresetId = getPref(PrefKey.MKB_DEFAULT_PRESET_ID);
this.bindingDialog = new Dialog({
className: 'bx-binding-dialog',
content: CE('div', {},
CE('p', {}, t('press-to-bind')),
CE('i', {}, t('press-esc-to-cancel')),
),
hideCloseButton: true,
});
}
private clearEventListeners = () => {
window.removeEventListener('keydown', this.onKeyDown);
window.removeEventListener('mousedown', this.onMouseDown);
window.removeEventListener('wheel', this.onWheel);
};
private bindKey = ($elm: HTMLElement, key: any) => {
const buttonIndex = parseInt($elm.dataset.buttonIndex!);
const keySlot = parseInt($elm.dataset.keySlot!);
// Ignore if bind the save key to the same element
if ($elm.dataset.keyCode! === key.code) {
return;
}
// Unbind duplicated keys
for (const $otherElm of this.allKeyElements) {
if ($otherElm.dataset.keyCode === key.code) {
this.unbindKey($otherElm);
}
}
this.states.editingPresetData!.mapping[buttonIndex][keySlot] = key.code;
$elm.textContent = key.name;
$elm.dataset.keyCode = key.code;
}
private unbindKey = ($elm: HTMLElement) => {
const buttonIndex = parseInt($elm.dataset.buttonIndex!);
const keySlot = parseInt($elm.dataset.keySlot!);
// Remove key from preset
this.states.editingPresetData!.mapping[buttonIndex][keySlot] = null;
$elm.textContent = '';
delete $elm.dataset.keyCode;
}
private onWheel = (e: WheelEvent) => {
e.preventDefault();
this.clearEventListeners();
this.bindKey(this.$currentBindingKey!, KeyHelper.getKeyFromEvent(e));
window.setTimeout(() => this.bindingDialog.hide(), 200);
};
private onMouseDown = (e: MouseEvent) => {
e.preventDefault();
this.clearEventListeners();
this.bindKey(this.$currentBindingKey!, KeyHelper.getKeyFromEvent(e));
window.setTimeout(() => this.bindingDialog.hide(), 200);
};
private onKeyDown = (e: KeyboardEvent) => {
e.preventDefault();
e.stopPropagation();
this.clearEventListeners();
if (e.code !== 'Escape') {
this.bindKey(this.$currentBindingKey!, KeyHelper.getKeyFromEvent(e));
}
window.setTimeout(() => this.bindingDialog.hide(), 200);
};
private onBindingKey = (e: MouseEvent) => {
if (!this.states.isEditing || e.button !== 0) {
return;
}
console.log(e);
this.$currentBindingKey = e.target as HTMLElement;
window.addEventListener('keydown', this.onKeyDown);
window.addEventListener('mousedown', this.onMouseDown);
window.addEventListener('wheel', this.onWheel);
this.bindingDialog.show({title: this.$currentBindingKey.dataset.prompt!});
};
private onContextMenu = (e: Event) => {
e.preventDefault();
if (!this.states.isEditing) {
return;
}
this.unbindKey(e.target as HTMLElement);
};
private getPreset = (presetId: number) => {
return this.states.presets[presetId];
}
private getCurrentPreset = () => {
let preset = this.getPreset(this.states.currentPresetId);
if (!preset) {
// Get the first preset in the list
const firstPresetId = parseInt(Object.keys(this.states.presets)[0]);
preset = this.states.presets[firstPresetId];
this.states.currentPresetId = firstPresetId;
setPref(PrefKey.MKB_DEFAULT_PRESET_ID, firstPresetId);
}
return preset;
}
private switchPreset = (presetId: number) => {
this.states.currentPresetId = presetId;
const presetData = this.getCurrentPreset().data;
for (const $elm of this.allKeyElements) {
const buttonIndex = parseInt($elm.dataset.buttonIndex!);
const keySlot = parseInt($elm.dataset.keySlot!);
const buttonKeys = presetData.mapping[buttonIndex];
if (buttonKeys && buttonKeys[keySlot]) {
$elm.textContent = KeyHelper.codeToKeyName(buttonKeys[keySlot]!);
$elm.dataset.keyCode = buttonKeys[keySlot]!;
} else {
$elm.textContent = '';
delete $elm.dataset.keyCode;
}
}
let key: MkbPresetKey;
for (key in this.allMouseElements) {
const $elm = this.allMouseElements[key]!;
let value = presetData.mouse[key];
if (typeof value === 'undefined') {
value = MkbPreset.MOUSE_SETTINGS[key].default;
}
'setValue' in $elm && ($elm as any).setValue(value);
}
// Update state of Activate button
const activated = getPref(PrefKey.MKB_DEFAULT_PRESET_ID) === this.states.currentPresetId;
this.$activateButton.disabled = activated;
this.$activateButton.querySelector('span')!.textContent = activated ? t('activated') : t('activate');
}
private async refresh() {
// Clear presets select
removeChildElements(this.$presetsSelect);
const presets = await MkbPresetsDb.getInstance().getPresets();
this.states.presets = presets;
const fragment = document.createDocumentFragment();
let defaultPresetId;
if (this.states.currentPresetId === 0) {
this.states.currentPresetId = parseInt(Object.keys(presets)[0]);
defaultPresetId = this.states.currentPresetId;
setPref(PrefKey.MKB_DEFAULT_PRESET_ID, defaultPresetId);
EmulatedMkbHandler.getInstance().refreshPresetData();
} else {
defaultPresetId = getPref(PrefKey.MKB_DEFAULT_PRESET_ID);
}
for (let id in presets) {
const preset = presets[id];
let name = preset.name;
if (id === defaultPresetId) {
name = `🎮 ` + name;
}
const $options = CE<HTMLOptionElement>('option', {value: id}, name);
$options.selected = parseInt(id) === this.states.currentPresetId;
fragment.appendChild($options);
};
this.$presetsSelect.appendChild(fragment);
// Update state of Activate button
const activated = defaultPresetId === this.states.currentPresetId;
this.$activateButton.disabled = activated;
this.$activateButton.querySelector('span')!.textContent = activated ? t('activated') : t('activate');
!this.states.isEditing && this.switchPreset(this.states.currentPresetId);
}
private toggleEditing = (force?: boolean) => {
this.states.isEditing = typeof force !== 'undefined' ? force : !this.states.isEditing;
this.$wrapper.classList.toggle('bx-editing', this.states.isEditing);
if (this.states.isEditing) {
this.states.editingPresetData = deepClone(this.getCurrentPreset().data);
} else {
this.states.editingPresetData = null;
}
const childElements = this.$wrapper.querySelectorAll('select, button, input');
for (const $elm of Array.from(childElements)) {
if ($elm.parentElement!.parentElement!.classList.contains('bx-mkb-action-buttons')) {
continue;
}
let disable = !this.states.isEditing;
if ($elm.parentElement!.classList.contains('bx-mkb-preset-tools')) {
disable = !disable;
}
($elm as HTMLButtonElement).disabled = disable;
}
}
render() {
this.$wrapper = CE('div', {class: 'bx-mkb-settings'});
this.$presetsSelect = CE<HTMLSelectElement>('select', {tabindex: -1});
this.$presetsSelect.addEventListener('change', e => {
this.switchPreset(parseInt((e.target as HTMLSelectElement).value));
});
const promptNewName = (value: string) => {
let newName: string | null = '';
while (!newName) {
newName = prompt(t('prompt-preset-name'), value);
if (newName === null) {
return false;
}
newName = newName.trim();
}
return newName ? newName : false;
};
const $header = CE('div', {class: 'bx-mkb-preset-tools'},
this.$presetsSelect,
// Rename button
createButton({
title: t('rename'),
icon: BxIcon.CURSOR_TEXT,
tabIndex: -1,
onClick: async () => {
const preset = this.getCurrentPreset();
let newName = promptNewName(preset.name);
if (!newName || newName === preset.name) {
return;
}
// Update preset with new name
preset.name = newName;
await MkbPresetsDb.getInstance().updatePreset(preset);
await this.refresh();
},
}),
// New button
createButton({
icon: BxIcon.NEW,
title: t('new'),
tabIndex: -1,
onClick: e => {
let newName = promptNewName('');
if (!newName) {
return;
}
// Create new preset selected name
MkbPresetsDb.getInstance().newPreset(newName, MkbPreset.DEFAULT_PRESET).then(id => {
this.states.currentPresetId = id;
this.refresh();
});
},
}),
// Copy button
createButton({
icon: BxIcon.COPY,
title: t('copy'),
tabIndex: -1,
onClick: e => {
const preset = this.getCurrentPreset();
let newName = promptNewName(`${preset.name} (2)`);
if (!newName) {
return;
}
// Create new preset selected name
MkbPresetsDb.getInstance().newPreset(newName, preset.data).then(id => {
this.states.currentPresetId = id;
this.refresh();
});
},
}),
// Delete button
createButton({
icon: BxIcon.TRASH,
style: ButtonStyle.DANGER,
title: t('delete'),
tabIndex: -1,
onClick: e => {
if (!confirm(t('confirm-delete-preset'))) {
return;
}
MkbPresetsDb.getInstance().deletePreset(this.states.currentPresetId).then(id => {
this.states.currentPresetId = 0;
this.refresh();
});
},
}),
);
this.$wrapper.appendChild($header);
const $rows = CE('div', {class: 'bx-mkb-settings-rows'},
CE('i', {class: 'bx-mkb-note'}, t('right-click-to-unbind')),
);
// Render keys
const keysPerButton = 2;
for (const buttonIndex of this.BUTTON_ORDERS) {
const [buttonName, buttonPrompt] = GamepadKeyName[buttonIndex];
let $elm;
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,
}, ' ');
$elm.addEventListener('mouseup', this.onBindingKey);
$elm.addEventListener('contextmenu', this.onContextMenu);
$fragment.appendChild($elm);
this.allKeyElements.push($elm);
}
const $keyRow = CE('div', {class: 'bx-mkb-key-row'},
CE('label', {title: buttonName}, buttonPrompt),
$fragment,
);
$rows.appendChild($keyRow);
}
$rows.appendChild(CE('i', {class: 'bx-mkb-note'}, t('mkb-adjust-ingame-settings')),);
// Render mouse settings
const $mouseSettings = document.createDocumentFragment();
for (const key in MkbPreset.MOUSE_SETTINGS) {
const setting = MkbPreset.MOUSE_SETTINGS[key];
const value = setting.default;
let $elm;
const onChange = (e: Event, value: any) => {
(this.states.editingPresetData!.mouse as any)[key] = value;
};
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;
}
$rows.appendChild($mouseSettings);
this.$wrapper.appendChild($rows);
// Render action buttons
const $actionButtons = CE('div', {class: 'bx-mkb-action-buttons'},
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,
tabIndex: -1,
onClick: e => {
setPref(PrefKey.MKB_DEFAULT_PRESET_ID, this.states.currentPresetId);
EmulatedMkbHandler.getInstance().refreshPresetData();
this.refresh();
},
}),
),
CE('div', {},
// Cancel button
createButton({
label: t('cancel'),
style: ButtonStyle.GHOST,
tabIndex: -1,
onClick: e => {
// Restore preset
this.switchPreset(this.states.currentPresetId);
this.toggleEditing(false);
},
}),
// Save button
createButton({
label: t('save'),
style: ButtonStyle.PRIMARY,
tabIndex: -1,
onClick: e => {
const updatedPreset = deepClone(this.getCurrentPreset());
updatedPreset.data = this.states.editingPresetData as MkbPresetData;
MkbPresetsDb.getInstance().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.$wrapper.appendChild($actionButtons);
this.toggleEditing(false);
this.refresh();
return this.$wrapper;
}
}

54
src/modules/mkb/mouse-cursor-hider.ts Normal file → Executable file
View File

@@ -1,34 +1,52 @@
import { PrefKey } from "@/enums/pref-keys";
import { getPref } from "@/utils/settings-storages/global-settings-storage";
export class MouseCursorHider {
static #timeout: number | null;
static #cursorVisible = true;
private static instance: MouseCursorHider | null | undefined;
public static getInstance(): typeof MouseCursorHider['instance'] {
if (typeof MouseCursorHider.instance === 'undefined') {
if (!getPref(PrefKey.MKB_ENABLED) && getPref(PrefKey.MKB_HIDE_IDLE_CURSOR)) {
MouseCursorHider.instance = new MouseCursorHider();
} else {
MouseCursorHider.instance = null;
}
}
static show() {
return MouseCursorHider.instance;
}
private timeoutId!: number | null;
private isCursorVisible = true;
show() {
document.body && (document.body.style.cursor = 'unset');
MouseCursorHider.#cursorVisible = true;
this.isCursorVisible = true;
}
static hide() {
hide() {
document.body && (document.body.style.cursor = 'none');
MouseCursorHider.#timeout = null;
MouseCursorHider.#cursorVisible = false;
this.timeoutId = null;
this.isCursorVisible = false;
}
static onMouseMove(e: MouseEvent) {
onMouseMove = (e: MouseEvent) => {
// Toggle cursor
!MouseCursorHider.#cursorVisible && MouseCursorHider.show();
!this.isCursorVisible && this.show();
// Setup timeout
MouseCursorHider.#timeout && clearTimeout(MouseCursorHider.#timeout);
MouseCursorHider.#timeout = window.setTimeout(MouseCursorHider.hide, 3000);
this.timeoutId && clearTimeout(this.timeoutId);
this.timeoutId = window.setTimeout(this.hide, 3000);
}
static start() {
MouseCursorHider.show();
document.addEventListener('mousemove', MouseCursorHider.onMouseMove);
start() {
this.show();
document.addEventListener('mousemove', this.onMouseMove);
}
static stop() {
MouseCursorHider.#timeout && clearTimeout(MouseCursorHider.#timeout);
document.removeEventListener('mousemove', MouseCursorHider.onMouseMove);
MouseCursorHider.show();
stop() {
this.timeoutId && clearTimeout(this.timeoutId);
this.timeoutId = null;
document.removeEventListener('mousemove', this.onMouseMove);
this.show();
}
}

232
src/modules/mkb/native-mkb-handler.ts Normal file → Executable file
View File

@@ -4,10 +4,14 @@ import { AppInterface, STATES } from "@/utils/global";
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 } from "@/enums/pref-keys";
import { getPref } from "@/utils/settings-storages/global-settings-storage";
import { BxLogger } from "@/utils/bx-logger";
import { MkbPopup } from "./mkb-popup";
import { KeyHelper } from "./key-helper";
import { StreamSettings } from "@/utils/stream-settings";
import { ShortcutAction } from "@/enums/shortcut-actions";
import { NativeMkbMode } from "@/enums/pref-values";
type NativeMouseData = {
X: number,
@@ -15,7 +19,7 @@ type NativeMouseData = {
Buttons: number,
WheelX: number,
WheelY: number,
Type? : 0, // 0: Relative, 1: Absolute
Type?: 0, // 0: Relative, 1: Absolute
}
type XcloudInputSink = {
@@ -23,30 +27,47 @@ type XcloudInputSink = {
}
export class NativeMkbHandler extends MkbHandler {
private static instance: NativeMkbHandler;
public static getInstance = () => NativeMkbHandler.instance ?? (NativeMkbHandler.instance = new NativeMkbHandler());
private static instance: NativeMkbHandler | null | undefined;
public static getInstance(): typeof NativeMkbHandler['instance'] {
if (typeof NativeMkbHandler.instance === 'undefined') {
if (NativeMkbHandler.isAllowed()) {
NativeMkbHandler.instance = new NativeMkbHandler();
} else {
NativeMkbHandler.instance = null;
}
}
return NativeMkbHandler.instance;
}
private readonly LOG_TAG = 'NativeMkbHandler';
#pointerClient: PointerClient | undefined;
#enabled: boolean = false;
static isAllowed = () => {
return STATES.browser.capabilities.emulatedNativeMkb && getPref<NativeMkbMode>(PrefKey.NATIVE_MKB_MODE) === NativeMkbMode.ON;
}
#mouseButtonsPressed = 0;
#mouseWheelX = 0;
#mouseWheelY = 0;
private pointerClient: PointerClient | undefined;
private enabled = false;
#mouseVerticalMultiply = 0;
#mouseHorizontalMultiply = 0;
private mouseButtonsPressed = 0;
private mouseWheelX = 0;
private mouseWheelY = 0;
#inputSink: XcloudInputSink | undefined;
private mouseVerticalMultiply = 0;
private mouseHorizontalMultiply = 0;
#$message?: HTMLElement;
private inputSink: XcloudInputSink | undefined;
private popup!: MkbPopup;
private constructor() {
super();
BxLogger.info(this.LOG_TAG, 'constructor()');
this.popup = MkbPopup.getInstance();
this.popup.attachMkbHandler(this);
}
#onKeyboardEvent(e: KeyboardEvent) {
private onKeyboardEvent(e: KeyboardEvent) {
if (e.type === 'keyup' && e.code === 'F8') {
e.preventDefault();
this.toggle();
@@ -54,110 +75,63 @@ export class NativeMkbHandler extends MkbHandler {
}
}
#onPointerLockRequested(e: Event) {
private onPointerLockRequested(e: Event) {
AppInterface.requestPointerCapture();
this.start();
}
#onPointerLockExited(e: Event) {
private onPointerLockExited(e: Event) {
AppInterface.releasePointerCapture();
this.stop();
}
#onPollingModeChanged = (e: Event) => {
if (!this.#$message) {
return;
}
const mode = (e as any).mode;
if (mode === 'none') {
this.#$message.classList.remove('bx-offscreen');
} else {
this.#$message.classList.add('bx-offscreen');
}
private onPollingModeChanged = (e: Event) => {
const move = window.BX_STREAM_SETTINGS.xCloudPollingMode !== 'none';
this.popup.moveOffscreen(move);
}
#onDialogShown = () => {
private onDialogShown = () => {
document.pointerLockElement && document.exitPointerLock();
}
#initMessage() {
if (!this.#$message) {
this.#$message = CE('div', {'class': 'bx-mkb-pointer-lock-msg'},
CE('div', {},
CE('p', {}, t('native-mkb')),
CE('p', {}, t('press-key-to-toggle-mkb', {key: 'F8'})),
),
CE('div', {'data-type': 'native'},
createButton({
style: ButtonStyle.PRIMARY | ButtonStyle.FULL_WIDTH | ButtonStyle.TALL,
label: t('activate'),
onClick: ((e: Event) => {
e.preventDefault();
e.stopPropagation();
this.toggle(true);
}).bind(this),
}),
createButton({
style: ButtonStyle.GHOST | ButtonStyle.FULL_WIDTH,
label: t('ignore'),
onClick: e => {
e.preventDefault();
e.stopPropagation();
this.#$message?.classList.add('bx-gone');
},
}),
),
);
}
if (!this.#$message.isConnected) {
document.documentElement.appendChild(this.#$message);
}
}
handleEvent(event: Event) {
switch (event.type) {
case 'keyup':
this.#onKeyboardEvent(event as KeyboardEvent);
this.onKeyboardEvent(event as KeyboardEvent);
break;
case BxEvent.XCLOUD_DIALOG_SHOWN:
this.#onDialogShown();
this.onDialogShown();
break;
case BxEvent.POINTER_LOCK_REQUESTED:
this.#onPointerLockRequested(event);
this.onPointerLockRequested(event);
break;
case BxEvent.POINTER_LOCK_EXITED:
this.#onPointerLockExited(event);
this.onPointerLockExited(event);
break;
case BxEvent.XCLOUD_POLLING_MODE_CHANGED:
this.#onPollingModeChanged(event);
this.onPollingModeChanged(event);
break;
}
}
init() {
this.#pointerClient = PointerClient.getInstance();
this.#inputSink = window.BX_EXPOSED.inputSink;
this.pointerClient = PointerClient.getInstance();
this.inputSink = window.BX_EXPOSED.inputSink;
// Stop keyboard input at startup
this.#updateInputConfigurationAsync(false);
this.updateInputConfigurationAsync(false);
try {
this.#pointerClient.start(STATES.pointerServerPort, this);
this.pointerClient.start(STATES.pointerServerPort, this);
} catch (e) {
Toast.show('Cannot enable Mouse & Keyboard feature');
}
this.#mouseVerticalMultiply = getPref(PrefKey.NATIVE_MKB_SCROLL_VERTICAL_SENSITIVITY);
this.#mouseHorizontalMultiply = getPref(PrefKey.NATIVE_MKB_SCROLL_HORIZONTAL_SENSITIVITY);
this.mouseVerticalMultiply = getPref(PrefKey.NATIVE_MKB_SCROLL_VERTICAL_SENSITIVITY);
this.mouseHorizontalMultiply = getPref(PrefKey.NATIVE_MKB_SCROLL_HORIZONTAL_SENSITIVITY);
window.addEventListener('keyup', this);
@@ -166,14 +140,13 @@ export class NativeMkbHandler extends MkbHandler {
window.addEventListener(BxEvent.POINTER_LOCK_EXITED, this);
window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this);
this.#initMessage();
if (AppInterface) {
Toast.show(t('press-key-to-toggle-mkb', {key: `<b>F8</b>`}), t('native-mkb'), {html: true});
this.#$message?.classList.add('bx-gone');
} else {
this.#$message?.classList.remove('bx-gone');
const shortcutKey = StreamSettings.findKeyboardShortcut(ShortcutAction.MKB_TOGGLE);
if (shortcutKey) {
const msg = t('press-key-to-toggle-mkb', { key: `<b>${KeyHelper.codeToKeyName(shortcutKey)}</b>` });
Toast.show(msg, t('native-mkb'), { html: true });
}
this.waitForMouseData(false);
}
toggle(force?: boolean) {
@@ -181,7 +154,7 @@ export class NativeMkbHandler extends MkbHandler {
if (typeof force !== 'undefined') {
setEnable = force;
} else {
setEnable = !this.#enabled;
setEnable = !this.enabled;
}
if (setEnable) {
@@ -191,7 +164,7 @@ export class NativeMkbHandler extends MkbHandler {
}
}
#updateInputConfigurationAsync(enabled: boolean) {
private updateInputConfigurationAsync(enabled: boolean) {
window.BX_EXPOSED.streamSession.updateInputConfigurationAsync({
enableKeyboardInput: enabled,
enableMouseInput: enabled,
@@ -201,27 +174,27 @@ export class NativeMkbHandler extends MkbHandler {
}
start() {
this.#resetMouseInput();
this.#enabled = true;
this.resetMouseInput();
this.enabled = true;
this.#updateInputConfigurationAsync(true);
this.updateInputConfigurationAsync(true);
window.BX_EXPOSED.stopTakRendering = true;
this.#$message?.classList.add('bx-gone');
this.waitForMouseData(false);
Toast.show(t('native-mkb'), t('enabled'), {instant: true});
}
stop() {
this.#resetMouseInput();
this.#enabled = false;
this.#updateInputConfigurationAsync(false);
this.resetMouseInput();
this.enabled = false;
this.updateInputConfigurationAsync(false);
this.#$message?.classList.remove('bx-gone');
this.waitForMouseData(true);
}
destroy(): void {
this.#pointerClient?.stop();
this.pointerClient?.stop();
window.removeEventListener('keyup', this);
window.removeEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this);
@@ -229,16 +202,16 @@ export class NativeMkbHandler extends MkbHandler {
window.removeEventListener(BxEvent.POINTER_LOCK_EXITED, this);
window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this);
this.#$message?.classList.add('bx-gone');
this.waitForMouseData(false);
}
handleMouseMove(data: MkbMouseMove): void {
this.#sendMouseInput({
this.sendMouseInput({
X: data.movementX,
Y: data.movementY,
Buttons: this.#mouseButtonsPressed,
WheelX: this.#mouseWheelX,
WheelY: this.#mouseWheelY,
Buttons: this.mouseButtonsPressed,
WheelX: this.mouseWheelX,
WheelY: this.mouseWheelY,
});
}
@@ -246,71 +219,72 @@ export class NativeMkbHandler extends MkbHandler {
const { pointerButton, pressed } = data;
if (pressed) {
this.#mouseButtonsPressed |= pointerButton!;
this.mouseButtonsPressed |= pointerButton!;
} else {
this.#mouseButtonsPressed ^= pointerButton!;
this.mouseButtonsPressed ^= pointerButton!;
}
this.#mouseButtonsPressed = Math.max(0, this.#mouseButtonsPressed);
this.mouseButtonsPressed = Math.max(0, this.mouseButtonsPressed);
this.#sendMouseInput({
this.sendMouseInput({
X: 0,
Y: 0,
Buttons: this.#mouseButtonsPressed,
WheelX: this.#mouseWheelX,
WheelY: this.#mouseWheelY,
Buttons: this.mouseButtonsPressed,
WheelX: this.mouseWheelX,
WheelY: this.mouseWheelY,
});
}
handleMouseWheel(data: MkbMouseWheel): boolean {
const { vertical, horizontal } = data;
this.#mouseWheelX = horizontal;
if (this.#mouseHorizontalMultiply && this.#mouseHorizontalMultiply !== 1) {
this.#mouseWheelX *= this.#mouseHorizontalMultiply;
this.mouseWheelX = horizontal;
if (this.mouseHorizontalMultiply && this.mouseHorizontalMultiply !== 1) {
this.mouseWheelX *= this.mouseHorizontalMultiply;
}
this.#mouseWheelY = vertical;
if (this.#mouseVerticalMultiply && this.#mouseVerticalMultiply !== 1) {
this.#mouseWheelY *= this.#mouseVerticalMultiply;
this.mouseWheelY = vertical;
if (this.mouseVerticalMultiply && this.mouseVerticalMultiply !== 1) {
this.mouseWheelY *= this.mouseVerticalMultiply;
}
this.#sendMouseInput({
this.sendMouseInput({
X: 0,
Y: 0,
Buttons: this.#mouseButtonsPressed,
WheelX: this.#mouseWheelX,
WheelY: this.#mouseWheelY,
Buttons: this.mouseButtonsPressed,
WheelX: this.mouseWheelX,
WheelY: this.mouseWheelY,
});
return true;
}
setVerticalScrollMultiplier(vertical: number) {
this.#mouseVerticalMultiply = vertical;
this.mouseVerticalMultiply = vertical;
}
setHorizontalScrollMultiplier(horizontal: number) {
this.#mouseHorizontalMultiply = horizontal;
this.mouseHorizontalMultiply = horizontal;
}
waitForMouseData(enabled: boolean): void {
waitForMouseData(showPopup: boolean) {
this.popup.toggleVisibility(showPopup);
}
isEnabled(): boolean {
return this.#enabled;
return this.enabled;
}
#sendMouseInput(data: NativeMouseData) {
private sendMouseInput(data: NativeMouseData) {
data.Type = 0; // Relative
this.#inputSink?.onMouseInput(data);
this.inputSink?.onMouseInput(data);
}
#resetMouseInput() {
this.#mouseButtonsPressed = 0;
this.#mouseWheelX = 0;
this.#mouseWheelY = 0;
private resetMouseInput() {
this.mouseButtonsPressed = 0;
this.mouseWheelX = 0;
this.mouseWheelY = 0;
this.#sendMouseInput({
this.sendMouseInput({
X: 0,
Y: 0,
Buttons: 0,

0
src/modules/mkb/pointer-client.ts Normal file → Executable file
View File

131
src/modules/patcher.ts Normal file → Executable file
View File

@@ -1,6 +1,5 @@
import { AppInterface, SCRIPT_VERSION, STATES } from "@utils/global";
import { SCRIPT_VERSION, STATES } from "@utils/global";
import { BX_FLAGS } from "@utils/bx-flags";
import { VibrationManager } from "@modules/vibration-manager";
import { BxLogger } from "@utils/bx-logger";
import { hashCode, renderString } from "@utils/utils";
import { BxEvent } from "@/utils/bx-event";
@@ -13,13 +12,14 @@ import codeRemotePlayEnable from "./patches/remote-play-enable.js" with { type:
import codeRemotePlayKeepAlive from "./patches/remote-play-keep-alive.js" with { type: "text" };
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, StreamTouchController } from "@/utils/settings-storages/global-settings-storage";
import { GamePassCloudGallery } from "@/enums/game-pass-gallery.js";
import { t } from "@/utils/translation.js";
import { PrefKey, StorageKey } from "@/enums/pref-keys.js";
import { getPref } from "@/utils/settings-storages/global-settings-storage";
import { GamePassCloudGallery } from "@/enums/game-pass-gallery";
import { t } from "@/utils/translation";
import { NativeMkbMode, TouchControllerMode, UiLayout, UiSection } from "@/enums/pref-values";
type PatchArray = (keyof typeof PATCHES)[];
type PathName = keyof typeof PATCHES;
type PatchArray = PathName[];
class PatcherUtils {
static indexOf(txt: string, searchString: string, startIndex: number, maxRange: number): number {
@@ -117,7 +117,7 @@ const PATCHES = {
return false;
}
const layout = getPref(PrefKey.UI_LAYOUT) === 'tv' ? 'tv' : 'default';
const layout = getPref<UiLayout>(PrefKey.UI_LAYOUT) === UiLayout.TV ? UiLayout.TV : UiLayout.DEFAULT;
return str.replace(text, `?"${layout}":"${layout}"`);
},
@@ -211,7 +211,7 @@ const PATCHES = {
// Patch polling rate
const tmp = str.substring(setTimeoutIndex, setTimeoutIndex + 150);
const tmpPatched = tmp.replaceAll('Math.max(0,4-', 'Math.max(0,window.BX_CONTROLLER_POLLING_RATE-');
const tmpPatched = tmp.replaceAll('Math.max(0,4-', 'Math.max(0,window.BX_STREAM_SETTINGS.controllerPollingRate - ');
str = PatcherUtils.replaceWith(str, setTimeoutIndex, tmp, tmpPatched);
// Block gamepad stats collecting
@@ -268,7 +268,6 @@ logFunc(logTag, '//', logMessage);
return false;
}
VibrationManager.updateGlobalVars();
str = str.replaceAll(text, text + codeVibrationAdjust);
return str;
},
@@ -419,9 +418,9 @@ if (window.BX_EXPOSED.stopTakRendering) {
}
let autoOffCode = '';
if (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === StreamTouchController.OFF) {
if (getPref<TouchControllerMode>(PrefKey.TOUCH_CONTROLLER_MODE) === TouchControllerMode.OFF) {
autoOffCode = 'return;';
} else if (getPref(PrefKey.STREAM_TOUCH_CONTROLLER_AUTO_OFF)) {
} else if (getPref(PrefKey.TOUCH_CONTROLLER_AUTO_OFF)) {
autoOffCode = `
const gamepads = window.navigator.getGamepads();
let gamepadFound = false;
@@ -476,7 +475,7 @@ e.guideUI = null;
`;
// Remove the TAK Edit button when the touch controller is disabled
if (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === StreamTouchController.OFF) {
if (getPref<TouchControllerMode>(PrefKey.TOUCH_CONTROLLER_MODE) === TouchControllerMode.OFF) {
newCode += 'e.canShowTakHUD = false;';
}
@@ -491,7 +490,8 @@ e.guideUI = null;
}
const newCode = `
BxEvent.dispatch(window, BxEvent.XCLOUD_POLLING_MODE_CHANGED, {mode: e.toLowerCase()});
window.BX_STREAM_SETTINGS.xCloudPollingMode = e.toLowerCase();
BxEvent.dispatch(window, BxEvent.XCLOUD_POLLING_MODE_CHANGED);
`;
str = str.replace(text, text + newCode);
return str;
@@ -587,7 +587,7 @@ BxLogger.info('patchRemotePlayMkb', ${configsVar});
return false;
}
const opacity = (getPref(PrefKey.STREAM_TOUCH_CONTROLLER_DEFAULT_OPACITY) / 100).toFixed(1);
const opacity = (getPref<TouchControllerDefaultOpacity>(PrefKey.TOUCH_CONTROLLER_DEFAULT_OPACITY) / 100).toFixed(1);
const newCode = `opacityMultiplier: ${opacity}`;
str = str.replace(text, newCode);
return str;
@@ -648,7 +648,16 @@ true` + text;
},
enableNativeMkb(str: string) {
let text = 'e.mouseSupported&&e.keyboardSupported&&e.fullscreenSupported;';
// l = t.mouseSupported && t.keyboardSupported && t.fullscreenSupported;
let index = str.indexOf('.mouseSupported&&');
if (index < 0) {
return false;
}
// Get the variable name "t"
const varName = str.charAt(index - 1);
// Find the full text
let text = `${varName}.mouseSupported&&${varName}.keyboardSupported&&${varName}.fullscreenSupported;`;
if ((!str.includes(text))) {
return false;
}
@@ -827,7 +836,7 @@ true` + text;
return false;
}
const PREF_HIDE_SECTIONS = getPref(PrefKey.UI_HIDE_SECTIONS) as UiSection[];
const PREF_HIDE_SECTIONS = getPref<UiSection[]>(PrefKey.UI_HIDE_SECTIONS);
const siglIds: GamePassCloudGallery[] = [];
const sections: PartialRecord<UiSection, GamePassCloudGallery> = {
@@ -906,31 +915,19 @@ 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",');
index >= 0 && (index = PatcherUtils.lastIndexOf('return', str, index, 200));
if (index < 0) {
return false;
}
index = str.indexOf('return', index - 40);
if (index < 0) {
return false;
}
str = str.substring(0, index) + 'BxEvent.dispatch(window, BxEvent.XCLOUD_RENDERING_COMPONENT, {component: "product-details"});' + str.substring(index);
str = str.substring(0, index) + 'BxEvent.dispatch(window, BxEvent.XCLOUD_RENDERING_COMPONENT, { component: "product-details" });' + str.substring(index);
return str;
},
detectBrowserRouterReady(str: string) {
let text = 'BrowserRouter:()=>';
if (!str.includes(text)) {
return false;
}
let index = str.indexOf('{history:this.history,');
if (index < 0) {
return false;
}
index = PatcherUtils.lastIndexOf(str, 'return', index, 100);
index >= 0 && (index = PatcherUtils.lastIndexOf(str, 'return', index, 100));
if (index < 0) {
return false;
}
@@ -998,10 +995,8 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) {
};
let PATCH_ORDERS: PatchArray = [
...(getPref(PrefKey.NATIVE_MKB_ENABLED) === 'on' ? [
...(getPref<NativeMkbMode>(PrefKey.NATIVE_MKB_MODE) === NativeMkbMode.ON ? [
'enableNativeMkb',
'patchMouseAndKeyboardEnabled',
'disableNativeRequestPointerLock',
'exposeInputSink',
] : []),
@@ -1023,19 +1018,19 @@ let PATCH_ORDERS: PatchArray = [
'guideAchievementsDefaultLocked',
'enableTvRoutes',
AppInterface && 'detectProductDetailsPage',
// AppInterface && 'detectProductDetailsPage',
'supportLocalCoOp',
'overrideStorageGetSettings',
getPref(PrefKey.UI_GAME_CARD_SHOW_WAIT_TIME) && 'patchSetCurrentlyFocusedInteractable',
getPref(PrefKey.UI_LAYOUT) !== 'default' && 'websiteLayout',
getPref(PrefKey.LOCAL_CO_OP_ENABLED) && 'supportLocalCoOp',
getPref<UiLayout>(PrefKey.UI_LAYOUT) !== UiLayout.DEFAULT && 'websiteLayout',
getPref(PrefKey.GAME_FORTNITE_FORCE_CONSOLE) && 'forceFortniteConsole',
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<UiSection>(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.FRIENDS) && 'ignorePlayWithFriendsSection',
getPref<UiSection>(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.ALL_GAMES) && 'ignoreAllGamesSection',
getPref<UiSection>(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.TOUCH) && 'ignorePlayWithTouchSection',
(getPref<UiSection>(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.NATIVE_MKB) || getPref<UiSection>(PrefKey.UI_HIDE_SECTIONS).includes(UiSection.MOST_POPULAR)) && 'ignoreSiglSections',
...(STATES.userAgent.capabilities.touch ? [
'disableTouchContextMenu',
@@ -1064,9 +1059,11 @@ let PATCH_ORDERS: PatchArray = [
'enableConsoleLogging',
'enableXcloudLogger',
] : []),
].filter(item => !!item);
].filter((item): item is string => !!item) as PatchArray;
// Only when playing
// TODO: check this
// @ts-ignore
let PLAYING_PATCH_ORDERS: PatchArray = [
'patchXcloudTitleInfo',
'disableGamepadDisconnectedScreen',
@@ -1078,18 +1075,18 @@ let PLAYING_PATCH_ORDERS: PatchArray = [
// 'exposeEventTarget',
// Patch volume control for normal stream
getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && !getPref(PrefKey.STREAM_COMBINE_SOURCES) && 'patchAudioMediaStream',
getPref(PrefKey.AUDIO_VOLUME_CONTROL_ENABLED) && !getPref(PrefKey.STREAM_COMBINE_SOURCES) && 'patchAudioMediaStream',
// Patch volume control for combined audio+video stream
getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && getPref(PrefKey.STREAM_COMBINE_SOURCES) && 'patchCombinedAudioVideoMediaStream',
getPref(PrefKey.AUDIO_VOLUME_CONTROL_ENABLED) && getPref(PrefKey.STREAM_COMBINE_SOURCES) && 'patchCombinedAudioVideoMediaStream',
// Skip feedback dialog
getPref(PrefKey.STREAM_DISABLE_FEEDBACK_DIALOG) && 'skipFeedbackDialog',
getPref(PrefKey.UI_DISABLE_FEEDBACK_DIALOG) && 'skipFeedbackDialog',
...(STATES.userAgent.capabilities.touch ? [
getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === StreamTouchController.ALL && 'patchShowSensorControls',
getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === StreamTouchController.ALL && 'exposeTouchLayoutManager',
(getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === StreamTouchController.OFF || getPref(PrefKey.STREAM_TOUCH_CONTROLLER_AUTO_OFF) || !STATES.userAgent.capabilities.touch) && 'disableTakRenderer',
getPref(PrefKey.STREAM_TOUCH_CONTROLLER_DEFAULT_OPACITY) !== 100 && 'patchTouchControlDefaultOpacity',
getPref<TouchControllerMode>(PrefKey.TOUCH_CONTROLLER_MODE) === TouchControllerMode.ALL && 'patchShowSensorControls',
getPref<TouchControllerMode>(PrefKey.TOUCH_CONTROLLER_MODE) === TouchControllerMode.ALL && 'exposeTouchLayoutManager',
(getPref<TouchControllerMode>(PrefKey.TOUCH_CONTROLLER_MODE) === TouchControllerMode.OFF || getPref(PrefKey.TOUCH_CONTROLLER_AUTO_OFF) || !STATES.userAgent.capabilities.touch) && 'disableTakRenderer',
getPref<TouchControllerDefaultOpacity>(PrefKey.TOUCH_CONTROLLER_DEFAULT_OPACITY) !== 100 && 'patchTouchControlDefaultOpacity',
'patchBabylonRendererClass',
] : []),
@@ -1103,7 +1100,13 @@ let PLAYING_PATCH_ORDERS: PatchArray = [
'patchRemotePlayMkb',
'remotePlayConnectMode',
] : []),
].filter(item => !!item);
// Native MKB
...(getPref<NativeMkbMode>(PrefKey.NATIVE_MKB_MODE) === NativeMkbMode.ON ? [
'patchMouseAndKeyboardEnabled',
'disableNativeRequestPointerLock',
] : []),
].filter((item): item is string => !!item);
const ALL_PATCHES = [...PATCH_ORDERS, ...PLAYING_PATCH_ORDERS];
@@ -1138,7 +1141,7 @@ export class Patcher {
const orgFunc = this;
const newFunc = (a: any, item: any) => {
Patcher.patch(item);
Patcher.checkChunks(item);
orgFunc(a, item);
}
@@ -1147,20 +1150,22 @@ export class Patcher {
};
}
static patch(item: [[number], { [key: string]: () => {} }]) {
static checkChunks(item: [[number], { [key: string]: () => {} }]) {
// !!! Use "caches" as variable name will break touch controller???
// console.log('patch', '-----');
let patchesToCheck: PatchArray;
let appliedPatches: PatchArray;
const chunkData = item[1];
const patchesMap: Record<string, PatchArray> = {};
const patcherCache = PatcherCache.getInstance();
for (let id in item[1]) {
for (const chunkId in chunkData) {
appliedPatches = [];
const cachedPatches = patcherCache.getPatches(id);
const cachedPatches = patcherCache.getPatches(chunkId);
if (cachedPatches) {
// clone cachedPatches
patchesToCheck = cachedPatches.slice(0);
patchesToCheck.push(...PATCH_ORDERS);
} else {
@@ -1172,7 +1177,7 @@ export class Patcher {
continue;
}
const func = item[1][id];
const func = chunkData[chunkId];
const funcStr = func.toString();
let patchedFuncStr = funcStr;
@@ -1211,7 +1216,7 @@ export class Patcher {
// Apply patched functions
if (modified) {
try {
item[1][id] = eval(patchedFuncStr);
chunkData[chunkId] = eval(patchedFuncStr);
} catch (e: unknown) {
if (e instanceof Error) {
BxLogger.error(LOG_TAG, 'Error', appliedPatches, e.message, patchedFuncStr);
@@ -1221,7 +1226,7 @@ export class Patcher {
// Save to cache
if (appliedPatches.length) {
patchesMap[id] = appliedPatches;
patchesMap[chunkId] = appliedPatches;
}
}
@@ -1239,10 +1244,10 @@ export class PatcherCache {
private static instance: PatcherCache;
public static getInstance = () => PatcherCache.instance ?? (PatcherCache.instance = new PatcherCache());
private readonly KEY_CACHE = 'better_xcloud_patches_cache';
private readonly KEY_SIGNATURE = 'better_xcloud_patches_cache_signature';
private readonly KEY_CACHE = StorageKey.PATCHES_CACHE;
private readonly KEY_SIGNATURE = StorageKey.PATCHES_SIGNATURE;
private CACHE: any;
private CACHE!: { [key: string]: PatchArray };
private isInitialized = false;

2
src/modules/patches/controller-shortcuts.js Normal file → Executable file
View File

@@ -85,7 +85,7 @@ if (btnHome) {
this.inputSink.onGamepadInput(performance.now() - intervalMs, fakeGamepadMappings);
} else {
intervalMs = window.BX_CONTROLLER_POLLING_RATE;
intervalMs = window.BX_STREAM_SETTINGS.controllerPollingRate;
}
}

0
src/modules/patches/expose-stream-session.js Normal file → Executable file
View File

40
src/modules/patches/local-co-op-enable.js Normal file → Executable file
View File

@@ -1,21 +1,57 @@
// Save the original onGamepadChanged() and onGamepadInput()
this.orgOnGamepadChanged = this.onGamepadChanged;
this.orgOnGamepadInput = this.onGamepadInput;
let match;
let onGamepadChangedStr = this.onGamepadChanged.toString();
// Fix problem with Safari
if (onGamepadChangedStr.startsWith('function ')) {
onGamepadChangedStr = onGamepadChangedStr.substring(9);
}
onGamepadChangedStr = onGamepadChangedStr.replaceAll('0', 'arguments[1]');
eval(`this.onGamepadChanged = function ${onGamepadChangedStr}`);
eval(`this.patchedOnGamepadChanged = function ${onGamepadChangedStr}`);
let onGamepadInputStr = this.onGamepadInput.toString();
// Fix problem with Safari
if (onGamepadInputStr.startsWith('function ')) {
onGamepadInputStr = onGamepadInputStr.substring(9);
}
match = onGamepadInputStr.match(/(\w+\.GamepadIndex)/);
if (match) {
const gamepadIndexVar = match[0];
onGamepadInputStr = onGamepadInputStr.replace('this.gamepadStates.get(', `this.gamepadStates.get(${gamepadIndexVar},`);
eval(`this.onGamepadInput = function ${onGamepadInputStr}`);
eval(`this.patchedOnGamepadInput = function ${onGamepadInputStr}`);
BxLogger.info('supportLocalCoOp', '✅ Successfully patched local co-op support');
} else {
BxLogger.error('supportLocalCoOp', '❌ Unable to patch local co-op support');
}
// Add method to switch between patched and original methods
this.toggleLocalCoOp = enable => {
BxLogger.info('toggleLocalCoOp', enable ? 'Enabled' : 'Disabled');
this.onGamepadChanged = enable ? this.patchedOnGamepadChanged : this.orgOnGamepadChanged;
this.onGamepadInput = enable ? this.patchedOnGamepadInput : this.orgOnGamepadInput;
// Reconnect all gamepads
const gamepads = window.navigator.getGamepads();
for (const gamepad of gamepads) {
if (!gamepad?.connected) {
continue;
}
// Ignore virtual controller
if (gamepad.id.includes('Better xCloud')) {
continue;
}
window.dispatchEvent(new GamepadEvent('gamepaddisconnected', { gamepad }));
window.dispatchEvent(new GamepadEvent('gamepadconnected', { gamepad }));
}
};
// Expose this method
window.BX_EXPOSED.toggleLocalCoOp = this.toggleLocalCoOp.bind(this);

0
src/modules/patches/remote-play-enable.js Normal file → Executable file
View File

Some files were not shown because too many files have changed in this diff Show More