Add "Controller-friendly UI" option

This commit is contained in:
redphx 2024-07-15 20:54:35 +07:00
parent 66120d6970
commit 394dc68ece
9 changed files with 155 additions and 73 deletions

File diff suppressed because one or more lines are too long

View File

@ -83,24 +83,24 @@
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
} }
}
&.bx-focusable { .bx-focusable {
position: relative; position: relative;
&::after { &::after {
border: 2px solid transparent; border: 2px solid transparent;
border-radius: 4px; border-radius: 4px;
} }
&:focus::after { &:focus::after {
content: ''; content: '';
border-color: white; border-color: white;
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
}
} }
} }

View File

@ -100,11 +100,16 @@
position: relative; position: relative;
label { label {
flex: 1;
align-self: center; align-self: center;
margin-bottom: 0; margin-bottom: 0;
} }
.bx-setting-control {
flex: 1;
display: flex;
justify-content: right;
}
&:hover, &:focus-within { &:hover, &:focus-within {
background-color: #242424; background-color: #242424;

View File

@ -1,19 +1,25 @@
.bx-select { .bx-select {
display: flex;
align-items: center;
select { select {
display: none; display: none;
} }
> div { > div, button.bx-select-value {
display: inline-block;
min-width: 110px; min-width: 110px;
text-align: center; text-align: center;
margin: 0 10px; margin: 0 8px;
line-height: 24px; line-height: 24px;
vertical-align: middle; vertical-align: middle;
background: #fff; background: #fff;
color: #000; color: #000;
border-radius: 4px; border-radius: 4px;
padding: 2px 4px; padding: 2px 8px;
}
> div {
display: inline-block;
input { input {
display: inline-block; display: inline-block;
@ -22,23 +28,55 @@
label { label {
margin-bottom: 0; margin-bottom: 0;
font-size: 14px;
} }
} }
button { button.bx-select-value {
border: none; border: none;
display: inline-flex;
cursor: pointer;
height: 30px;
align-items: center;
span {
flex: 1;
text-align: center;
display: inline-block;
}
input {
margin: 0 4px;
accent-color: var(--bx-primary-button-color);
}
&:hover,
&:focus {
input {
accent-color: var(--bx-danger-button-color);
}
&::after {
border-color: #4d4d4d !important;
}
}
}
button.bx-button {
border: none;
height: 30px;
width: 24px; width: 24px;
height: 24px; padding: 0;
line-height: 24px; line-height: 30px;
color: #fff; color: #fff;
border-radius: 4px; border-radius: 4px;
font-weight: bold; font-weight: bold;
font-size: 14px; font-size: 12px;
font-family: var(--bx-monospaced-font); font-family: var(--bx-monospaced-font);
&.bx-inactive { &.bx-inactive {
pointer-events: none; pointer-events: none;
opacity: 0.2; opacity: 0.1;
} }
span { span {

View File

@ -11,7 +11,6 @@ import { SoundShortcut } from "../shortcuts/shortcut-sound";
import { TouchController } from "../touch-controller"; import { TouchController } from "../touch-controller";
import { VibrationManager } from "../vibration-manager"; import { VibrationManager } from "../vibration-manager";
import { StreamStats } from "./stream-stats"; import { StreamStats } from "./stream-stats";
import { BX_FLAGS } from "@/utils/bx-flags";
import { BxSelectElement } from "@/web-components/bx-select"; import { BxSelectElement } from "@/web-components/bx-select";
import { onChangeVideoPlayerType, updateVideoPlayer } from "./stream-settings-utils"; import { onChangeVideoPlayerType, updateVideoPlayer } from "./stream-settings-utils";
@ -362,7 +361,8 @@ export class StreamSettings {
} else if (!setting.unsupported) { } else if (!setting.unsupported) {
$control = toPrefElement(pref, setting.onChange, setting.params); $control = toPrefElement(pref, setting.onChange, setting.params);
if ($control instanceof HTMLSelectElement && BX_FLAGS.ScriptUi === 'tv') { // Replace <select> with controller-friendly one
if ($control instanceof HTMLSelectElement && getPref(PrefKey.UI_CONTROLLER_FRIENDLY)) {
$control = BxSelectElement.wrap($control); $control = BxSelectElement.wrap($control);
} }
} }

View File

@ -7,7 +7,6 @@ import { getPref, Preferences, PrefKey, setPref, toPrefElement } from "@utils/pr
import { t, Translations } from "@utils/translation"; import { t, Translations } from "@utils/translation";
import { PatcherCache } from "../patcher"; import { PatcherCache } from "../patcher";
import { UserAgentProfile } from "@enums/user-agent"; import { UserAgentProfile } from "@enums/user-agent";
import { BX_FLAGS } from "@/utils/bx-flags";
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";
@ -16,6 +15,7 @@ const SETTINGS_UI = {
items: [ items: [
PrefKey.BETTER_XCLOUD_LOCALE, PrefKey.BETTER_XCLOUD_LOCALE,
PrefKey.SERVER_BYPASS_RESTRICTION, PrefKey.SERVER_BYPASS_RESTRICTION,
PrefKey.UI_CONTROLLER_FRIENDLY,
PrefKey.REMOTE_PLAY_ENABLED, PrefKey.REMOTE_PLAY_ENABLED,
], ],
}, },
@ -218,8 +218,8 @@ export function setupSettingsUi() {
Translations.refreshCurrentLocale(); Translations.refreshCurrentLocale();
await Translations.updateTranslations(); await Translations.updateTranslations();
// Don't refresh the page on TV // Don't refresh the page when using controller-friendly UI
if (BX_FLAGS.ScriptUi !== 'tv') { if (!getPref(PrefKey.UI_CONTROLLER_FRIENDLY)) {
$btnReload.textContent = t('settings-reloading'); $btnReload.textContent = t('settings-reloading');
$btnReload.click(); $btnReload.click();
} }
@ -388,15 +388,16 @@ export function setupSettingsUi() {
let $elm: HTMLElement; let $elm: HTMLElement;
if ($control instanceof HTMLSelectElement && BX_FLAGS.ScriptUi === 'tv') { if ($control instanceof HTMLSelectElement && getPref(PrefKey.UI_CONTROLLER_FRIENDLY)) {
// Controller-friendly <select>
$elm = CE('div', {'class': 'bx-settings-row'}, $elm = CE('div', {'class': 'bx-settings-row'},
$label, $label,
BxSelectElement.wrap($control), CE('div', {class: 'bx-setting-control', 'data-group': 0}, BxSelectElement.wrap($control)),
); );
} else { } else {
$elm = CE('div', {'class': 'bx-settings-row'}, $elm = CE('div', {'class': 'bx-settings-row', 'data-group': 0},
$label, $label,
$control, CE('div', {class: 'bx-setting-control'}, $control),
); );
} }

View File

@ -10,8 +10,6 @@ type BxFlags = Partial<{
ForceNativeMkbTitles: string[]; ForceNativeMkbTitles: string[];
FeatureGates: {[key: string]: boolean} | null, FeatureGates: {[key: string]: boolean} | null,
ScriptUi: 'default' | 'tv',
IsSupportedTvBrowser: boolean, IsSupportedTvBrowser: boolean,
}> }>
@ -27,8 +25,6 @@ const DEFAULT_FLAGS: BxFlags = {
ForceNativeMkbTitles: [], ForceNativeMkbTitles: [],
FeatureGates: null, FeatureGates: null,
ScriptUi: 'default',
} }
export const BX_FLAGS: BxFlags = Object.assign(DEFAULT_FLAGS, window.BX_FLAGS || {}); export const BX_FLAGS: BxFlags = Object.assign(DEFAULT_FLAGS, window.BX_FLAGS || {});

View File

@ -1,5 +1,5 @@
import { CE } from "@utils/html"; import { CE } from "@utils/html";
import { SUPPORTED_LANGUAGES, t} from "@utils/translation"; import { SUPPORTED_LANGUAGES, t, ut} from "@utils/translation";
import { SettingElement, SettingElementType } from "@utils/settings"; import { SettingElement, SettingElementType } from "@utils/settings";
import { UserAgent } from "@utils/user-agent"; import { UserAgent } from "@utils/user-agent";
import { StreamStat } from "@modules/stream/stream-stats"; import { StreamStat } from "@modules/stream/stream-stats";
@ -72,6 +72,7 @@ export enum PrefKey {
UI_LOADING_SCREEN_WAIT_TIME = 'ui_loading_screen_wait_time', UI_LOADING_SCREEN_WAIT_TIME = 'ui_loading_screen_wait_time',
UI_LOADING_SCREEN_ROCKET = 'ui_loading_screen_rocket', UI_LOADING_SCREEN_ROCKET = 'ui_loading_screen_rocket',
UI_CONTROLLER_FRIENDLY = 'ui_controller_friendly',
UI_LAYOUT = 'ui_layout', UI_LAYOUT = 'ui_layout',
UI_SCROLLBAR_HIDE = 'ui_scrollbar_hide', UI_SCROLLBAR_HIDE = 'ui_scrollbar_hide',
UI_HIDE_SECTIONS = 'ui_hide_sections', UI_HIDE_SECTIONS = 'ui_hide_sections',
@ -553,6 +554,12 @@ export class Preferences {
hide: t('rocket-always-hide'), hide: t('rocket-always-hide'),
}, },
}, },
[PrefKey.UI_CONTROLLER_FRIENDLY]: {
label: ut('Controller-friendly UI'),
default: false,
},
[PrefKey.UI_LAYOUT]: { [PrefKey.UI_LAYOUT]: {
label: t('layout'), label: t('layout'),
default: 'default', default: 'default',

View File

@ -23,20 +23,32 @@ export class BxSelectElement {
let $checkBox: HTMLInputElement; let $checkBox: HTMLInputElement;
let $label: HTMLElement; let $label: HTMLElement;
const $content = CE('div', {}, let $content;
$checkBox = CE('input', {type: 'checkbox', id: $select.id + '_checkbox'}),
$label = CE('label', {for: $select.id + '_checkbox'}, ''),
);
isMultiple && $checkBox.addEventListener('input', e => { if (isMultiple) {
const $option = getOptionAtIndex(visibleIndex); $content = CE('button', {
$option && ($option.selected = (e.target as HTMLInputElement).checked); class: 'bx-select-value bx-focusable',
tabindex: 0,
},
$checkBox = CE('input', {type: 'checkbox'}),
$label = CE('span', {}, ''),
);
$select.dispatchEvent(new Event('input')); $content.addEventListener('click', e => {
}); $checkBox.click();
});
// Only show checkbox in "multiple" <select> $checkBox.addEventListener('input', e => {
$checkBox.classList.toggle('bx-gone', !isMultiple); const $option = getOptionAtIndex(visibleIndex);
$option && ($option.selected = (e.target as HTMLInputElement).checked);
$select.dispatchEvent(new Event('input'));
});
} else {
$content = CE('div', {},
$label = CE('label', {for: $select.id + '_checkbox'}, ''),
);
}
const getOptionAtIndex = (index: number): HTMLOptionElement | undefined => { const getOptionAtIndex = (index: number): HTMLOptionElement | undefined => {
return $select.querySelector(`option:nth-of-type(${visibleIndex + 1})`) as HTMLOptionElement; return $select.querySelector(`option:nth-of-type(${visibleIndex + 1})`) as HTMLOptionElement;
@ -51,22 +63,28 @@ export class BxSelectElement {
let content = ''; let content = '';
if ($option) { if ($option) {
content = $option.textContent || ''; content = $option.textContent || '';
if (content && $option.parentElement!.tagName === 'OPTGROUP') {
content = ($option.parentElement as HTMLOptionElement).label + ' ' + content;
}
} }
$label.textContent = content; $label.textContent = content;
// Hide checkbox when the selection is empty // Hide checkbox when the selection is empty
isMultiple && ($checkBox.checked = $option?.selected || false); if (isMultiple) {
$checkBox.classList.toggle('bx-gone', !isMultiple || !content); $checkBox.checked = $option?.selected || false;
$checkBox.classList.toggle('bx-gone', !content);
}
const disablePrev = visibleIndex <= 0; const disablePrev = visibleIndex <= 0;
const disableNext = visibleIndex === $select.querySelectorAll('option').length - 1; const disableNext = visibleIndex === $select.querySelectorAll('option').length - 1;
$btnPrev.classList.toggle('bx-inactive', disablePrev); $btnPrev.classList.toggle('bx-inactive', disablePrev);
disablePrev && $btnNext.focus(); disablePrev && document.activeElement === $btnPrev && $btnNext.focus();
$btnNext.classList.toggle('bx-inactive', disableNext); $btnNext.classList.toggle('bx-inactive', disableNext);
disableNext && $btnPrev.focus(); disableNext && document.activeElement === $btnNext &&$btnPrev.focus();
} }
const normalizeIndex = (index: number): number => { const normalizeIndex = (index: number): number => {