diff --git a/spec/components/listselector.spec.ts b/spec/components/listselector.spec.ts index c806f0253..88483ab2e 100644 --- a/spec/components/listselector.spec.ts +++ b/spec/components/listselector.spec.ts @@ -165,7 +165,7 @@ describe('ListSelector', () => { it('returns false if selected item does not exist', () => { const result = listSelector.selectItem('notExistingKey'); - expect(listSelector.getSelectedItem()).toBeUndefined(); + expect(listSelector.getSelectedItem()).toBeNull(); expect(result).toBeFalsy(); }); diff --git a/spec/components/settingspanel.spec.ts b/spec/components/settingspanel.spec.ts index 99cb17ab6..ec2e092b8 100644 --- a/spec/components/settingspanel.spec.ts +++ b/spec/components/settingspanel.spec.ts @@ -224,8 +224,8 @@ describe('SettingsPanel', () => { const selectBox = new SelectBox(); const closeDropdownSpy = jest.spyOn(selectBox, 'closeDropdown'); - settingsPanel.getActivePage().addComponent(new SettingsPanelItem(new Label(), selectBox)); - settingsPanel.getActivePage().addComponent(new SettingsPanelItem(new Label(), new VolumeSlider())); + settingsPanel.getActivePage().addComponent(new SettingsPanelItem({ label: new Label(), settingComponent: selectBox })); + settingsPanel.getActivePage().addComponent(new SettingsPanelItem({ label: new Label(), settingComponent: new VolumeSlider() })); settingsPanel['hideHoveredSelectBoxes'](); diff --git a/src/scss/skin-modern/_skin.scss b/src/scss/skin-modern/_skin.scss index b335105d7..93431eb7f 100644 --- a/src/scss/skin-modern/_skin.scss +++ b/src/scss/skin-modern/_skin.scss @@ -51,7 +51,7 @@ @import 'skin-smallscreen'; @import 'skin-tv'; - .#{$prefix}-ui-uicontainer { + &.#{$prefix}-ui-uicontainer { color: $color-primary; font-family: $font-family; font-size: $font-size; diff --git a/src/scss/skin-modern/components/_settingspanelpage.scss b/src/scss/skin-modern/components/_settingspanelpage.scss index 97a0d3ec1..5bd94716a 100644 --- a/src/scss/skin-modern/components/_settingspanelpage.scss +++ b/src/scss/skin-modern/components/_settingspanelpage.scss @@ -16,6 +16,15 @@ width: 45%; } + &.#{$prefix}-ui-subtitle-settings-label { + display: inline-block; + width: 45%; + + .#{$prefix}-ui-label { + margin-right: 10px; + } + } + // Controls (e.g. selectbox) &.#{$prefix}-ui-selectbox { margin-left: 10%; diff --git a/src/scss/skin-super-modern/_skin-modern-smallscreen.scss b/src/scss/skin-super-modern/_skin-modern-smallscreen.scss index f8f9d6d6a..03294ffbe 100644 --- a/src/scss/skin-super-modern/_skin-modern-smallscreen.scss +++ b/src/scss/skin-super-modern/_skin-modern-smallscreen.scss @@ -63,36 +63,12 @@ // Adjustments for screen width x <= 400 &.#{$prefix}-layout-max-width-400 { - .#{$prefix}-ui-settings-panel, - .#{$prefix}-ui-hugeplaybacktogglebutton, - .#{$prefix}-ui-smallcenteredplaybacktogglebutton, - .#{$prefix}-ui-hugereplaybutton, - .#{$prefix}-ui-errormessage-overlay, - .#{$prefix}-ui-buffering-overlay, - .#{$prefix}-ui-subtitle-overlay, - .#{$prefix}-ui-cast-status-overlay { - font-size: .6em; + .#{$prefix}-ui-settings-panel { + left: 1em; + right: 1em; + margin-top: 3.5em; + max-height: calc(100% - 3.5em - 3.5em); + width: unset; } } - - // Adjustments for screen width 400 < x <= 600 - &.#{$prefix}-layout-max-width-600 { - .#{$prefix}-ui-settings-panel, - .#{$prefix}-ui-hugeplaybacktogglebutton, - .#{$prefix}-ui-smallcenteredplaybacktogglebutton, - .#{$prefix}-ui-hugereplaybutton, - .#{$prefix}-ui-errormessage-overlay, - .#{$prefix}-ui-buffering-overlay, - .#{$prefix}-ui-subtitle-overlay, - .#{$prefix}-ui-cast-status-overlay { - font-size: .8em; - } - } - - // Adjustments for screen width x <= 600 - // sass-lint:disable no-empty-rulesets - &.#{$prefix}-layout-max-width-400, - &.#{$prefix}-layout-max-width-600 { - // none yet - } } diff --git a/src/scss/skin-super-modern/_skin.scss b/src/scss/skin-super-modern/_skin.scss index f422101fe..847e42b40 100644 --- a/src/scss/skin-super-modern/_skin.scss +++ b/src/scss/skin-super-modern/_skin.scss @@ -20,6 +20,7 @@ @import 'components/label'; @import 'components/settingspanel'; @import 'components/settingspanelpage'; + @import 'components/settingspanelitem'; @import 'components/settingspanelpageopenbutton'; @import 'components/settingspanelpagebackbutton'; @import 'components/settingstogglebutton'; @@ -55,7 +56,7 @@ @import 'skin-tv'; // sass-lint:disable nesting-depth - .#{$prefix}-ui-uicontainer { + &.#{$prefix}-ui-uicontainer { color: $color-primary; font-family: $font-family; font-size: $font-size; diff --git a/src/scss/skin-super-modern/components/_button.scss b/src/scss/skin-super-modern/components/_button.scss index d56865c8a..a00b3f805 100644 --- a/src/scss/skin-super-modern/components/_button.scss +++ b/src/scss/skin-super-modern/components/_button.scss @@ -16,6 +16,7 @@ min-width: 1.5em; padding: .25em; transition: transform .15s ease; + -webkit-tap-highlight-color: transparent; .#{$prefix}-label { color: $color-primary; diff --git a/src/scss/skin-super-modern/components/_label.scss b/src/scss/skin-super-modern/components/_label.scss index 9200bd328..9de508a1c 100644 --- a/src/scss/skin-super-modern/components/_label.scss +++ b/src/scss/skin-super-modern/components/_label.scss @@ -7,6 +7,7 @@ cursor: default; white-space: nowrap; + -webkit-tap-highlight-color: transparent; } .#{$prefix}-ui-label { diff --git a/src/scss/skin-super-modern/components/_settingspanel.scss b/src/scss/skin-super-modern/components/_settingspanel.scss index b59a2be1c..2d23c440c 100644 --- a/src/scss/skin-super-modern/components/_settingspanel.scss +++ b/src/scss/skin-super-modern/components/_settingspanel.scss @@ -13,9 +13,11 @@ $background-color: transparentize($color-background-menu, .15); + backdrop-filter: blur(10px); background-color: $background-color; border-radius: 4px; bottom: 3.5em; + box-shadow: 0 0 3px 0 transparentize($color: #000, $amount: .75); height: fit-content; max-height: 60%; min-width: fit-content; diff --git a/src/scss/skin-super-modern/components/_settingspanelitem.scss b/src/scss/skin-super-modern/components/_settingspanelitem.scss new file mode 100644 index 000000000..574cf9392 --- /dev/null +++ b/src/scss/skin-super-modern/components/_settingspanelitem.scss @@ -0,0 +1,82 @@ +@import '../variables'; + +%ui-settings-panel-item { + line-height: 1.5em; + padding: .5em .7em; + white-space: nowrap; + + cursor: pointer; + * { + cursor: pointer; + } + + &:hover { + background-color: transparentize($color-item-hover, .15); + } + + &.#{$prefix}-hidden { + display: none; + } + + .#{$prefix}-container-wrapper { + align-items: center; + display: flex; + height: 100%; + width: 100%; + } + + > .#{$prefix}-container-wrapper { + column-gap: 10px; + } + + .#{$prefix}-ui-label { + display: inline-block; + font-size: .8em; + font-weight: 500; + margin: 0; + text-align: justify; + } + + .#{$prefix}-ui-label-setting-selected-option { + align-self: center; + font-weight: normal; + margin-left: auto; + width: fit-content; + + &::after { + background-image: url('../../assets/skin-super-modern/images/angle-right.svg'); + background-repeat: no-repeat; + background-size: 1.5em auto; + content: ' '; + display: inline-block; + height: 1.5em; + vertical-align: -.4em; + width: 1.5em; + } + } + + &.#{$prefix}-ui-settings-panel-item-select-option { + &.#{$prefix}-selected { + .#{$prefix}-ui-label { + &::before { + background-image: url('../../assets/skin-super-modern/images/check.svg'); + background-repeat: no-repeat; + background-size: 1.5em auto; + content: ' '; + display: inline-block; + height: 1.5em; + vertical-align: -.4em; + width: 1.5em; + } + } + } + } + + &.#{$prefix}-title-item { + border-bottom: 1px solid transparentize($color-item-hover, .15); + } +} + +.#{$prefix}-ui-settings-panel-item { + @extend %ui-settings-panel-item; +} diff --git a/src/scss/skin-super-modern/components/_settingspanelpage.scss b/src/scss/skin-super-modern/components/_settingspanelpage.scss index bcb5e1e34..4e687f702 100644 --- a/src/scss/skin-super-modern/components/_settingspanelpage.scss +++ b/src/scss/skin-super-modern/components/_settingspanelpage.scss @@ -6,92 +6,6 @@ &.#{$prefix}-active { display: block; } - - // A "line" in the panel: a container holding a label + control - .#{$prefix}-container-wrapper > * { - &.#{$prefix}-ui-label { - display: inline-block; - font-size: .8em; - font-weight: 500; - margin: 0; - text-align: justify; - width: 45%; - } - - &.#{$prefix}-ui-label-setting-selected-option { - align-self: center; - font-weight: normal; - margin-left: auto; - width: fit-content; - - &::after { - background-image: url('../../assets/skin-super-modern/images/angle-right.svg'); - background-repeat: no-repeat; - background-size: 1.5em auto; - content: ' '; - display: inline-block; - height: 1.5em; - vertical-align: -.4em; - width: 1.5em; - } - } - - &.#{$prefix}-option { - font-weight: normal; - } - - &.#{$prefix}-heading { - &::before { - background-image: url('../../assets/skin-super-modern/images/angle-left.svg'); - background-repeat: no-repeat; - background-size: 1.5em auto; - content: ' '; - display: inline-block; - height: 1.5em; - vertical-align: -.4em; - width: 1.5em; - } - } - - &.#{$prefix}-selected { - &::before { - background-image: url('../../assets/skin-super-modern/images/check.svg'); - background-repeat: no-repeat; - background-size: 1.5em auto; - content: ' '; - display: inline-block; - height: 1.5em; - vertical-align: -.4em; - width: 1.5em; - } - } - - // Controls (e.g. selectbox) - &.#{$prefix}-ui-selectbox { - margin-left: 10%; - width: 45%; - } - } - - .#{$prefix}-ui-settings-panel-item { - padding: .5em .7em; - white-space: nowrap; - - &:hover { - background-color: transparentize($color-item-hover, .15); - } - - &.#{$prefix}-hidden { - display: none; - } - - .#{$prefix}-container-wrapper { - align-items: center; - display: flex; - height: 100%; - width: 100%; - } - } } .#{$prefix}-ui-settings-panel-page { diff --git a/src/scss/skin-super-modern/components/_settingspanelpagebackbutton.scss b/src/scss/skin-super-modern/components/_settingspanelpagebackbutton.scss index a5a1a4e87..5d1bcb218 100644 --- a/src/scss/skin-super-modern/components/_settingspanelpagebackbutton.scss +++ b/src/scss/skin-super-modern/components/_settingspanelpagebackbutton.scss @@ -3,23 +3,31 @@ %ui-settingspanelpagebackbutton { @extend %ui-button; - font-size: .8em; - position: relative; - width: 8em; + border-collapse: collapse; + padding: unset; + flex: 1; + text-align: left; + + &:active { + transform: unset; + } .#{$prefix}-label { display: inline-block; + font-size: .8em; + font-weight: 500; + margin: 0; + text-align: justify; &::before { - border-bottom: .2em solid $color-primary; - border-left: .2em solid $color-primary; - content: ''; - height: .6em; - margin-left: -.8em; - position: absolute; - top: .6em; - transform: rotate(45deg); - width: .6em; + background-image: url('../../assets/skin-super-modern/images/angle-left.svg'); + background-repeat: no-repeat; + background-size: 1.5em auto; + content: ' '; + display: inline-block; + height: 1.5em; + vertical-align: -.4em; + width: 1.5em; } } } diff --git a/src/scss/skin-super-modern/components/_settingspanelpageopenbutton.scss b/src/scss/skin-super-modern/components/_settingspanelpageopenbutton.scss index cc517ab2d..c4a99e3ae 100644 --- a/src/scss/skin-super-modern/components/_settingspanelpageopenbutton.scss +++ b/src/scss/skin-super-modern/components/_settingspanelpageopenbutton.scss @@ -6,15 +6,13 @@ background-image: url('../../assets/skin-super-modern/images/setting.svg'); max-height: .8em; - padding: .5em 0; - vertical-align: bottom; &:hover { @include svg-icon-shadow; } &.#{$prefix}-on { - background-image: url('../../assets/skin-super-modern/images/setting.svg'); + transform: rotate(30deg); } } diff --git a/src/scss/skin-super-modern/components/_settingstogglebutton.scss b/src/scss/skin-super-modern/components/_settingstogglebutton.scss index 889daaa23..35130618d 100644 --- a/src/scss/skin-super-modern/components/_settingstogglebutton.scss +++ b/src/scss/skin-super-modern/components/_settingstogglebutton.scss @@ -15,6 +15,6 @@ background-image: url('../../assets/skin-super-modern/images/setting.svg'); &.#{$prefix}-on { - background-image: url('../../assets/skin-super-modern/images/setting.svg'); + transform: rotate(30deg); } } diff --git a/src/scss/skin-super-modern/components/subtitlesettings/_subtitlesettingsresetbutton.scss b/src/scss/skin-super-modern/components/subtitlesettings/_subtitlesettingsresetbutton.scss index 1c5043bfb..799f0b9ba 100644 --- a/src/scss/skin-super-modern/components/subtitlesettings/_subtitlesettingsresetbutton.scss +++ b/src/scss/skin-super-modern/components/subtitlesettings/_subtitlesettingsresetbutton.scss @@ -4,7 +4,7 @@ @extend %ui-button; font-size: .8em; - width: 12em; + font-weight: 500; .#{$prefix}-label { display: inline-block; diff --git a/src/ts/components/button.ts b/src/ts/components/button.ts index d5fe59f16..210039f98 100644 --- a/src/ts/components/button.ts +++ b/src/ts/components/button.ts @@ -71,7 +71,8 @@ export class Button extends Component { }).html(i18n.performLocalization(this.config.text))); // Listen for the click event on the button element and trigger the corresponding event on the button component - buttonElement.on('click', () => { + buttonElement.on('click', (e) => { + e.stopPropagation(); this.onClickEvent(); }); diff --git a/src/ts/components/component.ts b/src/ts/components/component.ts index 50cfa79f6..dc3e9fd46 100644 --- a/src/ts/components/component.ts +++ b/src/ts/components/component.ts @@ -339,7 +339,7 @@ export class Component { * @param base configuration inherited from a superclass * @returns {Config} */ - protected mergeConfig(config: Config, defaults: Config, base: Config): Config { + protected mergeConfig(config: Config, defaults: Partial, base: Config): Config { // Extend default config with supplied config let merged = Object.assign({}, base, defaults, config); diff --git a/src/ts/components/dynamicsettingspanelitem.ts b/src/ts/components/dynamicsettingspanelitem.ts new file mode 100644 index 000000000..72ae6a01b --- /dev/null +++ b/src/ts/components/dynamicsettingspanelitem.ts @@ -0,0 +1,159 @@ +import { Label, LabelConfig } from './label'; +import { UIInstanceManager } from '../uimanager'; +import { PlayerAPI } from 'bitmovin-player'; +import { LocalizableText } from '../localization/i18n'; +import { ListSelector, ListSelectorConfig } from './listselector'; +import { SubtitleSelectBox } from './subtitleselectbox'; +import { SettingsPanelItem, SettingsPanelItemConfig } from './settingspanelitem'; +import { SettingsPanelSelectOption } from './settingspanelselectoption'; +import { SettingsPanelPage } from './settingspanelpage'; +import { SubtitleSettingsLabel } from './subtitlesettings/subtitlesettingslabel'; +import { SettingsPanel } from './settingspanel'; +import { SettingsPanelPageBackButton } from './settingspanelpagebackbutton'; +import { SubtitleSettingSelectBox } from './subtitlesettings/subtitlesettingselectbox'; + +/** + * Configuration interface for a {@link DynamicSettingsPanelItem}. + * + * @category Configs + */ +export interface DynamicSettingsPanelItemConfig extends SettingsPanelItemConfig { + /** + * The label component or the text for the label. + */ + label: LocalizableText | SubtitleSettingsLabel; + /** + * The list selector component which will be used to build the sub page. + */ + settingComponent: ListSelector; + /** + * The enclosing {@link SettingsPanel} where the sub page will be added. + */ + container: SettingsPanel; +} + +/** + * A dynamic settings panel item which can build a sub page with the items of a {@link ListSelector}. + * The page will be dynamically added and removed from the {@link SettingsPanel}. +* + * @category Components + */ +export class DynamicSettingsPanelItem extends SettingsPanelItem { + private selectedOptionLabel: Label; + protected settingComponent: ListSelector; + + private player: PlayerAPI; + private uimanager: UIInstanceManager; + + constructor(config: DynamicSettingsPanelItemConfig) { + super(config); + + this.settingComponent = config.settingComponent; + + this.selectedOptionLabel = new Label({ + text: '-', + for: this.getConfig().id, + cssClasses: ['ui-label-setting-selected-option'], + }); + + this.config = this.mergeConfig(config, { + components: [ + this.selectedOptionLabel, + ], + cssClass: 'ui-settings-panel-item', + role: 'menuitem', + addSettingAsComponent: false, + }, this.config); + } + + configure(player: PlayerAPI, uimanager: UIInstanceManager): void { + super.configure(player, uimanager); + + this.player = player; + this.uimanager = uimanager; + + if (this.config.label instanceof SubtitleSettingsLabel) { + this.config.label.opener.configure(player, uimanager); + } + + if (this.settingComponent != null) { + this.settingComponent.configure(this.player, this.uimanager); + } + + const handleSelectedItemChanged = () => { + let selectedItem = this.settingComponent.getItemForKey(this.settingComponent.getSelectedItem()); + if (selectedItem == null) { + this.selectedOptionLabel.hide(); + return; + } + + this.selectedOptionLabel.show(); + let selectedOptionLabelText = selectedItem.label; + if (this.settingComponent instanceof SubtitleSelectBox) { + let availableSettings = this.settingComponent.getItems().length; + selectedOptionLabelText = selectedOptionLabelText + ' (' + (availableSettings - 1) + ')'; + } + this.selectedOptionLabel.setText(selectedOptionLabelText); + }; + this.settingComponent.onItemSelected.subscribe(handleSelectedItemChanged); + + handleSelectedItemChanged(); + + const handleItemClick = () => { + this.displayItemsSubPage(); + }; + this.getDomElement().on('click', (e) => { + e.stopPropagation(); + handleItemClick(); + }); + } + + private buildSubPanelPage(): SettingsPanelPage { + const menuOptions = this.settingComponent.getItems(); + const page = new SettingsPanelPage({ removeOnPop: true }); + + const text = this.config.label instanceof SubtitleSettingsLabel ? this.config.label.text : this.config.label; + + const backButton = new SettingsPanelPageBackButton({ + text: text, + container: this.config.container, + }); + backButton.configure(this.player, this.uimanager); + const backSettingsPanelItem = new SettingsPanelItem({ + label: backButton, + cssClasses: ['title-item'], + }); + page.addComponent(backSettingsPanelItem); + + menuOptions + .map((option) => { + return new SettingsPanelSelectOption({ + label: option.label, + settingComponent: this.settingComponent, + settingsValue: option.key, + addSettingAsComponent: false, + }); + }) + .forEach((selectOption) => { + selectOption.configure(this.player, this.uimanager); + page.addComponent(selectOption); + }); + + page.configure(this.player, this.uimanager); + + const setting = this.settingComponent; + if (setting instanceof SubtitleSettingSelectBox) { + // Keep the preview subtitle overlay visible when the sub-page is for a subtitle setting + page.onActive.subscribe(() => setting.overlay.enablePreviewSubtitleLabel()); + page.onInactive.subscribe(() => setting.overlay.removePreviewSubtitleLabel()); + } + + return page; + } + + public displayItemsSubPage(): void { + let page = this.buildSubPanelPage(); + this.config.container.addPage(page); + this.config.container.setActivePage(page); + } +} diff --git a/src/ts/components/ecomodecontainer.ts b/src/ts/components/ecomodecontainer.ts index 02ea81ead..77bb7b4e5 100644 --- a/src/ts/components/ecomodecontainer.ts +++ b/src/ts/components/ecomodecontainer.ts @@ -3,14 +3,14 @@ import { i18n } from '../localization/i18n'; import { Container, ContainerConfig } from './container'; import { EcoModeToggleButton } from './ecomodetogglebutton'; import { Label, LabelConfig } from './label'; -import { SettingsPanelItem } from './settingspanelitem'; +import { SettingsPanelItem, SettingsPanelItemConfig } from './settingspanelitem'; /** * @category Containers */ export class EcoModeContainer extends Container { - private ecoModeSavedEmissionsItem: SettingsPanelItem; - private ecoModeToggleButtonItem: SettingsPanelItem; + private ecoModeSavedEmissionsItem: SettingsPanelItem; + private ecoModeToggleButtonItem: SettingsPanelItem; private emissionsSavedLabel: Label; private savedEmissons = 0; private currentEnergyEmission: number; @@ -29,8 +29,10 @@ export class EcoModeContainer extends Container { cssClass: 'ui-label-savedEnergy', }); - this.ecoModeToggleButtonItem = new SettingsPanelItem(labelEcoMode, ecoModeToggleButton); - this.ecoModeSavedEmissionsItem = new SettingsPanelItem('Saved Emissions', this.emissionsSavedLabel, { + this.ecoModeToggleButtonItem = new SettingsPanelItem({ label: labelEcoMode, settingComponent: ecoModeToggleButton }); + this.ecoModeSavedEmissionsItem = new SettingsPanelItem({ + label: 'Saved Emissions', + settingComponent: this.emissionsSavedLabel, hidden: true, }); diff --git a/src/ts/components/listselector.ts b/src/ts/components/listselector.ts index c8157295a..a029ca340 100644 --- a/src/ts/components/listselector.ts +++ b/src/ts/components/listselector.ts @@ -57,7 +57,7 @@ export interface ListSelectorConfig extends ComponentConfig { export abstract class ListSelector extends Component { protected items: ListItem[]; - protected selectedItem: string; + protected selectedItem: string | null = null; private listSelectorEvents = { onItemAdded: new EventDispatcher, string>(), @@ -191,7 +191,7 @@ export abstract class ListSelector extends Co * @param key the key of the item to return * @returns {ListItem} the item with the requested key. Undefined if no item with the given key exists. */ - getItemForKey(key: string): ListItem { + getItemForKey(key: string): ListItem | null { return this.items.find((item) => item.key === key); } diff --git a/src/ts/components/modernsettingspanel.ts b/src/ts/components/modernsettingspanel.ts deleted file mode 100644 index e6b3a4b2b..000000000 --- a/src/ts/components/modernsettingspanel.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { UIInstanceManager } from '../uimanager'; -import { ModernSettingsPanelPage } from './modernsettingspanelpage'; -import { PlayerAPI } from 'bitmovin-player'; -import { SettingsPanel, SettingsPanelConfig, NavigationDirection } from './settingspanel'; - - -export class ModernSettingsPanel extends SettingsPanel { - constructor(config: SettingsPanelConfig) { - super(config); - - this.config = this.mergeConfig(config, { - cssClass: 'ui-settings-panel', - hideDelay: 3000, - pageTransitionAnimation: true, - } as SettingsPanelConfig, this.config); - - (this.getActivePage()).onRequestsDisplaySubMenu.subscribe(this.handleShowSubPage); - } - - configure(player: PlayerAPI, uimanager: UIInstanceManager): void { - super.configure(player, uimanager); - - uimanager.onPreviewControlsHide.subscribe(() => { - this.hide(); - }); - } - - private handleShowSubPage = (sender: ModernSettingsPanelPage, subPage: ModernSettingsPanelPage) => { - this.show(); - this.addComponent(subPage); - this.updateComponents(); - this.setActivePage(subPage); - } - - private handleNavigateBack = (page: ModernSettingsPanelPage) => { - this.popSettingsPanelPage(); - this.removeComponent(page); - this.updateComponents(); - } - - protected navigateToPage( - targetPage: ModernSettingsPanelPage, - sourcePage: ModernSettingsPanelPage, - direction: NavigationDirection, - skipAnimation: boolean, - ): void { - super.navigateToPage(targetPage, sourcePage, direction, skipAnimation); - - if (direction === NavigationDirection.Forwards) { - targetPage.onRequestsDisplaySubMenu.subscribe(this.handleShowSubPage); - targetPage.onRequestsNavigateBack.subscribe(() => this.handleNavigateBack(targetPage)); - } - } -} diff --git a/src/ts/components/modernsettingspanelitem.ts b/src/ts/components/modernsettingspanelitem.ts deleted file mode 100644 index 1494b8f75..000000000 --- a/src/ts/components/modernsettingspanelitem.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { ContainerConfig} from './container'; -import {Component, ComponentConfig} from './component'; -import {Event as EDEvent, EventDispatcher, NoArgs} from '../eventdispatcher'; -import { Label, LabelConfig } from './label'; -import {UIInstanceManager} from '../uimanager'; -import {SelectBox} from './selectbox'; -import {ListBox} from './listbox'; -import {VideoQualitySelectBox} from './videoqualityselectbox'; -import {AudioQualitySelectBox} from './audioqualityselectbox'; -import {PlaybackSpeedSelectBox} from './playbackspeedselectbox'; -import { PlayerAPI } from 'bitmovin-player'; -import { i18n, LocalizableText } from '../localization/i18n'; -import { ModernSettingsPanelPage } from './modernsettingspanelpage'; -import { ListSelector, ListSelectorConfig } from './listselector'; -import { SubtitleSelectBox } from './subtitleselectbox'; -import { SettingsPanelItem } from './settingspanelitem'; - -/** - * An item for a {@link ModernSettingsPanelPage}, - * Containing an optional {@link Label} and a component that configures a setting. - * If the components is a {@link SelectBox} it will handle the logic of displaying it or not - */ -export class ModernSettingsPanelItem extends SettingsPanelItem { - - /** - * If setting is null, that means that the item is not an option and does not - * have a submenu. So if setting is null we can assume that the item should be - * used as a back button - */ - private selectedOptionLabel: Label; - private isOption: Boolean; - private key: string; - - private player: PlayerAPI; - private uimanager: UIInstanceManager; - - private modernSettingsPanelItemEvents = { - onRequestSubPage: new EventDispatcher(), - onRequestNavigateBack: new EventDispatcher(), - onItemSelect: new EventDispatcher(), - }; - - constructor(label: LocalizableText | Component, setting: Component, key: string = null, config: ContainerConfig = {}) { - super(label, setting, config, false); - - this.isOption = Boolean(key); - this.key = key; - - this.config = this.mergeConfig(config, { - cssClass: 'ui-settings-panel-item', - role: 'menuitem', - }, this.config); - } - - configure(player: PlayerAPI, uimanager: UIInstanceManager): void { - this.player = player; - this.uimanager = uimanager; - - if (this.setting !== null) { - this.setting.configure(this.player, this.uimanager); - } - - if (!this.isOption && (this.setting instanceof SelectBox || this.setting instanceof ListBox)) { - this.setting.onItemSelected.subscribe(() => { - this.removeComponent(this.selectedOptionLabel); - const setting = this.setting as ListSelector; - let selectedOptionLabel: LocalizableText = setting.getItemForKey(setting.getSelectedItem()).label; - - if (this.setting instanceof SubtitleSelectBox) { - let availableSettings = setting.getItems().length; - selectedOptionLabel = selectedOptionLabel + ' (' + (availableSettings - 1) + ')'; - } - this.selectedOptionLabel = new Label({ text: selectedOptionLabel, for: this.getConfig().id } as LabelConfig); - this.selectedOptionLabel.getDomElement().addClass(this.prefixCss('ui-label-setting-selected-option')); - this.addComponent(this.selectedOptionLabel); - this.updateComponents(); - }); - - let handleConfigItemChanged = () => { - if (!(this.setting instanceof SelectBox) && !(this.setting instanceof ListBox)) { - return; - } - // The minimum number of items that must be available for the setting to be displayed - // By default, at least two items must be available, else a selection is not possible - let minItemsToDisplay = 2; - // Audio/video quality select boxes contain an additional 'auto' mode, which in combination with a single - // available quality also does not make sense - if ((this.setting instanceof VideoQualitySelectBox && this.setting.hasAutoItem()) - || this.setting instanceof AudioQualitySelectBox) { - minItemsToDisplay = 3; - } - if (this.setting.itemCount() < minItemsToDisplay) { - // Hide the setting if no meaningful choice is available - this.hide(); - } else if (this.setting instanceof PlaybackSpeedSelectBox - && !uimanager.getConfig().playbackSpeedSelectionEnabled) { - // Hide the PlaybackSpeedSelectBox if disabled in config - this.hide(); - } else { - this.show(); - } - - // Visibility might have changed and therefore the active state might have changed so we fire the event - // TODO fire only when state has really changed (e.g. check if visibility has really changed) - this.onActiveChangedEvent(); - - this.getDomElement().attr('aria-haspopup', 'true'); - }; - - this.setting.onItemAdded.subscribe(handleConfigItemChanged); - this.setting.onItemRemoved.subscribe(handleConfigItemChanged); - - // Initialize hidden state - handleConfigItemChanged(); - } - else if (this.isOption) { - this.show(); - this.onActiveChangedEvent(); - this.getLabel.getDomElement().addClass(this.prefixCss('option')); - } - - const handleItemClick = (e: Event) => { - if (this.setting !== null) { - if (!this.isOption) { - this.displayItemsSubPage(); - } - - else { - if (this.setting instanceof SelectBox || this.setting instanceof ListBox) { - this.setting.selectItem(this.key); - this.modernSettingsPanelItemEvents.onItemSelect.dispatch(this, this.key); - this.getLabel.getDomElement().addClass(this.prefixCss('selected')); - } - } - } else { - this.modernSettingsPanelItemEvents.onRequestNavigateBack.dispatch(this); - } - }; - const domElement = this.getDomElement(); - domElement.on('click', (e) => handleItemClick(e)); - } - - private getSubPage(): ModernSettingsPanelPage { - if (this.setting instanceof SelectBox || this.setting instanceof ListBox) { - let menuOptions = this.setting.getItems(); - let selectedItem = this.setting.getSelectedItem(); - let page = new ModernSettingsPanelPage({}); - let label = this.getLabel instanceof Label ? new Label({ text: this.getLabel.getConfig().text, for: page.getConfig().id } as LabelConfig) : new Label({ text: i18n.getLocalizer('back'), for: page.getConfig().id } as LabelConfig); - label.getDomElement().addClass(this.prefixCss('heading')); - let itemToAdd = new ModernSettingsPanelItem(label, null); - itemToAdd.configure(this.player, this.uimanager); - page.addComponent(itemToAdd); - - for (let option of menuOptions) { - let itemToAdd = new ModernSettingsPanelItem(option.label, this.setting, option.key); - itemToAdd.configure(this.player, this.uimanager); - - if (option.key === selectedItem) { - itemToAdd.getLabel.getDomElement().addClass(this.prefixCss('selected')); - } - page.addComponent(itemToAdd); - } - page.configure(this.player, this.uimanager); - return page; - } - } - - public getSetting(): Component { - return this.setting; - } - - public displayItemsSubPage(): void { - let page = this.getSubPage(); - this.modernSettingsPanelItemEvents.onRequestSubPage.dispatch(this, page); - } - - /** - * Gets the event that is fired, when the SettingsPanelItem has been clicked - * and wants to display its sub menu on the {@link ModernSettingsPanel} as a seperate {@link ModernSettingsPanelPage} - */ - get getOnDisplaySubPage(): EDEvent { - return this.modernSettingsPanelItemEvents.onRequestSubPage.getEvent(); - } - - get getOnRequestNavigateBack(): EDEvent { - return this.modernSettingsPanelItemEvents.onRequestNavigateBack.getEvent(); - } - - get onItemSelect(): EDEvent { - return this.modernSettingsPanelItemEvents.onItemSelect.getEvent(); - } -} diff --git a/src/ts/components/modernsettingspanelpage.ts b/src/ts/components/modernsettingspanelpage.ts deleted file mode 100644 index 59023b90f..000000000 --- a/src/ts/components/modernsettingspanelpage.ts +++ /dev/null @@ -1,58 +0,0 @@ -import {ContainerConfig} from './container'; -import {UIInstanceManager} from '../uimanager'; -import {Event, EventDispatcher, NoArgs} from '../eventdispatcher'; -import { PlayerAPI } from 'bitmovin-player'; -import { ModernSettingsPanelItem } from './modernsettingspanelitem'; -import { SettingsPanelPage } from './settingspanelpage'; - -/** - * A panel containing a list of {@link ModernSettingsPanelItem items} that represent labelled settings. - */ -export class ModernSettingsPanelPage extends SettingsPanelPage { - - private modernSettingsPanelPageEvents = { - onRequestsDisplaySubPage: new EventDispatcher(), - onRequestsNavigateBack: new EventDispatcher(), - }; - - constructor(config: ContainerConfig) { - super(config); - - this.config = this.mergeConfig(config, { - cssClass: 'ui-settings-panel-page', - role: 'menu', - }, this.config); - } - - configure(player: PlayerAPI, uimanager: UIInstanceManager): void { - super.configure(player, uimanager); - - for (let component of this.getItems()) { - (component).getOnDisplaySubPage.subscribe((_, args: ModernSettingsPanelPage) => this.requestsDisplaySubMenu(this, args)); - (component).getOnRequestNavigateBack.subscribe(() => this.modernSettingsPanelPageEvents.onRequestsNavigateBack.dispatch(this)); - (component).onItemSelect.subscribe(() => { - for (let component of this.getItems()) { - component.getLabel.getDomElement().removeClass(this.prefixCss('selected')); - } - }); - } - } - - requestsDisplaySubMenu(_: Sender, args: ModernSettingsPanelPage) { - this.modernSettingsPanelPageEvents.onRequestsDisplaySubPage.dispatch(this, args); - } - - /** - * Is fired, when an item inside this page wants to show its sub-page - * This event is subscribed by the {@link ModernSettingsPanel}, which - * takes the page as an argument in order to display it. - */ - get onRequestsDisplaySubMenu(): Event { - return this.modernSettingsPanelPageEvents.onRequestsDisplaySubPage.getEvent(); - } - - get onRequestsNavigateBack(): Event { - return this.modernSettingsPanelPageEvents.onRequestsNavigateBack.getEvent(); - } - -} diff --git a/src/ts/components/settingspanel.ts b/src/ts/components/settingspanel.ts index 950cc3899..dade69b86 100644 --- a/src/ts/components/settingspanel.ts +++ b/src/ts/components/settingspanel.ts @@ -3,8 +3,8 @@ import { SelectBox } from './selectbox'; import { UIInstanceManager } from '../uimanager'; import { Timeout } from '../timeout'; import { Event, EventDispatcher, NoArgs } from '../eventdispatcher'; -import { SettingsPanelPage } from './settingspanelpage'; -import { SettingsPanelItem } from './settingspanelitem'; +import { SettingsPanelPage, SettingsPanelPageConfig } from './settingspanelpage'; +import { SettingsPanelItem, SettingsPanelItemConfig } from './settingspanelitem'; import { PlayerAPI } from 'bitmovin-player'; import { Component, ComponentConfig } from './component'; @@ -97,9 +97,11 @@ export class SettingsPanel extends Container { this.hide(); this.hideHoveredSelectBoxes(); }); - this.getDomElement().on('mouseenter', () => { - // On mouse enter clear the timeout - this.hideTimeout.clear(); + this.getDomElement().on('mouseenter mousemove', () => { + // On mouse enter and mouse move clear the timeout + if (this.hideTimeout.isActive()) { + this.hideTimeout.clear(); + } }); this.getDomElement().on('mouseleave', () => { // On mouse leave activate the timeout @@ -113,6 +115,15 @@ export class SettingsPanel extends Container { }); } + if (config.pageTransitionAnimation) { + const handleResize = () => { + // Reset the dimension of the settingsPanel to let the browser calculate the new dimension after resizing + this.getDomElement().css({ width: '', height: '' }); + }; + + player.on(player.exports.PlayerEvent.PlayerResized, handleResize); + } + this.onHide.subscribe(() => { if (config.hideDelay > -1) { // Clear timeout when hidden from outside @@ -206,12 +217,18 @@ export class SettingsPanel extends Container { targetPage = this.getRootPage(); } + const currentActivePage = this.activePage; this.navigateToPage( targetPage, this.activePage, NavigationDirection.Backwards, !(this.config as SettingsPanelConfig).pageTransitionAnimation, ); + + if (currentActivePage.getConfig().removeOnPop) { + this.removeComponent(currentActivePage); + this.updateComponents(); + } } /** @@ -250,6 +267,11 @@ export class SettingsPanel extends Container { super.addComponent(component); } + addPage(page: SettingsPanelPage) { + this.addComponent(page); + this.updateComponents(); + } + protected suspendHideTimeout() { this.hideTimeout.suspend(); } @@ -300,8 +322,8 @@ export class SettingsPanel extends Container { this.animateNavigation(targetPage, sourcePage, skipAnimation); this.updateActivePageClass(); - targetPage.onActiveEvent(); sourcePage.onInactiveEvent(); + targetPage.onActiveEvent(); } /** @@ -377,14 +399,14 @@ export class SettingsPanel extends Container { */ private hideHoveredSelectBoxes(): void { this.getComputedItems() - .map(item => item['setting']) + .map(item => item['settingComponent']) .filter(component => component instanceof SelectBox) .forEach((selectBox: SelectBox) => selectBox.closeDropdown()); } // collect all items from all pages (see hideHoveredSelectBoxes) - private getComputedItems(): SettingsPanelItem[] { - const allItems: SettingsPanelItem[] = []; + private getComputedItems(): SettingsPanelItem[] { + const allItems: SettingsPanelItem[] = []; for (let page of this.getPages()) { allItems.push(...page.getItems()); } diff --git a/src/ts/components/settingspanelitem.ts b/src/ts/components/settingspanelitem.ts index 0ca84091c..50a8fddea 100644 --- a/src/ts/components/settingspanelitem.ts +++ b/src/ts/components/settingspanelitem.ts @@ -11,6 +11,26 @@ import {PlaybackSpeedSelectBox} from './playbackspeedselectbox'; import { PlayerAPI } from 'bitmovin-player'; import { LocalizableText } from '../localization/i18n'; +/** + * Configuration interface for a {@link SettingsPanelItem} + * + * @category Configs + */ +export interface SettingsPanelItemConfig extends ContainerConfig { + /** + * The label component or the text for the label. + */ + label?: LocalizableText | Component; + /** + * The component that configures a setting. + */ + settingComponent?: Component; + /** + * If the setting should be added as a component to this item. + */ + addSettingAsComponent?: boolean; +} + /** * An item for a {@link SettingsPanelPage}, * Containing an optional {@link Label} and a component that configures a setting. @@ -18,43 +38,50 @@ import { LocalizableText } from '../localization/i18n'; * * @category Components */ -export class SettingsPanelItem extends Container { +export class SettingsPanelItem extends Container { private label: Component; - protected setting: Component; + protected settingComponent: Component | null; private settingsPanelItemEvents = { - onActiveChanged: new EventDispatcher(), + onActiveChanged: new EventDispatcher, NoArgs>(), }; - constructor(label: LocalizableText | Component, setting: Component, config: ContainerConfig = {}, addSettingAsComponent: boolean = true) { + constructor(config: SettingsPanelItemConfig) + constructor(config: Config) { super(config); - this.setting = setting; + this.settingComponent = config.settingComponent; this.config = this.mergeConfig(config, { cssClass: 'ui-settings-panel-item', role: 'menuitem', - }, this.config); + addSettingAsComponent: true, + } as Config, this.config); + const label = config.label; if (label !== null) { if (label instanceof Component) { this.label = label; } else { - this.label = new Label({ text: label, for: this.setting.getConfig() ? this.setting.getConfig().id : this.getConfig().id } as LabelConfig); + this.label = new Label({ text: label } as LabelConfig); } - this.addComponent(this.label); - } - if (addSettingAsComponent) { - this.addComponent(this.setting); + this.addComponent(this.label); } } configure(player: PlayerAPI, uimanager: UIInstanceManager): void { - if (this.setting instanceof SelectBox || this.setting instanceof ListBox) { + super.configure(player, uimanager); + + if (this.settingComponent != null && this.config.addSettingAsComponent) { + this.addComponent(this.settingComponent); + this.updateComponents(); + } + + if (this.settingComponent instanceof SelectBox || this.settingComponent instanceof ListBox) { let handleConfigItemChanged = () => { - if (!(this.setting instanceof SelectBox) && !(this.setting instanceof ListBox)) { + if (!(this.settingComponent instanceof SelectBox) && !(this.settingComponent instanceof ListBox)) { return; } // The minimum number of items that must be available for the setting to be displayed @@ -62,15 +89,15 @@ export class SettingsPanelItem extends Container { let minItemsToDisplay = 2; // Audio/video quality select boxes contain an additional 'auto' mode, which in combination with a single // available quality also does not make sense - if ((this.setting instanceof VideoQualitySelectBox && this.setting.hasAutoItem()) - || this.setting instanceof AudioQualitySelectBox) { + if ((this.settingComponent instanceof VideoQualitySelectBox && this.settingComponent.hasAutoItem()) + || this.settingComponent instanceof AudioQualitySelectBox) { minItemsToDisplay = 3; } - if (this.setting.itemCount() < minItemsToDisplay) { + if (this.settingComponent.itemCount() < minItemsToDisplay) { // Hide the setting if no meaningful choice is available this.hide(); - } else if (this.setting instanceof PlaybackSpeedSelectBox + } else if (this.settingComponent instanceof PlaybackSpeedSelectBox && !uimanager.getConfig().playbackSpeedSelectionEnabled) { // Hide the PlaybackSpeedSelectBox if disabled in config this.hide(); @@ -85,8 +112,8 @@ export class SettingsPanelItem extends Container { this.getDomElement().attr('aria-haspopup', 'true'); }; - this.setting.onItemAdded.subscribe(handleConfigItemChanged); - this.setting.onItemRemoved.subscribe(handleConfigItemChanged); + this.settingComponent.onItemAdded.subscribe(handleConfigItemChanged); + this.settingComponent.onItemRemoved.subscribe(handleConfigItemChanged); // Initialize hidden state handleConfigItemChanged(); @@ -110,10 +137,7 @@ export class SettingsPanelItem extends Container { * @see #isActive * @returns {Event} */ - get onActiveChanged(): Event { + get onActiveChanged(): Event, NoArgs> { return this.settingsPanelItemEvents.onActiveChanged.getEvent(); } - get getLabel(): Component { - return this.label; - } } diff --git a/src/ts/components/settingspanelpage.ts b/src/ts/components/settingspanelpage.ts index 8e8f089ff..de6e09456 100644 --- a/src/ts/components/settingspanelpage.ts +++ b/src/ts/components/settingspanelpage.ts @@ -1,16 +1,28 @@ import {Container, ContainerConfig} from './container'; -import {SettingsPanelItem} from './settingspanelitem'; +import { SettingsPanelItem, SettingsPanelItemConfig } from './settingspanelitem'; import {UIInstanceManager} from '../uimanager'; import {Event, EventDispatcher, NoArgs} from '../eventdispatcher'; import { PlayerAPI } from 'bitmovin-player'; import { BrowserUtils } from '../browserutils'; +/** + * Configuration interface for a {@link SettingsPanelPage} + * + * @category Configs + */ +export interface SettingsPanelPageConfig extends ContainerConfig { + /** + * If the page should be removed from the DOM when it is popped from the navigation stack. + */ + removeOnPop?: Boolean; +} + /** * A panel containing a list of {@link SettingsPanelItem items} that represent labelled settings. * * @category Components */ -export class SettingsPanelPage extends Container { +export class SettingsPanelPage extends Container { private static readonly CLASS_LAST = 'last'; @@ -20,13 +32,14 @@ export class SettingsPanelPage extends Container { onInactive: new EventDispatcher(), }; - constructor(config: ContainerConfig) { + constructor(config: SettingsPanelPageConfig) { super(config); - this.config = this.mergeConfig(config, { + this.config = this.mergeConfig(config, { cssClass: 'ui-settings-panel-page', role: 'menu', - }, this.config); + removeOnPop: false, + } as SettingsPanelPageConfig, this.config); } configure(player: PlayerAPI, uimanager: UIInstanceManager): void { @@ -63,8 +76,8 @@ export class SettingsPanelPage extends Container { return false; } - getItems(): SettingsPanelItem[] { - return this.config.components.filter(component => component instanceof SettingsPanelItem); + getItems(): SettingsPanelItem[] { + return []>this.config.components.filter(component => component instanceof SettingsPanelItem); } onSettingsStateChangedEvent() { diff --git a/src/ts/components/settingspanelpagebackbutton.ts b/src/ts/components/settingspanelpagebackbutton.ts index e02b4be31..43d16ba9e 100644 --- a/src/ts/components/settingspanelpagebackbutton.ts +++ b/src/ts/components/settingspanelpagebackbutton.ts @@ -1,6 +1,7 @@ import {UIInstanceManager} from '../uimanager'; import {SettingsPanelPageNavigatorButton, SettingsPanelPageNavigatorConfig} from './settingspanelpagenavigatorbutton'; import { PlayerAPI } from 'bitmovin-player'; +import { i18n } from '../localization/i18n'; /** * @category Buttons @@ -12,7 +13,7 @@ export class SettingsPanelPageBackButton extends SettingsPanelPageNavigatorButto this.config = this.mergeConfig(config, { cssClass: 'ui-settingspanelpagebackbutton', - text: 'back', + text: i18n.getLocalizer('back'), } as SettingsPanelPageNavigatorConfig, this.config); } diff --git a/src/ts/components/settingspanelselectoption.ts b/src/ts/components/settingspanelselectoption.ts new file mode 100644 index 000000000..bb1a55b5b --- /dev/null +++ b/src/ts/components/settingspanelselectoption.ts @@ -0,0 +1,65 @@ +import { SettingsPanelItem, SettingsPanelItemConfig } from './settingspanelitem'; +import { PlayerAPI } from 'bitmovin-player'; +import { UIInstanceManager } from '../uimanager'; +import { ListSelector, ListSelectorConfig } from './listselector'; + +/** + * Configuration interface for a {@link SettingsPanelSelectOption}. + * + * @category Configs + */ +export interface SettingsPanelSelectOptionConfig extends SettingsPanelItemConfig { + /** + * The setting that will be changed when this option is clicked. + */ + settingComponent: ListSelector; + /** + * The value of the setting that will be selected when this option is clicked. + */ + settingsValue: string | undefined; +} + +/** + * A custom select option for a {@link ListSelector} option. + * Is used for building dynamic sub pages from a {@link DynamicSettingsPanelItem}. + * + * @category Components + */ +export class SettingsPanelSelectOption extends SettingsPanelItem { + private settingsValue: string | undefined; + protected settingComponent: ListSelector; + + constructor(config: SettingsPanelSelectOptionConfig) { + super(config); + + this.settingsValue = config.settingsValue; + + this.config = this.mergeConfig(config, { + cssClasses: ['ui-settings-panel-item-select-option'], + role: 'menuitem', + } as SettingsPanelSelectOptionConfig, this.config); + } + + configure(player: PlayerAPI, uimanager: UIInstanceManager) { + super.configure(player, uimanager); + + const handleSelectedOptionChanged = () => { + let selectedItem = this.settingComponent.getSelectedItem(); + + if (this.settingsValue === selectedItem) { + this.getDomElement().addClass(this.prefixCss('selected')); + } else { + this.getDomElement().removeClass(this.prefixCss('selected')); + } + }; + this.settingComponent.onItemSelected.subscribe(handleSelectedOptionChanged); + + const handleItemClick = () => { + this.settingComponent.selectItem(this.settingsValue); + }; + this.getDomElement().on('click', () => handleItemClick()); + + // Initial state + handleSelectedOptionChanged(); + } +} diff --git a/src/ts/components/subtitlesettings/subtitlesettingselectbox.ts b/src/ts/components/subtitlesettings/subtitlesettingselectbox.ts index e6448d56f..605849d10 100644 --- a/src/ts/components/subtitlesettings/subtitlesettingselectbox.ts +++ b/src/ts/components/subtitlesettings/subtitlesettingselectbox.ts @@ -20,7 +20,7 @@ export interface SubtitleSettingSelectBoxConfig extends ListSelectorConfig { export class SubtitleSettingSelectBox extends SelectBox { protected settingsManager?: SubtitleSettingsManager; - protected overlay: SubtitleOverlay; + readonly overlay: SubtitleOverlay; private currentCssClass: string; constructor(config: SubtitleSettingSelectBoxConfig) { @@ -48,6 +48,6 @@ export class SubtitleSettingSelectBox extends SelectBox { } configure(player: PlayerAPI, uimanager: UIInstanceManager): void { - this.settingsManager = uimanager.getSubtitleSettingsManager(); + this.settingsManager = uimanager.getSubtitleSettingsManager(); } } diff --git a/src/ts/components/subtitlesettings/subtitlesettingslabel.ts b/src/ts/components/subtitlesettings/subtitlesettingslabel.ts index 723db03c5..184c8c265 100644 --- a/src/ts/components/subtitlesettings/subtitlesettingslabel.ts +++ b/src/ts/components/subtitlesettings/subtitlesettingslabel.ts @@ -1,8 +1,7 @@ -import {LabelConfig} from '../label'; +import { Label, LabelConfig } from '../label'; import {Container, ContainerConfig} from '../container'; -import {DOM} from '../../dom'; import {SettingsPanelPageOpenButton} from '../settingspanelpageopenbutton'; -import { LocalizableText, i18n } from '../../localization/i18n'; +import { LocalizableText } from '../../localization/i18n'; /** * @category Configs @@ -16,9 +15,9 @@ export interface SubtitleSettingsLabelConfig extends LabelConfig { */ export class SubtitleSettingsLabel extends Container { - private opener: SettingsPanelPageOpenButton; + readonly opener: SettingsPanelPageOpenButton; - private text: LocalizableText; + readonly text: LocalizableText; private for: string; @@ -30,23 +29,11 @@ export class SubtitleSettingsLabel extends Container { this.for = config.for; this.config = this.mergeConfig(config, { - cssClass: 'ui-label', components: [ + new Label({ text: this.text, for: this.for } as LabelConfig), this.opener, ], + cssClass: 'ui-subtitle-settings-label', }, this.config); } - - protected toDomElement(): DOM { - let labelElement = new DOM('label', { - 'id': this.config.id, - 'class': this.getCssClasses(), - 'for': this.for, - }, this).append( - new DOM('span', {}).html(i18n.performLocalization(this.text)), - this.opener.getDomElement(), - ); - - return labelElement; - } } diff --git a/src/ts/components/subtitlesettings/subtitlesettingspanelpage.ts b/src/ts/components/subtitlesettings/subtitlesettingspanelpage.ts index 95b560a53..235229ed1 100644 --- a/src/ts/components/subtitlesettings/subtitlesettingspanelpage.ts +++ b/src/ts/components/subtitlesettings/subtitlesettingspanelpage.ts @@ -1,8 +1,6 @@ -import {SettingsPanelPage} from '../settingspanelpage'; +import { SettingsPanelPage, SettingsPanelPageConfig } from '../settingspanelpage'; import {SettingsPanel} from '../settingspanel'; import {SubtitleOverlay} from '../subtitleoverlay'; -import {ContainerConfig} from '../container'; -import {SubtitleSettingsManager} from './subtitlesettingsmanager'; import {Component, ComponentConfig} from '../component'; import {FontSizeSelectBox} from './fontsizeselectbox'; import {FontFamilySelectBox} from './fontfamilyselectbox'; @@ -16,16 +14,19 @@ import {WindowOpacitySelectBox} from './windowopacityselectbox'; import {SubtitleSettingsResetButton} from './subtitlesettingsresetbutton'; import {UIInstanceManager} from '../../uimanager'; import {SettingsPanelPageBackButton} from '../settingspanelpagebackbutton'; -import {SettingsPanelItem} from '../settingspanelitem'; +import { SettingsPanelItem, SettingsPanelItemConfig } from '../settingspanelitem'; import { PlayerAPI } from 'bitmovin-player'; -import { i18n } from '../../localization/i18n'; +import { i18n, LocalizableText } from '../../localization/i18n'; +import { DynamicSettingsPanelItem } from '../dynamicsettingspanelitem'; +import { ListSelector, ListSelectorConfig } from '../listselector'; /** * @category Configs */ -export interface SubtitleSettingsPanelPageConfig extends ContainerConfig { +export interface SubtitleSettingsPanelPageConfig extends SettingsPanelPageConfig { settingsPanel: SettingsPanel; overlay: SubtitleOverlay; + useDynamicSettingsPanelItem?: boolean; } /** @@ -42,43 +43,88 @@ export class SubtitleSettingsPanelPage extends SettingsPanelPage { this.overlay = config.overlay; this.settingsPanel = config.settingsPanel; - - this.config = this.mergeConfig(config, { - components: []>[ - new SettingsPanelItem(i18n.getLocalizer('settings.subtitles.font.size'), new FontSizeSelectBox({ + const components = [ + this.buildSettingsPanelItem( + i18n.getLocalizer('settings.subtitles.font.size'), + new FontSizeSelectBox({ overlay: this.overlay, - })), - new SettingsPanelItem(i18n.getLocalizer('settings.subtitles.font.family'), new FontFamilySelectBox({ + }), + config.useDynamicSettingsPanelItem, + ), + this.buildSettingsPanelItem( + i18n.getLocalizer('settings.subtitles.font.family'), + new FontFamilySelectBox({ overlay: this.overlay, - })), - new SettingsPanelItem(i18n.getLocalizer('settings.subtitles.font.color'), new FontColorSelectBox({ + }), + config.useDynamicSettingsPanelItem, + ), + this.buildSettingsPanelItem( + i18n.getLocalizer('settings.subtitles.font.color'), + new FontColorSelectBox({ overlay: this.overlay, - })), - new SettingsPanelItem(i18n.getLocalizer('settings.subtitles.font.opacity'), new FontOpacitySelectBox({ + }), + config.useDynamicSettingsPanelItem, + ), + this.buildSettingsPanelItem( + i18n.getLocalizer('settings.subtitles.font.opacity'), + new FontOpacitySelectBox({ overlay: this.overlay, - })), - new SettingsPanelItem(i18n.getLocalizer('settings.subtitles.characterEdge'), new CharacterEdgeSelectBox({ + }), + config.useDynamicSettingsPanelItem, + ), + this.buildSettingsPanelItem( + i18n.getLocalizer('settings.subtitles.characterEdge'), + new CharacterEdgeSelectBox({ overlay: this.overlay, - })), - new SettingsPanelItem(i18n.getLocalizer('settings.subtitles.background.color'), new BackgroundColorSelectBox({ + }), + config.useDynamicSettingsPanelItem, + ), + this.buildSettingsPanelItem( + i18n.getLocalizer('settings.subtitles.background.color'), + new BackgroundColorSelectBox({ overlay: this.overlay, - })), - new SettingsPanelItem(i18n.getLocalizer('settings.subtitles.background.opacity'), new BackgroundOpacitySelectBox({ + }), + config.useDynamicSettingsPanelItem, + ), + this.buildSettingsPanelItem( + i18n.getLocalizer('settings.subtitles.background.opacity'), + new BackgroundOpacitySelectBox({ overlay: this.overlay, - })), - new SettingsPanelItem(i18n.getLocalizer('settings.subtitles.window.color'), new WindowColorSelectBox({ + }), + config.useDynamicSettingsPanelItem, + ), + this.buildSettingsPanelItem( + i18n.getLocalizer('settings.subtitles.window.color'), + new WindowColorSelectBox({ overlay: this.overlay, - })), - new SettingsPanelItem(i18n.getLocalizer('settings.subtitles.window.opacity'), new WindowOpacitySelectBox({ + }), + config.useDynamicSettingsPanelItem, + ), + this.buildSettingsPanelItem( + i18n.getLocalizer('settings.subtitles.window.opacity'), + new WindowOpacitySelectBox({ overlay: this.overlay, - })), - new SettingsPanelItem(new SettingsPanelPageBackButton({ - container: this.settingsPanel, - text: i18n.getLocalizer('back'), - }), new SubtitleSettingsResetButton({}), { - role: 'menubar', }), - ], + config.useDynamicSettingsPanelItem, + ), + ]; + + const backItem = new SettingsPanelItem({ + label: new SettingsPanelPageBackButton({ + container: this.settingsPanel, + }), + settingComponent: new SubtitleSettingsResetButton({}), + cssClasses: ['title-item'], + }); + + if (config.useDynamicSettingsPanelItem) { + components.unshift(backItem); + } else { + components.push(backItem); + } + + this.config = this.mergeConfig(config, { + components: components, }, this.config); } @@ -93,4 +139,23 @@ export class SubtitleSettingsPanelPage extends SettingsPanelPage { this.overlay.removePreviewSubtitleLabel(); }); } -} \ No newline at end of file + + private buildSettingsPanelItem( + label: LocalizableText, + setting: ListSelector, + useDynamicSettingsPanelItem: boolean = false, + ): SettingsPanelItem { + if (useDynamicSettingsPanelItem) { + return new DynamicSettingsPanelItem({ + label: label, + settingComponent: setting, + container: this.settingsPanel, + }); + } else { + return new SettingsPanelItem({ + label: label, + settingComponent: setting, + }); + } + } +} diff --git a/src/ts/components/subtitletogglebutton.ts b/src/ts/components/subtitletogglebutton.ts index dee419ff6..60405ee71 100644 --- a/src/ts/components/subtitletogglebutton.ts +++ b/src/ts/components/subtitletogglebutton.ts @@ -6,7 +6,7 @@ import { StorageUtils } from '../storageutils'; import { SubtitleSelectBox } from './subtitleselectbox'; import { prefixCss } from './dummycomponent'; import { SubtitleSwitchHandler } from '../subtitleutils'; -import { ModernSettingsPanelItem } from './modernsettingspanelitem'; +import { DynamicSettingsPanelItem } from './dynamicsettingspanelitem'; export interface StoredSubtitleLanguage { language: string; @@ -17,11 +17,11 @@ export class SubtitleToggleButton extends ToggleButton { /** * Requires the settingsPanelItem which holds the SubtitleSelectBox in its setting */ - private settingsPanelItem: ModernSettingsPanelItem; + private settingsPanelItem: DynamicSettingsPanelItem; private subtitleSelectBox: SubtitleSelectBox; private player: PlayerAPI; - constructor(settingsPanelItem: ModernSettingsPanelItem, subtitleSelectBox: SubtitleSelectBox, config: ToggleButtonConfig = {}) { + constructor(settingsPanelItem: DynamicSettingsPanelItem, subtitleSelectBox: SubtitleSelectBox, config: ToggleButtonConfig = {}) { super(config); this.settingsPanelItem = settingsPanelItem; diff --git a/src/ts/demofactory.ts b/src/ts/demofactory.ts index e9f5eb144..125e27c2c 100644 --- a/src/ts/demofactory.ts +++ b/src/ts/demofactory.ts @@ -70,9 +70,9 @@ export namespace DemoFactory { components: [ new SettingsPanelPage({ components: [ - new SettingsPanelItem('Video Quality', new VideoQualitySelectBox()), - new SettingsPanelItem('Speed', new PlaybackSpeedSelectBox()), - new SettingsPanelItem('Audio Quality', new AudioQualitySelectBox()), + new SettingsPanelItem({ label: 'Video Quality', settingComponent: new VideoQualitySelectBox() }), + new SettingsPanelItem({ label: 'Speed', settingComponent: new PlaybackSpeedSelectBox() }), + new SettingsPanelItem({ label: 'Audio Quality', settingComponent: new AudioQualitySelectBox() }), ], }), ], @@ -84,7 +84,7 @@ export namespace DemoFactory { components: [ new SettingsPanelPage({ components: [ - new SettingsPanelItem(null, subtitleListBox), + new SettingsPanelItem({ label: null, settingComponent: subtitleListBox }), ], }), ], @@ -96,7 +96,7 @@ export namespace DemoFactory { components: [ new SettingsPanelPage({ components: [ - new SettingsPanelItem(null, audioTrackListBox), + new SettingsPanelItem({ label: null, settingComponent: audioTrackListBox }), ], }), ], diff --git a/src/ts/uifactory.ts b/src/ts/uifactory.ts index e888571b5..65c5bad8f 100644 --- a/src/ts/uifactory.ts +++ b/src/ts/uifactory.ts @@ -39,7 +39,7 @@ import { AdSkipButton } from './components/adskipbutton'; import { CloseButton } from './components/closebutton'; import { MetadataLabel, MetadataLabelContent } from './components/metadatalabel'; import { PlayerUtils } from './playerutils'; -import { Label, LabelConfig } from './components/label'; +import { Label } from './components/label'; import { CastUIContainer } from './components/castuicontainer'; import { UIConditionContext, UIManager } from './uimanager'; import { UIConfig } from './uiconfig'; @@ -51,10 +51,7 @@ import { SpatialNavigation } from './spatialnavigation/spatialnavigation'; import { RootNavigationGroup } from './spatialnavigation/rootnavigationgroup'; import { ListNavigationGroup, ListOrientation } from './spatialnavigation/ListNavigationGroup'; import { EcoModeContainer } from './components/ecomodecontainer'; -import { SubtitleToggleButton } from './components/subtitletogglebutton'; -import { ModernSettingsPanelItem } from './components/modernsettingspanelitem'; -import { ModernSettingsPanelPage } from './components/modernsettingspanelpage'; -import { ModernSettingsPanel } from './components/modernsettingspanel'; +import { DynamicSettingsPanelItem } from './components/dynamicsettingspanelitem'; import { TouchControlOverlay } from './components/touchcontroloverlay'; import { AdStatusOverlay } from './components/adstatusoverlay'; @@ -85,10 +82,10 @@ export namespace UIFactory { let mainSettingsPanelPage: SettingsPanelPage; const components: Container[] = [ - new SettingsPanelItem(i18n.getLocalizer('settings.video.quality'), new VideoQualitySelectBox()), - new SettingsPanelItem(i18n.getLocalizer('speed'), new PlaybackSpeedSelectBox()), - new SettingsPanelItem(i18n.getLocalizer('settings.audio.track'), new AudioTrackSelectBox()), - new SettingsPanelItem(i18n.getLocalizer('settings.audio.quality'), new AudioQualitySelectBox()), + new SettingsPanelItem({ label: i18n.getLocalizer('settings.video.quality'), settingComponent: new VideoQualitySelectBox() }), + new SettingsPanelItem({ label: i18n.getLocalizer('speed'), settingComponent: new PlaybackSpeedSelectBox() }), + new SettingsPanelItem({ label: i18n.getLocalizer('settings.audio.track'), settingComponent: new AudioTrackSelectBox() }), + new SettingsPanelItem({ label: i18n.getLocalizer('settings.audio.quality'), settingComponent: new AudioQualitySelectBox() }), ]; if (config.ecoMode) { @@ -126,16 +123,14 @@ export namespace UIFactory { }); mainSettingsPanelPage.addComponent( - new SettingsPanelItem( - new SubtitleSettingsLabel({ + new SettingsPanelItem({ + label: new SubtitleSettingsLabel({ text: i18n.getLocalizer('settings.subtitles'), opener: subtitleSettingsOpenButton, }), - subtitleSelectBox, - { - role: 'menubar', - }, - ), + settingComponent: subtitleSelectBox, + role: 'menubar', + }), ); settingsPanel.addComponent(subtitleSettingsPanelPage); @@ -237,10 +232,10 @@ export namespace UIFactory { let mainSettingsPanelPage = new SettingsPanelPage({ components: [ - new SettingsPanelItem(i18n.getLocalizer('settings.video.quality'), new VideoQualitySelectBox()), - new SettingsPanelItem(i18n.getLocalizer('speed'), new PlaybackSpeedSelectBox()), - new SettingsPanelItem(i18n.getLocalizer('settings.audio.track'), new AudioTrackSelectBox()), - new SettingsPanelItem(i18n.getLocalizer('settings.audio.quality'), new AudioQualitySelectBox()), + new SettingsPanelItem({ label: i18n.getLocalizer('settings.video.quality'), settingComponent: new VideoQualitySelectBox() }), + new SettingsPanelItem({ label: i18n.getLocalizer('speed'), settingComponent: new PlaybackSpeedSelectBox() }), + new SettingsPanelItem({ label: i18n.getLocalizer('settings.audio.track'), settingComponent: new AudioTrackSelectBox() }), + new SettingsPanelItem({ label: i18n.getLocalizer('settings.audio.quality'), settingComponent: new AudioQualitySelectBox() }), ], }); @@ -266,16 +261,14 @@ export namespace UIFactory { const subtitleSelectBox = new SubtitleSelectBox(); mainSettingsPanelPage.addComponent( - new SettingsPanelItem( - new SubtitleSettingsLabel({ + new SettingsPanelItem({ + label: new SubtitleSettingsLabel({ text: i18n.getLocalizer('settings.subtitles'), opener: subtitleSettingsOpenButton, }), - subtitleSelectBox, - { - role: 'menubar', - }, - ), + settingComponent: subtitleSelectBox, + role: 'menubar', + }), ); settingsPanel.addComponent(subtitleSettingsPanelPage); @@ -540,32 +533,65 @@ export namespace UIFactory { export function superModernMobileUI() { let subtitleOverlay = new SubtitleOverlay(); - let mainSettingsPanelPage = new ModernSettingsPanelPage({ + let settingsPanel = new SettingsPanel({ + components: [], + hidden: true, + pageTransitionAnimation: true, + hideDelay: -1, + }); + + let mainSettingsPanelPage = new SettingsPanelPage({ components: [ - new ModernSettingsPanelItem(i18n.getLocalizer('settings.video.quality'), new VideoQualitySelectBox()), - new ModernSettingsPanelItem(i18n.getLocalizer('speed'), new PlaybackSpeedSelectBox()), - new ModernSettingsPanelItem(i18n.getLocalizer('settings.audio.track'), new AudioTrackSelectBox()), - new ModernSettingsPanelItem(i18n.getLocalizer('settings.audio.quality'), new AudioQualitySelectBox()), + new DynamicSettingsPanelItem({ + label: i18n.getLocalizer('settings.video.quality'), + settingComponent: new VideoQualitySelectBox(), + container: settingsPanel, + }), + new DynamicSettingsPanelItem({ + label: i18n.getLocalizer('speed'), + settingComponent: new PlaybackSpeedSelectBox(), + container: settingsPanel, + }), + new DynamicSettingsPanelItem({ + label: i18n.getLocalizer('settings.audio.track'), + settingComponent: new AudioTrackSelectBox() , + container: settingsPanel, + }), + new DynamicSettingsPanelItem({ + label: i18n.getLocalizer('settings.audio.quality'), + settingComponent: new AudioQualitySelectBox(), + container: settingsPanel, + }), ], }); - let settingsPanel = new ModernSettingsPanel({ - components: [mainSettingsPanelPage], - hidden: true, - pageTransitionAnimation: true, - hideDelay: -1, + settingsPanel.addComponent(mainSettingsPanelPage); + + let subtitleSettingsPanelPage = new SubtitleSettingsPanelPage({ + settingsPanel: settingsPanel, + overlay: subtitleOverlay, + useDynamicSettingsPanelItem: true, + }); + + let subtitleSettingsOpenButton = new SettingsPanelPageOpenButton({ + targetPage: subtitleSettingsPanelPage, + container: settingsPanel, + ariaLabel: i18n.getLocalizer('settings.subtitles'), + text: i18n.getLocalizer('open'), }); const subtitleSelectBox = new SubtitleSelectBox(); - let subtitleSelectItem = new ModernSettingsPanelItem( - new Label({ text: i18n.getLocalizer('settings.subtitles') } as LabelConfig), - subtitleSelectBox, - null, - { - role: 'menubar', - }, - ); + let subtitleSelectItem = new DynamicSettingsPanelItem({ + label: new SubtitleSettingsLabel({ + text: i18n.getLocalizer('settings.subtitles'), + opener: subtitleSettingsOpenButton, + }), + settingComponent: subtitleSelectBox, + role: 'menubar', + container: settingsPanel, + }); mainSettingsPanelPage.addComponent(subtitleSelectItem); + settingsPanel.addComponent(subtitleSettingsPanelPage); let controlBar = new ControlBar({ components: [ @@ -589,7 +615,7 @@ export namespace UIFactory { new VolumeToggleButton(), new Spacer(), new SettingsToggleButton({ settingsPanel: settingsPanel }), - new SubtitleToggleButton(subtitleSelectItem, subtitleSelectBox), + // new SubtitleToggleButton(subtitleSelectItem, subtitleSelectBox), new FullscreenToggleButton(), ], cssClasses: ['controlbar-bottom'], @@ -626,52 +652,82 @@ export namespace UIFactory { }); } - export function superModernUI() { + export function superModernUI(config: UIConfig) { let subtitleOverlay = new SubtitleOverlay(); let mainSettingsPanelPage: SettingsPanelPage; + let settingsPanel = new SettingsPanel({ + components: [], + hidden: true, + pageTransitionAnimation: true, + }); + const components: Container[] = [ - new ModernSettingsPanelItem(i18n.getLocalizer('settings.video.quality'), new VideoQualitySelectBox()), - new ModernSettingsPanelItem(i18n.getLocalizer('speed'), new PlaybackSpeedSelectBox()), - new ModernSettingsPanelItem(i18n.getLocalizer('settings.audio.track'), new AudioTrackSelectBox()), - new ModernSettingsPanelItem(i18n.getLocalizer('settings.audio.quality'), new AudioQualitySelectBox()), + new DynamicSettingsPanelItem({ + label: i18n.getLocalizer('settings.video.quality'), + settingComponent: new VideoQualitySelectBox(), + container: settingsPanel, + }), + new DynamicSettingsPanelItem({ + label: i18n.getLocalizer('speed'), + settingComponent: new PlaybackSpeedSelectBox(), + container: settingsPanel, + }), + new DynamicSettingsPanelItem({ + label: i18n.getLocalizer('settings.audio.track'), + settingComponent: new AudioTrackSelectBox(), + container: settingsPanel, + }), + new DynamicSettingsPanelItem({ + label: i18n.getLocalizer('settings.audio.quality'), + settingComponent: new AudioQualitySelectBox(), + container: settingsPanel, + }), ]; - mainSettingsPanelPage = new ModernSettingsPanelPage({ + if (config.ecoMode) { + const ecoModeContainer = new EcoModeContainer(); + + ecoModeContainer.setOnToggleCallback(() => { + // forces the browser to re-calculate the height of the settings panel when adding/removing elements + settingsPanel.getDomElement().css({ width: '', height: '' }); + }); + + components.unshift(ecoModeContainer); + } + + mainSettingsPanelPage = new SettingsPanelPage({ components, }); - let settingsPanel = new ModernSettingsPanel({ - components: [mainSettingsPanelPage], - hidden: true, - pageTransitionAnimation: true, + settingsPanel.addComponent(mainSettingsPanelPage); + + let subtitleSettingsPanelPage = new SubtitleSettingsPanelPage({ + settingsPanel: settingsPanel, + overlay: subtitleOverlay, + useDynamicSettingsPanelItem: true, }); - // let subtitleSettingsPanelPage = new SubtitleSettingsPanelPage({ - // settingsPanel: settingsPanel, - // overlay: subtitleOverlay, - // }); - // - // let subtitleSettingsOpenButton = new SettingsPanelPageOpenButton({ - // targetPage: subtitleSettingsPanelPage, - // container: settingsPanel, - // ariaLabel: i18n.getLocalizer('settings.subtitles'), - // text: i18n.getLocalizer('open'), - // }); + let subtitleSettingsOpenButton = new SettingsPanelPageOpenButton({ + targetPage: subtitleSettingsPanelPage, + container: settingsPanel, + ariaLabel: i18n.getLocalizer('settings.subtitles'), + text: i18n.getLocalizer('open'), + }); const subtitleSelectBox = new SubtitleSelectBox(); - let subtitleSelectItem = new ModernSettingsPanelItem( - new Label({ text: i18n.getLocalizer('settings.subtitles') } as LabelConfig), - subtitleSelectBox, - null, - { - role: 'menubar', - }, - ); + let subtitleSelectItem = new DynamicSettingsPanelItem({ + label: new SubtitleSettingsLabel({ + text: i18n.getLocalizer('settings.subtitles'), + opener: subtitleSettingsOpenButton, + }), + settingComponent: subtitleSelectBox, + role: 'menubar', + container: settingsPanel, + }); mainSettingsPanelPage.addComponent(subtitleSelectItem); - - // settingsPanel.addComponent(subtitleSettingsPanelPage); + settingsPanel.addComponent(subtitleSettingsPanelPage); let controlBar = new ControlBar({ components: [ @@ -763,7 +819,7 @@ export namespace UIFactory { }, }, { - ui: superModernUI(), + ui: superModernUI(config), condition: (context: UIConditionContext) => { return !context.isAd && !context.adRequiresUi; }, @@ -836,7 +892,7 @@ export namespace UIFactory { const subtitleListPanel = new SettingsPanel({ components: [ new SettingsPanelPage({ - components: [new SettingsPanelItem(null, subtitleListBox)], + components: [new SettingsPanelItem({ settingComponent: subtitleListBox })], }), ], hidden: true, @@ -846,7 +902,7 @@ export namespace UIFactory { const audioTrackListPanel = new SettingsPanel({ components: [ new SettingsPanelPage({ - components: [new SettingsPanelItem(null, audioTrackListBox)], + components: [new SettingsPanelItem({ settingComponent: audioTrackListBox })], }), ], hidden: true,