mirror of
https://github.com/redphx/better-xcloud.git
synced 2025-08-06 13:18:27 +02:00
Merge Global settings and Stream settings into one dialog
This commit is contained in:
608
src/modules/ui/dialog/navigation-dialog.ts
Normal file
608
src/modules/ui/dialog/navigation-dialog.ts
Normal file
@@ -0,0 +1,608 @@
|
||||
import { GamepadKey } from "@/enums/mkb";
|
||||
import { EmulatedMkbHandler } from "@/modules/mkb/mkb-handler";
|
||||
import { BxEvent } from "@/utils/bx-event";
|
||||
import { STATES } from "@/utils/global";
|
||||
import { CE } from "@/utils/html";
|
||||
import { setNearby } from "@/utils/navigation-utils";
|
||||
|
||||
export enum NavigationDirection {
|
||||
UP = 1,
|
||||
RIGHT,
|
||||
DOWN,
|
||||
LEFT,
|
||||
}
|
||||
|
||||
export type NavigationNearbyElements = Partial<{
|
||||
orientation: 'horizontal' | 'vertical',
|
||||
selfOrientation: 'horizontal' | 'vertical',
|
||||
|
||||
focus: NavigationElement | (() => boolean),
|
||||
loop: ((direction: NavigationDirection) => boolean),
|
||||
[NavigationDirection.UP]: NavigationElement | (() => void) | 'previous' | 'next',
|
||||
[NavigationDirection.DOWN]: NavigationElement | (() => void) | 'previous' | 'next',
|
||||
[NavigationDirection.LEFT]: NavigationElement | (() => void) | 'previous' | 'next',
|
||||
[NavigationDirection.RIGHT]: NavigationElement | (() => void) | 'previous' | 'next',
|
||||
}>;
|
||||
|
||||
export interface NavigationElement extends HTMLElement {
|
||||
nearby?: NavigationNearbyElements;
|
||||
}
|
||||
|
||||
|
||||
export abstract class NavigationDialog {
|
||||
abstract getDialog(): NavigationDialog;
|
||||
abstract getContent(): HTMLElement;
|
||||
|
||||
abstract focusIfNeeded(): void;
|
||||
|
||||
abstract $container: HTMLElement;
|
||||
dialogManager: NavigationDialogManager;
|
||||
|
||||
constructor() {
|
||||
this.dialogManager = NavigationDialogManager.getInstance();
|
||||
}
|
||||
|
||||
show() {
|
||||
NavigationDialogManager.getInstance().show(this);
|
||||
|
||||
const $currentFocus = this.getFocusedElement();
|
||||
// If not focusing on any element
|
||||
if (!$currentFocus) {
|
||||
this.focusIfNeeded();
|
||||
}
|
||||
}
|
||||
|
||||
hide() {
|
||||
NavigationDialogManager.getInstance().hide();
|
||||
}
|
||||
|
||||
getFocusedElement() {
|
||||
const $activeElement = document.activeElement as HTMLElement;
|
||||
if (!$activeElement) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if focused element is a child of dialog
|
||||
if (this.$container.contains($activeElement)) {
|
||||
return $activeElement;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
onBeforeMount(): void {}
|
||||
onMounted(): void {}
|
||||
onBeforeUnmount(): void {}
|
||||
onUnmounted(): void {}
|
||||
|
||||
handleKeyPress(key: string): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
handleGamepad(button: GamepadKey): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export class NavigationDialogManager {
|
||||
private static instance: NavigationDialogManager;
|
||||
public static getInstance(): NavigationDialogManager {
|
||||
if (!NavigationDialogManager.instance) {
|
||||
NavigationDialogManager.instance = new NavigationDialogManager();
|
||||
}
|
||||
return NavigationDialogManager.instance;
|
||||
}
|
||||
|
||||
private static readonly GAMEPAD_POLLING_INTERVAL = 50;
|
||||
private static readonly GAMEPAD_KEYS = [
|
||||
GamepadKey.UP,
|
||||
GamepadKey.DOWN,
|
||||
GamepadKey.LEFT,
|
||||
GamepadKey.RIGHT,
|
||||
GamepadKey.A,
|
||||
GamepadKey.B,
|
||||
GamepadKey.LB,
|
||||
GamepadKey.RB,
|
||||
GamepadKey.LT,
|
||||
GamepadKey.RT,
|
||||
];
|
||||
|
||||
private static readonly GAMEPAD_DIRECTION_MAP = {
|
||||
[GamepadKey.UP]: NavigationDirection.UP,
|
||||
[GamepadKey.DOWN]: NavigationDirection.DOWN,
|
||||
[GamepadKey.LEFT]: NavigationDirection.LEFT,
|
||||
[GamepadKey.RIGHT]: NavigationDirection.RIGHT,
|
||||
|
||||
[GamepadKey.LS_UP]: NavigationDirection.UP,
|
||||
[GamepadKey.LS_DOWN]: NavigationDirection.DOWN,
|
||||
[GamepadKey.LS_LEFT]: NavigationDirection.LEFT,
|
||||
[GamepadKey.LS_RIGHT]: NavigationDirection.RIGHT,
|
||||
};
|
||||
|
||||
private static readonly SIBLING_PROPERTY_MAP = {
|
||||
'horizontal': {
|
||||
[NavigationDirection.LEFT]: 'previousElementSibling',
|
||||
[NavigationDirection.RIGHT]: 'nextElementSibling',
|
||||
},
|
||||
|
||||
'vertical': {
|
||||
[NavigationDirection.UP]: 'previousElementSibling',
|
||||
[NavigationDirection.DOWN]: 'nextElementSibling',
|
||||
},
|
||||
};
|
||||
|
||||
private gamepadPollingIntervalId: number | null = null;
|
||||
private gamepadLastStates: Array<[number, GamepadKey, boolean] | null> = [];
|
||||
private gamepadHoldingIntervalId: number | null = null;
|
||||
|
||||
private $overlay: HTMLElement;
|
||||
private $container: HTMLElement;
|
||||
private dialog: NavigationDialog | null = null;
|
||||
|
||||
constructor() {
|
||||
this.$overlay = CE('div', {class: 'bx-navigation-dialog-overlay bx-gone'});
|
||||
this.$overlay.addEventListener('click', e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.hide();
|
||||
});
|
||||
|
||||
document.documentElement.appendChild(this.$overlay);
|
||||
|
||||
this.$container = CE('div', {class: 'bx-navigation-dialog bx-gone'});
|
||||
document.documentElement.appendChild(this.$container);
|
||||
|
||||
// Hide dialog when the Guide menu is shown
|
||||
window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, e => this.hide());
|
||||
}
|
||||
|
||||
handleEvent(event: Event) {
|
||||
switch (event.type) {
|
||||
case 'keydown':
|
||||
const $target = event.target as HTMLElement;
|
||||
const keyboardEvent = event as KeyboardEvent;
|
||||
const keyCode = keyboardEvent.code || keyboardEvent.key;
|
||||
|
||||
let handled = this.dialog?.handleKeyPress(keyCode);
|
||||
if (handled) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
if (keyCode === 'ArrowUp' || keyCode === 'ArrowDown') {
|
||||
handled = true;
|
||||
this.focusDirection(keyCode === 'ArrowUp' ? NavigationDirection.UP : NavigationDirection.DOWN);
|
||||
} else if (keyCode === 'ArrowLeft' || keyCode === 'ArrowRight') {
|
||||
if (!($target instanceof HTMLInputElement && ($target.type === 'text' || $target.type === 'range'))) {
|
||||
handled = true;
|
||||
this.focusDirection(keyCode === 'ArrowLeft' ? NavigationDirection.LEFT : NavigationDirection.RIGHT);
|
||||
}
|
||||
} else if (keyCode === 'Enter' || keyCode === 'NumpadEnter' || keyCode === 'Space') {
|
||||
if (!($target instanceof HTMLInputElement && $target.type === 'text')) {
|
||||
handled = true;
|
||||
$target.dispatchEvent(new MouseEvent('click'));
|
||||
}
|
||||
} else if (keyCode === 'Escape') {
|
||||
handled = true;
|
||||
this.hide();
|
||||
}
|
||||
|
||||
if (handled) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
isShowing() {
|
||||
return this.$container && !this.$container.classList.contains('bx-gone');
|
||||
}
|
||||
|
||||
private pollGamepad() {
|
||||
const gamepads = window.navigator.getGamepads();
|
||||
|
||||
for (const gamepad of gamepads) {
|
||||
if (!gamepad || !gamepad.connected) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ignore virtual controller
|
||||
if (gamepad.id === EmulatedMkbHandler.VIRTUAL_GAMEPAD_ID) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const axes = gamepad.axes;
|
||||
const buttons = gamepad.buttons;
|
||||
|
||||
let releasedButton: GamepadKey | null = null;
|
||||
let heldButton: GamepadKey | null = null;
|
||||
|
||||
let lastState = this.gamepadLastStates[gamepad.index];
|
||||
let lastTimestamp;
|
||||
let lastKey;
|
||||
let lastKeyPressed;
|
||||
if (lastState) {
|
||||
[lastTimestamp, lastKey, lastKeyPressed] = lastState;
|
||||
}
|
||||
|
||||
if (lastTimestamp && lastTimestamp === gamepad.timestamp) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const key of NavigationDialogManager.GAMEPAD_KEYS) {
|
||||
// Key released
|
||||
if (lastKey === key && !buttons[key].pressed) {
|
||||
releasedButton = key;
|
||||
break;
|
||||
} else if (buttons[key].pressed) {
|
||||
// Key pressed
|
||||
heldButton = key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If not pressing any key => check analog sticks
|
||||
if (heldButton === null && releasedButton === null && axes && axes.length >= 2) {
|
||||
// [LEFT left-right, LEFT up-down]
|
||||
if (lastKey) {
|
||||
const releasedHorizontal = Math.abs(axes[0]) < 0.1 && (lastKey === GamepadKey.LS_LEFT || lastKey === GamepadKey.LS_RIGHT);
|
||||
const releasedVertical = Math.abs(axes[1]) < 0.1 && (lastKey === GamepadKey.LS_UP || lastKey === GamepadKey.LS_DOWN);
|
||||
|
||||
if (releasedHorizontal || releasedVertical) {
|
||||
releasedButton = lastKey;
|
||||
} else {
|
||||
heldButton = lastKey;
|
||||
}
|
||||
} else {
|
||||
if (axes[0] < -0.5) {
|
||||
heldButton = GamepadKey.LS_LEFT;
|
||||
} else if (axes[0] > 0.5) {
|
||||
heldButton = GamepadKey.LS_RIGHT;
|
||||
} else if (axes[1] < -0.5) {
|
||||
heldButton = GamepadKey.LS_UP;
|
||||
} else if (axes[1] > 0.5) {
|
||||
heldButton = GamepadKey.LS_DOWN;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save state if holding a button
|
||||
if (heldButton !== null) {
|
||||
this.gamepadLastStates[gamepad.index] = [gamepad.timestamp, heldButton, false];
|
||||
|
||||
this.clearGamepadHoldingInterval();
|
||||
|
||||
// Only set turbo for d-pad and stick
|
||||
if (NavigationDialogManager.GAMEPAD_DIRECTION_MAP[heldButton as keyof typeof NavigationDialogManager.GAMEPAD_DIRECTION_MAP]) {
|
||||
this.gamepadHoldingIntervalId = window.setInterval(() => {
|
||||
const lastState = this.gamepadLastStates[gamepad.index];
|
||||
// Avoid pressing the incorrect key
|
||||
if (lastState) {
|
||||
[lastTimestamp, lastKey, lastKeyPressed] = lastState;
|
||||
if (lastKey === heldButton) {
|
||||
this.handleGamepad(gamepad, heldButton);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.clearGamepadHoldingInterval();
|
||||
}, 200);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Continue if the button hasn't been released
|
||||
if (releasedButton === null) {
|
||||
this.clearGamepadHoldingInterval();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Button released
|
||||
this.gamepadLastStates[gamepad.index] = null;
|
||||
|
||||
if (lastKeyPressed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (releasedButton === GamepadKey.A) {
|
||||
document.activeElement && document.activeElement.dispatchEvent(new MouseEvent('click'));
|
||||
return;
|
||||
} else if (releasedButton === GamepadKey.B) {
|
||||
this.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.handleGamepad(gamepad, releasedButton)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handleGamepad(gamepad: Gamepad, key: GamepadKey): boolean {
|
||||
let handled = this.dialog?.handleGamepad(key);
|
||||
if (handled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Handle d-pad & sticks
|
||||
let direction = NavigationDialogManager.GAMEPAD_DIRECTION_MAP[key as keyof typeof NavigationDialogManager.GAMEPAD_DIRECTION_MAP];
|
||||
if (!direction) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (document.activeElement instanceof HTMLInputElement && document.activeElement.type === 'range') {
|
||||
const $range = document.activeElement;
|
||||
if (direction === NavigationDirection.LEFT || direction === NavigationDirection.RIGHT) {
|
||||
$range.value = (parseInt($range.value) + parseInt($range.step) * (direction === NavigationDirection.LEFT ? -1 : 1)).toString();
|
||||
$range.dispatchEvent(new InputEvent('input'));
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!handled) {
|
||||
this.focusDirection(direction);
|
||||
}
|
||||
|
||||
this.gamepadLastStates[gamepad.index] && (this.gamepadLastStates[gamepad.index]![2] = true);
|
||||
return true;
|
||||
}
|
||||
|
||||
private clearGamepadHoldingInterval() {
|
||||
this.gamepadHoldingIntervalId && window.clearInterval(this.gamepadHoldingIntervalId);
|
||||
this.gamepadHoldingIntervalId = null;
|
||||
}
|
||||
|
||||
show(dialog: NavigationDialog) {
|
||||
this.clearGamepadHoldingInterval();
|
||||
|
||||
BxEvent.dispatch(window, BxEvent.XCLOUD_DIALOG_SHOWN);
|
||||
|
||||
// Stop xCloud's navigation polling
|
||||
(window as any).BX_EXPOSED.disableGamepadPolling = true;
|
||||
|
||||
// Lock scroll bar
|
||||
document.body.classList.add('bx-no-scroll');
|
||||
|
||||
// Show overlay
|
||||
this.$overlay.classList.remove('bx-gone');
|
||||
if (STATES.isPlaying) {
|
||||
this.$overlay.classList.add('bx-invisible');
|
||||
}
|
||||
|
||||
// Unmount current dialog
|
||||
this.unmountCurrentDialog();
|
||||
|
||||
// Setup new dialog
|
||||
this.dialog = dialog;
|
||||
dialog.onBeforeMount();
|
||||
this.$container.appendChild(dialog.getContent());
|
||||
dialog.onMounted();
|
||||
|
||||
// Show content
|
||||
this.$container.classList.remove('bx-gone');
|
||||
|
||||
// Add event listeners
|
||||
this.$container.addEventListener('keydown', this);
|
||||
|
||||
// Start gamepad polling
|
||||
this.startGamepadPolling();
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.clearGamepadHoldingInterval();
|
||||
|
||||
// Unlock scroll bar
|
||||
document.body.classList.remove('bx-no-scroll');
|
||||
|
||||
BxEvent.dispatch(window, BxEvent.XCLOUD_DIALOG_DISMISSED);
|
||||
|
||||
// Hide content
|
||||
this.$overlay.classList.add('bx-gone');
|
||||
this.$overlay.classList.remove('bx-invisible');
|
||||
this.$container.classList.add('bx-gone');
|
||||
|
||||
// Remove event listeners
|
||||
this.$container.removeEventListener('keydown', this);
|
||||
|
||||
// Stop gamepad polling
|
||||
this.stopGamepadPolling();
|
||||
|
||||
// Unmount dialog
|
||||
this.unmountCurrentDialog();
|
||||
|
||||
// Enable xCloud's navigation polling
|
||||
(window as any).BX_EXPOSED.disableGamepadPolling = false;
|
||||
}
|
||||
|
||||
focus($elm: NavigationElement | null): boolean {
|
||||
if (!$elm) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// console.log('focus', $elm);
|
||||
|
||||
if ($elm.nearby && $elm.nearby.focus) {
|
||||
if ($elm.nearby.focus instanceof HTMLElement) {
|
||||
return this.focus($elm.nearby.focus);
|
||||
} else {
|
||||
return $elm.nearby.focus();
|
||||
}
|
||||
}
|
||||
|
||||
$elm.focus();
|
||||
return $elm === document.activeElement;
|
||||
}
|
||||
|
||||
private getOrientation($elm: NavigationElement): NavigationNearbyElements['orientation'] {
|
||||
const nearby = $elm.nearby || {};
|
||||
if (nearby.selfOrientation) {
|
||||
return nearby.selfOrientation;
|
||||
}
|
||||
|
||||
let orientation;
|
||||
|
||||
let $current = $elm.parentElement! as NavigationElement;
|
||||
while ($current !== this.$container) {
|
||||
const tmp = $current.nearby?.orientation;
|
||||
if ($current.nearby && tmp) {
|
||||
orientation = tmp;
|
||||
break;
|
||||
}
|
||||
|
||||
$current = $current.parentElement!;
|
||||
}
|
||||
|
||||
orientation = orientation || 'vertical';
|
||||
setNearby($elm, {
|
||||
selfOrientation: orientation,
|
||||
});
|
||||
|
||||
return orientation;
|
||||
}
|
||||
|
||||
findNextTarget($focusing: HTMLElement | null, direction: NavigationDirection, checkParent = false, checked: Array<HTMLElement> = []): HTMLElement | null {
|
||||
if (!$focusing || $focusing === this.$container) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (checked.includes($focusing)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
checked.push($focusing);
|
||||
|
||||
let $target: HTMLElement = $focusing;
|
||||
const $parent = $target.parentElement;
|
||||
|
||||
const nearby = ($target as NavigationElement).nearby || {};
|
||||
const orientation = this.getOrientation($target)!;
|
||||
|
||||
// @ts-ignore
|
||||
let siblingProperty = (NavigationDialogManager.SIBLING_PROPERTY_MAP[orientation])[direction];
|
||||
if (siblingProperty) {
|
||||
let $sibling = $target as any;
|
||||
while ($sibling[siblingProperty]) {
|
||||
$sibling = $sibling[siblingProperty] as HTMLElement;
|
||||
|
||||
const $focusable = this.findFocusableElement($sibling, direction);
|
||||
if ($focusable) {
|
||||
return $focusable;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (nearby.loop) {
|
||||
// Loop
|
||||
if (nearby.loop(direction)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (checkParent) {
|
||||
return this.findNextTarget($parent, direction, checkParent, checked);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
findFocusableElement($elm: HTMLElement | null, direction?: NavigationDirection): HTMLElement | null {
|
||||
if (!$elm) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Ignore disabled element
|
||||
const isDisabled = !!($elm as any).disabled;
|
||||
if (isDisabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rect = $elm.getBoundingClientRect();
|
||||
const isVisible = !!rect.width && !!rect.height;
|
||||
|
||||
// Ignore hidden element
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Accept element with tabIndex
|
||||
if ($elm.tabIndex > -1) {
|
||||
return $elm;
|
||||
}
|
||||
|
||||
const focus = ($elm as NavigationElement).nearby?.focus;
|
||||
if (focus) {
|
||||
if (focus instanceof HTMLElement) {
|
||||
return this.findFocusableElement(focus, direction);
|
||||
} else if (typeof focus === 'function') {
|
||||
if (focus()) {
|
||||
return document.activeElement as HTMLElement;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Look for child focusable elemnet
|
||||
const children = Array.from($elm.children);
|
||||
|
||||
// Search from right to left if the orientation is horizontal
|
||||
const orientation = ($elm as NavigationElement).nearby?.orientation;
|
||||
if (orientation === 'horizontal' || (orientation === 'vertical' && direction === NavigationDirection.UP)) {
|
||||
children.reverse();
|
||||
}
|
||||
|
||||
for (const $child of children) {
|
||||
if (!$child || !($child instanceof HTMLElement)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const $target = this.findFocusableElement($child, direction);
|
||||
if ($target) {
|
||||
return $target;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private startGamepadPolling() {
|
||||
this.stopGamepadPolling();
|
||||
|
||||
this.gamepadPollingIntervalId = window.setInterval(this.pollGamepad.bind(this), NavigationDialogManager.GAMEPAD_POLLING_INTERVAL);
|
||||
}
|
||||
|
||||
private stopGamepadPolling() {
|
||||
this.gamepadLastStates = [];
|
||||
|
||||
this.gamepadPollingIntervalId && window.clearInterval(this.gamepadPollingIntervalId);
|
||||
this.gamepadPollingIntervalId = null;
|
||||
}
|
||||
|
||||
private focusDirection(direction: NavigationDirection) {
|
||||
const dialog = this.dialog;
|
||||
if (!dialog) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current focused element
|
||||
const $focusing = dialog.getFocusedElement();
|
||||
if (!$focusing || !this.findFocusableElement($focusing, direction)) {
|
||||
dialog.focusIfNeeded();
|
||||
return null;
|
||||
}
|
||||
|
||||
const $target = this.findNextTarget($focusing, direction, true);
|
||||
this.focus($target);
|
||||
}
|
||||
|
||||
private unmountCurrentDialog() {
|
||||
const dialog = this.dialog;
|
||||
|
||||
dialog && dialog.onBeforeUnmount();
|
||||
this.$container.firstChild?.remove();
|
||||
dialog && dialog.onUnmounted();
|
||||
|
||||
this.dialog = null;
|
||||
}
|
||||
}
|
1160
src/modules/ui/dialog/settings-dialog.ts
Normal file
1160
src/modules/ui/dialog/settings-dialog.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,507 +0,0 @@
|
||||
import { STATES, AppInterface, SCRIPT_VERSION, deepClone } from "@utils/global";
|
||||
import { CE, createButton, ButtonStyle } from "@utils/html";
|
||||
import { BxIcon } from "@utils/bx-icon";
|
||||
import { getPreferredServerRegion } from "@utils/region";
|
||||
import { UserAgent } from "@utils/user-agent";
|
||||
import { getPref, Preferences, PrefKey, setPref, toPrefElement } from "@utils/preferences";
|
||||
import { t, Translations } from "@utils/translation";
|
||||
import { PatcherCache } from "../patcher";
|
||||
import { UserAgentProfile } from "@enums/user-agent";
|
||||
import { BxSelectElement } from "@/web-components/bx-select";
|
||||
import { StreamSettings } from "../stream/stream-settings";
|
||||
import { BX_FLAGS } from "@/utils/bx-flags";
|
||||
import { Toast } from "@/utils/toast";
|
||||
|
||||
const SETTINGS_UI = {
|
||||
'Better xCloud': {
|
||||
items: [
|
||||
PrefKey.BETTER_XCLOUD_LOCALE,
|
||||
PrefKey.SERVER_BYPASS_RESTRICTION,
|
||||
PrefKey.UI_CONTROLLER_FRIENDLY,
|
||||
PrefKey.REMOTE_PLAY_ENABLED,
|
||||
],
|
||||
},
|
||||
|
||||
[t('server')]: {
|
||||
items: [
|
||||
PrefKey.SERVER_REGION,
|
||||
PrefKey.STREAM_PREFERRED_LOCALE,
|
||||
PrefKey.PREFER_IPV6_SERVER,
|
||||
],
|
||||
},
|
||||
|
||||
[t('stream')]: {
|
||||
items: [
|
||||
PrefKey.STREAM_TARGET_RESOLUTION,
|
||||
PrefKey.STREAM_CODEC_PROFILE,
|
||||
|
||||
PrefKey.BITRATE_VIDEO_MAX,
|
||||
|
||||
PrefKey.AUDIO_ENABLE_VOLUME_CONTROL,
|
||||
PrefKey.STREAM_DISABLE_FEEDBACK_DIALOG,
|
||||
|
||||
PrefKey.SCREENSHOT_APPLY_FILTERS,
|
||||
|
||||
PrefKey.AUDIO_MIC_ON_PLAYING,
|
||||
PrefKey.GAME_FORTNITE_FORCE_CONSOLE,
|
||||
PrefKey.STREAM_COMBINE_SOURCES,
|
||||
],
|
||||
},
|
||||
|
||||
[t('game-bar')]: {
|
||||
items: [
|
||||
PrefKey.GAME_BAR_POSITION,
|
||||
],
|
||||
},
|
||||
|
||||
[t('local-co-op')]: {
|
||||
items: [
|
||||
PrefKey.LOCAL_CO_OP_ENABLED,
|
||||
],
|
||||
},
|
||||
|
||||
[t('mouse-and-keyboard')]: {
|
||||
items: [
|
||||
PrefKey.NATIVE_MKB_ENABLED,
|
||||
PrefKey.MKB_ENABLED,
|
||||
PrefKey.MKB_HIDE_IDLE_CURSOR,
|
||||
],
|
||||
},
|
||||
|
||||
[t('touch-controller')]: {
|
||||
note: !STATES.userAgent.capabilities.touch ? '⚠️ ' + t('device-unsupported-touch') : null,
|
||||
unsupported: !STATES.userAgent.capabilities.touch,
|
||||
items: [
|
||||
PrefKey.STREAM_TOUCH_CONTROLLER,
|
||||
PrefKey.STREAM_TOUCH_CONTROLLER_AUTO_OFF,
|
||||
PrefKey.STREAM_TOUCH_CONTROLLER_DEFAULT_OPACITY,
|
||||
PrefKey.STREAM_TOUCH_CONTROLLER_STYLE_STANDARD,
|
||||
PrefKey.STREAM_TOUCH_CONTROLLER_STYLE_CUSTOM,
|
||||
],
|
||||
},
|
||||
|
||||
[t('loading-screen')]: {
|
||||
items: [
|
||||
PrefKey.UI_LOADING_SCREEN_GAME_ART,
|
||||
PrefKey.UI_LOADING_SCREEN_WAIT_TIME,
|
||||
PrefKey.UI_LOADING_SCREEN_ROCKET,
|
||||
],
|
||||
},
|
||||
|
||||
[t('ui')]: {
|
||||
items: [
|
||||
PrefKey.UI_LAYOUT,
|
||||
PrefKey.UI_HOME_CONTEXT_MENU_DISABLED,
|
||||
PrefKey.UI_GAME_CARD_SHOW_WAIT_TIME,
|
||||
PrefKey.CONTROLLER_SHOW_CONNECTION_STATUS,
|
||||
PrefKey.STREAM_SIMPLIFY_MENU,
|
||||
PrefKey.SKIP_SPLASH_VIDEO,
|
||||
!AppInterface && PrefKey.UI_SCROLLBAR_HIDE,
|
||||
PrefKey.HIDE_DOTS_ICON,
|
||||
PrefKey.REDUCE_ANIMATIONS,
|
||||
PrefKey.BLOCK_SOCIAL_FEATURES,
|
||||
PrefKey.UI_HIDE_SECTIONS,
|
||||
],
|
||||
},
|
||||
|
||||
[t('other')]: {
|
||||
items: [
|
||||
PrefKey.BLOCK_TRACKING,
|
||||
],
|
||||
},
|
||||
|
||||
[t('advanced')]: {
|
||||
items: [
|
||||
PrefKey.USER_AGENT_PROFILE,
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
export function setupSettingsUi() {
|
||||
// Avoid rendering the Settings multiple times
|
||||
if (document.querySelector('.bx-settings-container')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const PREF_PREFERRED_REGION = getPreferredServerRegion();
|
||||
const PREF_LATEST_VERSION = getPref(PrefKey.LATEST_VERSION);
|
||||
|
||||
let $btnReload: HTMLButtonElement;
|
||||
|
||||
// Setup Settings UI
|
||||
const $container = CE('div', {
|
||||
'class': 'bx-settings-container bx-gone',
|
||||
});
|
||||
|
||||
const $wrapper = CE('div', {'class': 'bx-settings-wrapper'},
|
||||
CE('div', {'class': 'bx-settings-title-wrapper'},
|
||||
createButton({
|
||||
classes: ['bx-settings-title'],
|
||||
style: ButtonStyle.FOCUSABLE | ButtonStyle.GHOST,
|
||||
label: 'Better xCloud ' + SCRIPT_VERSION,
|
||||
url: 'https://github.com/redphx/better-xcloud/releases',
|
||||
}),
|
||||
createButton({
|
||||
icon: BxIcon.QUESTION,
|
||||
style: ButtonStyle.FOCUSABLE,
|
||||
label: t('help'),
|
||||
url: 'https://better-xcloud.github.io/features/',
|
||||
}),
|
||||
)
|
||||
);
|
||||
|
||||
const topButtons = [];
|
||||
|
||||
// "New version available" button
|
||||
if (!SCRIPT_VERSION.includes('beta') && PREF_LATEST_VERSION && PREF_LATEST_VERSION != SCRIPT_VERSION) {
|
||||
// Show new version indicator
|
||||
topButtons.push(createButton({
|
||||
label: `🌟 Version ${PREF_LATEST_VERSION} available`,
|
||||
style: ButtonStyle.PRIMARY | ButtonStyle.FOCUSABLE | ButtonStyle.FULL_WIDTH,
|
||||
url: 'https://github.com/redphx/better-xcloud/releases/latest',
|
||||
}));
|
||||
}
|
||||
|
||||
// "Stream settings" button
|
||||
(STATES.supportedRegion && STATES.isSignedIn) && topButtons.push(createButton({
|
||||
label: t('stream-settings'),
|
||||
icon: BxIcon.STREAM_SETTINGS,
|
||||
style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE,
|
||||
onClick: e => {
|
||||
StreamSettings.getInstance().show();
|
||||
},
|
||||
}));
|
||||
|
||||
// Buttons for Android app
|
||||
if (AppInterface) {
|
||||
// Show Android app settings button
|
||||
topButtons.push(createButton({
|
||||
label: t('app-settings'),
|
||||
icon: BxIcon.STREAM_SETTINGS,
|
||||
style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE,
|
||||
onClick: e => {
|
||||
AppInterface.openAppSettings && AppInterface.openAppSettings();
|
||||
},
|
||||
}));
|
||||
} else {
|
||||
// Show link to Android app
|
||||
const userAgent = UserAgent.getDefault().toLowerCase();
|
||||
if (userAgent.includes('android')) {
|
||||
topButtons.push(createButton({
|
||||
label: '🔥 ' + t('install-android'),
|
||||
style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE,
|
||||
url: 'https://better-xcloud.github.io/android',
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if (topButtons.length) {
|
||||
const $div = CE('div', {class: 'bx-top-buttons'});
|
||||
for (const $button of topButtons) {
|
||||
$div.appendChild($button);
|
||||
}
|
||||
|
||||
$wrapper.appendChild($div);
|
||||
}
|
||||
|
||||
let localeSwitchingTimeout: number | null;
|
||||
|
||||
const onChange = async (e: Event) => {
|
||||
// Clear PatcherCache;
|
||||
PatcherCache.clear();
|
||||
|
||||
$btnReload.classList.add('bx-danger');
|
||||
|
||||
// Highlight the Settings button in the Header to remind user to reload the page
|
||||
const $btnHeaderSettings = document.querySelector('.bx-header-settings-button');
|
||||
$btnHeaderSettings && $btnHeaderSettings.classList.add('bx-danger');
|
||||
|
||||
if ((e.target as HTMLElement).id === 'bx_setting_' + PrefKey.BETTER_XCLOUD_LOCALE) {
|
||||
if (getPref(PrefKey.UI_CONTROLLER_FRIENDLY)) {
|
||||
localeSwitchingTimeout && window.clearTimeout(localeSwitchingTimeout);
|
||||
localeSwitchingTimeout = window.setTimeout(() => {
|
||||
Translations.refreshCurrentLocale();
|
||||
Translations.updateTranslations();
|
||||
}, 1000);
|
||||
} else {
|
||||
// Update locale
|
||||
Translations.refreshCurrentLocale();
|
||||
await Translations.updateTranslations();
|
||||
|
||||
$btnReload.textContent = t('settings-reloading');
|
||||
$btnReload.click();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Render settings
|
||||
for (let groupLabel in SETTINGS_UI) {
|
||||
// Don't render other settings when not signed in
|
||||
if (groupLabel !== 'Better xCloud' && (!STATES.supportedRegion || !STATES.isSignedIn)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const $group = CE('span', {'class': 'bx-settings-group-label'}, groupLabel);
|
||||
|
||||
// Render note
|
||||
if (SETTINGS_UI[groupLabel].note) {
|
||||
const $note = CE('b', {}, SETTINGS_UI[groupLabel].note);
|
||||
$group.appendChild($note);
|
||||
}
|
||||
|
||||
$wrapper.appendChild($group);
|
||||
|
||||
// Don't render settings if this is an unsupported feature
|
||||
if (SETTINGS_UI[groupLabel].unsupported) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const settingItems = SETTINGS_UI[groupLabel].items;
|
||||
for (let settingId of settingItems) {
|
||||
// Don't render custom settings
|
||||
if (!settingId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const setting = Preferences.SETTINGS[settingId];
|
||||
if (!setting) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let settingLabel = setting.label;
|
||||
let settingNote = setting.note || '';
|
||||
|
||||
// Add Experimental text
|
||||
if (setting.experimental) {
|
||||
settingLabel = '🧪 ' + settingLabel;
|
||||
if (!settingNote) {
|
||||
settingNote = t('experimental');
|
||||
} else {
|
||||
settingNote = `${t('experimental')}: ${settingNote}`;
|
||||
}
|
||||
}
|
||||
|
||||
let $control: any;
|
||||
let $inpCustomUserAgent: HTMLInputElement;
|
||||
let labelAttrs: any = {
|
||||
tabindex: '-1',
|
||||
};
|
||||
|
||||
if (settingId === PrefKey.USER_AGENT_PROFILE) {
|
||||
let defaultUserAgent = (window.navigator as any).orgUserAgent || window.navigator.userAgent;
|
||||
$inpCustomUserAgent = CE('input', {
|
||||
id: `bx_setting_inp_${settingId}`,
|
||||
type: 'text',
|
||||
placeholder: defaultUserAgent,
|
||||
'class': 'bx-settings-custom-user-agent',
|
||||
});
|
||||
$inpCustomUserAgent.addEventListener('input', e => {
|
||||
const profile = $control.value;
|
||||
const custom = (e.target as HTMLInputElement).value.trim();
|
||||
|
||||
UserAgent.updateStorage(profile, custom);
|
||||
onChange(e);
|
||||
});
|
||||
|
||||
$control = toPrefElement(PrefKey.USER_AGENT_PROFILE, (e: Event) => {
|
||||
const value = (e.target as HTMLInputElement).value as UserAgentProfile;
|
||||
let isCustom = value === UserAgentProfile.CUSTOM;
|
||||
let userAgent = UserAgent.get(value as UserAgentProfile);
|
||||
|
||||
UserAgent.updateStorage(value);
|
||||
|
||||
$inpCustomUserAgent.value = userAgent;
|
||||
$inpCustomUserAgent.readOnly = !isCustom;
|
||||
$inpCustomUserAgent.disabled = !isCustom;
|
||||
|
||||
!(e.target as HTMLInputElement).disabled && onChange(e);
|
||||
});
|
||||
} else if (settingId === PrefKey.SERVER_REGION) {
|
||||
let selectedValue;
|
||||
|
||||
$control = CE<HTMLSelectElement>('select', {
|
||||
id: `bx_setting_${settingId}`,
|
||||
title: settingLabel,
|
||||
tabindex: 0,
|
||||
});
|
||||
$control.name = $control.id;
|
||||
|
||||
$control.addEventListener('input', (e: Event) => {
|
||||
setPref(settingId, (e.target as HTMLSelectElement).value);
|
||||
onChange(e);
|
||||
});
|
||||
|
||||
selectedValue = PREF_PREFERRED_REGION;
|
||||
|
||||
setting.options = {};
|
||||
for (let regionName in STATES.serverRegions) {
|
||||
const region = STATES.serverRegions[regionName];
|
||||
let value = regionName;
|
||||
|
||||
let label = `${region.shortName} - ${regionName}`;
|
||||
if (region.isDefault) {
|
||||
label += ` (${t('default')})`;
|
||||
value = 'default';
|
||||
|
||||
if (selectedValue === regionName) {
|
||||
selectedValue = 'default';
|
||||
}
|
||||
}
|
||||
|
||||
setting.options[value] = label;
|
||||
}
|
||||
|
||||
for (let value in setting.options) {
|
||||
const label = setting.options[value];
|
||||
|
||||
const $option = CE('option', {value: value}, label);
|
||||
$control.appendChild($option);
|
||||
}
|
||||
|
||||
$control.disabled = Object.keys(STATES.serverRegions).length === 0;
|
||||
|
||||
// Select preferred region
|
||||
$control.value = selectedValue;
|
||||
} else {
|
||||
if (settingId === PrefKey.BETTER_XCLOUD_LOCALE) {
|
||||
$control = toPrefElement(settingId, (e: Event) => {
|
||||
localStorage.setItem('better_xcloud_locale', (e.target as HTMLSelectElement).value);
|
||||
onChange(e);
|
||||
});
|
||||
} else {
|
||||
$control = toPrefElement(settingId, onChange);
|
||||
}
|
||||
}
|
||||
|
||||
if (!!$control.id) {
|
||||
labelAttrs['for'] = $control.id;
|
||||
} else {
|
||||
labelAttrs['for'] = `bx_setting_${settingId}`;
|
||||
}
|
||||
|
||||
// Disable unsupported settings
|
||||
if (setting.unsupported) {
|
||||
($control as HTMLInputElement).disabled = true;
|
||||
}
|
||||
|
||||
// Make disabled control elements un-focusable
|
||||
if ($control.disabled && !!$control.getAttribute('tabindex')) {
|
||||
$control.setAttribute('tabindex', -1);
|
||||
}
|
||||
|
||||
const $label = CE<HTMLLabelElement>('label', labelAttrs, settingLabel);
|
||||
if (settingNote) {
|
||||
$label.appendChild(CE('b', {}, settingNote));
|
||||
}
|
||||
|
||||
let $elm: HTMLElement;
|
||||
|
||||
if ($control instanceof HTMLSelectElement && getPref(PrefKey.UI_CONTROLLER_FRIENDLY)) {
|
||||
// Controller-friendly <select>
|
||||
$elm = CE('div', {'class': 'bx-settings-row', 'data-group': 0},
|
||||
$label,
|
||||
CE('div', {class: 'bx-setting-control'}, BxSelectElement.wrap($control)),
|
||||
);
|
||||
} else {
|
||||
$elm = CE('div', {'class': 'bx-settings-row', 'data-group': 0},
|
||||
$label,
|
||||
$control instanceof HTMLInputElement ? CE('label', {
|
||||
class: 'bx-setting-control',
|
||||
for: $label.getAttribute('for'),
|
||||
}, $control) : CE('div', {class: 'bx-setting-control'}, $control),
|
||||
);
|
||||
}
|
||||
|
||||
$wrapper.appendChild($elm);
|
||||
|
||||
// Add User-Agent input
|
||||
if (settingId === PrefKey.USER_AGENT_PROFILE) {
|
||||
$wrapper.appendChild($inpCustomUserAgent!);
|
||||
// Trigger 'change' event
|
||||
$control.disabled = true;
|
||||
$control.dispatchEvent(new Event('input'));
|
||||
$control.disabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Setup Reload button
|
||||
$btnReload = createButton({
|
||||
label: t('settings-reload'),
|
||||
classes: ['bx-settings-reload-button'],
|
||||
style: ButtonStyle.FOCUSABLE | ButtonStyle.FULL_WIDTH | ButtonStyle.TALL,
|
||||
onClick: e => {
|
||||
window.location.reload();
|
||||
$btnReload.disabled = true;
|
||||
$btnReload.textContent = t('settings-reloading');
|
||||
},
|
||||
});
|
||||
$btnReload.setAttribute('tabindex', '0');
|
||||
|
||||
$wrapper.appendChild($btnReload);
|
||||
|
||||
// Donation link
|
||||
const $donationLink = CE('a', {
|
||||
'class': 'bx-donation-link',
|
||||
href: 'https://ko-fi.com/redphx',
|
||||
target: '_blank',
|
||||
tabindex: 0,
|
||||
}, `❤️ ${t('support-better-xcloud')}`);
|
||||
$wrapper.appendChild($donationLink);
|
||||
|
||||
// Show Game Pass app version
|
||||
try {
|
||||
const appVersion = (document.querySelector('meta[name=gamepass-app-version]') as HTMLMetaElement).content;
|
||||
const appDate = new Date((document.querySelector('meta[name=gamepass-app-date]') as HTMLMetaElement).content).toISOString().substring(0, 10);
|
||||
$wrapper.appendChild(CE('div', {'class': 'bx-settings-app-version'}, `xCloud website version ${appVersion} (${appDate})`));
|
||||
} catch (e) {}
|
||||
|
||||
// Show Debug info
|
||||
const debugInfo = deepClone(BX_FLAGS.DeviceInfo);
|
||||
const debugSettings = [
|
||||
PrefKey.STREAM_TARGET_RESOLUTION,
|
||||
PrefKey.STREAM_CODEC_PROFILE,
|
||||
|
||||
PrefKey.VIDEO_PLAYER_TYPE,
|
||||
PrefKey.VIDEO_PROCESSING,
|
||||
PrefKey.VIDEO_POWER_PREFERENCE,
|
||||
PrefKey.VIDEO_SHARPNESS,
|
||||
];
|
||||
|
||||
debugInfo['settings'] = {};
|
||||
for (const key of debugSettings) {
|
||||
debugInfo['settings'][key] = getPref(key);
|
||||
}
|
||||
|
||||
const $debugInfo = CE('div', {class: 'bx-debug-info'},
|
||||
createButton({
|
||||
label: 'Debug info',
|
||||
style: ButtonStyle.GHOST | ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE,
|
||||
onClick: e => {
|
||||
console.log(e);
|
||||
(e.target as HTMLElement).closest('button')?.nextElementSibling?.classList.toggle('bx-gone');
|
||||
},
|
||||
}),
|
||||
CE('pre', {
|
||||
class: 'bx-gone',
|
||||
on: {
|
||||
click: async (e: Event) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText((e.target as HTMLElement).innerText);
|
||||
Toast.show('Copied to clipboard', '', {instant: true});
|
||||
} catch (err) {
|
||||
console.error('Failed to copy: ', err);
|
||||
}
|
||||
},
|
||||
},
|
||||
}, '```\n' + JSON.stringify(debugInfo, null, ' ') + '\n```'),
|
||||
);
|
||||
$wrapper.appendChild($debugInfo);
|
||||
|
||||
$container.appendChild($wrapper);
|
||||
|
||||
// Add Settings UI to the web page
|
||||
const $pageContent = document.getElementById('PageContent');
|
||||
$pageContent?.parentNode?.insertBefore($container, $pageContent);
|
||||
}
|
@@ -2,21 +2,21 @@ import { BxEvent } from "@/utils/bx-event";
|
||||
import { AppInterface, STATES } from "@/utils/global";
|
||||
import { createButton, ButtonStyle, CE } from "@/utils/html";
|
||||
import { t } from "@/utils/translation";
|
||||
import { StreamSettings } from "../stream/stream-settings";
|
||||
import { SettingsNavigationDialog } from "./dialog/settings-dialog";
|
||||
|
||||
export enum GuideMenuTab {
|
||||
HOME,
|
||||
HOME = 'home',
|
||||
}
|
||||
|
||||
export class GuideMenu {
|
||||
static #BUTTONS = {
|
||||
streamSetting: createButton({
|
||||
label: t('stream-settings'),
|
||||
style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE,
|
||||
scriptSettings: createButton({
|
||||
label: t('better-xcloud'),
|
||||
style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE | ButtonStyle.PRIMARY,
|
||||
onClick: e => {
|
||||
// Wait until the Guide dialog is closed
|
||||
window.addEventListener(BxEvent.XCLOUD_DIALOG_DISMISSED, e => {
|
||||
setTimeout(() => StreamSettings.getInstance().show(), 50);
|
||||
setTimeout(() => SettingsNavigationDialog.getInstance().show(), 50);
|
||||
}, {once: true});
|
||||
|
||||
// Close all xCloud's dialogs
|
||||
@@ -86,8 +86,8 @@ export class GuideMenu {
|
||||
|
||||
const buttons: HTMLElement[] = [];
|
||||
|
||||
// "Stream settings" button
|
||||
buttons.push(GuideMenu.#BUTTONS.streamSetting);
|
||||
// "Better xCloud" button
|
||||
buttons.push(GuideMenu.#BUTTONS.scriptSettings);
|
||||
|
||||
// "App settings" button
|
||||
AppInterface && buttons.push(GuideMenu.#BUTTONS.appSettings);
|
||||
@@ -112,7 +112,7 @@ export class GuideMenu {
|
||||
|
||||
const buttons: HTMLElement[] = [];
|
||||
|
||||
buttons.push(GuideMenu.#BUTTONS.streamSetting);
|
||||
buttons.push(GuideMenu.#BUTTONS.scriptSettings);
|
||||
AppInterface && buttons.push(GuideMenu.#BUTTONS.appSettings);
|
||||
|
||||
// Reload page
|
||||
|
@@ -2,17 +2,18 @@ import { SCRIPT_VERSION } from "@utils/global";
|
||||
import { createButton, ButtonStyle, CE } from "@utils/html";
|
||||
import { BxIcon } from "@utils/bx-icon";
|
||||
import { getPreferredServerRegion } from "@utils/region";
|
||||
import { PrefKey, getPref } from "@utils/preferences";
|
||||
import { RemotePlay } from "@modules/remote-play";
|
||||
import { t } from "@utils/translation";
|
||||
import { setupSettingsUi } from "./global-settings";
|
||||
import { SettingsNavigationDialog } from "./dialog/settings-dialog";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref } from "@/utils/settings-storages/global-settings-storage";
|
||||
|
||||
export class HeaderSection {
|
||||
static #$remotePlayBtn = createButton({
|
||||
classes: ['bx-header-remote-play-button', 'bx-gone'],
|
||||
icon: BxIcon.REMOTE_PLAY,
|
||||
title: t('remote-play'),
|
||||
style: ButtonStyle.GHOST | ButtonStyle.FOCUSABLE,
|
||||
style: ButtonStyle.GHOST | ButtonStyle.FOCUSABLE | ButtonStyle.CIRCULAR,
|
||||
onClick: e => {
|
||||
RemotePlay.togglePopup();
|
||||
},
|
||||
@@ -21,14 +22,9 @@ export class HeaderSection {
|
||||
static #$settingsBtn = createButton({
|
||||
classes: ['bx-header-settings-button'],
|
||||
label: '???',
|
||||
style: ButtonStyle.GHOST | ButtonStyle.FOCUSABLE | ButtonStyle.FULL_HEIGHT,
|
||||
style: ButtonStyle.FROSTED | ButtonStyle.DROP_SHADOW | ButtonStyle.FOCUSABLE | ButtonStyle.FULL_HEIGHT,
|
||||
onClick: e => {
|
||||
setupSettingsUi();
|
||||
|
||||
const $settings = document.querySelector('.bx-settings-container')!;
|
||||
$settings.classList.toggle('bx-gone');
|
||||
window.scrollTo(0, 0);
|
||||
document.activeElement && (document.activeElement as HTMLElement).blur();
|
||||
SettingsNavigationDialog.getInstance().show();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -49,7 +45,7 @@ export class HeaderSection {
|
||||
|
||||
// Setup Settings button
|
||||
const $settingsBtn = HeaderSection.#$settingsBtn;
|
||||
$settingsBtn.querySelector('span')!.textContent = getPreferredServerRegion(true);
|
||||
$settingsBtn.querySelector('span')!.textContent = getPreferredServerRegion(true) || t('better-xcloud');
|
||||
|
||||
// Show new update status
|
||||
if (!SCRIPT_VERSION.includes('beta') && PREF_LATEST_VERSION && PREF_LATEST_VERSION !== SCRIPT_VERSION) {
|
||||
|
@@ -19,7 +19,7 @@ export class ProductDetailsPage {
|
||||
private static shortcutTimeoutId: number | null = null;
|
||||
|
||||
static injectShortcutButton() {
|
||||
if (!AppInterface || BX_FLAGS.DeviceInfo?.deviceType !== 'android') {
|
||||
if (!AppInterface || BX_FLAGS.DeviceInfo!.deviceType !== 'android') {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,4 @@
|
||||
import { CE } from "@utils/html";
|
||||
import { onChangeVideoPlayerType } from "../stream/stream-settings-utils";
|
||||
import { StreamSettings } from "../stream/stream-settings";
|
||||
|
||||
|
||||
export function localRedirect(path: string) {
|
||||
@@ -26,10 +24,4 @@ export function localRedirect(path: string) {
|
||||
$anchor.click();
|
||||
}
|
||||
|
||||
export function setupStreamUi() {
|
||||
StreamSettings.getInstance();
|
||||
onChangeVideoPlayerType();
|
||||
}
|
||||
|
||||
|
||||
(window as any).localRedirect = localRedirect;
|
||||
|
Reference in New Issue
Block a user