mirror of
https://github.com/redphx/better-xcloud.git
synced 2025-07-04 13:21:43 +02:00
Compare commits
16 Commits
Author | SHA1 | Date | |
---|---|---|---|
6a8eecab06 | |||
640dd2fb5a | |||
30bb8cfbeb | |||
42b57a2cf8 | |||
210fdfbabe | |||
dbbdc48aab | |||
66123bc4ef | |||
2ecd995e47 | |||
0e03d4dc32 | |||
5b4088cc81 | |||
1f3e4b8250 | |||
daf3f72736 | |||
fbebb12965 | |||
43ef2b7cd0 | |||
e1eca20792 | |||
c2d8f1fbf7 |
2
dist/better-xcloud.meta.js
vendored
2
dist/better-xcloud.meta.js
vendored
@ -1,5 +1,5 @@
|
|||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name Better xCloud
|
// @name Better xCloud
|
||||||
// @namespace https://github.com/redphx
|
// @namespace https://github.com/redphx
|
||||||
// @version 5.4.0
|
// @version 5.4.1
|
||||||
// ==/UserScript==
|
// ==/UserScript==
|
||||||
|
182
dist/better-xcloud.user.js
vendored
182
dist/better-xcloud.user.js
vendored
File diff suppressed because one or more lines are too long
@ -69,7 +69,7 @@
|
|||||||
height: var(--bx-button-height);
|
height: var(--bx-button-height);
|
||||||
|
|
||||||
&:not(:only-child) {
|
&:not(:only-child) {
|
||||||
margin-right: 4px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,3 +117,7 @@ button.bx-inactive {
|
|||||||
opacity: 0.2;
|
opacity: 0.2;
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bx-button-shortcut {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
@ -203,3 +203,23 @@
|
|||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bx-debug-info {
|
||||||
|
button {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
margin-top: 10px;
|
||||||
|
cursor: copy;
|
||||||
|
color: white;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid #2d2d2d;
|
||||||
|
background: #212121;
|
||||||
|
white-space: break-spaces;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #272727;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
--bx-monospaced-font: Consolas, "Courier New", Courier, monospace;
|
--bx-monospaced-font: Consolas, "Courier New", Courier, monospace;
|
||||||
--bx-promptfont-font: promptfont;
|
--bx-promptfont-font: promptfont;
|
||||||
|
|
||||||
--bx-button-height: 36px;
|
--bx-button-height: 40px;
|
||||||
|
|
||||||
--bx-default-button-color: #2d3036;
|
--bx-default-button-color: #2d3036;
|
||||||
--bx-default-button-hover-color: #515863;
|
--bx-default-button-hover-color: #515863;
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
.bx-stream-settings-dialog {
|
.bx-stream-settings-dialog {
|
||||||
display: flex;
|
display: flex;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
z-index: var(--bx-stream-settings-z-index);
|
z-index: var(--bx-stream-settings-z-index);
|
||||||
opacity: 0.98;
|
opacity: 0.98;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
@ -22,10 +25,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.bx-stream-settings-tabs {
|
.bx-stream-settings-tabs {
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
right: 420px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
position: fixed;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
border-radius: 0 0 0 8px;
|
border-radius: 0 0 0 8px;
|
||||||
box-shadow: 0px 0px 6px #000;
|
box-shadow: 0px 0px 6px #000;
|
||||||
@ -60,10 +61,6 @@
|
|||||||
|
|
||||||
.bx-stream-settings-tab-contents {
|
.bx-stream-settings-tab-contents {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
position: fixed;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
padding: 14px 14px 0;
|
padding: 14px 14px 0;
|
||||||
width: 420px;
|
width: 420px;
|
||||||
background: #1a1b1e;
|
background: #1a1b1e;
|
||||||
@ -74,6 +71,8 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
box-shadow: 0px 0px 6px #000;
|
box-shadow: 0px 0px 6px #000;
|
||||||
overflow: overlay;
|
overflow: overlay;
|
||||||
|
margin-left: 56px;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
> div[data-tab-group=mkb] {
|
> div[data-tab-group=mkb] {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -108,6 +107,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 500px) {
|
||||||
|
.bx-stream-settings-tab-contents {
|
||||||
|
width: calc(100vw - 56px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.bx-stream-settings-row {
|
.bx-stream-settings-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
4
src/assets/svg/create-shortcut.svg
Normal file
4
src/assets/svg/create-shortcut.svg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='#fff' fill-rule='evenodd' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 32 32'>
|
||||||
|
<path d="M13.253 3.639c0-.758-.615-1.373-1.373-1.373H3.639c-.758 0-1.373.615-1.373 1.373v8.241c0 .758.615 1.373 1.373 1.373h8.241c.758 0 1.373-.615 1.373-1.373V3.639zm0 16.481c0-.758-.615-1.373-1.373-1.373H3.639c-.758 0-1.373.615-1.373 1.373v8.241c0 .758.615 1.373 1.373 1.373h8.241c.758 0 1.373-.615 1.373-1.373V20.12zm16.481 0c0-.758-.615-1.373-1.373-1.373H20.12c-.758 0-1.373.615-1.373 1.373v8.241c0 .758.615 1.373 1.373 1.373h8.241c.758 0 1.373-.615 1.373-1.373V20.12zM19.262 7.76h9.957"/>
|
||||||
|
<path d="M24.24 2.781v9.957"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 711 B |
@ -35,6 +35,7 @@ import { updateVideoPlayer } from "./modules/stream/stream-settings-utils";
|
|||||||
import { UiSection } from "./enums/ui-sections";
|
import { UiSection } from "./enums/ui-sections";
|
||||||
import { HeaderSection } from "./modules/ui/header";
|
import { HeaderSection } from "./modules/ui/header";
|
||||||
import { GameTile } from "./modules/ui/game-tile";
|
import { GameTile } from "./modules/ui/game-tile";
|
||||||
|
import { ProductDetailsPage } from "./modules/ui/product-details";
|
||||||
|
|
||||||
|
|
||||||
// Handle login page
|
// Handle login page
|
||||||
@ -198,6 +199,13 @@ window.addEventListener(BxEvent.STREAM_ERROR_PAGE, e => {
|
|||||||
BxEvent.dispatch(window, BxEvent.STREAM_STOPPED);
|
BxEvent.dispatch(window, BxEvent.STREAM_STOPPED);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
window.addEventListener(BxEvent.XCLOUD_RENDERING_COMPONENT, e => {
|
||||||
|
const component = (e as any).component;
|
||||||
|
if (component === 'product-details') {
|
||||||
|
ProductDetailsPage.injectShortcutButton();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function unload() {
|
function unload() {
|
||||||
if (!STATES.isPlaying) {
|
if (!STATES.isPlaying) {
|
||||||
return;
|
return;
|
||||||
|
@ -183,14 +183,14 @@ export class ControllerShortcut {
|
|||||||
$fragment.appendChild($option);
|
$fragment.appendChild($option);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$container.dataset.hasGamepad = hasGamepad.toString();
|
||||||
if (hasGamepad) {
|
if (hasGamepad) {
|
||||||
$select.appendChild($fragment);
|
$select.appendChild($fragment);
|
||||||
|
|
||||||
$select.selectedIndex = 0;
|
$select.selectedIndex = 0;
|
||||||
$select.dispatchEvent(new Event('change'));
|
$select.dispatchEvent(new Event('input'));
|
||||||
}
|
}
|
||||||
|
|
||||||
$container.dataset.hasGamepad = hasGamepad.toString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static #switchProfile(profile: string) {
|
static #switchProfile(profile: string) {
|
||||||
@ -205,9 +205,9 @@ export class ControllerShortcut {
|
|||||||
const $select = ControllerShortcut.#$selectActions[button as GamepadKey]!;
|
const $select = ControllerShortcut.#$selectActions[button as GamepadKey]!;
|
||||||
$select.value = actions[button] || '';
|
$select.value = actions[button] || '';
|
||||||
|
|
||||||
BxEvent.dispatch($select, 'change', {
|
BxEvent.dispatch($select, 'input', {
|
||||||
ignoreOnChange: true,
|
ignoreOnChange: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { SCRIPT_VERSION, STATES } from "@utils/global";
|
import { AppInterface, SCRIPT_VERSION, STATES } from "@utils/global";
|
||||||
import { BX_FLAGS } from "@utils/bx-flags";
|
import { BX_FLAGS } from "@utils/bx-flags";
|
||||||
import { getPref, PrefKey } from "@utils/preferences";
|
import { getPref, PrefKey } from "@utils/preferences";
|
||||||
import { VibrationManager } from "@modules/vibration-manager";
|
import { VibrationManager } from "@modules/vibration-manager";
|
||||||
@ -799,6 +799,22 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) {
|
|||||||
str = str.substring(0, index) + codeSetCurrentlyFocusedInteractable + str.substring(index);
|
str = str.substring(0, index) + codeSetCurrentlyFocusedInteractable + str.substring(index);
|
||||||
return str;
|
return str;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// product-details-page.js#2388, 24.17.20
|
||||||
|
detectProductDetailsPage(str: string) {
|
||||||
|
let index = str.indexOf('{location:"ProductDetailPage",');
|
||||||
|
if (index === -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
index = str.indexOf('return', index - 40);
|
||||||
|
if (index === -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
str = str.substring(0, index) + 'BxEvent.dispatch(window, BxEvent.XCLOUD_RENDERING_COMPONENT, {component: "product-details"});' + str.substring(index);
|
||||||
|
return str;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let PATCH_ORDERS: PatchArray = [
|
let PATCH_ORDERS: PatchArray = [
|
||||||
@ -820,6 +836,7 @@ let PATCH_ORDERS: PatchArray = [
|
|||||||
'exposeDialogRoutes',
|
'exposeDialogRoutes',
|
||||||
|
|
||||||
'enableTvRoutes',
|
'enableTvRoutes',
|
||||||
|
AppInterface && 'detectProductDetailsPage',
|
||||||
|
|
||||||
'overrideStorageGetSettings',
|
'overrideStorageGetSettings',
|
||||||
getPref(PrefKey.UI_GAME_CARD_SHOW_WAIT_TIME) && 'patchSetCurrentlyFocusedInteractable',
|
getPref(PrefKey.UI_GAME_CARD_SHOW_WAIT_TIME) && 'patchSetCurrentlyFocusedInteractable',
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import vertClarityBoost from "./shaders/clarity_boost.vert" with { type: "text" };
|
import vertClarityBoost from "./shaders/clarity_boost.vert" with { type: "text" };
|
||||||
import fsClarityBoost from "./shaders/clarity_boost.fs" with { type: "text" };
|
import fsClarityBoost from "./shaders/clarity_boost.fs" with { type: "text" };
|
||||||
import { BxLogger } from "@/utils/bx-logger";
|
import { BxLogger } from "@/utils/bx-logger";
|
||||||
|
import { getPref, PrefKey } from "@/utils/preferences";
|
||||||
|
|
||||||
|
|
||||||
const LOG_TAG = 'WebGL2Player';
|
const LOG_TAG = 'WebGL2Player';
|
||||||
@ -120,11 +121,13 @@ export class WebGL2Player {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#setupShaders() {
|
#setupShaders() {
|
||||||
|
BxLogger.info(LOG_TAG, 'Setting up', getPref(PrefKey.VIDEO_POWER_PREFERENCE));
|
||||||
|
|
||||||
const gl = this.#$canvas.getContext('webgl2', {
|
const gl = this.#$canvas.getContext('webgl2', {
|
||||||
isBx: true,
|
isBx: true,
|
||||||
antialias: true,
|
antialias: true,
|
||||||
alpha: false,
|
alpha: false,
|
||||||
powerPreference: 'high-performance',
|
powerPreference: getPref(PrefKey.VIDEO_POWER_PREFERENCE),
|
||||||
}) as WebGL2RenderingContext;
|
}) as WebGL2RenderingContext;
|
||||||
this.#gl = gl;
|
this.#gl = gl;
|
||||||
|
|
||||||
|
@ -260,9 +260,20 @@ export class StreamPlayer {
|
|||||||
this.#resizePlayer();
|
this.#resizePlayer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reloadPlayer() {
|
||||||
|
this.#cleanUpWebGL2Player();
|
||||||
|
|
||||||
|
this.#playerType = StreamPlayerType.VIDEO;
|
||||||
|
this.setPlayerType(StreamPlayerType.WEBGL2, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
#cleanUpWebGL2Player() {
|
||||||
|
// Clean up WebGL2 Player
|
||||||
|
this.#webGL2Player?.destroy();
|
||||||
|
this.#webGL2Player = null;
|
||||||
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
// Cleanup WebGL2 Player
|
this.#cleanUpWebGL2Player();
|
||||||
this.#webGL2Player?.destroy();
|
|
||||||
this.#webGL2Player = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ export function onChangeVideoPlayerType() {
|
|||||||
const playerType = getPref(PrefKey.VIDEO_PLAYER_TYPE);
|
const playerType = getPref(PrefKey.VIDEO_PLAYER_TYPE);
|
||||||
const $videoProcessing = document.getElementById('bx_setting_video_processing') as HTMLSelectElement;
|
const $videoProcessing = document.getElementById('bx_setting_video_processing') as HTMLSelectElement;
|
||||||
const $videoSharpness = document.getElementById('bx_setting_video_sharpness') as HTMLElement;
|
const $videoSharpness = document.getElementById('bx_setting_video_sharpness') as HTMLElement;
|
||||||
|
const $videoPowerPreference = document.getElementById('bx_setting_video_power_preference') as HTMLElement;
|
||||||
|
|
||||||
let isDisabled = false;
|
let isDisabled = false;
|
||||||
|
|
||||||
@ -28,6 +29,9 @@ export function onChangeVideoPlayerType() {
|
|||||||
$videoProcessing.disabled = isDisabled;
|
$videoProcessing.disabled = isDisabled;
|
||||||
$videoSharpness.dataset.disabled = isDisabled.toString();
|
$videoSharpness.dataset.disabled = isDisabled.toString();
|
||||||
|
|
||||||
|
// Hide Power Preference setting if renderer isn't WebGL2
|
||||||
|
$videoPowerPreference.closest('.bx-stream-settings-row')!.classList.toggle('bx-gone', playerType !== StreamPlayerType.WEBGL2);
|
||||||
|
|
||||||
updateVideoPlayer();
|
updateVideoPlayer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,6 +112,17 @@ export class StreamSettings {
|
|||||||
}, {
|
}, {
|
||||||
pref: PrefKey.VIDEO_PROCESSING,
|
pref: PrefKey.VIDEO_PROCESSING,
|
||||||
onChange: updateVideoPlayer,
|
onChange: updateVideoPlayer,
|
||||||
|
}, {
|
||||||
|
pref: PrefKey.VIDEO_POWER_PREFERENCE,
|
||||||
|
onChange: () => {
|
||||||
|
const streamPlayer = STATES.currentStream.streamPlayer;
|
||||||
|
if (!streamPlayer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
streamPlayer.reloadPlayer();
|
||||||
|
updateVideoPlayer();
|
||||||
|
},
|
||||||
}, {
|
}, {
|
||||||
pref: PrefKey.VIDEO_SHARPNESS,
|
pref: PrefKey.VIDEO_SHARPNESS,
|
||||||
onChange: updateVideoPlayer,
|
onChange: updateVideoPlayer,
|
||||||
@ -324,6 +335,9 @@ export class StreamSettings {
|
|||||||
(window as any).BX_EXPOSED.disableGamepadPolling = true;
|
(window as any).BX_EXPOSED.disableGamepadPolling = true;
|
||||||
|
|
||||||
BxEvent.dispatch(window, BxEvent.XCLOUD_DIALOG_SHOWN);
|
BxEvent.dispatch(window, BxEvent.XCLOUD_DIALOG_SHOWN);
|
||||||
|
|
||||||
|
// Update video's settings
|
||||||
|
onChangeVideoPlayerType();
|
||||||
}
|
}
|
||||||
|
|
||||||
hide() {
|
hide() {
|
||||||
@ -476,6 +490,11 @@ export class StreamSettings {
|
|||||||
$sibling && $sibling.focus();
|
$sibling && $sibling.focus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If it's the first/last item -> loop around
|
||||||
|
const pseudo = direction === NavigationDirection.UP ? 'last-of-type' : 'first-of-type';
|
||||||
|
const $target = this.$tabs!.querySelector(`svg:not(.bx-gone):${pseudo}`);
|
||||||
|
$target && ($target as HTMLElement).focus();
|
||||||
} else if (direction === NavigationDirection.RIGHT) {
|
} else if (direction === NavigationDirection.RIGHT) {
|
||||||
this.#focusFirstVisibleSetting();
|
this.#focusFirstVisibleSetting();
|
||||||
}
|
}
|
||||||
@ -513,6 +532,12 @@ export class StreamSettings {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If it's the first/last item -> loop around
|
||||||
|
// TODO: bugged if pseudo is "first-of-type" and the first setting is disabled
|
||||||
|
const pseudo = direction === NavigationDirection.UP ? ':last-of-type' : '';
|
||||||
|
const $target = this.$settings!.querySelector(`div[data-tab-group]:not(.bx-gone) div[data-focus-container]${pseudo} [tabindex="0"]:not(:disabled):last-of-type`);
|
||||||
|
$target && ($target as HTMLElement).focus();
|
||||||
} else if (direction === NavigationDirection.LEFT || direction === NavigationDirection.RIGHT) {
|
} else if (direction === NavigationDirection.LEFT || direction === NavigationDirection.RIGHT) {
|
||||||
// Find all child elements with tabindex
|
// Find all child elements with tabindex
|
||||||
const children = Array.from($parent.querySelectorAll('[tabindex="0"]'));
|
const children = Array.from($parent.querySelectorAll('[tabindex="0"]'));
|
||||||
@ -643,6 +668,15 @@ export class StreamSettings {
|
|||||||
this.hide();
|
this.hide();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Close dialog when not clicking on any child elements in the dialog
|
||||||
|
$container.addEventListener('click', e => {
|
||||||
|
if (e.target === $container) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
for (const settingTab of this.SETTINGS_UI) {
|
for (const settingTab of this.SETTINGS_UI) {
|
||||||
if (!settingTab) {
|
if (!settingTab) {
|
||||||
continue;
|
continue;
|
||||||
@ -753,8 +787,5 @@ export class StreamSettings {
|
|||||||
|
|
||||||
document.documentElement.appendChild($overlay);
|
document.documentElement.appendChild($overlay);
|
||||||
document.documentElement.appendChild($container);
|
document.documentElement.appendChild($container);
|
||||||
|
|
||||||
// Update video's settings
|
|
||||||
onChangeVideoPlayerType();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { STATES, AppInterface, SCRIPT_VERSION } from "@utils/global";
|
import { STATES, AppInterface, SCRIPT_VERSION, deepClone } from "@utils/global";
|
||||||
import { CE, createButton, ButtonStyle } from "@utils/html";
|
import { CE, createButton, ButtonStyle } from "@utils/html";
|
||||||
import { BxIcon } from "@utils/bx-icon";
|
import { BxIcon } from "@utils/bx-icon";
|
||||||
import { getPreferredServerRegion } from "@utils/region";
|
import { getPreferredServerRegion } from "@utils/region";
|
||||||
@ -9,6 +9,8 @@ import { PatcherCache } from "../patcher";
|
|||||||
import { UserAgentProfile } from "@enums/user-agent";
|
import { UserAgentProfile } from "@enums/user-agent";
|
||||||
import { BxSelectElement } from "@/web-components/bx-select";
|
import { BxSelectElement } from "@/web-components/bx-select";
|
||||||
import { StreamSettings } from "../stream/stream-settings";
|
import { StreamSettings } from "../stream/stream-settings";
|
||||||
|
import { BX_FLAGS } from "@/utils/bx-flags";
|
||||||
|
import { Toast } from "@/utils/toast";
|
||||||
|
|
||||||
const SETTINGS_UI = {
|
const SETTINGS_UI = {
|
||||||
'Better xCloud': {
|
'Better xCloud': {
|
||||||
@ -455,6 +457,47 @@ export function setupSettingsUi() {
|
|||||||
$wrapper.appendChild(CE('div', {'class': 'bx-settings-app-version'}, `xCloud website version ${appVersion} (${appDate})`));
|
$wrapper.appendChild(CE('div', {'class': 'bx-settings-app-version'}, `xCloud website version ${appVersion} (${appDate})`));
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
|
// Show Debug info
|
||||||
|
const debugInfo = deepClone(BX_FLAGS.DeviceInfo);
|
||||||
|
const debugSettings = [
|
||||||
|
PrefKey.STREAM_TARGET_RESOLUTION,
|
||||||
|
PrefKey.STREAM_CODEC_PROFILE,
|
||||||
|
|
||||||
|
PrefKey.VIDEO_PLAYER_TYPE,
|
||||||
|
PrefKey.VIDEO_PROCESSING,
|
||||||
|
PrefKey.VIDEO_SHARPNESS,
|
||||||
|
];
|
||||||
|
|
||||||
|
debugInfo['settings'] = {};
|
||||||
|
for (const key of debugSettings) {
|
||||||
|
debugInfo['settings'][key] = getPref(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
const $debugInfo = CE('div', {class: 'bx-debug-info'},
|
||||||
|
createButton({
|
||||||
|
label: 'Debug info',
|
||||||
|
style: ButtonStyle.GHOST | ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE,
|
||||||
|
onClick: e => {
|
||||||
|
console.log(e);
|
||||||
|
(e.target as HTMLElement).closest('button')?.nextElementSibling?.classList.toggle('bx-gone');
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
CE('pre', {
|
||||||
|
class: 'bx-gone',
|
||||||
|
on: {
|
||||||
|
click: async (e: Event) => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText((e.target as HTMLElement).innerText);
|
||||||
|
Toast.show('Copied to clipboard', '', {instant: true});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to copy: ', err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, '```\n' + JSON.stringify(debugInfo, null, ' ') + '\n```'),
|
||||||
|
);
|
||||||
|
$wrapper.appendChild($debugInfo);
|
||||||
|
|
||||||
$container.appendChild($wrapper);
|
$container.appendChild($wrapper);
|
||||||
|
|
||||||
// Add Settings UI to the web page
|
// Add Settings UI to the web page
|
||||||
|
@ -89,15 +89,15 @@ export class GuideMenu {
|
|||||||
// "Stream settings" button
|
// "Stream settings" button
|
||||||
buttons.push(GuideMenu.#BUTTONS.streamSetting);
|
buttons.push(GuideMenu.#BUTTONS.streamSetting);
|
||||||
|
|
||||||
// "App settings" & "Close app" buttons
|
// "App settings" button
|
||||||
if (AppInterface) {
|
AppInterface && buttons.push(GuideMenu.#BUTTONS.appSettings);
|
||||||
buttons.push(GuideMenu.#BUTTONS.appSettings);
|
|
||||||
buttons.push(GuideMenu.#BUTTONS.closeApp);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reload page
|
// "Reload page" button
|
||||||
buttons.push(GuideMenu.#BUTTONS.reloadPage);
|
buttons.push(GuideMenu.#BUTTONS.reloadPage);
|
||||||
|
|
||||||
|
// "Close app" buttons
|
||||||
|
AppInterface && buttons.push(GuideMenu.#BUTTONS.closeApp);
|
||||||
|
|
||||||
const $buttons = GuideMenu.#renderButtons(buttons);
|
const $buttons = GuideMenu.#renderButtons(buttons);
|
||||||
|
|
||||||
const $lastDivider = $dividers[$dividers.length - 1];
|
const $lastDivider = $dividers[$dividers.length - 1];
|
||||||
|
36
src/modules/ui/product-details.ts
Normal file
36
src/modules/ui/product-details.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { BX_FLAGS } from "@/utils/bx-flags";
|
||||||
|
import { BxIcon } from "@/utils/bx-icon";
|
||||||
|
import { AppInterface } from "@/utils/global";
|
||||||
|
import { ButtonStyle, createButton } from "@/utils/html";
|
||||||
|
import { t } from "@/utils/translation";
|
||||||
|
|
||||||
|
export class ProductDetailsPage {
|
||||||
|
private static $btnShortcut = createButton({
|
||||||
|
classes: ['bx-button-shortcut'],
|
||||||
|
icon: BxIcon.CREATE_SHORTCUT,
|
||||||
|
label: t('create-shortcut'),
|
||||||
|
style: ButtonStyle.FOCUSABLE,
|
||||||
|
tabIndex: 0,
|
||||||
|
onClick: e => {
|
||||||
|
AppInterface && AppInterface.createShortcut(window.location.pathname.substring(6));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
private static shortcutTimeoutId: number | null = null;
|
||||||
|
|
||||||
|
static injectShortcutButton() {
|
||||||
|
if (!AppInterface || BX_FLAGS.DeviceInfo?.deviceType !== 'android') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ProductDetailsPage.shortcutTimeoutId && clearTimeout(ProductDetailsPage.shortcutTimeoutId);
|
||||||
|
ProductDetailsPage.shortcutTimeoutId = window.setTimeout(() => {
|
||||||
|
// Find action buttons container
|
||||||
|
const $container = document.querySelector('div[class*=ActionButtons-module__container]');
|
||||||
|
if ($container) {
|
||||||
|
this.$btnShortcut.style.width = $container.getBoundingClientRect().width + 'px';
|
||||||
|
$container.parentElement?.appendChild(ProductDetailsPage.$btnShortcut);
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
}
|
@ -48,6 +48,8 @@ export enum BxEvent {
|
|||||||
XCLOUD_GUIDE_MENU_SHOWN = 'bx-xcloud-guide-menu-shown',
|
XCLOUD_GUIDE_MENU_SHOWN = 'bx-xcloud-guide-menu-shown',
|
||||||
|
|
||||||
XCLOUD_POLLING_MODE_CHANGED = 'bx-xcloud-polling-mode-changed',
|
XCLOUD_POLLING_MODE_CHANGED = 'bx-xcloud-polling-mode-changed',
|
||||||
|
|
||||||
|
XCLOUD_RENDERING_COMPONENT = 'bx-xcloud-rendering-page',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum XcloudEvent {
|
export enum XcloudEvent {
|
||||||
|
@ -11,6 +11,11 @@ type BxFlags = Partial<{
|
|||||||
FeatureGates: {[key: string]: boolean} | null,
|
FeatureGates: {[key: string]: boolean} | null,
|
||||||
|
|
||||||
IsSupportedTvBrowser: boolean,
|
IsSupportedTvBrowser: boolean,
|
||||||
|
|
||||||
|
DeviceInfo: Partial<{
|
||||||
|
deviceType: 'android' | 'android-tv' | 'webos' | 'unknown',
|
||||||
|
userAgent?: string,
|
||||||
|
}>,
|
||||||
}>
|
}>
|
||||||
|
|
||||||
// Setup flags
|
// Setup flags
|
||||||
@ -25,6 +30,10 @@ const DEFAULT_FLAGS: BxFlags = {
|
|||||||
|
|
||||||
ForceNativeMkbTitles: [],
|
ForceNativeMkbTitles: [],
|
||||||
FeatureGates: null,
|
FeatureGates: null,
|
||||||
|
|
||||||
|
DeviceInfo: {
|
||||||
|
deviceType: 'unknown',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BX_FLAGS: BxFlags = Object.assign(DEFAULT_FLAGS, window.BX_FLAGS || {});
|
export const BX_FLAGS: BxFlags = Object.assign(DEFAULT_FLAGS, window.BX_FLAGS || {});
|
||||||
@ -32,4 +41,8 @@ try {
|
|||||||
delete window.BX_FLAGS;
|
delete window.BX_FLAGS;
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
|
if (!BX_FLAGS.DeviceInfo!.userAgent) {
|
||||||
|
BX_FLAGS.DeviceInfo!.userAgent = window.navigator.userAgent;
|
||||||
|
}
|
||||||
|
|
||||||
export const NATIVE_FETCH = window.fetch;
|
export const NATIVE_FETCH = window.fetch;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import iconCommand from "@assets/svg/command.svg" with { type: "text" };
|
import iconCommand from "@assets/svg/command.svg" with { type: "text" };
|
||||||
import iconController from "@assets/svg/controller.svg" with { type: "text" };
|
import iconController from "@assets/svg/controller.svg" with { type: "text" };
|
||||||
import iconCopy from "@assets/svg/copy.svg" with { type: "text" };
|
import iconCopy from "@assets/svg/copy.svg" with { type: "text" };
|
||||||
|
import iconCreateShortcut from "@assets/svg/create-shortcut.svg" with { type: "text" };
|
||||||
import iconCursorText from "@assets/svg/cursor-text.svg" with { type: "text" };
|
import iconCursorText from "@assets/svg/cursor-text.svg" with { type: "text" };
|
||||||
import iconDisplay from "@assets/svg/display.svg" with { type: "text" };
|
import iconDisplay from "@assets/svg/display.svg" with { type: "text" };
|
||||||
import iconHome from "@assets/svg/home.svg" with { type: "text" };
|
import iconHome from "@assets/svg/home.svg" with { type: "text" };
|
||||||
@ -11,9 +12,9 @@ import iconRefresh from "@assets/svg/refresh.svg" with { type: "text" };
|
|||||||
import iconRemotePlay from "@assets/svg/remote-play.svg" with { type: "text" };
|
import iconRemotePlay from "@assets/svg/remote-play.svg" with { type: "text" };
|
||||||
import iconStreamSettings from "@assets/svg/stream-settings.svg" with { type: "text" };
|
import iconStreamSettings from "@assets/svg/stream-settings.svg" with { type: "text" };
|
||||||
import iconStreamStats from "@assets/svg/stream-stats.svg" with { type: "text" };
|
import iconStreamStats from "@assets/svg/stream-stats.svg" with { type: "text" };
|
||||||
import iconTrash from "@assets/svg/trash.svg" with { type: "text" };
|
|
||||||
import iconTouchControlEnable from "@assets/svg/touch-control-enable.svg" with { type: "text" };
|
|
||||||
import iconTouchControlDisable from "@assets/svg/touch-control-disable.svg" with { type: "text" };
|
import iconTouchControlDisable from "@assets/svg/touch-control-disable.svg" with { type: "text" };
|
||||||
|
import iconTouchControlEnable from "@assets/svg/touch-control-enable.svg" with { type: "text" };
|
||||||
|
import iconTrash from "@assets/svg/trash.svg" with { type: "text" };
|
||||||
import iconVirtualController from "@assets/svg/virtual-controller.svg" with { type: "text" };
|
import iconVirtualController from "@assets/svg/virtual-controller.svg" with { type: "text" };
|
||||||
|
|
||||||
// Game Bar
|
// Game Bar
|
||||||
@ -37,6 +38,7 @@ export const BxIcon = {
|
|||||||
STREAM_STATS: iconStreamStats,
|
STREAM_STATS: iconStreamStats,
|
||||||
COMMAND: iconCommand,
|
COMMAND: iconCommand,
|
||||||
CONTROLLER: iconController,
|
CONTROLLER: iconController,
|
||||||
|
CREATE_SHORTCUT: iconCreateShortcut,
|
||||||
DISPLAY: iconDisplay,
|
DISPLAY: iconDisplay,
|
||||||
HOME: iconHome,
|
HOME: iconHome,
|
||||||
NATIVE_MKB: iconNativeMkb,
|
NATIVE_MKB: iconNativeMkb,
|
||||||
|
@ -4,6 +4,8 @@ import { getPref, PrefKey } from "./preferences";
|
|||||||
export let FeatureGates: {[key: string]: boolean} = {
|
export let FeatureGates: {[key: string]: boolean} = {
|
||||||
'PwaPrompt': false,
|
'PwaPrompt': false,
|
||||||
'EnableWifiWarnings': false,
|
'EnableWifiWarnings': false,
|
||||||
|
'EnableUpdateRequiredPage': false,
|
||||||
|
'ShowForcedUpdateScreen': false,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Disable context menu in Home page
|
// Disable context menu in Home page
|
||||||
|
@ -35,18 +35,23 @@ function createElement<T=HTMLElement>(elmName: string, props: {[index: string]:
|
|||||||
if (hasNs) {
|
if (hasNs) {
|
||||||
$elm.setAttributeNS(null, key, props[key]);
|
$elm.setAttributeNS(null, key, props[key]);
|
||||||
} else {
|
} else {
|
||||||
$elm.setAttribute(key, props[key]);
|
if (key === 'on') {
|
||||||
|
for (const eventName in props[key]) {
|
||||||
|
$elm.addEventListener(eventName, props[key][eventName]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$elm.setAttribute(key, props[key]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 2, size = arguments.length; i < size; i++) {
|
for (let i = 2, size = arguments.length; i < size; i++) {
|
||||||
const arg = arguments[i];
|
const arg = arguments[i];
|
||||||
const argType = typeof arg;
|
|
||||||
|
|
||||||
if (argType === 'string' || argType === 'number') {
|
if (arg instanceof Node) {
|
||||||
$elm.appendChild(document.createTextNode(arg));
|
|
||||||
} else if (arg) {
|
|
||||||
$elm.appendChild(arg);
|
$elm.appendChild(arg);
|
||||||
|
} else if (arg !== null && typeof arg !== 'undefined') {
|
||||||
|
$elm.appendChild(document.createTextNode(arg));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import { StreamPlayerType, StreamVideoProcessing } from "@enums/stream-player";
|
|||||||
import { UserAgentProfile } from "@/enums/user-agent";
|
import { UserAgentProfile } from "@/enums/user-agent";
|
||||||
import { UiSection } from "@/enums/ui-sections";
|
import { UiSection } from "@/enums/ui-sections";
|
||||||
import { BypassServers } from "@/enums/bypass-servers";
|
import { BypassServers } from "@/enums/bypass-servers";
|
||||||
|
import { BX_FLAGS } from "./bx-flags";
|
||||||
|
|
||||||
export enum PrefKey {
|
export enum PrefKey {
|
||||||
LAST_UPDATE_CHECK = 'version_last_check',
|
LAST_UPDATE_CHECK = 'version_last_check',
|
||||||
@ -82,6 +83,7 @@ export enum PrefKey {
|
|||||||
|
|
||||||
VIDEO_PLAYER_TYPE = 'video_player_type',
|
VIDEO_PLAYER_TYPE = 'video_player_type',
|
||||||
VIDEO_PROCESSING = 'video_processing',
|
VIDEO_PROCESSING = 'video_processing',
|
||||||
|
VIDEO_POWER_PREFERENCE = 'video_power_preference',
|
||||||
VIDEO_SHARPNESS = 'video_sharpness',
|
VIDEO_SHARPNESS = 'video_sharpness',
|
||||||
VIDEO_RATIO = 'video_ratio',
|
VIDEO_RATIO = 'video_ratio',
|
||||||
VIDEO_BRIGHTNESS = 'video_brightness',
|
VIDEO_BRIGHTNESS = 'video_brightness',
|
||||||
@ -557,7 +559,7 @@ export class Preferences {
|
|||||||
|
|
||||||
[PrefKey.UI_CONTROLLER_FRIENDLY]: {
|
[PrefKey.UI_CONTROLLER_FRIENDLY]: {
|
||||||
label: t('controller-friendly-ui'),
|
label: t('controller-friendly-ui'),
|
||||||
default: false,
|
default: !STATES.browser.capabilities.touch || BX_FLAGS.DeviceInfo?.deviceType === "android-tv",
|
||||||
},
|
},
|
||||||
|
|
||||||
[PrefKey.UI_LAYOUT]: {
|
[PrefKey.UI_LAYOUT]: {
|
||||||
@ -610,7 +612,7 @@ export class Preferences {
|
|||||||
[PrefKey.USER_AGENT_PROFILE]: {
|
[PrefKey.USER_AGENT_PROFILE]: {
|
||||||
label: t('user-agent-profile'),
|
label: t('user-agent-profile'),
|
||||||
note: '⚠️ ' + t('unexpected-behavior'),
|
note: '⚠️ ' + t('unexpected-behavior'),
|
||||||
default: 'default',
|
default: BX_FLAGS.DeviceInfo?.deviceType === 'android-tv' ? UserAgentProfile.VR_OCULUS : 'default',
|
||||||
options: {
|
options: {
|
||||||
[UserAgentProfile.DEFAULT]: t('default'),
|
[UserAgentProfile.DEFAULT]: t('default'),
|
||||||
[UserAgentProfile.WINDOWS_EDGE]: 'Edge + Windows',
|
[UserAgentProfile.WINDOWS_EDGE]: 'Edge + Windows',
|
||||||
@ -637,6 +639,15 @@ export class Preferences {
|
|||||||
[StreamVideoProcessing.CAS]: t('amd-fidelity-cas'),
|
[StreamVideoProcessing.CAS]: t('amd-fidelity-cas'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
[PrefKey.VIDEO_POWER_PREFERENCE]: {
|
||||||
|
label: t('gpu-configuration'),
|
||||||
|
default: 'default',
|
||||||
|
options: {
|
||||||
|
'default': t('default'),
|
||||||
|
'high-performance': t('high-performance'),
|
||||||
|
'low-power': t('low-power'),
|
||||||
|
},
|
||||||
|
},
|
||||||
[PrefKey.VIDEO_SHARPNESS]: {
|
[PrefKey.VIDEO_SHARPNESS]: {
|
||||||
label: t('sharpness'),
|
label: t('sharpness'),
|
||||||
type: SettingElementType.NUMBER_STEPPER,
|
type: SettingElementType.NUMBER_STEPPER,
|
||||||
|
@ -76,6 +76,7 @@ const Texts = {
|
|||||||
"controller-shortcuts-xbox-note": "Button to open the Guide menu",
|
"controller-shortcuts-xbox-note": "Button to open the Guide menu",
|
||||||
"controller-vibration": "Controller vibration",
|
"controller-vibration": "Controller vibration",
|
||||||
"copy": "Copy",
|
"copy": "Copy",
|
||||||
|
"create-shortcut": "Create shortcut",
|
||||||
"custom": "Custom",
|
"custom": "Custom",
|
||||||
"deadzone-counterweight": "Deadzone counterweight",
|
"deadzone-counterweight": "Deadzone counterweight",
|
||||||
"decrease": "Decrease",
|
"decrease": "Decrease",
|
||||||
@ -109,6 +110,7 @@ const Texts = {
|
|||||||
"fortnite-force-console-version": "Fortnite: force console version",
|
"fortnite-force-console-version": "Fortnite: force console version",
|
||||||
"game-bar": "Game Bar",
|
"game-bar": "Game Bar",
|
||||||
"getting-consoles-list": "Getting the list of consoles...",
|
"getting-consoles-list": "Getting the list of consoles...",
|
||||||
|
"gpu-configuration": "GPU configuration",
|
||||||
"help": "Help",
|
"help": "Help",
|
||||||
"hide": "Hide",
|
"hide": "Hide",
|
||||||
"hide-idle-cursor": "Hide mouse cursor on idle",
|
"hide-idle-cursor": "Hide mouse cursor on idle",
|
||||||
@ -116,6 +118,7 @@ const Texts = {
|
|||||||
"hide-sections": "Hide sections",
|
"hide-sections": "Hide sections",
|
||||||
"hide-system-menu-icon": "Hide System menu's icon",
|
"hide-system-menu-icon": "Hide System menu's icon",
|
||||||
"hide-touch-controller": "Hide touch controller",
|
"hide-touch-controller": "Hide touch controller",
|
||||||
|
"high-performance": "High performance",
|
||||||
"horizontal-scroll-sensitivity": "Horizontal scroll sensitivity",
|
"horizontal-scroll-sensitivity": "Horizontal scroll sensitivity",
|
||||||
"horizontal-sensitivity": "Horizontal sensitivity",
|
"horizontal-sensitivity": "Horizontal sensitivity",
|
||||||
"ignore": "Ignore",
|
"ignore": "Ignore",
|
||||||
@ -129,6 +132,7 @@ const Texts = {
|
|||||||
"left-stick": "Left stick",
|
"left-stick": "Left stick",
|
||||||
"loading-screen": "Loading screen",
|
"loading-screen": "Loading screen",
|
||||||
"local-co-op": "Local co-op",
|
"local-co-op": "Local co-op",
|
||||||
|
"low-power": "Low power",
|
||||||
"map-mouse-to": "Map mouse to",
|
"map-mouse-to": "Map mouse to",
|
||||||
"may-not-work-properly": "May not work properly!",
|
"may-not-work-properly": "May not work properly!",
|
||||||
"menu": "Menu",
|
"menu": "Menu",
|
||||||
|
@ -22,7 +22,6 @@ export class BxSelectElement {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const isMultiple = $select.multiple;
|
const isMultiple = $select.multiple;
|
||||||
let visibleIndex = $select.selectedIndex;
|
|
||||||
let $checkBox: HTMLInputElement;
|
let $checkBox: HTMLInputElement;
|
||||||
let $label: HTMLElement;
|
let $label: HTMLElement;
|
||||||
|
|
||||||
@ -42,7 +41,7 @@ export class BxSelectElement {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$checkBox.addEventListener('input', e => {
|
$checkBox.addEventListener('input', e => {
|
||||||
const $option = getOptionAtIndex(visibleIndex);
|
const $option = getOptionAtIndex($select.selectedIndex);
|
||||||
$option && ($option.selected = (e.target as HTMLInputElement).checked);
|
$option && ($option.selected = (e.target as HTMLInputElement).checked);
|
||||||
|
|
||||||
$select.dispatchEvent(new Event('input'));
|
$select.dispatchEvent(new Event('input'));
|
||||||
@ -61,7 +60,7 @@ export class BxSelectElement {
|
|||||||
const render = () => {
|
const render = () => {
|
||||||
// console.log('options', this.options, 'selectedIndices', this.selectedIndices, 'selectedOptions', this.selectedOptions);
|
// console.log('options', this.options, 'selectedIndices', this.selectedIndices, 'selectedOptions', this.selectedOptions);
|
||||||
|
|
||||||
visibleIndex = normalizeIndex(visibleIndex);
|
const visibleIndex = normalizeIndex($select.selectedIndex);
|
||||||
|
|
||||||
const $option = getOptionAtIndex(visibleIndex);
|
const $option = getOptionAtIndex(visibleIndex);
|
||||||
let content = '';
|
let content = '';
|
||||||
@ -108,11 +107,10 @@ export class BxSelectElement {
|
|||||||
const onPrevNext = (e: Event) => {
|
const onPrevNext = (e: Event) => {
|
||||||
const goNext = e.target === $btnNext;
|
const goNext = e.target === $btnNext;
|
||||||
|
|
||||||
const currentIndex = visibleIndex;
|
const currentIndex = $select.selectedIndex;
|
||||||
let newIndex = goNext ? currentIndex + 1 : currentIndex - 1;
|
let newIndex = goNext ? currentIndex + 1 : currentIndex - 1;
|
||||||
newIndex = normalizeIndex(newIndex);
|
newIndex = normalizeIndex(newIndex);
|
||||||
|
|
||||||
visibleIndex = newIndex;
|
|
||||||
if (!isMultiple && newIndex !== currentIndex) {
|
if (!isMultiple && newIndex !== currentIndex) {
|
||||||
$select.selectedIndex = newIndex;
|
$select.selectedIndex = newIndex;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user